##// END OF EJS Templates
Fixed: links to changesets in activity and atom feeds uses project id instead project identifier (#3137)....
Jean-Philippe Lang -
r2571:81b84f641d9e
parent child
Show More
@@ -1,327 +1,327
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 '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 before_filter :find_repository, :except => :edit
27 before_filter :find_repository, :except => :edit
28 before_filter :find_project, :only => :edit
28 before_filter :find_project, :only => :edit
29 before_filter :authorize
29 before_filter :authorize
30 accept_key_auth :revisions
30 accept_key_auth :revisions
31
31
32 rescue_from Redmine::Scm::Adapters::CommandFailed, :with => :show_error_command_failed
32 rescue_from Redmine::Scm::Adapters::CommandFailed, :with => :show_error_command_failed
33
33
34 def edit
34 def edit
35 @repository = @project.repository
35 @repository = @project.repository
36 if !@repository
36 if !@repository
37 @repository = Repository.factory(params[:repository_scm])
37 @repository = Repository.factory(params[:repository_scm])
38 @repository.project = @project if @repository
38 @repository.project = @project if @repository
39 end
39 end
40 if request.post? && @repository
40 if request.post? && @repository
41 @repository.attributes = params[:repository]
41 @repository.attributes = params[:repository]
42 @repository.save
42 @repository.save
43 end
43 end
44 render(:update) {|page| page.replace_html "tab-content-repository", :partial => 'projects/settings/repository'}
44 render(:update) {|page| page.replace_html "tab-content-repository", :partial => 'projects/settings/repository'}
45 end
45 end
46
46
47 def committers
47 def committers
48 @committers = @repository.committers
48 @committers = @repository.committers
49 @users = @project.users
49 @users = @project.users
50 additional_user_ids = @committers.collect(&:last).collect(&:to_i) - @users.collect(&:id)
50 additional_user_ids = @committers.collect(&:last).collect(&:to_i) - @users.collect(&:id)
51 @users += User.find_all_by_id(additional_user_ids) unless additional_user_ids.empty?
51 @users += User.find_all_by_id(additional_user_ids) unless additional_user_ids.empty?
52 @users.compact!
52 @users.compact!
53 @users.sort!
53 @users.sort!
54 if request.post? && params[:committers].is_a?(Hash)
54 if request.post? && params[:committers].is_a?(Hash)
55 # Build a hash with repository usernames as keys and corresponding user ids as values
55 # Build a hash with repository usernames as keys and corresponding user ids as values
56 @repository.committer_ids = params[:committers].values.inject({}) {|h, c| h[c.first] = c.last; h}
56 @repository.committer_ids = params[:committers].values.inject({}) {|h, c| h[c.first] = c.last; h}
57 flash[:notice] = l(:notice_successful_update)
57 flash[:notice] = l(:notice_successful_update)
58 redirect_to :action => 'committers', :id => @project
58 redirect_to :action => 'committers', :id => @project
59 end
59 end
60 end
60 end
61
61
62 def destroy
62 def destroy
63 @repository.destroy
63 @repository.destroy
64 redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'repository'
64 redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'repository'
65 end
65 end
66
66
67 def show
67 def show
68 # check if new revisions have been committed in the repository
68 # check if new revisions have been committed in the repository
69 @repository.fetch_changesets if Setting.autofetch_changesets?
69 @repository.fetch_changesets if Setting.autofetch_changesets?
70 # root entries
70 # root entries
71 @entries = @repository.entries('', @rev)
71 @entries = @repository.entries('', @rev)
72 # latest changesets
72 # latest changesets
73 @changesets = @repository.changesets.find(:all, :limit => 10, :order => "committed_on DESC")
73 @changesets = @repository.changesets.find(:all, :limit => 10, :order => "committed_on DESC")
74 show_error_not_found unless @entries || @changesets.any?
74 show_error_not_found unless @entries || @changesets.any?
75 end
75 end
76
76
77 def browse
77 def browse
78 @entries = @repository.entries(@path, @rev)
78 @entries = @repository.entries(@path, @rev)
79 if request.xhr?
79 if request.xhr?
80 @entries ? render(:partial => 'dir_list_content') : render(:nothing => true)
80 @entries ? render(:partial => 'dir_list_content') : render(:nothing => true)
81 else
81 else
82 show_error_not_found and return unless @entries
82 show_error_not_found and return unless @entries
83 @properties = @repository.properties(@path, @rev)
83 @properties = @repository.properties(@path, @rev)
84 render :action => 'browse'
84 render :action => 'browse'
85 end
85 end
86 end
86 end
87
87
88 def changes
88 def changes
89 @entry = @repository.entry(@path, @rev)
89 @entry = @repository.entry(@path, @rev)
90 show_error_not_found and return unless @entry
90 show_error_not_found and return unless @entry
91 @changesets = @repository.changesets_for_path(@path, :limit => Setting.repository_log_display_limit.to_i)
91 @changesets = @repository.changesets_for_path(@path, :limit => Setting.repository_log_display_limit.to_i)
92 @properties = @repository.properties(@path, @rev)
92 @properties = @repository.properties(@path, @rev)
93 end
93 end
94
94
95 def revisions
95 def revisions
96 @changeset_count = @repository.changesets.count
96 @changeset_count = @repository.changesets.count
97 @changeset_pages = Paginator.new self, @changeset_count,
97 @changeset_pages = Paginator.new self, @changeset_count,
98 per_page_option,
98 per_page_option,
99 params['page']
99 params['page']
100 @changesets = @repository.changesets.find(:all,
100 @changesets = @repository.changesets.find(:all,
101 :limit => @changeset_pages.items_per_page,
101 :limit => @changeset_pages.items_per_page,
102 :offset => @changeset_pages.current.offset,
102 :offset => @changeset_pages.current.offset,
103 :include => :user)
103 :include => [:user, :repository])
104
104
105 respond_to do |format|
105 respond_to do |format|
106 format.html { render :layout => false if request.xhr? }
106 format.html { render :layout => false if request.xhr? }
107 format.atom { render_feed(@changesets, :title => "#{@project.name}: #{l(:label_revision_plural)}") }
107 format.atom { render_feed(@changesets, :title => "#{@project.name}: #{l(:label_revision_plural)}") }
108 end
108 end
109 end
109 end
110
110
111 def entry
111 def entry
112 @entry = @repository.entry(@path, @rev)
112 @entry = @repository.entry(@path, @rev)
113 show_error_not_found and return unless @entry
113 show_error_not_found and return unless @entry
114
114
115 # If the entry is a dir, show the browser
115 # If the entry is a dir, show the browser
116 browse and return if @entry.is_dir?
116 browse and return if @entry.is_dir?
117
117
118 @content = @repository.cat(@path, @rev)
118 @content = @repository.cat(@path, @rev)
119 show_error_not_found and return unless @content
119 show_error_not_found and return unless @content
120 if 'raw' == params[:format] || @content.is_binary_data? || (@entry.size && @entry.size > Setting.file_max_size_displayed.to_i.kilobyte)
120 if 'raw' == params[:format] || @content.is_binary_data? || (@entry.size && @entry.size > Setting.file_max_size_displayed.to_i.kilobyte)
121 # Force the download
121 # Force the download
122 send_data @content, :filename => @path.split('/').last
122 send_data @content, :filename => @path.split('/').last
123 else
123 else
124 # Prevent empty lines when displaying a file with Windows style eol
124 # Prevent empty lines when displaying a file with Windows style eol
125 @content.gsub!("\r\n", "\n")
125 @content.gsub!("\r\n", "\n")
126 end
126 end
127 end
127 end
128
128
129 def annotate
129 def annotate
130 @entry = @repository.entry(@path, @rev)
130 @entry = @repository.entry(@path, @rev)
131 show_error_not_found and return unless @entry
131 show_error_not_found and return unless @entry
132
132
133 @annotate = @repository.scm.annotate(@path, @rev)
133 @annotate = @repository.scm.annotate(@path, @rev)
134 render_error l(:error_scm_annotate) and return if @annotate.nil? || @annotate.empty?
134 render_error l(:error_scm_annotate) and return if @annotate.nil? || @annotate.empty?
135 end
135 end
136
136
137 def revision
137 def revision
138 @changeset = @repository.changesets.find_by_revision(@rev)
138 @changeset = @repository.changesets.find_by_revision(@rev)
139 raise ChangesetNotFound unless @changeset
139 raise ChangesetNotFound unless @changeset
140
140
141 respond_to do |format|
141 respond_to do |format|
142 format.html
142 format.html
143 format.js {render :layout => false}
143 format.js {render :layout => false}
144 end
144 end
145 rescue ChangesetNotFound
145 rescue ChangesetNotFound
146 show_error_not_found
146 show_error_not_found
147 end
147 end
148
148
149 def diff
149 def diff
150 if params[:format] == 'diff'
150 if params[:format] == 'diff'
151 @diff = @repository.diff(@path, @rev, @rev_to)
151 @diff = @repository.diff(@path, @rev, @rev_to)
152 show_error_not_found and return unless @diff
152 show_error_not_found and return unless @diff
153 filename = "changeset_r#{@rev}"
153 filename = "changeset_r#{@rev}"
154 filename << "_r#{@rev_to}" if @rev_to
154 filename << "_r#{@rev_to}" if @rev_to
155 send_data @diff.join, :filename => "#{filename}.diff",
155 send_data @diff.join, :filename => "#{filename}.diff",
156 :type => 'text/x-patch',
156 :type => 'text/x-patch',
157 :disposition => 'attachment'
157 :disposition => 'attachment'
158 else
158 else
159 @diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
159 @diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
160 @diff_type = 'inline' unless %w(inline sbs).include?(@diff_type)
160 @diff_type = 'inline' unless %w(inline sbs).include?(@diff_type)
161
161
162 # Save diff type as user preference
162 # Save diff type as user preference
163 if User.current.logged? && @diff_type != User.current.pref[:diff_type]
163 if User.current.logged? && @diff_type != User.current.pref[:diff_type]
164 User.current.pref[:diff_type] = @diff_type
164 User.current.pref[:diff_type] = @diff_type
165 User.current.preference.save
165 User.current.preference.save
166 end
166 end
167
167
168 @cache_key = "repositories/diff/#{@repository.id}/" + Digest::MD5.hexdigest("#{@path}-#{@rev}-#{@rev_to}-#{@diff_type}")
168 @cache_key = "repositories/diff/#{@repository.id}/" + Digest::MD5.hexdigest("#{@path}-#{@rev}-#{@rev_to}-#{@diff_type}")
169 unless read_fragment(@cache_key)
169 unless read_fragment(@cache_key)
170 @diff = @repository.diff(@path, @rev, @rev_to)
170 @diff = @repository.diff(@path, @rev, @rev_to)
171 show_error_not_found unless @diff
171 show_error_not_found unless @diff
172 end
172 end
173 end
173 end
174 end
174 end
175
175
176 def stats
176 def stats
177 end
177 end
178
178
179 def graph
179 def graph
180 data = nil
180 data = nil
181 case params[:graph]
181 case params[:graph]
182 when "commits_per_month"
182 when "commits_per_month"
183 data = graph_commits_per_month(@repository)
183 data = graph_commits_per_month(@repository)
184 when "commits_per_author"
184 when "commits_per_author"
185 data = graph_commits_per_author(@repository)
185 data = graph_commits_per_author(@repository)
186 end
186 end
187 if data
187 if data
188 headers["Content-Type"] = "image/svg+xml"
188 headers["Content-Type"] = "image/svg+xml"
189 send_data(data, :type => "image/svg+xml", :disposition => "inline")
189 send_data(data, :type => "image/svg+xml", :disposition => "inline")
190 else
190 else
191 render_404
191 render_404
192 end
192 end
193 end
193 end
194
194
195 private
195 private
196 def find_project
196 def find_project
197 @project = Project.find(params[:id])
197 @project = Project.find(params[:id])
198 rescue ActiveRecord::RecordNotFound
198 rescue ActiveRecord::RecordNotFound
199 render_404
199 render_404
200 end
200 end
201
201
202 REV_PARAM_RE = %r{^[a-f0-9]*$}
202 REV_PARAM_RE = %r{^[a-f0-9]*$}
203
203
204 def find_repository
204 def find_repository
205 @project = Project.find(params[:id])
205 @project = Project.find(params[:id])
206 @repository = @project.repository
206 @repository = @project.repository
207 render_404 and return false unless @repository
207 render_404 and return false unless @repository
208 @path = params[:path].join('/') unless params[:path].nil?
208 @path = params[:path].join('/') unless params[:path].nil?
209 @path ||= ''
209 @path ||= ''
210 @rev = params[:rev]
210 @rev = params[:rev]
211 @rev_to = params[:rev_to]
211 @rev_to = params[:rev_to]
212 raise InvalidRevisionParam unless @rev.to_s.match(REV_PARAM_RE) && @rev.to_s.match(REV_PARAM_RE)
212 raise InvalidRevisionParam unless @rev.to_s.match(REV_PARAM_RE) && @rev.to_s.match(REV_PARAM_RE)
213 rescue ActiveRecord::RecordNotFound
213 rescue ActiveRecord::RecordNotFound
214 render_404
214 render_404
215 rescue InvalidRevisionParam
215 rescue InvalidRevisionParam
216 show_error_not_found
216 show_error_not_found
217 end
217 end
218
218
219 def show_error_not_found
219 def show_error_not_found
220 render_error l(:error_scm_not_found)
220 render_error l(:error_scm_not_found)
221 end
221 end
222
222
223 # Handler for Redmine::Scm::Adapters::CommandFailed exception
223 # Handler for Redmine::Scm::Adapters::CommandFailed exception
224 def show_error_command_failed(exception)
224 def show_error_command_failed(exception)
225 render_error l(:error_scm_command_failed, exception.message)
225 render_error l(:error_scm_command_failed, exception.message)
226 end
226 end
227
227
228 def graph_commits_per_month(repository)
228 def graph_commits_per_month(repository)
229 @date_to = Date.today
229 @date_to = Date.today
230 @date_from = @date_to << 11
230 @date_from = @date_to << 11
231 @date_from = Date.civil(@date_from.year, @date_from.month, 1)
231 @date_from = Date.civil(@date_from.year, @date_from.month, 1)
232 commits_by_day = repository.changesets.count(:all, :group => :commit_date, :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
232 commits_by_day = repository.changesets.count(:all, :group => :commit_date, :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
233 commits_by_month = [0] * 12
233 commits_by_month = [0] * 12
234 commits_by_day.each {|c| commits_by_month[c.first.to_date.months_ago] += c.last }
234 commits_by_day.each {|c| commits_by_month[c.first.to_date.months_ago] += c.last }
235
235
236 changes_by_day = repository.changes.count(:all, :group => :commit_date, :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
236 changes_by_day = repository.changes.count(:all, :group => :commit_date, :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
237 changes_by_month = [0] * 12
237 changes_by_month = [0] * 12
238 changes_by_day.each {|c| changes_by_month[c.first.to_date.months_ago] += c.last }
238 changes_by_day.each {|c| changes_by_month[c.first.to_date.months_ago] += c.last }
239
239
240 fields = []
240 fields = []
241 12.times {|m| fields << month_name(((Date.today.month - 1 - m) % 12) + 1)}
241 12.times {|m| fields << month_name(((Date.today.month - 1 - m) % 12) + 1)}
242
242
243 graph = SVG::Graph::Bar.new(
243 graph = SVG::Graph::Bar.new(
244 :height => 300,
244 :height => 300,
245 :width => 800,
245 :width => 800,
246 :fields => fields.reverse,
246 :fields => fields.reverse,
247 :stack => :side,
247 :stack => :side,
248 :scale_integers => true,
248 :scale_integers => true,
249 :step_x_labels => 2,
249 :step_x_labels => 2,
250 :show_data_values => false,
250 :show_data_values => false,
251 :graph_title => l(:label_commits_per_month),
251 :graph_title => l(:label_commits_per_month),
252 :show_graph_title => true
252 :show_graph_title => true
253 )
253 )
254
254
255 graph.add_data(
255 graph.add_data(
256 :data => commits_by_month[0..11].reverse,
256 :data => commits_by_month[0..11].reverse,
257 :title => l(:label_revision_plural)
257 :title => l(:label_revision_plural)
258 )
258 )
259
259
260 graph.add_data(
260 graph.add_data(
261 :data => changes_by_month[0..11].reverse,
261 :data => changes_by_month[0..11].reverse,
262 :title => l(:label_change_plural)
262 :title => l(:label_change_plural)
263 )
263 )
264
264
265 graph.burn
265 graph.burn
266 end
266 end
267
267
268 def graph_commits_per_author(repository)
268 def graph_commits_per_author(repository)
269 commits_by_author = repository.changesets.count(:all, :group => :committer)
269 commits_by_author = repository.changesets.count(:all, :group => :committer)
270 commits_by_author.sort! {|x, y| x.last <=> y.last}
270 commits_by_author.sort! {|x, y| x.last <=> y.last}
271
271
272 changes_by_author = repository.changes.count(:all, :group => :committer)
272 changes_by_author = repository.changes.count(:all, :group => :committer)
273 h = changes_by_author.inject({}) {|o, i| o[i.first] = i.last; o}
273 h = changes_by_author.inject({}) {|o, i| o[i.first] = i.last; o}
274
274
275 fields = commits_by_author.collect {|r| r.first}
275 fields = commits_by_author.collect {|r| r.first}
276 commits_data = commits_by_author.collect {|r| r.last}
276 commits_data = commits_by_author.collect {|r| r.last}
277 changes_data = commits_by_author.collect {|r| h[r.first] || 0}
277 changes_data = commits_by_author.collect {|r| h[r.first] || 0}
278
278
279 fields = fields + [""]*(10 - fields.length) if fields.length<10
279 fields = fields + [""]*(10 - fields.length) if fields.length<10
280 commits_data = commits_data + [0]*(10 - commits_data.length) if commits_data.length<10
280 commits_data = commits_data + [0]*(10 - commits_data.length) if commits_data.length<10
281 changes_data = changes_data + [0]*(10 - changes_data.length) if changes_data.length<10
281 changes_data = changes_data + [0]*(10 - changes_data.length) if changes_data.length<10
282
282
283 # Remove email adress in usernames
283 # Remove email adress in usernames
284 fields = fields.collect {|c| c.gsub(%r{<.+@.+>}, '') }
284 fields = fields.collect {|c| c.gsub(%r{<.+@.+>}, '') }
285
285
286 graph = SVG::Graph::BarHorizontal.new(
286 graph = SVG::Graph::BarHorizontal.new(
287 :height => 400,
287 :height => 400,
288 :width => 800,
288 :width => 800,
289 :fields => fields,
289 :fields => fields,
290 :stack => :side,
290 :stack => :side,
291 :scale_integers => true,
291 :scale_integers => true,
292 :show_data_values => false,
292 :show_data_values => false,
293 :rotate_y_labels => false,
293 :rotate_y_labels => false,
294 :graph_title => l(:label_commits_per_author),
294 :graph_title => l(:label_commits_per_author),
295 :show_graph_title => true
295 :show_graph_title => true
296 )
296 )
297
297
298 graph.add_data(
298 graph.add_data(
299 :data => commits_data,
299 :data => commits_data,
300 :title => l(:label_revision_plural)
300 :title => l(:label_revision_plural)
301 )
301 )
302
302
303 graph.add_data(
303 graph.add_data(
304 :data => changes_data,
304 :data => changes_data,
305 :title => l(:label_change_plural)
305 :title => l(:label_change_plural)
306 )
306 )
307
307
308 graph.burn
308 graph.burn
309 end
309 end
310
310
311 end
311 end
312
312
313 class Date
313 class Date
314 def months_ago(date = Date.today)
314 def months_ago(date = Date.today)
315 (date.year - self.year)*12 + (date.month - self.month)
315 (date.year - self.year)*12 + (date.month - self.month)
316 end
316 end
317
317
318 def weeks_ago(date = Date.today)
318 def weeks_ago(date = Date.today)
319 (date.year - self.year)*52 + (date.cweek - self.cweek)
319 (date.year - self.year)*52 + (date.cweek - self.cweek)
320 end
320 end
321 end
321 end
322
322
323 class String
323 class String
324 def with_leading_slash
324 def with_leading_slash
325 starts_with?('/') ? self : "/#{self}"
325 starts_with?('/') ? self : "/#{self}"
326 end
326 end
327 end
327 end
@@ -1,168 +1,168
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 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.revision}" + (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_id, :rev => o.revision}}
29 :url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project, :rev => o.revision}}
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 => {: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 def revision=(r)
44 def revision=(r)
45 write_attribute :revision, (r.nil? ? nil : r.to_s)
45 write_attribute :revision, (r.nil? ? nil : r.to_s)
46 end
46 end
47
47
48 def comments=(comment)
48 def comments=(comment)
49 write_attribute(:comments, Changeset.normalize_comments(comment))
49 write_attribute(:comments, Changeset.normalize_comments(comment))
50 end
50 end
51
51
52 def committed_on=(date)
52 def committed_on=(date)
53 self.commit_date = date
53 self.commit_date = date
54 super
54 super
55 end
55 end
56
56
57 def project
57 def project
58 repository.project
58 repository.project
59 end
59 end
60
60
61 def author
61 def author
62 user || committer.to_s.split('<').first
62 user || committer.to_s.split('<').first
63 end
63 end
64
64
65 def before_create
65 def before_create
66 self.user = repository.find_committer_user(committer)
66 self.user = repository.find_committer_user(committer)
67 end
67 end
68
68
69 def after_create
69 def after_create
70 scan_comment_for_issue_ids
70 scan_comment_for_issue_ids
71 end
71 end
72 require 'pp'
72 require 'pp'
73
73
74 def scan_comment_for_issue_ids
74 def scan_comment_for_issue_ids
75 return if comments.blank?
75 return if comments.blank?
76 # keywords used to reference issues
76 # keywords used to reference issues
77 ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
77 ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
78 # keywords used to fix issues
78 # keywords used to fix issues
79 fix_keywords = Setting.commit_fix_keywords.downcase.split(",").collect(&:strip)
79 fix_keywords = Setting.commit_fix_keywords.downcase.split(",").collect(&:strip)
80 # status and optional done ratio applied
80 # status and optional done ratio applied
81 fix_status = IssueStatus.find_by_id(Setting.commit_fix_status_id)
81 fix_status = IssueStatus.find_by_id(Setting.commit_fix_status_id)
82 done_ratio = Setting.commit_fix_done_ratio.blank? ? nil : Setting.commit_fix_done_ratio.to_i
82 done_ratio = Setting.commit_fix_done_ratio.blank? ? nil : Setting.commit_fix_done_ratio.to_i
83
83
84 kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|")
84 kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|")
85 return if kw_regexp.blank?
85 return if kw_regexp.blank?
86
86
87 referenced_issues = []
87 referenced_issues = []
88
88
89 if ref_keywords.delete('*')
89 if ref_keywords.delete('*')
90 # find any issue ID in the comments
90 # find any issue ID in the comments
91 target_issue_ids = []
91 target_issue_ids = []
92 comments.scan(%r{([\s\(,-]|^)#(\d+)(?=[[:punct:]]|\s|<|$)}).each { |m| target_issue_ids << m[1] }
92 comments.scan(%r{([\s\(,-]|^)#(\d+)(?=[[:punct:]]|\s|<|$)}).each { |m| target_issue_ids << m[1] }
93 referenced_issues += repository.project.issues.find_all_by_id(target_issue_ids)
93 referenced_issues += repository.project.issues.find_all_by_id(target_issue_ids)
94 end
94 end
95
95
96 comments.scan(Regexp.new("(#{kw_regexp})[\s:]+(([\s,;&]*#?\\d+)+)", Regexp::IGNORECASE)).each do |match|
96 comments.scan(Regexp.new("(#{kw_regexp})[\s:]+(([\s,;&]*#?\\d+)+)", Regexp::IGNORECASE)).each do |match|
97 action = match[0]
97 action = match[0]
98 target_issue_ids = match[1].scan(/\d+/)
98 target_issue_ids = match[1].scan(/\d+/)
99 target_issues = repository.project.issues.find_all_by_id(target_issue_ids)
99 target_issues = repository.project.issues.find_all_by_id(target_issue_ids)
100 if fix_status && fix_keywords.include?(action.downcase)
100 if fix_status && fix_keywords.include?(action.downcase)
101 # update status of issues
101 # update status of issues
102 logger.debug "Issues fixed by changeset #{self.revision}: #{issue_ids.join(', ')}." if logger && logger.debug?
102 logger.debug "Issues fixed by changeset #{self.revision}: #{issue_ids.join(', ')}." if logger && logger.debug?
103 target_issues.each do |issue|
103 target_issues.each do |issue|
104 # the issue may have been updated by the closure of another one (eg. duplicate)
104 # the issue may have been updated by the closure of another one (eg. duplicate)
105 issue.reload
105 issue.reload
106 # don't change the status is the issue is closed
106 # don't change the status is the issue is closed
107 next if issue.status.is_closed?
107 next if issue.status.is_closed?
108 csettext = "r#{self.revision}"
108 csettext = "r#{self.revision}"
109 if self.scmid && (! (csettext =~ /^r[0-9]+$/))
109 if self.scmid && (! (csettext =~ /^r[0-9]+$/))
110 csettext = "commit:\"#{self.scmid}\""
110 csettext = "commit:\"#{self.scmid}\""
111 end
111 end
112 journal = issue.init_journal(user || User.anonymous, ll(Setting.default_language, :text_status_changed_by_changeset, csettext))
112 journal = issue.init_journal(user || User.anonymous, ll(Setting.default_language, :text_status_changed_by_changeset, csettext))
113 issue.status = fix_status
113 issue.status = fix_status
114 issue.done_ratio = done_ratio if done_ratio
114 issue.done_ratio = done_ratio if done_ratio
115 issue.save
115 issue.save
116 end
116 end
117 end
117 end
118 referenced_issues += target_issues
118 referenced_issues += target_issues
119 end
119 end
120
120
121 self.issues = referenced_issues.uniq
121 self.issues = referenced_issues.uniq
122 end
122 end
123
123
124 def short_comments
124 def short_comments
125 @short_comments || split_comments.first
125 @short_comments || split_comments.first
126 end
126 end
127
127
128 def long_comments
128 def long_comments
129 @long_comments || split_comments.last
129 @long_comments || split_comments.last
130 end
130 end
131
131
132 # Returns the previous changeset
132 # Returns the previous changeset
133 def previous
133 def previous
134 @previous ||= Changeset.find(:first, :conditions => ['id < ? AND repository_id = ?', self.id, self.repository_id], :order => 'id DESC')
134 @previous ||= Changeset.find(:first, :conditions => ['id < ? AND repository_id = ?', self.id, self.repository_id], :order => 'id DESC')
135 end
135 end
136
136
137 # Returns the next changeset
137 # Returns the next changeset
138 def next
138 def next
139 @next ||= Changeset.find(:first, :conditions => ['id > ? AND repository_id = ?', self.id, self.repository_id], :order => 'id ASC')
139 @next ||= Changeset.find(:first, :conditions => ['id > ? AND repository_id = ?', self.id, self.repository_id], :order => 'id ASC')
140 end
140 end
141
141
142 # Strips and reencodes a commit log before insertion into the database
142 # Strips and reencodes a commit log before insertion into the database
143 def self.normalize_comments(str)
143 def self.normalize_comments(str)
144 to_utf8(str.to_s.strip)
144 to_utf8(str.to_s.strip)
145 end
145 end
146
146
147 private
147 private
148
148
149 def split_comments
149 def split_comments
150 comments =~ /\A(.+?)\r?\n(.*)$/m
150 comments =~ /\A(.+?)\r?\n(.*)$/m
151 @short_comments = $1 || comments
151 @short_comments = $1 || comments
152 @long_comments = $2.to_s.strip
152 @long_comments = $2.to_s.strip
153 return @short_comments, @long_comments
153 return @short_comments, @long_comments
154 end
154 end
155
155
156 def self.to_utf8(str)
156 def self.to_utf8(str)
157 return str if /\A[\r\n\t\x20-\x7e]*\Z/n.match(str) # for us-ascii
157 return str if /\A[\r\n\t\x20-\x7e]*\Z/n.match(str) # for us-ascii
158 encoding = Setting.commit_logs_encoding.to_s.strip
158 encoding = Setting.commit_logs_encoding.to_s.strip
159 unless encoding.blank? || encoding == 'UTF-8'
159 unless encoding.blank? || encoding == 'UTF-8'
160 begin
160 begin
161 return Iconv.conv('UTF-8', encoding, str)
161 return Iconv.conv('UTF-8', encoding, str)
162 rescue Iconv::Failure
162 rescue Iconv::Failure
163 # do nothing here
163 # do nothing here
164 end
164 end
165 end
165 end
166 str
166 str
167 end
167 end
168 end
168 end
General Comments 0
You need to be logged in to leave comments. Login now