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