##// END OF EJS Templates
Changing revision label and identifier at SCM adapter level (#3724, #6092)...
Toshi MARUYAMA -
r4493:2e1bcb2abff6
parent child
Show More
@@ -1,331 +1,334
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2009 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require 'SVG/Graph/Bar'
19 19 require 'SVG/Graph/BarHorizontal'
20 20 require 'digest/sha1'
21 21
22 22 class ChangesetNotFound < Exception; end
23 23 class InvalidRevisionParam < Exception; end
24 24
25 25 class RepositoriesController < ApplicationController
26 26 menu_item :repository
27 27 menu_item :settings, :only => :edit
28 28 default_search_scope :changesets
29 29
30 30 before_filter :find_repository, :except => :edit
31 31 before_filter :find_project, :only => :edit
32 32 before_filter :authorize
33 33 accept_key_auth :revisions
34 34
35 35 rescue_from Redmine::Scm::Adapters::CommandFailed, :with => :show_error_command_failed
36 36
37 37 def edit
38 38 @repository = @project.repository
39 39 if !@repository
40 40 @repository = Repository.factory(params[:repository_scm])
41 41 @repository.project = @project if @repository
42 42 end
43 43 if request.post? && @repository
44 44 @repository.attributes = params[:repository]
45 45 @repository.save
46 46 end
47 47 render(:update) do |page|
48 48 page.replace_html "tab-content-repository", :partial => 'projects/settings/repository'
49 49 if @repository && !@project.repository
50 50 @project.reload #needed to reload association
51 51 page.replace_html "main-menu", render_main_menu(@project)
52 52 end
53 53 end
54 54 end
55 55
56 56 def committers
57 57 @committers = @repository.committers
58 58 @users = @project.users
59 59 additional_user_ids = @committers.collect(&:last).collect(&:to_i) - @users.collect(&:id)
60 60 @users += User.find_all_by_id(additional_user_ids) unless additional_user_ids.empty?
61 61 @users.compact!
62 62 @users.sort!
63 63 if request.post? && params[:committers].is_a?(Hash)
64 64 # Build a hash with repository usernames as keys and corresponding user ids as values
65 65 @repository.committer_ids = params[:committers].values.inject({}) {|h, c| h[c.first] = c.last; h}
66 66 flash[:notice] = l(:notice_successful_update)
67 67 redirect_to :action => 'committers', :id => @project
68 68 end
69 69 end
70 70
71 71 def destroy
72 72 @repository.destroy
73 73 redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'repository'
74 74 end
75 75
76 76 def show
77 77 @repository.fetch_changesets if Setting.autofetch_changesets? && @path.empty?
78 78
79 79 @entries = @repository.entries(@path, @rev)
80 80 if request.xhr?
81 81 @entries ? render(:partial => 'dir_list_content') : render(:nothing => true)
82 82 else
83 83 (show_error_not_found; return) unless @entries
84 84 @changesets = @repository.latest_changesets(@path, @rev)
85 85 @properties = @repository.properties(@path, @rev)
86 86 render :action => 'show'
87 87 end
88 88 end
89 89
90 90 alias_method :browse, :show
91 91
92 92 def changes
93 93 @entry = @repository.entry(@path, @rev)
94 94 (show_error_not_found; return) unless @entry
95 95 @changesets = @repository.latest_changesets(@path, @rev, Setting.repository_log_display_limit.to_i)
96 96 @properties = @repository.properties(@path, @rev)
97 97 end
98 98
99 99 def revisions
100 100 @changeset_count = @repository.changesets.count
101 101 @changeset_pages = Paginator.new self, @changeset_count,
102 102 per_page_option,
103 103 params['page']
104 104 @changesets = @repository.changesets.find(:all,
105 105 :limit => @changeset_pages.items_per_page,
106 106 :offset => @changeset_pages.current.offset,
107 107 :include => [:user, :repository])
108 108
109 109 respond_to do |format|
110 110 format.html { render :layout => false if request.xhr? }
111 111 format.atom { render_feed(@changesets, :title => "#{@project.name}: #{l(:label_revision_plural)}") }
112 112 end
113 113 end
114 114
115 115 def entry
116 116 @entry = @repository.entry(@path, @rev)
117 117 (show_error_not_found; return) unless @entry
118 118
119 119 # If the entry is a dir, show the browser
120 120 (show; return) if @entry.is_dir?
121 121
122 122 @content = @repository.cat(@path, @rev)
123 123 (show_error_not_found; return) unless @content
124 124 if 'raw' == params[:format] || @content.is_binary_data? || (@entry.size && @entry.size > Setting.file_max_size_displayed.to_i.kilobyte)
125 125 # Force the download
126 126 send_data @content, :filename => @path.split('/').last
127 127 else
128 128 # Prevent empty lines when displaying a file with Windows style eol
129 129 @content.gsub!("\r\n", "\n")
130 130 end
131 131 end
132 132
133 133 def annotate
134 134 @entry = @repository.entry(@path, @rev)
135 135 (show_error_not_found; return) unless @entry
136 136
137 137 @annotate = @repository.scm.annotate(@path, @rev)
138 138 (render_error l(:error_scm_annotate); return) if @annotate.nil? || @annotate.empty?
139 139 end
140 140
141 141 def revision
142 142 @changeset = @repository.find_changeset_by_name(@rev)
143 143 raise ChangesetNotFound unless @changeset
144 144
145 145 respond_to do |format|
146 146 format.html
147 147 format.js {render :layout => false}
148 148 end
149 149 rescue ChangesetNotFound
150 150 show_error_not_found
151 151 end
152 152
153 153 def diff
154 154 if params[:format] == 'diff'
155 155 @diff = @repository.diff(@path, @rev, @rev_to)
156 156 (show_error_not_found; return) unless @diff
157 157 filename = "changeset_r#{@rev}"
158 158 filename << "_r#{@rev_to}" if @rev_to
159 159 send_data @diff.join, :filename => "#{filename}.diff",
160 160 :type => 'text/x-patch',
161 161 :disposition => 'attachment'
162 162 else
163 163 @diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
164 164 @diff_type = 'inline' unless %w(inline sbs).include?(@diff_type)
165 165
166 166 # Save diff type as user preference
167 167 if User.current.logged? && @diff_type != User.current.pref[:diff_type]
168 168 User.current.pref[:diff_type] = @diff_type
169 169 User.current.preference.save
170 170 end
171 171
172 172 @cache_key = "repositories/diff/#{@repository.id}/" + Digest::MD5.hexdigest("#{@path}-#{@rev}-#{@rev_to}-#{@diff_type}")
173 173 unless read_fragment(@cache_key)
174 174 @diff = @repository.diff(@path, @rev, @rev_to)
175 175 show_error_not_found unless @diff
176 176 end
177
178 @changeset = @repository.find_changeset_by_name(@rev)
179 @changeset_to = @rev_to ? @repository.find_changeset_by_name(@rev_to) : nil
177 180 end
178 181 end
179 182
180 183 def stats
181 184 end
182 185
183 186 def graph
184 187 data = nil
185 188 case params[:graph]
186 189 when "commits_per_month"
187 190 data = graph_commits_per_month(@repository)
188 191 when "commits_per_author"
189 192 data = graph_commits_per_author(@repository)
190 193 end
191 194 if data
192 195 headers["Content-Type"] = "image/svg+xml"
193 196 send_data(data, :type => "image/svg+xml", :disposition => "inline")
194 197 else
195 198 render_404
196 199 end
197 200 end
198 201
199 202 private
200 203
201 204 REV_PARAM_RE = %r{\A[a-f0-9]*\Z}i
202 205
203 206 def find_repository
204 207 @project = Project.find(params[:id])
205 208 @repository = @project.repository
206 209 (render_404; return false) unless @repository
207 210 @path = params[:path].join('/') unless params[:path].nil?
208 211 @path ||= ''
209 212 @rev = params[:rev].blank? ? @repository.default_branch : params[:rev].strip
210 213 @rev_to = params[:rev_to]
211 214
212 215 unless @rev.to_s.match(REV_PARAM_RE) && @rev.to_s.match(REV_PARAM_RE)
213 216 if @repository.branches.blank?
214 217 raise InvalidRevisionParam
215 218 end
216 219 end
217 220 rescue ActiveRecord::RecordNotFound
218 221 render_404
219 222 rescue InvalidRevisionParam
220 223 show_error_not_found
221 224 end
222 225
223 226 def show_error_not_found
224 227 render_error l(:error_scm_not_found)
225 228 end
226 229
227 230 # Handler for Redmine::Scm::Adapters::CommandFailed exception
228 231 def show_error_command_failed(exception)
229 232 render_error l(:error_scm_command_failed, exception.message)
230 233 end
231 234
232 235 def graph_commits_per_month(repository)
233 236 @date_to = Date.today
234 237 @date_from = @date_to << 11
235 238 @date_from = Date.civil(@date_from.year, @date_from.month, 1)
236 239 commits_by_day = repository.changesets.count(:all, :group => :commit_date, :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
237 240 commits_by_month = [0] * 12
238 241 commits_by_day.each {|c| commits_by_month[c.first.to_date.months_ago] += c.last }
239 242
240 243 changes_by_day = repository.changes.count(:all, :group => :commit_date, :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
241 244 changes_by_month = [0] * 12
242 245 changes_by_day.each {|c| changes_by_month[c.first.to_date.months_ago] += c.last }
243 246
244 247 fields = []
245 248 12.times {|m| fields << month_name(((Date.today.month - 1 - m) % 12) + 1)}
246 249
247 250 graph = SVG::Graph::Bar.new(
248 251 :height => 300,
249 252 :width => 800,
250 253 :fields => fields.reverse,
251 254 :stack => :side,
252 255 :scale_integers => true,
253 256 :step_x_labels => 2,
254 257 :show_data_values => false,
255 258 :graph_title => l(:label_commits_per_month),
256 259 :show_graph_title => true
257 260 )
258 261
259 262 graph.add_data(
260 263 :data => commits_by_month[0..11].reverse,
261 264 :title => l(:label_revision_plural)
262 265 )
263 266
264 267 graph.add_data(
265 268 :data => changes_by_month[0..11].reverse,
266 269 :title => l(:label_change_plural)
267 270 )
268 271
269 272 graph.burn
270 273 end
271 274
272 275 def graph_commits_per_author(repository)
273 276 commits_by_author = repository.changesets.count(:all, :group => :committer)
274 277 commits_by_author.to_a.sort! {|x, y| x.last <=> y.last}
275 278
276 279 changes_by_author = repository.changes.count(:all, :group => :committer)
277 280 h = changes_by_author.inject({}) {|o, i| o[i.first] = i.last; o}
278 281
279 282 fields = commits_by_author.collect {|r| r.first}
280 283 commits_data = commits_by_author.collect {|r| r.last}
281 284 changes_data = commits_by_author.collect {|r| h[r.first] || 0}
282 285
283 286 fields = fields + [""]*(10 - fields.length) if fields.length<10
284 287 commits_data = commits_data + [0]*(10 - commits_data.length) if commits_data.length<10
285 288 changes_data = changes_data + [0]*(10 - changes_data.length) if changes_data.length<10
286 289
287 290 # Remove email adress in usernames
288 291 fields = fields.collect {|c| c.gsub(%r{<.+@.+>}, '') }
289 292
290 293 graph = SVG::Graph::BarHorizontal.new(
291 294 :height => 400,
292 295 :width => 800,
293 296 :fields => fields,
294 297 :stack => :side,
295 298 :scale_integers => true,
296 299 :show_data_values => false,
297 300 :rotate_y_labels => false,
298 301 :graph_title => l(:label_commits_per_author),
299 302 :show_graph_title => true
300 303 )
301 304
302 305 graph.add_data(
303 306 :data => commits_data,
304 307 :title => l(:label_revision_plural)
305 308 )
306 309
307 310 graph.add_data(
308 311 :data => changes_data,
309 312 :title => l(:label_change_plural)
310 313 )
311 314
312 315 graph.burn
313 316 end
314 317
315 318 end
316 319
317 320 class Date
318 321 def months_ago(date = Date.today)
319 322 (date.year - self.year)*12 + (date.month - self.month)
320 323 end
321 324
322 325 def weeks_ago(date = Date.today)
323 326 (date.year - self.year)*52 + (date.cweek - self.cweek)
324 327 end
325 328 end
326 329
327 330 class String
328 331 def with_leading_slash
329 332 starts_with?('/') ? self : "/#{self}"
330 333 end
331 334 end
@@ -1,919 +1,921
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2010 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require 'forwardable'
19 19 require 'cgi'
20 20
21 21 module ApplicationHelper
22 22 include Redmine::WikiFormatting::Macros::Definitions
23 23 include Redmine::I18n
24 24 include GravatarHelper::PublicMethods
25 25
26 26 extend Forwardable
27 27 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
28 28
29 29 # Return true if user is authorized for controller/action, otherwise false
30 30 def authorize_for(controller, action)
31 31 User.current.allowed_to?({:controller => controller, :action => action}, @project)
32 32 end
33 33
34 34 # Display a link if user is authorized
35 35 #
36 36 # @param [String] name Anchor text (passed to link_to)
37 37 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
38 38 # @param [optional, Hash] html_options Options passed to link_to
39 39 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
40 40 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
41 41 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
42 42 end
43 43
44 44 # Display a link to remote if user is authorized
45 45 def link_to_remote_if_authorized(name, options = {}, html_options = nil)
46 46 url = options[:url] || {}
47 47 link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
48 48 end
49 49
50 50 # Displays a link to user's account page if active
51 51 def link_to_user(user, options={})
52 52 if user.is_a?(User)
53 53 name = h(user.name(options[:format]))
54 54 if user.active?
55 55 link_to name, :controller => 'users', :action => 'show', :id => user
56 56 else
57 57 name
58 58 end
59 59 else
60 60 h(user.to_s)
61 61 end
62 62 end
63 63
64 64 # Displays a link to +issue+ with its subject.
65 65 # Examples:
66 66 #
67 67 # link_to_issue(issue) # => Defect #6: This is the subject
68 68 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
69 69 # link_to_issue(issue, :subject => false) # => Defect #6
70 70 # link_to_issue(issue, :project => true) # => Foo - Defect #6
71 71 #
72 72 def link_to_issue(issue, options={})
73 73 title = nil
74 74 subject = nil
75 75 if options[:subject] == false
76 76 title = truncate(issue.subject, :length => 60)
77 77 else
78 78 subject = issue.subject
79 79 if options[:truncate]
80 80 subject = truncate(subject, :length => options[:truncate])
81 81 end
82 82 end
83 83 s = link_to "#{issue.tracker} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue},
84 84 :class => issue.css_classes,
85 85 :title => title
86 86 s << ": #{h subject}" if subject
87 87 s = "#{h issue.project} - " + s if options[:project]
88 88 s
89 89 end
90 90
91 91 # Generates a link to an attachment.
92 92 # Options:
93 93 # * :text - Link text (default to attachment filename)
94 94 # * :download - Force download (default: false)
95 95 def link_to_attachment(attachment, options={})
96 96 text = options.delete(:text) || attachment.filename
97 97 action = options.delete(:download) ? 'download' : 'show'
98 98
99 99 link_to(h(text), {:controller => 'attachments', :action => action, :id => attachment, :filename => attachment.filename }, options)
100 100 end
101 101
102 102 # Generates a link to a SCM revision
103 103 # Options:
104 104 # * :text - Link text (default to the formatted revision)
105 105 def link_to_revision(revision, project, options={})
106 106 text = options.delete(:text) || format_revision(revision)
107 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
107 108
108 link_to(text, {:controller => 'repositories', :action => 'revision', :id => project, :rev => revision}, :title => l(:label_revision_id, revision))
109 link_to(text, {:controller => 'repositories', :action => 'revision', :id => project, :rev => rev},
110 :title => l(:label_revision_id, format_revision(revision)))
109 111 end
110 112
111 113 # Generates a link to a project if active
112 114 # Examples:
113 115 #
114 116 # link_to_project(project) # => link to the specified project overview
115 117 # link_to_project(project, :action=>'settings') # => link to project settings
116 118 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
117 119 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
118 120 #
119 121 def link_to_project(project, options={}, html_options = nil)
120 122 if project.active?
121 123 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
122 124 link_to(h(project), url, html_options)
123 125 else
124 126 h(project)
125 127 end
126 128 end
127 129
128 130 def toggle_link(name, id, options={})
129 131 onclick = "Element.toggle('#{id}'); "
130 132 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
131 133 onclick << "return false;"
132 134 link_to(name, "#", :onclick => onclick)
133 135 end
134 136
135 137 def image_to_function(name, function, html_options = {})
136 138 html_options.symbolize_keys!
137 139 tag(:input, html_options.merge({
138 140 :type => "image", :src => image_path(name),
139 141 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
140 142 }))
141 143 end
142 144
143 145 def prompt_to_remote(name, text, param, url, html_options = {})
144 146 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
145 147 link_to name, {}, html_options
146 148 end
147 149
148 150 def format_activity_title(text)
149 151 h(truncate_single_line(text, :length => 100))
150 152 end
151 153
152 154 def format_activity_day(date)
153 155 date == Date.today ? l(:label_today).titleize : format_date(date)
154 156 end
155 157
156 158 def format_activity_description(text)
157 159 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')).gsub(/[\r\n]+/, "<br />")
158 160 end
159 161
160 162 def format_version_name(version)
161 163 if version.project == @project
162 164 h(version)
163 165 else
164 166 h("#{version.project} - #{version}")
165 167 end
166 168 end
167 169
168 170 def due_date_distance_in_words(date)
169 171 if date
170 172 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
171 173 end
172 174 end
173 175
174 176 def render_page_hierarchy(pages, node=nil)
175 177 content = ''
176 178 if pages[node]
177 179 content << "<ul class=\"pages-hierarchy\">\n"
178 180 pages[node].each do |page|
179 181 content << "<li>"
180 182 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title},
181 183 :title => (page.respond_to?(:updated_on) ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
182 184 content << "\n" + render_page_hierarchy(pages, page.id) if pages[page.id]
183 185 content << "</li>\n"
184 186 end
185 187 content << "</ul>\n"
186 188 end
187 189 content
188 190 end
189 191
190 192 # Renders flash messages
191 193 def render_flash_messages
192 194 s = ''
193 195 flash.each do |k,v|
194 196 s << content_tag('div', v, :class => "flash #{k}")
195 197 end
196 198 s
197 199 end
198 200
199 201 # Renders tabs and their content
200 202 def render_tabs(tabs)
201 203 if tabs.any?
202 204 render :partial => 'common/tabs', :locals => {:tabs => tabs}
203 205 else
204 206 content_tag 'p', l(:label_no_data), :class => "nodata"
205 207 end
206 208 end
207 209
208 210 # Renders the project quick-jump box
209 211 def render_project_jump_box
210 212 # Retrieve them now to avoid a COUNT query
211 213 projects = User.current.projects.all
212 214 if projects.any?
213 215 s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
214 216 "<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
215 217 '<option value="" disabled="disabled">---</option>'
216 218 s << project_tree_options_for_select(projects, :selected => @project) do |p|
217 219 { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
218 220 end
219 221 s << '</select>'
220 222 s
221 223 end
222 224 end
223 225
224 226 def project_tree_options_for_select(projects, options = {})
225 227 s = ''
226 228 project_tree(projects) do |project, level|
227 229 name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ') : '')
228 230 tag_options = {:value => project.id}
229 231 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
230 232 tag_options[:selected] = 'selected'
231 233 else
232 234 tag_options[:selected] = nil
233 235 end
234 236 tag_options.merge!(yield(project)) if block_given?
235 237 s << content_tag('option', name_prefix + h(project), tag_options)
236 238 end
237 239 s
238 240 end
239 241
240 242 # Yields the given block for each project with its level in the tree
241 243 #
242 244 # Wrapper for Project#project_tree
243 245 def project_tree(projects, &block)
244 246 Project.project_tree(projects, &block)
245 247 end
246 248
247 249 def project_nested_ul(projects, &block)
248 250 s = ''
249 251 if projects.any?
250 252 ancestors = []
251 253 projects.sort_by(&:lft).each do |project|
252 254 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
253 255 s << "<ul>\n"
254 256 else
255 257 ancestors.pop
256 258 s << "</li>"
257 259 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
258 260 ancestors.pop
259 261 s << "</ul></li>\n"
260 262 end
261 263 end
262 264 s << "<li>"
263 265 s << yield(project).to_s
264 266 ancestors << project
265 267 end
266 268 s << ("</li></ul>\n" * ancestors.size)
267 269 end
268 270 s
269 271 end
270 272
271 273 def principals_check_box_tags(name, principals)
272 274 s = ''
273 275 principals.sort.each do |principal|
274 276 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
275 277 end
276 278 s
277 279 end
278 280
279 281 # Truncates and returns the string as a single line
280 282 def truncate_single_line(string, *args)
281 283 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
282 284 end
283 285
284 286 # Truncates at line break after 250 characters or options[:length]
285 287 def truncate_lines(string, options={})
286 288 length = options[:length] || 250
287 289 if string.to_s =~ /\A(.{#{length}}.*?)$/m
288 290 "#{$1}..."
289 291 else
290 292 string
291 293 end
292 294 end
293 295
294 296 def html_hours(text)
295 297 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>')
296 298 end
297 299
298 300 def authoring(created, author, options={})
299 301 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created))
300 302 end
301 303
302 304 def time_tag(time)
303 305 text = distance_of_time_in_words(Time.now, time)
304 306 if @project
305 307 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => time.to_date}, :title => format_time(time))
306 308 else
307 309 content_tag('acronym', text, :title => format_time(time))
308 310 end
309 311 end
310 312
311 313 def syntax_highlight(name, content)
312 314 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
313 315 end
314 316
315 317 def to_path_param(path)
316 318 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
317 319 end
318 320
319 321 def pagination_links_full(paginator, count=nil, options={})
320 322 page_param = options.delete(:page_param) || :page
321 323 per_page_links = options.delete(:per_page_links)
322 324 url_param = params.dup
323 325 # don't reuse query params if filters are present
324 326 url_param.merge!(:fields => nil, :values => nil, :operators => nil) if url_param.delete(:set_filter)
325 327
326 328 html = ''
327 329 if paginator.current.previous
328 330 html << link_to_remote_content_update('&#171; ' + l(:label_previous), url_param.merge(page_param => paginator.current.previous)) + ' '
329 331 end
330 332
331 333 html << (pagination_links_each(paginator, options) do |n|
332 334 link_to_remote_content_update(n.to_s, url_param.merge(page_param => n))
333 335 end || '')
334 336
335 337 if paginator.current.next
336 338 html << ' ' + link_to_remote_content_update((l(:label_next) + ' &#187;'), url_param.merge(page_param => paginator.current.next))
337 339 end
338 340
339 341 unless count.nil?
340 342 html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
341 343 if per_page_links != false && links = per_page_links(paginator.items_per_page)
342 344 html << " | #{links}"
343 345 end
344 346 end
345 347
346 348 html
347 349 end
348 350
349 351 def per_page_links(selected=nil)
350 352 url_param = params.dup
351 353 url_param.clear if url_param.has_key?(:set_filter)
352 354
353 355 links = Setting.per_page_options_array.collect do |n|
354 356 n == selected ? n : link_to_remote(n, {:update => "content",
355 357 :url => params.dup.merge(:per_page => n),
356 358 :method => :get},
357 359 {:href => url_for(url_param.merge(:per_page => n))})
358 360 end
359 361 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
360 362 end
361 363
362 364 def reorder_links(name, url)
363 365 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)), url.merge({"#{name}[move_to]" => 'highest'}), :method => :post, :title => l(:label_sort_highest)) +
364 366 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)), url.merge({"#{name}[move_to]" => 'higher'}), :method => :post, :title => l(:label_sort_higher)) +
365 367 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)), url.merge({"#{name}[move_to]" => 'lower'}), :method => :post, :title => l(:label_sort_lower)) +
366 368 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), url.merge({"#{name}[move_to]" => 'lowest'}), :method => :post, :title => l(:label_sort_lowest))
367 369 end
368 370
369 371 def breadcrumb(*args)
370 372 elements = args.flatten
371 373 elements.any? ? content_tag('p', args.join(' &#187; ') + ' &#187; ', :class => 'breadcrumb') : nil
372 374 end
373 375
374 376 def other_formats_links(&block)
375 377 concat('<p class="other-formats">' + l(:label_export_to))
376 378 yield Redmine::Views::OtherFormatsBuilder.new(self)
377 379 concat('</p>')
378 380 end
379 381
380 382 def page_header_title
381 383 if @project.nil? || @project.new_record?
382 384 h(Setting.app_title)
383 385 else
384 386 b = []
385 387 ancestors = (@project.root? ? [] : @project.ancestors.visible)
386 388 if ancestors.any?
387 389 root = ancestors.shift
388 390 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
389 391 if ancestors.size > 2
390 392 b << '&#8230;'
391 393 ancestors = ancestors[-2, 2]
392 394 end
393 395 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
394 396 end
395 397 b << h(@project)
396 398 b.join(' &#187; ')
397 399 end
398 400 end
399 401
400 402 def html_title(*args)
401 403 if args.empty?
402 404 title = []
403 405 title << @project.name if @project
404 406 title += @html_title if @html_title
405 407 title << Setting.app_title
406 408 title.select {|t| !t.blank? }.join(' - ')
407 409 else
408 410 @html_title ||= []
409 411 @html_title += args
410 412 end
411 413 end
412 414
413 415 # Returns the theme, controller name, and action as css classes for the
414 416 # HTML body.
415 417 def body_css_classes
416 418 css = []
417 419 if theme = Redmine::Themes.theme(Setting.ui_theme)
418 420 css << 'theme-' + theme.name
419 421 end
420 422
421 423 css << 'controller-' + params[:controller]
422 424 css << 'action-' + params[:action]
423 425 css.join(' ')
424 426 end
425 427
426 428 def accesskey(s)
427 429 Redmine::AccessKeys.key_for s
428 430 end
429 431
430 432 # Formats text according to system settings.
431 433 # 2 ways to call this method:
432 434 # * with a String: textilizable(text, options)
433 435 # * with an object and one of its attribute: textilizable(issue, :description, options)
434 436 def textilizable(*args)
435 437 options = args.last.is_a?(Hash) ? args.pop : {}
436 438 case args.size
437 439 when 1
438 440 obj = options[:object]
439 441 text = args.shift
440 442 when 2
441 443 obj = args.shift
442 444 attr = args.shift
443 445 text = obj.send(attr).to_s
444 446 else
445 447 raise ArgumentError, 'invalid arguments to textilizable'
446 448 end
447 449 return '' if text.blank?
448 450 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
449 451 only_path = options.delete(:only_path) == false ? false : true
450 452
451 453 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr) { |macro, args| exec_macro(macro, obj, args) }
452 454
453 455 @parsed_headings = []
454 456 text = parse_non_pre_blocks(text) do |text|
455 457 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_headings].each do |method_name|
456 458 send method_name, text, project, obj, attr, only_path, options
457 459 end
458 460 end
459 461
460 462 if @parsed_headings.any?
461 463 replace_toc(text, @parsed_headings)
462 464 end
463 465
464 466 text
465 467 end
466 468
467 469 def parse_non_pre_blocks(text)
468 470 s = StringScanner.new(text)
469 471 tags = []
470 472 parsed = ''
471 473 while !s.eos?
472 474 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
473 475 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
474 476 if tags.empty?
475 477 yield text
476 478 end
477 479 parsed << text
478 480 if tag
479 481 if closing
480 482 if tags.last == tag.downcase
481 483 tags.pop
482 484 end
483 485 else
484 486 tags << tag.downcase
485 487 end
486 488 parsed << full_tag
487 489 end
488 490 end
489 491 # Close any non closing tags
490 492 while tag = tags.pop
491 493 parsed << "</#{tag}>"
492 494 end
493 495 parsed
494 496 end
495 497
496 498 def parse_inline_attachments(text, project, obj, attr, only_path, options)
497 499 # when using an image link, try to use an attachment, if possible
498 500 if options[:attachments] || (obj && obj.respond_to?(:attachments))
499 501 attachments = nil
500 502 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
501 503 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
502 504 attachments ||= (options[:attachments] || obj.attachments).sort_by(&:created_on).reverse
503 505 # search for the picture in attachments
504 506 if found = attachments.detect { |att| att.filename.downcase == filename }
505 507 image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
506 508 desc = found.description.to_s.gsub('"', '')
507 509 if !desc.blank? && alttext.blank?
508 510 alt = " title=\"#{desc}\" alt=\"#{desc}\""
509 511 end
510 512 "src=\"#{image_url}\"#{alt}"
511 513 else
512 514 m
513 515 end
514 516 end
515 517 end
516 518 end
517 519
518 520 # Wiki links
519 521 #
520 522 # Examples:
521 523 # [[mypage]]
522 524 # [[mypage|mytext]]
523 525 # wiki links can refer other project wikis, using project name or identifier:
524 526 # [[project:]] -> wiki starting page
525 527 # [[project:|mytext]]
526 528 # [[project:mypage]]
527 529 # [[project:mypage|mytext]]
528 530 def parse_wiki_links(text, project, obj, attr, only_path, options)
529 531 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
530 532 link_project = project
531 533 esc, all, page, title = $1, $2, $3, $5
532 534 if esc.nil?
533 535 if page =~ /^([^\:]+)\:(.*)$/
534 536 link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
535 537 page = $2
536 538 title ||= $1 if page.blank?
537 539 end
538 540
539 541 if link_project && link_project.wiki
540 542 # extract anchor
541 543 anchor = nil
542 544 if page =~ /^(.+?)\#(.+)$/
543 545 page, anchor = $1, $2
544 546 end
545 547 # check if page exists
546 548 wiki_page = link_project.wiki.find_page(page)
547 549 url = case options[:wiki_links]
548 550 when :local; "#{title}.html"
549 551 when :anchor; "##{title}" # used for single-file wiki export
550 552 else
551 553 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
552 554 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project, :id => wiki_page_id, :anchor => anchor)
553 555 end
554 556 link_to((title || page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
555 557 else
556 558 # project or wiki doesn't exist
557 559 all
558 560 end
559 561 else
560 562 all
561 563 end
562 564 end
563 565 end
564 566
565 567 # Redmine links
566 568 #
567 569 # Examples:
568 570 # Issues:
569 571 # #52 -> Link to issue #52
570 572 # Changesets:
571 573 # r52 -> Link to revision 52
572 574 # commit:a85130f -> Link to scmid starting with a85130f
573 575 # Documents:
574 576 # document#17 -> Link to document with id 17
575 577 # document:Greetings -> Link to the document with title "Greetings"
576 578 # document:"Some document" -> Link to the document with title "Some document"
577 579 # Versions:
578 580 # version#3 -> Link to version with id 3
579 581 # version:1.0.0 -> Link to version named "1.0.0"
580 582 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
581 583 # Attachments:
582 584 # attachment:file.zip -> Link to the attachment of the current object named file.zip
583 585 # Source files:
584 586 # source:some/file -> Link to the file located at /some/file in the project's repository
585 587 # source:some/file@52 -> Link to the file's revision 52
586 588 # source:some/file#L120 -> Link to line 120 of the file
587 589 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
588 590 # export:some/file -> Force the download of the file
589 591 # Forum messages:
590 592 # message#1218 -> Link to message with id 1218
591 593 def parse_redmine_links(text, project, obj, attr, only_path, options)
592 594 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(attachment|document|version|commit|source|export|message|project)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|\]|<|$)}) do |m|
593 595 leading, esc, prefix, sep, identifier = $1, $2, $3, $5 || $7, $6 || $8
594 596 link = nil
595 597 if esc.nil?
596 598 if prefix.nil? && sep == 'r'
597 599 if project && (changeset = project.changesets.find_by_revision(identifier))
598 600 link = link_to("r#{identifier}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
599 601 :class => 'changeset',
600 602 :title => truncate_single_line(changeset.comments, :length => 100))
601 603 end
602 604 elsif sep == '#'
603 605 oid = identifier.to_i
604 606 case prefix
605 607 when nil
606 608 if issue = Issue.visible.find_by_id(oid, :include => :status)
607 609 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
608 610 :class => issue.css_classes,
609 611 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
610 612 end
611 613 when 'document'
612 614 if document = Document.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
613 615 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
614 616 :class => 'document'
615 617 end
616 618 when 'version'
617 619 if version = Version.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
618 620 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
619 621 :class => 'version'
620 622 end
621 623 when 'message'
622 624 if message = Message.find_by_id(oid, :include => [:parent, {:board => :project}], :conditions => Project.visible_by(User.current))
623 625 link = link_to h(truncate(message.subject, :length => 60)), {:only_path => only_path,
624 626 :controller => 'messages',
625 627 :action => 'show',
626 628 :board_id => message.board,
627 629 :id => message.root,
628 630 :anchor => (message.parent ? "message-#{message.id}" : nil)},
629 631 :class => 'message'
630 632 end
631 633 when 'project'
632 634 if p = Project.visible.find_by_id(oid)
633 635 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
634 636 end
635 637 end
636 638 elsif sep == ':'
637 639 # removes the double quotes if any
638 640 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
639 641 case prefix
640 642 when 'document'
641 643 if project && document = project.documents.find_by_title(name)
642 644 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
643 645 :class => 'document'
644 646 end
645 647 when 'version'
646 648 if project && version = project.versions.find_by_name(name)
647 649 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
648 650 :class => 'version'
649 651 end
650 652 when 'commit'
651 653 if project && (changeset = project.changesets.find(:first, :conditions => ["scmid LIKE ?", "#{name}%"]))
652 654 link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
653 655 :class => 'changeset',
654 656 :title => truncate_single_line(changeset.comments, :length => 100)
655 657 end
656 658 when 'source', 'export'
657 659 if project && project.repository
658 660 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
659 661 path, rev, anchor = $1, $3, $5
660 662 link = link_to h("#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
661 663 :path => to_path_param(path),
662 664 :rev => rev,
663 665 :anchor => anchor,
664 666 :format => (prefix == 'export' ? 'raw' : nil)},
665 667 :class => (prefix == 'export' ? 'source download' : 'source')
666 668 end
667 669 when 'attachment'
668 670 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
669 671 if attachments && attachment = attachments.detect {|a| a.filename == name }
670 672 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
671 673 :class => 'attachment'
672 674 end
673 675 when 'project'
674 676 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
675 677 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
676 678 end
677 679 end
678 680 end
679 681 end
680 682 leading + (link || "#{prefix}#{sep}#{identifier}")
681 683 end
682 684 end
683 685
684 686 HEADING_RE = /<h(1|2|3|4)( [^>]+)?>(.+?)<\/h(1|2|3|4)>/i unless const_defined?(:HEADING_RE)
685 687
686 688 # Headings and TOC
687 689 # Adds ids and links to headings unless options[:headings] is set to false
688 690 def parse_headings(text, project, obj, attr, only_path, options)
689 691 return if options[:headings] == false
690 692
691 693 text.gsub!(HEADING_RE) do
692 694 level, attrs, content = $1.to_i, $2, $3
693 695 item = strip_tags(content).strip
694 696 anchor = item.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
695 697 @parsed_headings << [level, anchor, item]
696 698 "<h#{level} #{attrs} id=\"#{anchor}\">#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
697 699 end
698 700 end
699 701
700 702 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
701 703
702 704 # Renders the TOC with given headings
703 705 def replace_toc(text, headings)
704 706 text.gsub!(TOC_RE) do
705 707 if headings.empty?
706 708 ''
707 709 else
708 710 div_class = 'toc'
709 711 div_class << ' right' if $1 == '>'
710 712 div_class << ' left' if $1 == '<'
711 713 out = "<ul class=\"#{div_class}\"><li>"
712 714 root = headings.map(&:first).min
713 715 current = root
714 716 started = false
715 717 headings.each do |level, anchor, item|
716 718 if level > current
717 719 out << '<ul><li>' * (level - current)
718 720 elsif level < current
719 721 out << "</li></ul>\n" * (current - level) + "</li><li>"
720 722 elsif started
721 723 out << '</li><li>'
722 724 end
723 725 out << "<a href=\"##{anchor}\">#{item}</a>"
724 726 current = level
725 727 started = true
726 728 end
727 729 out << '</li></ul>' * (current - root)
728 730 out << '</li></ul>'
729 731 end
730 732 end
731 733 end
732 734
733 735 # Same as Rails' simple_format helper without using paragraphs
734 736 def simple_format_without_paragraph(text)
735 737 text.to_s.
736 738 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
737 739 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
738 740 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
739 741 end
740 742
741 743 def lang_options_for_select(blank=true)
742 744 (blank ? [["(auto)", ""]] : []) +
743 745 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
744 746 end
745 747
746 748 def label_tag_for(name, option_tags = nil, options = {})
747 749 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
748 750 content_tag("label", label_text)
749 751 end
750 752
751 753 def labelled_tabular_form_for(name, object, options, &proc)
752 754 options[:html] ||= {}
753 755 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
754 756 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
755 757 end
756 758
757 759 def back_url_hidden_field_tag
758 760 back_url = params[:back_url] || request.env['HTTP_REFERER']
759 761 back_url = CGI.unescape(back_url.to_s)
760 762 hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
761 763 end
762 764
763 765 def check_all_links(form_name)
764 766 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
765 767 " | " +
766 768 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
767 769 end
768 770
769 771 def progress_bar(pcts, options={})
770 772 pcts = [pcts, pcts] unless pcts.is_a?(Array)
771 773 pcts = pcts.collect(&:round)
772 774 pcts[1] = pcts[1] - pcts[0]
773 775 pcts << (100 - pcts[1] - pcts[0])
774 776 width = options[:width] || '100px;'
775 777 legend = options[:legend] || ''
776 778 content_tag('table',
777 779 content_tag('tr',
778 780 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : '') +
779 781 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : '') +
780 782 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : '')
781 783 ), :class => 'progress', :style => "width: #{width};") +
782 784 content_tag('p', legend, :class => 'pourcent')
783 785 end
784 786
785 787 def checked_image(checked=true)
786 788 if checked
787 789 image_tag 'toggle_check.png'
788 790 end
789 791 end
790 792
791 793 def context_menu(url)
792 794 unless @context_menu_included
793 795 content_for :header_tags do
794 796 javascript_include_tag('context_menu') +
795 797 stylesheet_link_tag('context_menu')
796 798 end
797 799 if l(:direction) == 'rtl'
798 800 content_for :header_tags do
799 801 stylesheet_link_tag('context_menu_rtl')
800 802 end
801 803 end
802 804 @context_menu_included = true
803 805 end
804 806 javascript_tag "new ContextMenu('#{ url_for(url) }')"
805 807 end
806 808
807 809 def context_menu_link(name, url, options={})
808 810 options[:class] ||= ''
809 811 if options.delete(:selected)
810 812 options[:class] << ' icon-checked disabled'
811 813 options[:disabled] = true
812 814 end
813 815 if options.delete(:disabled)
814 816 options.delete(:method)
815 817 options.delete(:confirm)
816 818 options.delete(:onclick)
817 819 options[:class] << ' disabled'
818 820 url = '#'
819 821 end
820 822 link_to name, url, options
821 823 end
822 824
823 825 def calendar_for(field_id)
824 826 include_calendar_headers_tags
825 827 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
826 828 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
827 829 end
828 830
829 831 def include_calendar_headers_tags
830 832 unless @calendar_headers_tags_included
831 833 @calendar_headers_tags_included = true
832 834 content_for :header_tags do
833 835 start_of_week = case Setting.start_of_week.to_i
834 836 when 1
835 837 'Calendar._FD = 1;' # Monday
836 838 when 7
837 839 'Calendar._FD = 0;' # Sunday
838 840 else
839 841 '' # use language
840 842 end
841 843
842 844 javascript_include_tag('calendar/calendar') +
843 845 javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
844 846 javascript_tag(start_of_week) +
845 847 javascript_include_tag('calendar/calendar-setup') +
846 848 stylesheet_link_tag('calendar')
847 849 end
848 850 end
849 851 end
850 852
851 853 def content_for(name, content = nil, &block)
852 854 @has_content ||= {}
853 855 @has_content[name] = true
854 856 super(name, content, &block)
855 857 end
856 858
857 859 def has_content?(name)
858 860 (@has_content && @has_content[name]) || false
859 861 end
860 862
861 863 # Returns the avatar image tag for the given +user+ if avatars are enabled
862 864 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
863 865 def avatar(user, options = { })
864 866 if Setting.gravatar_enabled?
865 867 options.merge!({:ssl => (defined?(request) && request.ssl?), :default => Setting.gravatar_default})
866 868 email = nil
867 869 if user.respond_to?(:mail)
868 870 email = user.mail
869 871 elsif user.to_s =~ %r{<(.+?)>}
870 872 email = $1
871 873 end
872 874 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
873 875 else
874 876 ''
875 877 end
876 878 end
877 879
878 880 def favicon
879 881 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />"
880 882 end
881 883
882 884 # Returns true if arg is expected in the API response
883 885 def include_in_api_response?(arg)
884 886 unless @included_in_api_response
885 887 param = params[:include]
886 888 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
887 889 @included_in_api_response.collect!(&:strip)
888 890 end
889 891 @included_in_api_response.include?(arg.to_s)
890 892 end
891 893
892 894 # Returns options or nil if nometa param or X-Redmine-Nometa header
893 895 # was set in the request
894 896 def api_meta(options)
895 897 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
896 898 # compatibility mode for activeresource clients that raise
897 899 # an error when unserializing an array with attributes
898 900 nil
899 901 else
900 902 options
901 903 end
902 904 end
903 905
904 906 private
905 907
906 908 def wiki_helper
907 909 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
908 910 extend helper
909 911 return self
910 912 end
911 913
912 914 def link_to_remote_content_update(text, url_params)
913 915 link_to_remote(text,
914 916 {:url => url_params, :method => :get, :update => 'content', :complete => 'window.scrollTo(0,0)'},
915 917 {:href => url_for(:params => url_params)}
916 918 )
917 919 end
918 920
919 921 end
@@ -1,194 +1,198
1 1 # redMine - project management software
2 2 # Copyright (C) 2006 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require 'iconv'
19 19
20 20 module RepositoriesHelper
21 def format_revision(txt)
22 txt.to_s[0,8]
21 def format_revision(revision)
22 if revision.respond_to? :format_identifier
23 revision.format_identifier
24 else
25 revision.to_s
26 end
23 27 end
24 28
25 29 def truncate_at_line_break(text, length = 255)
26 30 if text
27 31 text.gsub(%r{^(.{#{length}}[^\n]*)\n.+$}m, '\\1...')
28 32 end
29 33 end
30 34
31 35 def render_properties(properties)
32 36 unless properties.nil? || properties.empty?
33 37 content = ''
34 38 properties.keys.sort.each do |property|
35 39 content << content_tag('li', "<b>#{h property}</b>: <span>#{h properties[property]}</span>")
36 40 end
37 41 content_tag('ul', content, :class => 'properties')
38 42 end
39 43 end
40 44
41 45 def render_changeset_changes
42 46 changes = @changeset.changes.find(:all, :limit => 1000, :order => 'path').collect do |change|
43 47 case change.action
44 48 when 'A'
45 49 # Detects moved/copied files
46 50 if !change.from_path.blank?
47 51 change.action = @changeset.changes.detect {|c| c.action == 'D' && c.path == change.from_path} ? 'R' : 'C'
48 52 end
49 53 change
50 54 when 'D'
51 55 @changeset.changes.detect {|c| c.from_path == change.path} ? nil : change
52 56 else
53 57 change
54 58 end
55 59 end.compact
56 60
57 61 tree = { }
58 62 changes.each do |change|
59 63 p = tree
60 64 dirs = change.path.to_s.split('/').select {|d| !d.blank?}
61 65 path = ''
62 66 dirs.each do |dir|
63 67 path += '/' + dir
64 68 p[:s] ||= {}
65 69 p = p[:s]
66 70 p[path] ||= {}
67 71 p = p[path]
68 72 end
69 73 p[:c] = change
70 74 end
71 75
72 76 render_changes_tree(tree[:s])
73 77 end
74 78
75 79 def render_changes_tree(tree)
76 80 return '' if tree.nil?
77 81
78 82 output = ''
79 83 output << '<ul>'
80 84 tree.keys.sort.each do |file|
81 85 style = 'change'
82 86 text = File.basename(h(file))
83 87 if s = tree[file][:s]
84 88 style << ' folder'
85 89 path_param = to_path_param(@repository.relative_path(file))
86 90 text = link_to(text, :controller => 'repositories',
87 91 :action => 'show',
88 92 :id => @project,
89 93 :path => path_param,
90 :rev => @changeset.revision)
94 :rev => @changeset.identifier)
91 95 output << "<li class='#{style}'>#{text}</li>"
92 96 output << render_changes_tree(s)
93 97 elsif c = tree[file][:c]
94 98 style << " change-#{c.action}"
95 99 path_param = to_path_param(@repository.relative_path(c.path))
96 100 text = link_to(text, :controller => 'repositories',
97 101 :action => 'entry',
98 102 :id => @project,
99 103 :path => path_param,
100 :rev => @changeset.revision) unless c.action == 'D'
104 :rev => @changeset.identifier) unless c.action == 'D'
101 105 text << " - #{c.revision}" unless c.revision.blank?
102 106 text << ' (' + link_to('diff', :controller => 'repositories',
103 107 :action => 'diff',
104 108 :id => @project,
105 109 :path => path_param,
106 :rev => @changeset.revision) + ') ' if c.action == 'M'
110 :rev => @changeset.identifier) + ') ' if c.action == 'M'
107 111 text << ' ' + content_tag('span', c.from_path, :class => 'copied-from') unless c.from_path.blank?
108 112 output << "<li class='#{style}'>#{text}</li>"
109 113 end
110 114 end
111 115 output << '</ul>'
112 116 output
113 117 end
114 118
115 119 def to_utf8(str)
116 120 if str.respond_to?(:force_encoding)
117 121 str.force_encoding('UTF-8')
118 122 return str if str.valid_encoding?
119 123 else
120 124 return str if /\A[\r\n\t\x20-\x7e]*\Z/n.match(str) # for us-ascii
121 125 end
122 126
123 127 @encodings ||= Setting.repositories_encodings.split(',').collect(&:strip)
124 128 @encodings.each do |encoding|
125 129 begin
126 130 return Iconv.conv('UTF-8', encoding, str)
127 131 rescue Iconv::Failure
128 132 # do nothing here and try the next encoding
129 133 end
130 134 end
131 135 str
132 136 end
133 137
134 138 def repository_field_tags(form, repository)
135 139 method = repository.class.name.demodulize.underscore + "_field_tags"
136 140 send(method, form, repository) if repository.is_a?(Repository) && respond_to?(method) && method != 'repository_field_tags'
137 141 end
138 142
139 143 def scm_select_tag(repository)
140 144 scm_options = [["--- #{l(:actionview_instancetag_blank_option)} ---", '']]
141 145 Redmine::Scm::Base.all.each do |scm|
142 146 scm_options << ["Repository::#{scm}".constantize.scm_name, scm] if Setting.enabled_scm.include?(scm) || (repository && repository.class.name.demodulize == scm)
143 147 end
144 148
145 149 select_tag('repository_scm',
146 150 options_for_select(scm_options, repository.class.name.demodulize),
147 151 :disabled => (repository && !repository.new_record?),
148 152 :onchange => remote_function(:url => { :controller => 'repositories', :action => 'edit', :id => @project }, :method => :get, :with => "Form.serialize(this.form)")
149 153 )
150 154 end
151 155
152 156 def with_leading_slash(path)
153 157 path.to_s.starts_with?('/') ? path : "/#{path}"
154 158 end
155 159
156 160 def without_leading_slash(path)
157 161 path.gsub(%r{^/+}, '')
158 162 end
159 163
160 164 def subversion_field_tags(form, repository)
161 165 content_tag('p', form.text_field(:url, :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)) +
162 166 '<br />(file:///, http://, https://, svn://, svn+[tunnelscheme]://)') +
163 167 content_tag('p', form.text_field(:login, :size => 30)) +
164 168 content_tag('p', form.password_field(:password, :size => 30, :name => 'ignore',
165 169 :value => ((repository.new_record? || repository.password.blank?) ? '' : ('x'*15)),
166 170 :onfocus => "this.value=''; this.name='repository[password]';",
167 171 :onchange => "this.name='repository[password]';"))
168 172 end
169 173
170 174 def darcs_field_tags(form, repository)
171 175 content_tag('p', form.text_field(:url, :label => 'Root directory', :size => 60, :required => true, :disabled => (repository && !repository.new_record?)))
172 176 end
173 177
174 178 def mercurial_field_tags(form, repository)
175 179 content_tag('p', form.text_field(:url, :label => 'Root directory', :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)))
176 180 end
177 181
178 182 def git_field_tags(form, repository)
179 183 content_tag('p', form.text_field(:url, :label => 'Path to .git directory', :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)))
180 184 end
181 185
182 186 def cvs_field_tags(form, repository)
183 187 content_tag('p', form.text_field(:root_url, :label => 'CVSROOT', :size => 60, :required => true, :disabled => !repository.new_record?)) +
184 188 content_tag('p', form.text_field(:url, :label => 'Module', :size => 30, :required => true, :disabled => !repository.new_record?))
185 189 end
186 190
187 191 def bazaar_field_tags(form, repository)
188 192 content_tag('p', form.text_field(:url, :label => 'Root directory', :size => 60, :required => true, :disabled => (repository && !repository.new_record?)))
189 193 end
190 194
191 195 def filesystem_field_tags(form, repository)
192 196 content_tag('p', form.text_field(:url, :label => 'Root directory', :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)))
193 197 end
194 198 end
@@ -1,253 +1,271
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2010 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require 'iconv'
19 19
20 20 class Changeset < ActiveRecord::Base
21 21 belongs_to :repository
22 22 belongs_to :user
23 23 has_many :changes, :dependent => :delete_all
24 24 has_and_belongs_to_many :issues
25 25
26 acts_as_event :title => Proc.new {|o| "#{l(:label_revision)} #{o.revision}" + (o.short_comments.blank? ? '' : (': ' + o.short_comments))},
26 acts_as_event :title => Proc.new {|o| "#{l(:label_revision)} #{o.format_identifier}" + (o.short_comments.blank? ? '' : (': ' + o.short_comments))},
27 27 :description => :long_comments,
28 28 :datetime => :committed_on,
29 :url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project, :rev => o.revision}}
29 :url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project, :rev => o.identifier}}
30 30
31 31 acts_as_searchable :columns => 'comments',
32 32 :include => {:repository => :project},
33 33 :project_key => "#{Repository.table_name}.project_id",
34 34 :date_column => 'committed_on'
35 35
36 36 acts_as_activity_provider :timestamp => "#{table_name}.committed_on",
37 37 :author_key => :user_id,
38 38 :find_options => {:include => [:user, {:repository => :project}]}
39 39
40 40 validates_presence_of :repository_id, :revision, :committed_on, :commit_date
41 41 validates_uniqueness_of :revision, :scope => :repository_id
42 42 validates_uniqueness_of :scmid, :scope => :repository_id, :allow_nil => true
43 43
44 44 named_scope :visible, lambda {|*args| { :include => {:repository => :project},
45 45 :conditions => Project.allowed_to_condition(args.first || User.current, :view_changesets) } }
46 46
47 47 def revision=(r)
48 48 write_attribute :revision, (r.nil? ? nil : r.to_s)
49 49 end
50
51 # Returns the identifier of this changeset; depending on repository backends
52 def identifier
53 if repository.class.respond_to? :changeset_identifier
54 repository.class.changeset_identifier self
55 else
56 revision.to_s
57 end
58 end
50 59
51 60 def comments=(comment)
52 61 write_attribute(:comments, Changeset.normalize_comments(comment))
53 62 end
54 63
55 64 def committed_on=(date)
56 65 self.commit_date = date
57 66 super
58 67 end
68
69 # Returns the readable identifier
70 def format_identifier
71 if repository.class.respond_to? :format_changeset_identifier
72 repository.class.format_changeset_identifier self
73 else
74 identifier
75 end
76 end
59 77
60 78 def committer=(arg)
61 79 write_attribute(:committer, self.class.to_utf8(arg.to_s))
62 80 end
63 81
64 82 def project
65 83 repository.project
66 84 end
67 85
68 86 def author
69 87 user || committer.to_s.split('<').first
70 88 end
71 89
72 90 def before_create
73 91 self.user = repository.find_committer_user(committer)
74 92 end
75 93
76 94 def after_create
77 95 scan_comment_for_issue_ids
78 96 end
79 97
80 98 TIMELOG_RE = /
81 99 (
82 100 (\d+([.,]\d+)?)h?
83 101 |
84 102 (\d+):(\d+)
85 103 |
86 104 ((\d+)(h|hours?))?((\d+)(m|min)?)?
87 105 )
88 106 /x
89 107
90 108 def scan_comment_for_issue_ids
91 109 return if comments.blank?
92 110 # keywords used to reference issues
93 111 ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
94 112 ref_keywords_any = ref_keywords.delete('*')
95 113 # keywords used to fix issues
96 114 fix_keywords = Setting.commit_fix_keywords.downcase.split(",").collect(&:strip)
97 115
98 116 kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|")
99 117
100 118 referenced_issues = []
101 119
102 120 comments.scan(/([\s\(\[,-]|^)((#{kw_regexp})[\s:]+)?(#\d+(\s+@#{TIMELOG_RE})?([\s,;&]+#\d+(\s+@#{TIMELOG_RE})?)*)(?=[[:punct:]]|\s|<|$)/i) do |match|
103 121 action, refs = match[2], match[3]
104 122 next unless action.present? || ref_keywords_any
105 123
106 124 refs.scan(/#(\d+)(\s+@#{TIMELOG_RE})?/).each do |m|
107 125 issue, hours = find_referenced_issue_by_id(m[0].to_i), m[2]
108 126 if issue
109 127 referenced_issues << issue
110 128 fix_issue(issue) if fix_keywords.include?(action.to_s.downcase)
111 129 log_time(issue, hours) if hours && Setting.commit_logtime_enabled?
112 130 end
113 131 end
114 132 end
115 133
116 134 referenced_issues.uniq!
117 135 self.issues = referenced_issues unless referenced_issues.empty?
118 136 end
119 137
120 138 def short_comments
121 139 @short_comments || split_comments.first
122 140 end
123 141
124 142 def long_comments
125 143 @long_comments || split_comments.last
126 144 end
127 145
128 146 def text_tag
129 147 if scmid?
130 148 "commit:#{scmid}"
131 149 else
132 150 "r#{revision}"
133 151 end
134 152 end
135 153
136 154 # Returns the previous changeset
137 155 def previous
138 156 @previous ||= Changeset.find(:first, :conditions => ['id < ? AND repository_id = ?', self.id, self.repository_id], :order => 'id DESC')
139 157 end
140 158
141 159 # Returns the next changeset
142 160 def next
143 161 @next ||= Changeset.find(:first, :conditions => ['id > ? AND repository_id = ?', self.id, self.repository_id], :order => 'id ASC')
144 162 end
145 163
146 164 # Strips and reencodes a commit log before insertion into the database
147 165 def self.normalize_comments(str)
148 166 to_utf8(str.to_s.strip)
149 167 end
150 168
151 169 # Creates a new Change from it's common parameters
152 170 def create_change(change)
153 171 Change.create(:changeset => self,
154 172 :action => change[:action],
155 173 :path => change[:path],
156 174 :from_path => change[:from_path],
157 175 :from_revision => change[:from_revision])
158 176 end
159 177
160 178 private
161 179
162 180 # Finds an issue that can be referenced by the commit message
163 181 # i.e. an issue that belong to the repository project, a subproject or a parent project
164 182 def find_referenced_issue_by_id(id)
165 183 return nil if id.blank?
166 184 issue = Issue.find_by_id(id.to_i, :include => :project)
167 185 if issue
168 186 unless project == issue.project || project.is_ancestor_of?(issue.project) || project.is_descendant_of?(issue.project)
169 187 issue = nil
170 188 end
171 189 end
172 190 issue
173 191 end
174 192
175 193 def fix_issue(issue)
176 194 status = IssueStatus.find_by_id(Setting.commit_fix_status_id.to_i)
177 195 if status.nil?
178 196 logger.warn("No status macthes commit_fix_status_id setting (#{Setting.commit_fix_status_id})") if logger
179 197 return issue
180 198 end
181 199
182 200 # the issue may have been updated by the closure of another one (eg. duplicate)
183 201 issue.reload
184 202 # don't change the status is the issue is closed
185 203 return if issue.status && issue.status.is_closed?
186 204
187 205 journal = issue.init_journal(user || User.anonymous, ll(Setting.default_language, :text_status_changed_by_changeset, text_tag))
188 206 issue.status = status
189 207 unless Setting.commit_fix_done_ratio.blank?
190 208 issue.done_ratio = Setting.commit_fix_done_ratio.to_i
191 209 end
192 210 Redmine::Hook.call_hook(:model_changeset_scan_commit_for_issue_ids_pre_issue_update,
193 211 { :changeset => self, :issue => issue })
194 212 unless issue.save
195 213 logger.warn("Issue ##{issue.id} could not be saved by changeset #{id}: #{issue.errors.full_messages}") if logger
196 214 end
197 215 issue
198 216 end
199 217
200 218 def log_time(issue, hours)
201 219 time_entry = TimeEntry.new(
202 220 :user => user,
203 221 :hours => hours,
204 222 :issue => issue,
205 223 :spent_on => commit_date,
206 224 :comments => l(:text_time_logged_by_changeset, :value => text_tag, :locale => Setting.default_language)
207 225 )
208 226 time_entry.activity = log_time_activity unless log_time_activity.nil?
209 227
210 228 unless time_entry.save
211 229 logger.warn("TimeEntry could not be created by changeset #{id}: #{time_entry.errors.full_messages}") if logger
212 230 end
213 231 time_entry
214 232 end
215 233
216 234 def log_time_activity
217 235 if Setting.commit_logtime_activity_id.to_i > 0
218 236 TimeEntryActivity.find_by_id(Setting.commit_logtime_activity_id.to_i)
219 237 end
220 238 end
221 239
222 240 def split_comments
223 241 comments =~ /\A(.+?)\r?\n(.*)$/m
224 242 @short_comments = $1 || comments
225 243 @long_comments = $2.to_s.strip
226 244 return @short_comments, @long_comments
227 245 end
228 246
229 247 def self.to_utf8(str)
230 248 if str.respond_to?(:force_encoding)
231 249 str.force_encoding('UTF-8')
232 250 return str if str.valid_encoding?
233 251 else
234 252 return str if /\A[\r\n\t\x20-\x7e]*\Z/n.match(str) # for us-ascii
235 253 end
236 254
237 255 encoding = Setting.commit_logs_encoding.to_s.strip
238 256 unless encoding.blank? || encoding == 'UTF-8'
239 257 begin
240 258 str = Iconv.conv('UTF-8', encoding, str)
241 259 rescue Iconv::Failure
242 260 # do nothing here
243 261 end
244 262 end
245 263 # removes invalid UTF8 sequences
246 264 begin
247 265 Iconv.conv('UTF-8//IGNORE', 'UTF-8', str + ' ')[0..-3]
248 266 rescue Iconv::InvalidEncoding
249 267 # "UTF-8//IGNORE" is not supported on some OS
250 268 str
251 269 end
252 270 end
253 271 end
@@ -1,81 +1,91
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 # Copyright (C) 2007 Patrick Aljord patcito@Ε‹mail.com
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require 'redmine/scm/adapters/git_adapter'
19 19
20 20 class Repository::Git < Repository
21 21 attr_protected :root_url
22 22 validates_presence_of :url
23 23
24 24 def scm_adapter
25 25 Redmine::Scm::Adapters::GitAdapter
26 26 end
27 27
28 28 def self.scm_name
29 29 'Git'
30 30 end
31 31
32 # Returns the identifier for the given git changeset
33 def self.changeset_identifier(changeset)
34 changeset.scmid
35 end
36
37 # Returns the readable identifier for the given git changeset
38 def self.format_changeset_identifier(changeset)
39 changeset.revision[0, 8]
40 end
41
32 42 def branches
33 43 scm.branches
34 44 end
35 45
36 46 def tags
37 47 scm.tags
38 48 end
39 49
40 50 # With SCM's that have a sequential commit numbering, redmine is able to be
41 51 # clever and only fetch changesets going forward from the most recent one
42 52 # it knows about. However, with git, you never know if people have merged
43 53 # commits into the middle of the repository history, so we should parse
44 54 # the entire log. Since it's way too slow for large repositories, we only
45 55 # parse 1 week before the last known commit.
46 56 # The repository can still be fully reloaded by calling #clear_changesets
47 57 # before fetching changesets (eg. for offline resync)
48 58 def fetch_changesets
49 59 c = changesets.find(:first, :order => 'committed_on DESC')
50 60 since = (c ? c.committed_on - 7.days : nil)
51 61
52 62 revisions = scm.revisions('', nil, nil, :all => true, :since => since)
53 63 return if revisions.nil? || revisions.empty?
54 64
55 65 recent_changesets = changesets.find(:all, :conditions => ['committed_on >= ?', since])
56 66
57 67 # Clean out revisions that are no longer in git
58 68 recent_changesets.each {|c| c.destroy unless revisions.detect {|r| r.scmid.to_s == c.scmid.to_s }}
59 69
60 70 # Subtract revisions that redmine already knows about
61 71 recent_revisions = recent_changesets.map{|c| c.scmid}
62 72 revisions.reject!{|r| recent_revisions.include?(r.scmid)}
63 73
64 74 # Save the remaining ones to the database
65 75 revisions.each{|r| r.save(self)} unless revisions.nil?
66 76 end
67 77
68 78 def latest_changesets(path,rev,limit=10)
69 79 revisions = scm.revisions(path, nil, rev, :limit => limit, :all => false)
70 80 return [] if revisions.nil? || revisions.empty?
71 81
72 82 changesets.find(
73 83 :all,
74 84 :conditions => [
75 85 "scmid IN (?)",
76 86 revisions.map!{|c| c.scmid}
77 87 ],
78 88 :order => 'committed_on DESC'
79 89 )
80 90 end
81 91 end
@@ -1,25 +1,25
1 1 <% @entries.each do |entry| %>
2 2 <% tr_id = Digest::MD5.hexdigest(entry.path)
3 3 depth = params[:depth].to_i %>
4 4 <tr id="<%= tr_id %>" class="<%= h params[:parent_id] %> entry <%= entry.kind %>">
5 5 <td style="padding-left: <%=18 * depth%>px;" class="filename">
6 6 <% if entry.is_dir? %>
7 7 <span class="expander" onclick="<%= remote_function :url => {:action => 'show', :id => @project, :path => to_path_param(entry.path), :rev => @rev, :depth => (depth + 1), :parent_id => tr_id},
8 8 :method => :get,
9 9 :update => { :success => tr_id },
10 10 :position => :after,
11 11 :success => "scmEntryLoaded('#{tr_id}')",
12 12 :condition => "scmEntryClick('#{tr_id}')"%>">&nbsp</span>
13 13 <% end %>
14 14 <%= link_to h(entry.name),
15 15 {:action => (entry.is_dir? ? 'show' : 'changes'), :id => @project, :path => to_path_param(entry.path), :rev => @rev},
16 16 :class => (entry.is_dir? ? 'icon icon-folder' : "icon icon-file #{Redmine::MimeType.css_class_of(entry.name)}")%>
17 17 </td>
18 18 <td class="size"><%= (entry.size ? number_to_human_size(entry.size) : "?") unless entry.is_dir? %></td>
19 19 <% changeset = @project.repository.changesets.find_by_revision(entry.lastrev.identifier) if entry.lastrev && entry.lastrev.identifier %>
20 <td class="revision"><%= link_to_revision(changeset.revision, @project) if changeset %></td>
20 <td class="revision"><%= link_to_revision(changeset, @project) if changeset %></td>
21 21 <td class="age"><%= distance_of_time_in_words(entry.lastrev.time, Time.now) if entry.lastrev && entry.lastrev.time %></td>
22 22 <td class="author"><%= changeset.nil? ? h(entry.lastrev.author.to_s.split('<').first) : changeset.author if entry.lastrev %></td>
23 23 <td class="comments"><%=h truncate(changeset.comments, :length => 50) unless changeset.nil? %></td>
24 24 </tr>
25 25 <% end %>
@@ -1,28 +1,28
1 1 <% form_tag({:controller => 'repositories', :action => 'diff', :id => @project, :path => to_path_param(path)}, :method => :get) do %>
2 2 <table class="list changesets">
3 3 <thead><tr>
4 4 <th>#</th>
5 5 <th></th>
6 6 <th></th>
7 7 <th><%= l(:label_date) %></th>
8 8 <th><%= l(:field_author) %></th>
9 9 <th><%= l(:field_comments) %></th>
10 10 </tr></thead>
11 11 <tbody>
12 12 <% show_diff = revisions.size > 1 %>
13 13 <% line_num = 1 %>
14 14 <% revisions.each do |changeset| %>
15 15 <tr class="changeset <%= cycle 'odd', 'even' %>">
16 <td class="id"><%= link_to_revision(changeset.revision, project) %></td>
17 <td class="checkbox"><%= radio_button_tag('rev', changeset.revision, (line_num==1), :id => "cb-#{line_num}", :onclick => "$('cbto-#{line_num+1}').checked=true;") if show_diff && (line_num < revisions.size) %></td>
18 <td class="checkbox"><%= radio_button_tag('rev_to', changeset.revision, (line_num==2), :id => "cbto-#{line_num}", :onclick => "if ($('cb-#{line_num}').checked==true) {$('cb-#{line_num-1}').checked=true;}") if show_diff && (line_num > 1) %></td>
16 <td class="id"><%= link_to_revision(changeset, project) %></td>
17 <td class="checkbox"><%= radio_button_tag('rev', changeset.identifier, (line_num==1), :id => "cb-#{line_num}", :onclick => "$('cbto-#{line_num+1}').checked=true;") if show_diff && (line_num < revisions.size) %></td>
18 <td class="checkbox"><%= radio_button_tag('rev_to', changeset.identifier, (line_num==2), :id => "cbto-#{line_num}", :onclick => "if ($('cb-#{line_num}').checked==true) {$('cb-#{line_num-1}').checked=true;}") if show_diff && (line_num > 1) %></td>
19 19 <td class="committed_on"><%= format_time(changeset.committed_on) %></td>
20 20 <td class="author"><%=h changeset.author %></td>
21 21 <td class="comments"><%= textilizable(truncate_at_line_break(changeset.comments)) %></td>
22 22 </tr>
23 23 <% line_num += 1 %>
24 24 <% end %>
25 25 </tbody>
26 26 </table>
27 27 <%= submit_tag(l(:label_view_diff), :name => nil) if show_diff %>
28 28 <% end %>
@@ -1,36 +1,36
1 1 <%= call_hook(:view_repositories_show_contextual, { :repository => @repository, :project => @project }) %>
2 2
3 3 <div class="contextual">
4 4 <%= render :partial => 'navigation' %>
5 5 </div>
6 6
7 7 <h2><%= render :partial => 'breadcrumbs', :locals => { :path => @path, :kind => 'file', :revision => @rev } %></h2>
8 8
9 9 <p><%= render :partial => 'link_to_functions' %></p>
10 10
11 11 <% colors = Hash.new {|k,v| k[v] = (k.size % 12) } %>
12 12
13 13 <div class="autoscroll">
14 14 <table class="filecontent annotate syntaxhl">
15 15 <tbody>
16 16 <% line_num = 1 %>
17 17 <% syntax_highlight(@path, to_utf8(@annotate.content)).each_line do |line| %>
18 18 <% revision = @annotate.revisions[line_num-1] %>
19 19 <tr class="bloc-<%= revision.nil? ? 0 : colors[revision.identifier || revision.revision] %>">
20 20 <th class="line-num" id="L<%= line_num %>"><a href="#L<%= line_num %>"><%= line_num %></a></th>
21 21 <td class="revision">
22 <%= (revision.identifier ? link_to(format_revision(revision.identifier), :action => 'revision', :id => @project, :rev => revision.identifier) : format_revision(revision.revision)) if revision %></td>
22 <%= (revision.identifier ? link_to_revision(revision, @project) : format_revision(revision)) if revision %></td>
23 23 <td class="author"><%= h(revision.author.to_s.split('<').first) if revision %></td>
24 24 <td class="line-code"><pre><%= line %></pre></td>
25 25 </tr>
26 26 <% line_num += 1 %>
27 27 <% end %>
28 28 </tbody>
29 29 </table>
30 30 </div>
31 31
32 32 <% html_title(l(:button_annotate)) -%>
33 33
34 34 <% content_for :header_tags do %>
35 35 <%= stylesheet_link_tag 'scm' %>
36 36 <% end %>
@@ -1,23 +1,23
1 <h2><%= l(:label_revision) %> <%= format_revision(@rev_to) + ':' if @rev_to %><%= format_revision(@rev) %> <%=h @path %></h2>
1 <h2><%= l(:label_revision) %> <%= format_revision(@changeset_to) + ':' if @changeset_to %><%= format_revision(@changeset) %> <%=h @path %></h2>
2 2
3 3 <!-- Choose view type -->
4 4 <% form_tag({:path => to_path_param(@path)}, :method => 'get') do %>
5 5 <%= hidden_field_tag('rev', params[:rev]) if params[:rev] %>
6 6 <%= hidden_field_tag('rev_to', params[:rev_to]) if params[:rev_to] %>
7 7 <p><label><%= l(:label_view_diff) %></label>
8 8 <%= select_tag 'type', options_for_select([[l(:label_diff_inline), "inline"], [l(:label_diff_side_by_side), "sbs"]], @diff_type), :onchange => "if (this.value != '') {this.form.submit()}" %></p>
9 9 <% end %>
10 10
11 11 <% cache(@cache_key) do -%>
12 12 <%= render :partial => 'common/diff', :locals => {:diff => @diff, :diff_type => @diff_type} %>
13 13 <% end -%>
14 14
15 15 <% other_formats_links do |f| %>
16 16 <%= f.link_to 'Diff', :url => params, :caption => 'Unified diff' %>
17 17 <% end %>
18 18
19 19 <% html_title(with_leading_slash(@path), 'Diff') -%>
20 20
21 21 <% content_for :header_tags do %>
22 22 <%= stylesheet_link_tag "scm" %>
23 23 <% end %>
@@ -1,59 +1,59
1 1 <div class="contextual">
2 2 &#171;
3 3 <% unless @changeset.previous.nil? -%>
4 <%= link_to_revision(@changeset.previous.revision, @project, :text => l(:label_previous)) %>
4 <%= link_to_revision(@changeset.previous, @project, :text => l(:label_previous)) %>
5 5 <% else -%>
6 6 <%= l(:label_previous) %>
7 7 <% end -%>
8 8 |
9 9 <% unless @changeset.next.nil? -%>
10 <%= link_to_revision(@changeset.next.revision, @project, :text => l(:label_next)) %>
10 <%= link_to_revision(@changeset.next, @project, :text => l(:label_next)) %>
11 11 <% else -%>
12 12 <%= l(:label_next) %>
13 13 <% end -%>
14 14 &#187;&nbsp;
15 15
16 16 <% form_tag({:controller => 'repositories', :action => 'revision', :id => @project, :rev => nil}, :method => :get) do %>
17 <%= text_field_tag 'rev', @rev[0,8], :size => 8 %>
17 <%= text_field_tag 'rev', @rev, :size => 8 %>
18 18 <%= submit_tag 'OK', :name => nil %>
19 19 <% end %>
20 20 </div>
21 21
22 <h2><%= l(:label_revision) %> <%= format_revision(@changeset.revision) %></h2>
22 <h2><%= l(:label_revision) %> <%= format_revision(@changeset) %></h2>
23 23
24 24 <p><% if @changeset.scmid %>ID: <%= @changeset.scmid %><br /><% end %>
25 25 <span class="author"><%= authoring(@changeset.committed_on, @changeset.author) %></span></p>
26 26
27 27 <%= textilizable @changeset.comments %>
28 28
29 29 <% if @changeset.issues.visible.any? %>
30 30 <h3><%= l(:label_related_issues) %></h3>
31 31 <ul>
32 32 <% @changeset.issues.visible.each do |issue| %>
33 33 <li><%= link_to_issue issue %></li>
34 34 <% end %>
35 35 </ul>
36 36 <% end %>
37 37
38 38 <% if User.current.allowed_to?(:browse_repository, @project) %>
39 39 <h3><%= l(:label_attachment_plural) %></h3>
40 40 <ul id="changes-legend">
41 41 <li class="change change-A"><%= l(:label_added) %></li>
42 42 <li class="change change-M"><%= l(:label_modified) %></li>
43 43 <li class="change change-C"><%= l(:label_copied) %></li>
44 44 <li class="change change-R"><%= l(:label_renamed) %></li>
45 45 <li class="change change-D"><%= l(:label_deleted) %></li>
46 46 </ul>
47 47
48 <p><%= link_to(l(:label_view_diff), :action => 'diff', :id => @project, :path => "", :rev => @changeset.revision) if @changeset.changes.any? %></p>
48 <p><%= link_to(l(:label_view_diff), :action => 'diff', :id => @project, :path => "", :rev => @changeset.identifier) if @changeset.changes.any? %></p>
49 49
50 50 <div class="changeset-changes">
51 51 <%= render_changeset_changes %>
52 52 </div>
53 53 <% end %>
54 54
55 55 <% content_for :header_tags do %>
56 56 <%= stylesheet_link_tag "scm" %>
57 57 <% end %>
58 58
59 <% html_title("#{l(:label_revision)} #{@changeset.revision}") -%>
59 <% html_title("#{l(:label_revision)} #{format_revision(@changeset)}") -%>
@@ -1,333 +1,344
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require 'cgi'
19 19
20 20 module Redmine
21 21 module Scm
22 22 module Adapters
23 23 class CommandFailed < StandardError #:nodoc:
24 24 end
25 25
26 26 class AbstractAdapter #:nodoc:
27 27 class << self
28 28 # Returns the version of the scm client
29 29 # Eg: [1, 5, 0] or [] if unknown
30 30 def client_version
31 31 []
32 32 end
33 33
34 34 # Returns the version string of the scm client
35 35 # Eg: '1.5.0' or 'Unknown version' if unknown
36 36 def client_version_string
37 37 v = client_version || 'Unknown version'
38 38 v.is_a?(Array) ? v.join('.') : v.to_s
39 39 end
40 40
41 41 # Returns true if the current client version is above
42 42 # or equals the given one
43 43 # If option is :unknown is set to true, it will return
44 44 # true if the client version is unknown
45 45 def client_version_above?(v, options={})
46 46 ((client_version <=> v) >= 0) || (client_version.empty? && options[:unknown])
47 47 end
48 48 end
49 49
50 50 def initialize(url, root_url=nil, login=nil, password=nil)
51 51 @url = url
52 52 @login = login if login && !login.empty?
53 53 @password = (password || "") if @login
54 54 @root_url = root_url.blank? ? retrieve_root_url : root_url
55 55 end
56 56
57 57 def adapter_name
58 58 'Abstract'
59 59 end
60 60
61 61 def supports_cat?
62 62 true
63 63 end
64 64
65 65 def supports_annotate?
66 66 respond_to?('annotate')
67 67 end
68 68
69 69 def root_url
70 70 @root_url
71 71 end
72 72
73 73 def url
74 74 @url
75 75 end
76 76
77 77 # get info about the svn repository
78 78 def info
79 79 return nil
80 80 end
81 81
82 82 # Returns the entry identified by path and revision identifier
83 83 # or nil if entry doesn't exist in the repository
84 84 def entry(path=nil, identifier=nil)
85 85 parts = path.to_s.split(%r{[\/\\]}).select {|n| !n.blank?}
86 86 search_path = parts[0..-2].join('/')
87 87 search_name = parts[-1]
88 88 if search_path.blank? && search_name.blank?
89 89 # Root entry
90 90 Entry.new(:path => '', :kind => 'dir')
91 91 else
92 92 # Search for the entry in the parent directory
93 93 es = entries(search_path, identifier)
94 94 es ? es.detect {|e| e.name == search_name} : nil
95 95 end
96 96 end
97 97
98 98 # Returns an Entries collection
99 99 # or nil if the given path doesn't exist in the repository
100 100 def entries(path=nil, identifier=nil)
101 101 return nil
102 102 end
103 103
104 104 def branches
105 105 return nil
106 106 end
107 107
108 108 def tags
109 109 return nil
110 110 end
111 111
112 112 def default_branch
113 113 return nil
114 114 end
115 115
116 116 def properties(path, identifier=nil)
117 117 return nil
118 118 end
119 119
120 120 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
121 121 return nil
122 122 end
123 123
124 124 def diff(path, identifier_from, identifier_to=nil)
125 125 return nil
126 126 end
127 127
128 128 def cat(path, identifier=nil)
129 129 return nil
130 130 end
131 131
132 132 def with_leading_slash(path)
133 133 path ||= ''
134 134 (path[0,1]!="/") ? "/#{path}" : path
135 135 end
136 136
137 137 def with_trailling_slash(path)
138 138 path ||= ''
139 139 (path[-1,1] == "/") ? path : "#{path}/"
140 140 end
141 141
142 142 def without_leading_slash(path)
143 143 path ||= ''
144 144 path.gsub(%r{^/+}, '')
145 145 end
146 146
147 147 def without_trailling_slash(path)
148 148 path ||= ''
149 149 (path[-1,1] == "/") ? path[0..-2] : path
150 150 end
151 151
152 152 def shell_quote(str)
153 153 if Redmine::Platform.mswin?
154 154 '"' + str.gsub(/"/, '\\"') + '"'
155 155 else
156 156 "'" + str.gsub(/'/, "'\"'\"'") + "'"
157 157 end
158 158 end
159 159
160 160 private
161 161 def retrieve_root_url
162 162 info = self.info
163 163 info ? info.root_url : nil
164 164 end
165 165
166 166 def target(path)
167 167 path ||= ''
168 168 base = path.match(/^\//) ? root_url : url
169 169 shell_quote("#{base}/#{path}".gsub(/[?<>\*]/, ''))
170 170 end
171 171
172 172 def logger
173 173 self.class.logger
174 174 end
175 175
176 176 def shellout(cmd, &block)
177 177 self.class.shellout(cmd, &block)
178 178 end
179 179
180 180 def self.logger
181 181 RAILS_DEFAULT_LOGGER
182 182 end
183 183
184 184 def self.shellout(cmd, &block)
185 185 logger.debug "Shelling out: #{strip_credential(cmd)}" if logger && logger.debug?
186 186 if Rails.env == 'development'
187 187 # Capture stderr when running in dev environment
188 188 cmd = "#{cmd} 2>>#{RAILS_ROOT}/log/scm.stderr.log"
189 189 end
190 190 begin
191 191 IO.popen(cmd, "r+") do |io|
192 192 io.close_write
193 193 block.call(io) if block_given?
194 194 end
195 195 rescue Errno::ENOENT => e
196 196 msg = strip_credential(e.message)
197 197 # The command failed, log it and re-raise
198 198 logger.error("SCM command failed, make sure that your SCM binary (eg. svn) is in PATH (#{ENV['PATH']}): #{strip_credential(cmd)}\n with: #{msg}")
199 199 raise CommandFailed.new(msg)
200 200 end
201 201 end
202 202
203 203 # Hides username/password in a given command
204 204 def self.strip_credential(cmd)
205 205 q = (Redmine::Platform.mswin? ? '"' : "'")
206 206 cmd.to_s.gsub(/(\-\-(password|username))\s+(#{q}[^#{q}]+#{q}|[^#{q}]\S+)/, '\\1 xxxx')
207 207 end
208 208
209 209 def strip_credential(cmd)
210 210 self.class.strip_credential(cmd)
211 211 end
212 212 end
213 213
214 214 class Entries < Array
215 215 def sort_by_name
216 216 sort {|x,y|
217 217 if x.kind == y.kind
218 218 x.name.to_s <=> y.name.to_s
219 219 else
220 220 x.kind <=> y.kind
221 221 end
222 222 }
223 223 end
224 224
225 225 def revisions
226 226 revisions ||= Revisions.new(collect{|entry| entry.lastrev}.compact)
227 227 end
228 228 end
229 229
230 230 class Info
231 231 attr_accessor :root_url, :lastrev
232 232 def initialize(attributes={})
233 233 self.root_url = attributes[:root_url] if attributes[:root_url]
234 234 self.lastrev = attributes[:lastrev]
235 235 end
236 236 end
237 237
238 238 class Entry
239 239 attr_accessor :name, :path, :kind, :size, :lastrev
240 240 def initialize(attributes={})
241 241 self.name = attributes[:name] if attributes[:name]
242 242 self.path = attributes[:path] if attributes[:path]
243 243 self.kind = attributes[:kind] if attributes[:kind]
244 244 self.size = attributes[:size].to_i if attributes[:size]
245 245 self.lastrev = attributes[:lastrev]
246 246 end
247 247
248 248 def is_file?
249 249 'file' == self.kind
250 250 end
251 251
252 252 def is_dir?
253 253 'dir' == self.kind
254 254 end
255 255
256 256 def is_text?
257 257 Redmine::MimeType.is_type?('text', name)
258 258 end
259 259 end
260 260
261 261 class Revisions < Array
262 262 def latest
263 263 sort {|x,y|
264 264 unless x.time.nil? or y.time.nil?
265 265 x.time <=> y.time
266 266 else
267 267 0
268 268 end
269 269 }.last
270 270 end
271 271 end
272 272
273 273 class Revision
274 attr_accessor :identifier, :scmid, :name, :author, :time, :message, :paths, :revision, :branch
274 attr_accessor :scmid, :name, :author, :time, :message, :paths, :revision, :branch
275 attr_writer :identifier
275 276
276 277 def initialize(attributes={})
277 278 self.identifier = attributes[:identifier]
278 279 self.scmid = attributes[:scmid]
279 280 self.name = attributes[:name] || self.identifier
280 281 self.author = attributes[:author]
281 282 self.time = attributes[:time]
282 283 self.message = attributes[:message] || ""
283 284 self.paths = attributes[:paths]
284 285 self.revision = attributes[:revision]
285 286 self.branch = attributes[:branch]
286 287 end
287 288
289 # Returns the identifier of this revision; see also Changeset model
290 def identifier
291 (@identifier || revision).to_s
292 end
293
294 # Returns the readable identifier.
295 def format_identifier
296 identifier
297 end
298
288 299 def save(repo)
289 300 Changeset.transaction do
290 301 changeset = Changeset.new(
291 302 :repository => repo,
292 303 :revision => identifier,
293 304 :scmid => scmid,
294 305 :committer => author,
295 306 :committed_on => time,
296 307 :comments => message)
297 308
298 309 if changeset.save
299 310 paths.each do |file|
300 311 Change.create(
301 312 :changeset => changeset,
302 313 :action => file[:action],
303 314 :path => file[:path])
304 315 end
305 316 end
306 317 end
307 318 end
308 319 end
309 320
310 321 class Annotate
311 322 attr_reader :lines, :revisions
312 323
313 324 def initialize
314 325 @lines = []
315 326 @revisions = []
316 327 end
317 328
318 329 def add_line(line, revision)
319 330 @lines << line
320 331 @revisions << revision
321 332 end
322 333
323 334 def content
324 335 content = lines.join("\n")
325 336 end
326 337
327 338 def empty?
328 339 lines.empty?
329 340 end
330 341 end
331 342 end
332 343 end
333 344 end
@@ -1,270 +1,277
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require 'redmine/scm/adapters/abstract_adapter'
19 19
20 20 module Redmine
21 21 module Scm
22 22 module Adapters
23 23 class GitAdapter < AbstractAdapter
24 24 # Git executable name
25 25 GIT_BIN = "git"
26 26
27 27 def info
28 28 begin
29 29 Info.new(:root_url => url, :lastrev => lastrev('',nil))
30 30 rescue
31 31 nil
32 32 end
33 33 end
34 34
35 35 def branches
36 36 return @branches if @branches
37 37 @branches = []
38 38 cmd = "#{GIT_BIN} --git-dir #{target('')} branch --no-color"
39 39 shellout(cmd) do |io|
40 40 io.each_line do |line|
41 41 @branches << line.match('\s*\*?\s*(.*)$')[1]
42 42 end
43 43 end
44 44 @branches.sort!
45 45 end
46 46
47 47 def tags
48 48 return @tags if @tags
49 49 cmd = "#{GIT_BIN} --git-dir #{target('')} tag"
50 50 shellout(cmd) do |io|
51 51 @tags = io.readlines.sort!.map{|t| t.strip}
52 52 end
53 53 end
54 54
55 55 def default_branch
56 56 branches.include?('master') ? 'master' : branches.first
57 57 end
58 58
59 59 def entries(path=nil, identifier=nil)
60 60 path ||= ''
61 61 entries = Entries.new
62 62 cmd = "#{GIT_BIN} --git-dir #{target('')} ls-tree -l "
63 63 cmd << shell_quote("HEAD:" + path) if identifier.nil?
64 64 cmd << shell_quote(identifier + ":" + path) if identifier
65 65 shellout(cmd) do |io|
66 66 io.each_line do |line|
67 67 e = line.chomp.to_s
68 68 if e =~ /^\d+\s+(\w+)\s+([0-9a-f]{40})\s+([0-9-]+)\t(.+)$/
69 69 type = $1
70 70 sha = $2
71 71 size = $3
72 72 name = $4
73 73 full_path = path.empty? ? name : "#{path}/#{name}"
74 74 entries << Entry.new({:name => name,
75 75 :path => full_path,
76 76 :kind => (type == "tree") ? 'dir' : 'file',
77 77 :size => (type == "tree") ? nil : size,
78 78 :lastrev => lastrev(full_path,identifier)
79 79 }) unless entries.detect{|entry| entry.name == name}
80 80 end
81 81 end
82 82 end
83 83 return nil if $? && $?.exitstatus != 0
84 84 entries.sort_by_name
85 85 end
86 86
87 87 def lastrev(path,rev)
88 88 return nil if path.nil?
89 89 cmd = "#{GIT_BIN} --git-dir #{target('')} log --no-color --date=iso --pretty=fuller --no-merges -n 1 "
90 90 cmd << " #{shell_quote rev} " if rev
91 91 cmd << "-- #{shell_quote path} " unless path.empty?
92 92 shellout(cmd) do |io|
93 93 begin
94 94 id = io.gets.split[1]
95 95 author = io.gets.match('Author:\s+(.*)$')[1]
96 96 2.times { io.gets }
97 97 time = Time.parse(io.gets.match('CommitDate:\s+(.*)$')[1]).localtime
98 98
99 99 Revision.new({
100 100 :identifier => id,
101 101 :scmid => id,
102 102 :author => author,
103 103 :time => time,
104 104 :message => nil,
105 105 :paths => nil
106 106 })
107 107 rescue NoMethodError => e
108 108 logger.error("The revision '#{path}' has a wrong format")
109 109 return nil
110 110 end
111 111 end
112 112 end
113 113
114 114 def revisions(path, identifier_from, identifier_to, options={})
115 115 revisions = Revisions.new
116 116
117 117 cmd = "#{GIT_BIN} --git-dir #{target('')} log --no-color --raw --date=iso --pretty=fuller "
118 118 cmd << " --reverse " if options[:reverse]
119 119 cmd << " --all " if options[:all]
120 120 cmd << " -n #{options[:limit].to_i} " if options[:limit]
121 121 cmd << "#{shell_quote(identifier_from + '..')}" if identifier_from
122 122 cmd << "#{shell_quote identifier_to}" if identifier_to
123 123 cmd << " --since=#{shell_quote(options[:since].strftime("%Y-%m-%d %H:%M:%S"))}" if options[:since]
124 124 cmd << " -- #{shell_quote path}" if path && !path.empty?
125 125
126 126 shellout(cmd) do |io|
127 127 files=[]
128 128 changeset = {}
129 129 parsing_descr = 0 #0: not parsing desc or files, 1: parsing desc, 2: parsing files
130 130 revno = 1
131 131
132 132 io.each_line do |line|
133 133 if line =~ /^commit ([0-9a-f]{40})$/
134 134 key = "commit"
135 135 value = $1
136 136 if (parsing_descr == 1 || parsing_descr == 2)
137 137 parsing_descr = 0
138 138 revision = Revision.new({
139 139 :identifier => changeset[:commit],
140 140 :scmid => changeset[:commit],
141 141 :author => changeset[:author],
142 142 :time => Time.parse(changeset[:date]),
143 143 :message => changeset[:description],
144 144 :paths => files
145 145 })
146 146 if block_given?
147 147 yield revision
148 148 else
149 149 revisions << revision
150 150 end
151 151 changeset = {}
152 152 files = []
153 153 revno = revno + 1
154 154 end
155 155 changeset[:commit] = $1
156 156 elsif (parsing_descr == 0) && line =~ /^(\w+):\s*(.*)$/
157 157 key = $1
158 158 value = $2
159 159 if key == "Author"
160 160 changeset[:author] = value
161 161 elsif key == "CommitDate"
162 162 changeset[:date] = value
163 163 end
164 164 elsif (parsing_descr == 0) && line.chomp.to_s == ""
165 165 parsing_descr = 1
166 166 changeset[:description] = ""
167 167 elsif (parsing_descr == 1 || parsing_descr == 2) \
168 168 && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\t(.+)$/
169 169 parsing_descr = 2
170 170 fileaction = $1
171 171 filepath = $2
172 172 files << {:action => fileaction, :path => filepath}
173 173 elsif (parsing_descr == 1 || parsing_descr == 2) \
174 174 && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\d+\s+(\S+)\t(.+)$/
175 175 parsing_descr = 2
176 176 fileaction = $1
177 177 filepath = $3
178 178 files << {:action => fileaction, :path => filepath}
179 179 elsif (parsing_descr == 1) && line.chomp.to_s == ""
180 180 parsing_descr = 2
181 181 elsif (parsing_descr == 1)
182 182 changeset[:description] << line[4..-1]
183 183 end
184 184 end
185 185
186 186 if changeset[:commit]
187 187 revision = Revision.new({
188 188 :identifier => changeset[:commit],
189 189 :scmid => changeset[:commit],
190 190 :author => changeset[:author],
191 191 :time => Time.parse(changeset[:date]),
192 192 :message => changeset[:description],
193 193 :paths => files
194 194 })
195 195
196 196 if block_given?
197 197 yield revision
198 198 else
199 199 revisions << revision
200 200 end
201 201 end
202 202 end
203 203
204 204 return nil if $? && $?.exitstatus != 0
205 205 revisions
206 206 end
207 207
208 208 def diff(path, identifier_from, identifier_to=nil)
209 209 path ||= ''
210 210
211 211 if identifier_to
212 212 cmd = "#{GIT_BIN} --git-dir #{target('')} diff --no-color #{shell_quote identifier_to} #{shell_quote identifier_from}"
213 213 else
214 214 cmd = "#{GIT_BIN} --git-dir #{target('')} show --no-color #{shell_quote identifier_from}"
215 215 end
216 216
217 217 cmd << " -- #{shell_quote path}" unless path.empty?
218 218 diff = []
219 219 shellout(cmd) do |io|
220 220 io.each_line do |line|
221 221 diff << line
222 222 end
223 223 end
224 224 return nil if $? && $?.exitstatus != 0
225 225 diff
226 226 end
227 227
228 228 def annotate(path, identifier=nil)
229 229 identifier = 'HEAD' if identifier.blank?
230 230 cmd = "#{GIT_BIN} --git-dir #{target('')} blame -p #{shell_quote identifier} -- #{shell_quote path}"
231 231 blame = Annotate.new
232 232 content = nil
233 233 shellout(cmd) { |io| io.binmode; content = io.read }
234 234 return nil if $? && $?.exitstatus != 0
235 235 # git annotates binary files
236 236 return nil if content.is_binary_data?
237 237 identifier = ''
238 238 # git shows commit author on the first occurrence only
239 239 authors_by_commit = {}
240 240 content.split("\n").each do |line|
241 241 if line =~ /^([0-9a-f]{39,40})\s.*/
242 242 identifier = $1
243 243 elsif line =~ /^author (.+)/
244 244 authors_by_commit[identifier] = $1.strip
245 245 elsif line =~ /^\t(.*)/
246 246 blame.add_line($1, Revision.new(:identifier => identifier, :author => authors_by_commit[identifier]))
247 247 identifier = ''
248 248 author = ''
249 249 end
250 250 end
251 251 blame
252 252 end
253 253
254 254 def cat(path, identifier=nil)
255 255 if identifier.nil?
256 256 identifier = 'HEAD'
257 257 end
258 258 cmd = "#{GIT_BIN} --git-dir #{target('')} show --no-color #{shell_quote(identifier + ':' + path)}"
259 259 cat = nil
260 260 shellout(cmd) do |io|
261 261 io.binmode
262 262 cat = io.read
263 263 end
264 264 return nil if $? && $?.exitstatus != 0
265 265 cat
266 266 end
267
268 class Revision < Redmine::Scm::Adapters::Revision
269 # Returns the readable identifier
270 def format_identifier
271 identifier[0,8]
272 end
273 end
267 274 end
268 275 end
269 276 end
270 277 end
@@ -1,221 +1,226
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2010 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 require File.expand_path('../../test_helper', __FILE__)
21 21
22 22 class ChangesetTest < ActiveSupport::TestCase
23 23 fixtures :projects, :repositories, :issues, :issue_statuses, :changesets, :changes, :issue_categories, :enumerations, :custom_fields, :custom_values, :users, :members, :member_roles, :trackers
24 24
25 25 def setup
26 26 end
27 27
28 28 def test_ref_keywords_any
29 29 ActionMailer::Base.deliveries.clear
30 30 Setting.commit_fix_status_id = IssueStatus.find(:first, :conditions => ["is_closed = ?", true]).id
31 31 Setting.commit_fix_done_ratio = '90'
32 32 Setting.commit_ref_keywords = '*'
33 33 Setting.commit_fix_keywords = 'fixes , closes'
34 34
35 35 c = Changeset.new(:repository => Project.find(1).repository,
36 36 :committed_on => Time.now,
37 37 :comments => 'New commit (#2). Fixes #1')
38 38 c.scan_comment_for_issue_ids
39 39
40 40 assert_equal [1, 2], c.issue_ids.sort
41 41 fixed = Issue.find(1)
42 42 assert fixed.closed?
43 43 assert_equal 90, fixed.done_ratio
44 44 assert_equal 1, ActionMailer::Base.deliveries.size
45 45 end
46 46
47 47 def test_ref_keywords
48 48 Setting.commit_ref_keywords = 'refs'
49 49 Setting.commit_fix_keywords = ''
50 50
51 51 c = Changeset.new(:repository => Project.find(1).repository,
52 52 :committed_on => Time.now,
53 53 :comments => 'Ignores #2. Refs #1')
54 54 c.scan_comment_for_issue_ids
55 55
56 56 assert_equal [1], c.issue_ids.sort
57 57 end
58 58
59 59 def test_ref_keywords_any_only
60 60 Setting.commit_ref_keywords = '*'
61 61 Setting.commit_fix_keywords = ''
62 62
63 63 c = Changeset.new(:repository => Project.find(1).repository,
64 64 :committed_on => Time.now,
65 65 :comments => 'Ignores #2. Refs #1')
66 66 c.scan_comment_for_issue_ids
67 67
68 68 assert_equal [1, 2], c.issue_ids.sort
69 69 end
70 70
71 71 def test_ref_keywords_any_with_timelog
72 72 Setting.commit_ref_keywords = '*'
73 73 Setting.commit_logtime_enabled = '1'
74 74
75 75 c = Changeset.new(:repository => Project.find(1).repository,
76 76 :committed_on => 24.hours.ago,
77 77 :comments => 'Worked on this issue #1 @2h',
78 78 :revision => '520',
79 79 :user => User.find(2))
80 80 assert_difference 'TimeEntry.count' do
81 81 c.scan_comment_for_issue_ids
82 82 end
83 83 assert_equal [1], c.issue_ids.sort
84 84
85 85 time = TimeEntry.first(:order => 'id desc')
86 86 assert_equal 1, time.issue_id
87 87 assert_equal 1, time.project_id
88 88 assert_equal 2, time.user_id
89 89 assert_equal 2.0, time.hours
90 90 assert_equal Date.yesterday, time.spent_on
91 91 assert time.activity.is_default?
92 92 assert time.comments.include?('r520'), "r520 was expected in time_entry comments: #{time.comments}"
93 93 end
94 94
95 95 def test_ref_keywords_closing_with_timelog
96 96 Setting.commit_fix_status_id = IssueStatus.find(:first, :conditions => ["is_closed = ?", true]).id
97 97 Setting.commit_ref_keywords = '*'
98 98 Setting.commit_fix_keywords = 'fixes , closes'
99 99 Setting.commit_logtime_enabled = '1'
100 100
101 101 c = Changeset.new(:repository => Project.find(1).repository,
102 102 :committed_on => Time.now,
103 103 :comments => 'This is a comment. Fixes #1 @2.5, #2 @1',
104 104 :user => User.find(2))
105 105 assert_difference 'TimeEntry.count', 2 do
106 106 c.scan_comment_for_issue_ids
107 107 end
108 108
109 109 assert_equal [1, 2], c.issue_ids.sort
110 110 assert Issue.find(1).closed?
111 111 assert Issue.find(2).closed?
112 112
113 113 times = TimeEntry.all(:order => 'id desc', :limit => 2)
114 114 assert_equal [1, 2], times.collect(&:issue_id).sort
115 115 end
116 116
117 117 def test_ref_keywords_any_line_start
118 118 Setting.commit_ref_keywords = '*'
119 119
120 120 c = Changeset.new(:repository => Project.find(1).repository,
121 121 :committed_on => Time.now,
122 122 :comments => '#1 is the reason of this commit')
123 123 c.scan_comment_for_issue_ids
124 124
125 125 assert_equal [1], c.issue_ids.sort
126 126 end
127 127
128 128 def test_ref_keywords_allow_brackets_around_a_issue_number
129 129 Setting.commit_ref_keywords = '*'
130 130
131 131 c = Changeset.new(:repository => Project.find(1).repository,
132 132 :committed_on => Time.now,
133 133 :comments => '[#1] Worked on this issue')
134 134 c.scan_comment_for_issue_ids
135 135
136 136 assert_equal [1], c.issue_ids.sort
137 137 end
138 138
139 139 def test_ref_keywords_allow_brackets_around_multiple_issue_numbers
140 140 Setting.commit_ref_keywords = '*'
141 141
142 142 c = Changeset.new(:repository => Project.find(1).repository,
143 143 :committed_on => Time.now,
144 144 :comments => '[#1 #2, #3] Worked on these')
145 145 c.scan_comment_for_issue_ids
146 146
147 147 assert_equal [1,2,3], c.issue_ids.sort
148 148 end
149 149
150 150 def test_commit_referencing_a_subproject_issue
151 151 c = Changeset.new(:repository => Project.find(1).repository,
152 152 :committed_on => Time.now,
153 153 :comments => 'refs #5, a subproject issue')
154 154 c.scan_comment_for_issue_ids
155 155
156 156 assert_equal [5], c.issue_ids.sort
157 157 assert c.issues.first.project != c.project
158 158 end
159 159
160 160 def test_commit_referencing_a_parent_project_issue
161 161 # repository of child project
162 162 r = Repository::Subversion.create!(:project => Project.find(3), :url => 'svn://localhost/test')
163 163
164 164 c = Changeset.new(:repository => r,
165 165 :committed_on => Time.now,
166 166 :comments => 'refs #2, an issue of a parent project')
167 167 c.scan_comment_for_issue_ids
168 168
169 169 assert_equal [2], c.issue_ids.sort
170 170 assert c.issues.first.project != c.project
171 171 end
172 172
173 173 def test_text_tag_revision
174 174 c = Changeset.new(:revision => '520')
175 175 assert_equal 'r520', c.text_tag
176 176 end
177 177
178 178 def test_text_tag_hash
179 179 c = Changeset.new(:scmid => '7234cb2750b63f47bff735edc50a1c0a433c2518', :revision => '7234cb2750b63f47bff735edc50a1c0a433c2518')
180 180 assert_equal 'commit:7234cb2750b63f47bff735edc50a1c0a433c2518', c.text_tag
181 181 end
182 182
183 183 def test_text_tag_hash_all_number
184 184 c = Changeset.new(:scmid => '0123456789', :revision => '0123456789')
185 185 assert_equal 'commit:0123456789', c.text_tag
186 186 end
187 187
188 188 def test_previous
189 189 changeset = Changeset.find_by_revision('3')
190 190 assert_equal Changeset.find_by_revision('2'), changeset.previous
191 191 end
192 192
193 193 def test_previous_nil
194 194 changeset = Changeset.find_by_revision('1')
195 195 assert_nil changeset.previous
196 196 end
197 197
198 198 def test_next
199 199 changeset = Changeset.find_by_revision('2')
200 200 assert_equal Changeset.find_by_revision('3'), changeset.next
201 201 end
202 202
203 203 def test_next_nil
204 204 changeset = Changeset.find_by_revision('10')
205 205 assert_nil changeset.next
206 206 end
207 207
208 208 def test_comments_should_be_converted_to_utf8
209 209 with_settings :commit_logs_encoding => 'ISO-8859-1' do
210 210 c = Changeset.new
211 211 c.comments = File.read("#{RAILS_ROOT}/test/fixtures/encoding/iso-8859-1.txt")
212 212 assert_equal "Texte encodΓ© en ISO-8859-1.", c.comments
213 213 end
214 214 end
215 215
216 216 def test_invalid_utf8_sequences_in_comments_should_be_stripped
217 217 c = Changeset.new
218 218 c.comments = File.read("#{RAILS_ROOT}/test/fixtures/encoding/iso-8859-1.txt")
219 219 assert_equal "Texte encod en ISO-8859-1.", c.comments
220 220 end
221
222 def test_identifier
223 c = Changeset.find_by_revision('1')
224 assert_equal c.revision, c.identifier
225 end
221 226 end
@@ -1,88 +1,88
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../test_helper', __FILE__)
19 19
20 20 class RepositoryBazaarTest < ActiveSupport::TestCase
21 21 fixtures :projects
22 22
23 23 # No '..' in the repository path
24 24 REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/bazaar_repository'
25 25 REPOSITORY_PATH.gsub!(/\/+/, '/')
26 26
27 27 def setup
28 28 @project = Project.find(1)
29 29 assert @repository = Repository::Bazaar.create(:project => @project, :url => "file:///#{REPOSITORY_PATH}")
30 30 end
31 31
32 32 if File.directory?(REPOSITORY_PATH)
33 33 def test_fetch_changesets_from_scratch
34 34 @repository.fetch_changesets
35 35 @repository.reload
36 36
37 37 assert_equal 4, @repository.changesets.count
38 38 assert_equal 9, @repository.changes.count
39 39 assert_equal 'Initial import', @repository.changesets.find_by_revision('1').comments
40 40 end
41 41
42 42 def test_fetch_changesets_incremental
43 43 @repository.fetch_changesets
44 44 # Remove changesets with revision > 5
45 45 @repository.changesets.find(:all).each {|c| c.destroy if c.revision.to_i > 2}
46 46 @repository.reload
47 47 assert_equal 2, @repository.changesets.count
48 48
49 49 @repository.fetch_changesets
50 50 assert_equal 4, @repository.changesets.count
51 51 end
52 52
53 53 def test_entries
54 54 entries = @repository.entries
55 55 assert_equal 2, entries.size
56 56
57 57 assert_equal 'dir', entries[0].kind
58 58 assert_equal 'directory', entries[0].name
59 59
60 60 assert_equal 'file', entries[1].kind
61 61 assert_equal 'doc-mkdir.txt', entries[1].name
62 62 end
63 63
64 64 def test_entries_in_subdirectory
65 65 entries = @repository.entries('directory')
66 66 assert_equal 3, entries.size
67 67
68 68 assert_equal 'file', entries.last.kind
69 69 assert_equal 'edit.png', entries.last.name
70 70 end
71 71
72 72 def test_cat
73 73 cat = @repository.scm.cat('directory/document.txt')
74 74 assert cat =~ /Write the contents of a file as of a given revision to standard output/
75 75 end
76 76
77 77 def test_annotate
78 78 annotate = @repository.scm.annotate('doc-mkdir.txt')
79 79 assert_equal 17, annotate.lines.size
80 assert_equal 1, annotate.revisions[0].identifier
80 assert_equal '1', annotate.revisions[0].identifier
81 81 assert_equal 'jsmith@', annotate.revisions[0].author
82 82 assert_equal 'mkdir', annotate.lines[0]
83 83 end
84 84 else
85 85 puts "Bazaar test repository NOT FOUND. Skipping unit tests !!!"
86 86 def test_fake; assert true end
87 87 end
88 88 end
@@ -1,69 +1,95
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../test_helper', __FILE__)
19 19
20 20 class RepositoryGitTest < ActiveSupport::TestCase
21 fixtures :projects
21 fixtures :projects, :repositories, :enabled_modules, :users, :roles
22 22
23 23 # No '..' in the repository path
24 24 REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/git_repository'
25 25 REPOSITORY_PATH.gsub!(/\//, "\\") if Redmine::Platform.mswin?
26 26
27 27 def setup
28 28 @project = Project.find(1)
29 29 assert @repository = Repository::Git.create(:project => @project, :url => REPOSITORY_PATH)
30 30 end
31 31
32 32 if File.directory?(REPOSITORY_PATH)
33 33 def test_fetch_changesets_from_scratch
34 34 @repository.fetch_changesets
35 35 @repository.reload
36 36
37 37 assert_equal 15, @repository.changesets.count
38 38 assert_equal 24, @repository.changes.count
39 39
40 40 commit = @repository.changesets.find(:first, :order => 'committed_on ASC')
41 41 assert_equal "Initial import.\nThe repository contains 3 files.", commit.comments
42 42 assert_equal "jsmith <jsmith@foo.bar>", commit.committer
43 43 assert_equal User.find_by_login('jsmith'), commit.user
44 44 # TODO: add a commit with commit time <> author time to the test repository
45 45 assert_equal "2007-12-14 09:22:52".to_time, commit.committed_on
46 46 assert_equal "2007-12-14".to_date, commit.commit_date
47 47 assert_equal "7234cb2750b63f47bff735edc50a1c0a433c2518", commit.revision
48 48 assert_equal "7234cb2750b63f47bff735edc50a1c0a433c2518", commit.scmid
49 49 assert_equal 3, commit.changes.count
50 50 change = commit.changes.sort_by(&:path).first
51 51 assert_equal "README", change.path
52 52 assert_equal "A", change.action
53 53 end
54 54
55 55 def test_fetch_changesets_incremental
56 56 @repository.fetch_changesets
57 57 # Remove the 3 latest changesets
58 58 @repository.changesets.find(:all, :order => 'committed_on DESC', :limit => 3).each(&:destroy)
59 59 @repository.reload
60 60 assert_equal 12, @repository.changesets.count
61 61
62 62 @repository.fetch_changesets
63 63 assert_equal 15, @repository.changesets.count
64 64 end
65
66 def test_identifier
67 @repository.fetch_changesets
68 @repository.reload
69 c = @repository.changesets.find_by_revision('7234cb2750b63f47bff735edc50a1c0a433c2518')
70 assert_equal c.scmid, c.identifier
71 end
72
73 def test_format_identifier
74 @repository.fetch_changesets
75 @repository.reload
76 c = @repository.changesets.find_by_revision('7234cb2750b63f47bff735edc50a1c0a433c2518')
77 assert_equal c.format_identifier, '7234cb27'
78 end
79
80 def test_activities
81 @repository.fetch_changesets
82 @repository.reload
83 f = Redmine::Activity::Fetcher.new(User.anonymous, :project => Project.find(1))
84 f.scope = ['changesets']
85 events = f.events
86 assert_kind_of Array, events
87 eve = events[-9]
88 assert eve.event_title.include?('7234cb27:')
89 assert_equal eve.event_url[:rev], '7234cb2750b63f47bff735edc50a1c0a433c2518'
90 end
65 91 else
66 92 puts "Git test repository NOT FOUND. Skipping unit tests !!!"
67 93 def test_fake; assert true end
68 94 end
69 95 end
@@ -1,95 +1,146
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../test_helper', __FILE__)
19 19
20 20 class RepositorySubversionTest < ActiveSupport::TestCase
21 fixtures :projects, :repositories
21 fixtures :projects, :repositories, :enabled_modules, :users, :roles
22 22
23 23 def setup
24 24 @project = Project.find(1)
25 25 assert @repository = Repository::Subversion.create(:project => @project, :url => "file:///#{self.class.repository_path('subversion')}")
26 26 end
27 27
28 28 if repository_configured?('subversion')
29 29 def test_fetch_changesets_from_scratch
30 30 @repository.fetch_changesets
31 31 @repository.reload
32 32
33 33 assert_equal 11, @repository.changesets.count
34 34 assert_equal 20, @repository.changes.count
35 35 assert_equal 'Initial import.', @repository.changesets.find_by_revision('1').comments
36 36 end
37 37
38 38 def test_fetch_changesets_incremental
39 39 @repository.fetch_changesets
40 40 # Remove changesets with revision > 5
41 41 @repository.changesets.find(:all).each {|c| c.destroy if c.revision.to_i > 5}
42 42 @repository.reload
43 43 assert_equal 5, @repository.changesets.count
44 44
45 45 @repository.fetch_changesets
46 46 assert_equal 11, @repository.changesets.count
47 47 end
48 48
49 49 def test_latest_changesets
50 50 @repository.fetch_changesets
51 51
52 52 # with limit
53 53 changesets = @repository.latest_changesets('', nil, 2)
54 54 assert_equal 2, changesets.size
55 55 assert_equal @repository.latest_changesets('', nil).slice(0,2), changesets
56 56
57 57 # with path
58 58 changesets = @repository.latest_changesets('subversion_test/folder', nil)
59 59 assert_equal ["10", "9", "7", "6", "5", "2"], changesets.collect(&:revision)
60 60
61 61 # with path and revision
62 62 changesets = @repository.latest_changesets('subversion_test/folder', 8)
63 63 assert_equal ["7", "6", "5", "2"], changesets.collect(&:revision)
64 64 end
65 65
66 66 def test_directory_listing_with_square_brackets_in_path
67 67 @repository.fetch_changesets
68 68 @repository.reload
69 69
70 70 entries = @repository.entries('subversion_test/[folder_with_brackets]')
71 71 assert_not_nil entries, 'Expect to find entries in folder_with_brackets'
72 72 assert_equal 1, entries.size, 'Expect one entry in folder_with_brackets'
73 73 assert_equal 'README.txt', entries.first.name
74 74 end
75 75
76 76 def test_directory_listing_with_square_brackets_in_base
77 77 @project = Project.find(1)
78 78 @repository = Repository::Subversion.create(:project => @project, :url => "file:///#{self.class.repository_path('subversion')}/subversion_test/[folder_with_brackets]")
79 79
80 80 @repository.fetch_changesets
81 81 @repository.reload
82 82
83 83 assert_equal 1, @repository.changesets.count, 'Expected to see 1 revision'
84 84 assert_equal 2, @repository.changes.count, 'Expected to see 2 changes, dir add and file add'
85 85
86 86 entries = @repository.entries('')
87 87 assert_not_nil entries, 'Expect to find entries'
88 88 assert_equal 1, entries.size, 'Expect a single entry'
89 89 assert_equal 'README.txt', entries.first.name
90 90 end
91
92 def test_identifier
93 @repository.fetch_changesets
94 @repository.reload
95 c = @repository.changesets.find_by_revision('1')
96 assert_equal c.revision, c.identifier
97 end
98
99 def test_identifier_nine_digit
100 c = Changeset.new(:repository => @repository, :committed_on => Time.now,
101 :revision => '123456789', :comments => 'test')
102 assert_equal c.identifier, c.revision
103 end
104
105 def test_format_identifier
106 @repository.fetch_changesets
107 @repository.reload
108 c = @repository.changesets.find_by_revision('1')
109 assert_equal c.format_identifier, c.revision
110 end
111
112 def test_format_identifier_nine_digit
113 c = Changeset.new(:repository => @repository, :committed_on => Time.now,
114 :revision => '123456789', :comments => 'test')
115 assert_equal c.format_identifier, c.revision
116 end
117
118 def test_activities
119 @repository.fetch_changesets
120 @repository.reload
121 f = Redmine::Activity::Fetcher.new(User.anonymous, :project => Project.find(1))
122 f.scope = ['changesets']
123 events = f.events
124 assert_kind_of Array, events
125 eve = events[-9]
126 assert eve.event_title.include?('1:')
127 assert_equal eve.event_url[:rev], '1'
128 end
129
130 def test_activities_nine_digit
131 c = Changeset.new(:repository => @repository, :committed_on => Time.now,
132 :revision => '123456789', :comments => 'test')
133 assert( c.save )
134 f = Redmine::Activity::Fetcher.new(User.anonymous, :project => Project.find(1))
135 f.scope = ['changesets']
136 events = f.events
137 assert_kind_of Array, events
138 eve = events[-11]
139 assert eve.event_title.include?('123456789:')
140 assert_equal eve.event_url[:rev], '123456789'
141 end
91 142 else
92 143 puts "Subversion test repository NOT FOUND. Skipping unit tests !!!"
93 144 def test_fake; assert true end
94 145 end
95 146 end
General Comments 0
You need to be logged in to leave comments. Login now