##// END OF EJS Templates
scm: changing two revision diff text at SCM adapter level (#3724)....
Toshi MARUYAMA -
r4578:ebb19c58637c
parent child
Show More
@@ -1,334 +1,335
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
177
178 @changeset = @repository.find_changeset_by_name(@rev)
178 @changeset = @repository.find_changeset_by_name(@rev)
179 @changeset_to = @rev_to ? @repository.find_changeset_by_name(@rev_to) : nil
179 @changeset_to = @rev_to ? @repository.find_changeset_by_name(@rev_to) : nil
180 @diff_format_revisions = @repository.diff_format_revisions(@changeset, @changeset_to)
180 end
181 end
181 end
182 end
182
183
183 def stats
184 def stats
184 end
185 end
185
186
186 def graph
187 def graph
187 data = nil
188 data = nil
188 case params[:graph]
189 case params[:graph]
189 when "commits_per_month"
190 when "commits_per_month"
190 data = graph_commits_per_month(@repository)
191 data = graph_commits_per_month(@repository)
191 when "commits_per_author"
192 when "commits_per_author"
192 data = graph_commits_per_author(@repository)
193 data = graph_commits_per_author(@repository)
193 end
194 end
194 if data
195 if data
195 headers["Content-Type"] = "image/svg+xml"
196 headers["Content-Type"] = "image/svg+xml"
196 send_data(data, :type => "image/svg+xml", :disposition => "inline")
197 send_data(data, :type => "image/svg+xml", :disposition => "inline")
197 else
198 else
198 render_404
199 render_404
199 end
200 end
200 end
201 end
201
202
202 private
203 private
203
204
204 REV_PARAM_RE = %r{\A[a-f0-9]*\Z}i
205 REV_PARAM_RE = %r{\A[a-f0-9]*\Z}i
205
206
206 def find_repository
207 def find_repository
207 @project = Project.find(params[:id])
208 @project = Project.find(params[:id])
208 @repository = @project.repository
209 @repository = @project.repository
209 (render_404; return false) unless @repository
210 (render_404; return false) unless @repository
210 @path = params[:path].join('/') unless params[:path].nil?
211 @path = params[:path].join('/') unless params[:path].nil?
211 @path ||= ''
212 @path ||= ''
212 @rev = params[:rev].blank? ? @repository.default_branch : params[:rev].strip
213 @rev = params[:rev].blank? ? @repository.default_branch : params[:rev].strip
213 @rev_to = params[:rev_to]
214 @rev_to = params[:rev_to]
214
215
215 unless @rev.to_s.match(REV_PARAM_RE) && @rev.to_s.match(REV_PARAM_RE)
216 unless @rev.to_s.match(REV_PARAM_RE) && @rev.to_s.match(REV_PARAM_RE)
216 if @repository.branches.blank?
217 if @repository.branches.blank?
217 raise InvalidRevisionParam
218 raise InvalidRevisionParam
218 end
219 end
219 end
220 end
220 rescue ActiveRecord::RecordNotFound
221 rescue ActiveRecord::RecordNotFound
221 render_404
222 render_404
222 rescue InvalidRevisionParam
223 rescue InvalidRevisionParam
223 show_error_not_found
224 show_error_not_found
224 end
225 end
225
226
226 def show_error_not_found
227 def show_error_not_found
227 render_error l(:error_scm_not_found)
228 render_error l(:error_scm_not_found)
228 end
229 end
229
230
230 # Handler for Redmine::Scm::Adapters::CommandFailed exception
231 # Handler for Redmine::Scm::Adapters::CommandFailed exception
231 def show_error_command_failed(exception)
232 def show_error_command_failed(exception)
232 render_error l(:error_scm_command_failed, exception.message)
233 render_error l(:error_scm_command_failed, exception.message)
233 end
234 end
234
235
235 def graph_commits_per_month(repository)
236 def graph_commits_per_month(repository)
236 @date_to = Date.today
237 @date_to = Date.today
237 @date_from = @date_to << 11
238 @date_from = @date_to << 11
238 @date_from = Date.civil(@date_from.year, @date_from.month, 1)
239 @date_from = Date.civil(@date_from.year, @date_from.month, 1)
239 commits_by_day = repository.changesets.count(:all, :group => :commit_date, :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
240 commits_by_day = repository.changesets.count(:all, :group => :commit_date, :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
240 commits_by_month = [0] * 12
241 commits_by_month = [0] * 12
241 commits_by_day.each {|c| commits_by_month[c.first.to_date.months_ago] += c.last }
242 commits_by_day.each {|c| commits_by_month[c.first.to_date.months_ago] += c.last }
242
243
243 changes_by_day = repository.changes.count(:all, :group => :commit_date, :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
244 changes_by_day = repository.changes.count(:all, :group => :commit_date, :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
244 changes_by_month = [0] * 12
245 changes_by_month = [0] * 12
245 changes_by_day.each {|c| changes_by_month[c.first.to_date.months_ago] += c.last }
246 changes_by_day.each {|c| changes_by_month[c.first.to_date.months_ago] += c.last }
246
247
247 fields = []
248 fields = []
248 12.times {|m| fields << month_name(((Date.today.month - 1 - m) % 12) + 1)}
249 12.times {|m| fields << month_name(((Date.today.month - 1 - m) % 12) + 1)}
249
250
250 graph = SVG::Graph::Bar.new(
251 graph = SVG::Graph::Bar.new(
251 :height => 300,
252 :height => 300,
252 :width => 800,
253 :width => 800,
253 :fields => fields.reverse,
254 :fields => fields.reverse,
254 :stack => :side,
255 :stack => :side,
255 :scale_integers => true,
256 :scale_integers => true,
256 :step_x_labels => 2,
257 :step_x_labels => 2,
257 :show_data_values => false,
258 :show_data_values => false,
258 :graph_title => l(:label_commits_per_month),
259 :graph_title => l(:label_commits_per_month),
259 :show_graph_title => true
260 :show_graph_title => true
260 )
261 )
261
262
262 graph.add_data(
263 graph.add_data(
263 :data => commits_by_month[0..11].reverse,
264 :data => commits_by_month[0..11].reverse,
264 :title => l(:label_revision_plural)
265 :title => l(:label_revision_plural)
265 )
266 )
266
267
267 graph.add_data(
268 graph.add_data(
268 :data => changes_by_month[0..11].reverse,
269 :data => changes_by_month[0..11].reverse,
269 :title => l(:label_change_plural)
270 :title => l(:label_change_plural)
270 )
271 )
271
272
272 graph.burn
273 graph.burn
273 end
274 end
274
275
275 def graph_commits_per_author(repository)
276 def graph_commits_per_author(repository)
276 commits_by_author = repository.changesets.count(:all, :group => :committer)
277 commits_by_author = repository.changesets.count(:all, :group => :committer)
277 commits_by_author.to_a.sort! {|x, y| x.last <=> y.last}
278 commits_by_author.to_a.sort! {|x, y| x.last <=> y.last}
278
279
279 changes_by_author = repository.changes.count(:all, :group => :committer)
280 changes_by_author = repository.changes.count(:all, :group => :committer)
280 h = changes_by_author.inject({}) {|o, i| o[i.first] = i.last; o}
281 h = changes_by_author.inject({}) {|o, i| o[i.first] = i.last; o}
281
282
282 fields = commits_by_author.collect {|r| r.first}
283 fields = commits_by_author.collect {|r| r.first}
283 commits_data = commits_by_author.collect {|r| r.last}
284 commits_data = commits_by_author.collect {|r| r.last}
284 changes_data = commits_by_author.collect {|r| h[r.first] || 0}
285 changes_data = commits_by_author.collect {|r| h[r.first] || 0}
285
286
286 fields = fields + [""]*(10 - fields.length) if fields.length<10
287 fields = fields + [""]*(10 - fields.length) if fields.length<10
287 commits_data = commits_data + [0]*(10 - commits_data.length) if commits_data.length<10
288 commits_data = commits_data + [0]*(10 - commits_data.length) if commits_data.length<10
288 changes_data = changes_data + [0]*(10 - changes_data.length) if changes_data.length<10
289 changes_data = changes_data + [0]*(10 - changes_data.length) if changes_data.length<10
289
290
290 # Remove email adress in usernames
291 # Remove email adress in usernames
291 fields = fields.collect {|c| c.gsub(%r{<.+@.+>}, '') }
292 fields = fields.collect {|c| c.gsub(%r{<.+@.+>}, '') }
292
293
293 graph = SVG::Graph::BarHorizontal.new(
294 graph = SVG::Graph::BarHorizontal.new(
294 :height => 400,
295 :height => 400,
295 :width => 800,
296 :width => 800,
296 :fields => fields,
297 :fields => fields,
297 :stack => :side,
298 :stack => :side,
298 :scale_integers => true,
299 :scale_integers => true,
299 :show_data_values => false,
300 :show_data_values => false,
300 :rotate_y_labels => false,
301 :rotate_y_labels => false,
301 :graph_title => l(:label_commits_per_author),
302 :graph_title => l(:label_commits_per_author),
302 :show_graph_title => true
303 :show_graph_title => true
303 )
304 )
304
305
305 graph.add_data(
306 graph.add_data(
306 :data => commits_data,
307 :data => commits_data,
307 :title => l(:label_revision_plural)
308 :title => l(:label_revision_plural)
308 )
309 )
309
310
310 graph.add_data(
311 graph.add_data(
311 :data => changes_data,
312 :data => changes_data,
312 :title => l(:label_change_plural)
313 :title => l(:label_change_plural)
313 )
314 )
314
315
315 graph.burn
316 graph.burn
316 end
317 end
317
318
318 end
319 end
319
320
320 class Date
321 class Date
321 def months_ago(date = Date.today)
322 def months_ago(date = Date.today)
322 (date.year - self.year)*12 + (date.month - self.month)
323 (date.year - self.year)*12 + (date.month - self.month)
323 end
324 end
324
325
325 def weeks_ago(date = Date.today)
326 def weeks_ago(date = Date.today)
326 (date.year - self.year)*52 + (date.cweek - self.cweek)
327 (date.year - self.year)*52 + (date.cweek - self.cweek)
327 end
328 end
328 end
329 end
329
330
330 class String
331 class String
331 def with_leading_slash
332 def with_leading_slash
332 starts_with?('/') ? self : "/#{self}"
333 starts_with?('/') ? self : "/#{self}"
333 end
334 end
334 end
335 end
@@ -1,215 +1,222
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 class Repository < ActiveRecord::Base
18 class Repository < ActiveRecord::Base
19 belongs_to :project
19 belongs_to :project
20 has_many :changesets, :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC"
20 has_many :changesets, :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC"
21 has_many :changes, :through => :changesets
21 has_many :changes, :through => :changesets
22
22
23 # Raw SQL to delete changesets and changes in the database
23 # Raw SQL to delete changesets and changes in the database
24 # has_many :changesets, :dependent => :destroy is too slow for big repositories
24 # has_many :changesets, :dependent => :destroy is too slow for big repositories
25 before_destroy :clear_changesets
25 before_destroy :clear_changesets
26
26
27 # Checks if the SCM is enabled when creating a repository
27 # Checks if the SCM is enabled when creating a repository
28 validate_on_create { |r| r.errors.add(:type, :invalid) unless Setting.enabled_scm.include?(r.class.name.demodulize) }
28 validate_on_create { |r| r.errors.add(:type, :invalid) unless Setting.enabled_scm.include?(r.class.name.demodulize) }
29
29
30 # Removes leading and trailing whitespace
30 # Removes leading and trailing whitespace
31 def url=(arg)
31 def url=(arg)
32 write_attribute(:url, arg ? arg.to_s.strip : nil)
32 write_attribute(:url, arg ? arg.to_s.strip : nil)
33 end
33 end
34
34
35 # Removes leading and trailing whitespace
35 # Removes leading and trailing whitespace
36 def root_url=(arg)
36 def root_url=(arg)
37 write_attribute(:root_url, arg ? arg.to_s.strip : nil)
37 write_attribute(:root_url, arg ? arg.to_s.strip : nil)
38 end
38 end
39
39
40 def scm
40 def scm
41 @scm ||= self.scm_adapter.new url, root_url, login, password
41 @scm ||= self.scm_adapter.new url, root_url, login, password
42 update_attribute(:root_url, @scm.root_url) if root_url.blank?
42 update_attribute(:root_url, @scm.root_url) if root_url.blank?
43 @scm
43 @scm
44 end
44 end
45
45
46 def scm_name
46 def scm_name
47 self.class.scm_name
47 self.class.scm_name
48 end
48 end
49
49
50 def supports_cat?
50 def supports_cat?
51 scm.supports_cat?
51 scm.supports_cat?
52 end
52 end
53
53
54 def supports_annotate?
54 def supports_annotate?
55 scm.supports_annotate?
55 scm.supports_annotate?
56 end
56 end
57
57
58 def entry(path=nil, identifier=nil)
58 def entry(path=nil, identifier=nil)
59 scm.entry(path, identifier)
59 scm.entry(path, identifier)
60 end
60 end
61
61
62 def entries(path=nil, identifier=nil)
62 def entries(path=nil, identifier=nil)
63 scm.entries(path, identifier)
63 scm.entries(path, identifier)
64 end
64 end
65
65
66 def branches
66 def branches
67 scm.branches
67 scm.branches
68 end
68 end
69
69
70 def tags
70 def tags
71 scm.tags
71 scm.tags
72 end
72 end
73
73
74 def default_branch
74 def default_branch
75 scm.default_branch
75 scm.default_branch
76 end
76 end
77
77
78 def properties(path, identifier=nil)
78 def properties(path, identifier=nil)
79 scm.properties(path, identifier)
79 scm.properties(path, identifier)
80 end
80 end
81
81
82 def cat(path, identifier=nil)
82 def cat(path, identifier=nil)
83 scm.cat(path, identifier)
83 scm.cat(path, identifier)
84 end
84 end
85
85
86 def diff(path, rev, rev_to)
86 def diff(path, rev, rev_to)
87 scm.diff(path, rev, rev_to)
87 scm.diff(path, rev, rev_to)
88 end
88 end
89
89
90 def diff_format_revisions(cs, cs_to, sep=':')
91 text = ""
92 text << cs_to.format_identifier + sep if cs_to
93 text << cs.format_identifier if cs
94 text
95 end
96
90 # Returns a path relative to the url of the repository
97 # Returns a path relative to the url of the repository
91 def relative_path(path)
98 def relative_path(path)
92 path
99 path
93 end
100 end
94
101
95 # Finds and returns a revision with a number or the beginning of a hash
102 # Finds and returns a revision with a number or the beginning of a hash
96 def find_changeset_by_name(name)
103 def find_changeset_by_name(name)
97 changesets.find(:first, :conditions => (name.match(/^\d*$/) ? ["revision = ?", name.to_s] : ["revision LIKE ?", name + '%']))
104 changesets.find(:first, :conditions => (name.match(/^\d*$/) ? ["revision = ?", name.to_s] : ["revision LIKE ?", name + '%']))
98 end
105 end
99
106
100 def latest_changeset
107 def latest_changeset
101 @latest_changeset ||= changesets.find(:first)
108 @latest_changeset ||= changesets.find(:first)
102 end
109 end
103
110
104 # Returns the latest changesets for +path+
111 # Returns the latest changesets for +path+
105 # Default behaviour is to search in cached changesets
112 # Default behaviour is to search in cached changesets
106 def latest_changesets(path, rev, limit=10)
113 def latest_changesets(path, rev, limit=10)
107 if path.blank?
114 if path.blank?
108 changesets.find(:all, :include => :user,
115 changesets.find(:all, :include => :user,
109 :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC",
116 :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC",
110 :limit => limit)
117 :limit => limit)
111 else
118 else
112 changes.find(:all, :include => {:changeset => :user},
119 changes.find(:all, :include => {:changeset => :user},
113 :conditions => ["path = ?", path.with_leading_slash],
120 :conditions => ["path = ?", path.with_leading_slash],
114 :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC",
121 :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC",
115 :limit => limit).collect(&:changeset)
122 :limit => limit).collect(&:changeset)
116 end
123 end
117 end
124 end
118
125
119 def scan_changesets_for_issue_ids
126 def scan_changesets_for_issue_ids
120 self.changesets.each(&:scan_comment_for_issue_ids)
127 self.changesets.each(&:scan_comment_for_issue_ids)
121 end
128 end
122
129
123 # Returns an array of committers usernames and associated user_id
130 # Returns an array of committers usernames and associated user_id
124 def committers
131 def committers
125 @committers ||= Changeset.connection.select_rows("SELECT DISTINCT committer, user_id FROM #{Changeset.table_name} WHERE repository_id = #{id}")
132 @committers ||= Changeset.connection.select_rows("SELECT DISTINCT committer, user_id FROM #{Changeset.table_name} WHERE repository_id = #{id}")
126 end
133 end
127
134
128 # Maps committers username to a user ids
135 # Maps committers username to a user ids
129 def committer_ids=(h)
136 def committer_ids=(h)
130 if h.is_a?(Hash)
137 if h.is_a?(Hash)
131 committers.each do |committer, user_id|
138 committers.each do |committer, user_id|
132 new_user_id = h[committer]
139 new_user_id = h[committer]
133 if new_user_id && (new_user_id.to_i != user_id.to_i)
140 if new_user_id && (new_user_id.to_i != user_id.to_i)
134 new_user_id = (new_user_id.to_i > 0 ? new_user_id.to_i : nil)
141 new_user_id = (new_user_id.to_i > 0 ? new_user_id.to_i : nil)
135 Changeset.update_all("user_id = #{ new_user_id.nil? ? 'NULL' : new_user_id }", ["repository_id = ? AND committer = ?", id, committer])
142 Changeset.update_all("user_id = #{ new_user_id.nil? ? 'NULL' : new_user_id }", ["repository_id = ? AND committer = ?", id, committer])
136 end
143 end
137 end
144 end
138 @committers = nil
145 @committers = nil
139 @found_committer_users = nil
146 @found_committer_users = nil
140 true
147 true
141 else
148 else
142 false
149 false
143 end
150 end
144 end
151 end
145
152
146 # Returns the Redmine User corresponding to the given +committer+
153 # Returns the Redmine User corresponding to the given +committer+
147 # It will return nil if the committer is not yet mapped and if no User
154 # It will return nil if the committer is not yet mapped and if no User
148 # with the same username or email was found
155 # with the same username or email was found
149 def find_committer_user(committer)
156 def find_committer_user(committer)
150 unless committer.blank?
157 unless committer.blank?
151 @found_committer_users ||= {}
158 @found_committer_users ||= {}
152 return @found_committer_users[committer] if @found_committer_users.has_key?(committer)
159 return @found_committer_users[committer] if @found_committer_users.has_key?(committer)
153
160
154 user = nil
161 user = nil
155 c = changesets.find(:first, :conditions => {:committer => committer}, :include => :user)
162 c = changesets.find(:first, :conditions => {:committer => committer}, :include => :user)
156 if c && c.user
163 if c && c.user
157 user = c.user
164 user = c.user
158 elsif committer.strip =~ /^([^<]+)(<(.*)>)?$/
165 elsif committer.strip =~ /^([^<]+)(<(.*)>)?$/
159 username, email = $1.strip, $3
166 username, email = $1.strip, $3
160 u = User.find_by_login(username)
167 u = User.find_by_login(username)
161 u ||= User.find_by_mail(email) unless email.blank?
168 u ||= User.find_by_mail(email) unless email.blank?
162 user = u
169 user = u
163 end
170 end
164 @found_committer_users[committer] = user
171 @found_committer_users[committer] = user
165 user
172 user
166 end
173 end
167 end
174 end
168
175
169 # Fetches new changesets for all repositories of active projects
176 # Fetches new changesets for all repositories of active projects
170 # Can be called periodically by an external script
177 # Can be called periodically by an external script
171 # eg. ruby script/runner "Repository.fetch_changesets"
178 # eg. ruby script/runner "Repository.fetch_changesets"
172 def self.fetch_changesets
179 def self.fetch_changesets
173 Project.active.has_module(:repository).find(:all, :include => :repository).each do |project|
180 Project.active.has_module(:repository).find(:all, :include => :repository).each do |project|
174 if project.repository
181 if project.repository
175 project.repository.fetch_changesets
182 project.repository.fetch_changesets
176 end
183 end
177 end
184 end
178 end
185 end
179
186
180 # scan changeset comments to find related and fixed issues for all repositories
187 # scan changeset comments to find related and fixed issues for all repositories
181 def self.scan_changesets_for_issue_ids
188 def self.scan_changesets_for_issue_ids
182 find(:all).each(&:scan_changesets_for_issue_ids)
189 find(:all).each(&:scan_changesets_for_issue_ids)
183 end
190 end
184
191
185 def self.scm_name
192 def self.scm_name
186 'Abstract'
193 'Abstract'
187 end
194 end
188
195
189 def self.available_scm
196 def self.available_scm
190 subclasses.collect {|klass| [klass.scm_name, klass.name]}
197 subclasses.collect {|klass| [klass.scm_name, klass.name]}
191 end
198 end
192
199
193 def self.factory(klass_name, *args)
200 def self.factory(klass_name, *args)
194 klass = "Repository::#{klass_name}".constantize
201 klass = "Repository::#{klass_name}".constantize
195 klass.new(*args)
202 klass.new(*args)
196 rescue
203 rescue
197 nil
204 nil
198 end
205 end
199
206
200 private
207 private
201
208
202 def before_save
209 def before_save
203 # Strips url and root_url
210 # Strips url and root_url
204 url.strip!
211 url.strip!
205 root_url.strip!
212 root_url.strip!
206 true
213 true
207 end
214 end
208
215
209 def clear_changesets
216 def clear_changesets
210 cs, ch, ci = Changeset.table_name, Change.table_name, "#{table_name_prefix}changesets_issues#{table_name_suffix}"
217 cs, ch, ci = Changeset.table_name, Change.table_name, "#{table_name_prefix}changesets_issues#{table_name_suffix}"
211 connection.delete("DELETE FROM #{ch} WHERE #{ch}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
218 connection.delete("DELETE FROM #{ch} WHERE #{ch}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
212 connection.delete("DELETE FROM #{ci} WHERE #{ci}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
219 connection.delete("DELETE FROM #{ci} WHERE #{ci}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
213 connection.delete("DELETE FROM #{cs} WHERE #{cs}.repository_id = #{id}")
220 connection.delete("DELETE FROM #{cs} WHERE #{cs}.repository_id = #{id}")
214 end
221 end
215 end
222 end
@@ -1,23 +1,23
1 <h2><%= l(:label_revision) %> <%= format_revision(@changeset_to) + ':' if @changeset_to %><%= format_revision(@changeset) %> <%=h @path %></h2>
1 <h2><%= l(:label_revision) %> <%= @diff_format_revisions %> <%=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 %>
General Comments 0
You need to be logged in to leave comments. Login now