##// 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 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2009 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require 'SVG/Graph/Bar'
19 19 require 'SVG/Graph/BarHorizontal'
20 20 require 'digest/sha1'
21 21
22 22 class ChangesetNotFound < Exception; end
23 23 class InvalidRevisionParam < Exception; end
24 24
25 25 class RepositoriesController < ApplicationController
26 26 menu_item :repository
27 27 menu_item :settings, :only => :edit
28 28 default_search_scope :changesets
29 29
30 30 before_filter :find_repository, :except => :edit
31 31 before_filter :find_project, :only => :edit
32 32 before_filter :authorize
33 33 accept_key_auth :revisions
34 34
35 35 rescue_from Redmine::Scm::Adapters::CommandFailed, :with => :show_error_command_failed
36 36
37 37 def edit
38 38 @repository = @project.repository
39 39 if !@repository
40 40 @repository = Repository.factory(params[:repository_scm])
41 41 @repository.project = @project if @repository
42 42 end
43 43 if request.post? && @repository
44 44 @repository.attributes = params[:repository]
45 45 @repository.save
46 46 end
47 47 render(:update) do |page|
48 48 page.replace_html "tab-content-repository", :partial => 'projects/settings/repository'
49 49 if @repository && !@project.repository
50 50 @project.reload #needed to reload association
51 51 page.replace_html "main-menu", render_main_menu(@project)
52 52 end
53 53 end
54 54 end
55 55
56 56 def committers
57 57 @committers = @repository.committers
58 58 @users = @project.users
59 59 additional_user_ids = @committers.collect(&:last).collect(&:to_i) - @users.collect(&:id)
60 60 @users += User.find_all_by_id(additional_user_ids) unless additional_user_ids.empty?
61 61 @users.compact!
62 62 @users.sort!
63 63 if request.post? && params[:committers].is_a?(Hash)
64 64 # Build a hash with repository usernames as keys and corresponding user ids as values
65 65 @repository.committer_ids = params[:committers].values.inject({}) {|h, c| h[c.first] = c.last; h}
66 66 flash[:notice] = l(:notice_successful_update)
67 67 redirect_to :action => 'committers', :id => @project
68 68 end
69 69 end
70 70
71 71 def destroy
72 72 @repository.destroy
73 73 redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'repository'
74 74 end
75 75
76 76 def show
77 77 @repository.fetch_changesets if Setting.autofetch_changesets? && @path.empty?
78 78
79 79 @entries = @repository.entries(@path, @rev)
80 80 if request.xhr?
81 81 @entries ? render(:partial => 'dir_list_content') : render(:nothing => true)
82 82 else
83 83 (show_error_not_found; return) unless @entries
84 84 @changesets = @repository.latest_changesets(@path, @rev)
85 85 @properties = @repository.properties(@path, @rev)
86 86 render :action => 'show'
87 87 end
88 88 end
89 89
90 90 alias_method :browse, :show
91 91
92 92 def changes
93 93 @entry = @repository.entry(@path, @rev)
94 94 (show_error_not_found; return) unless @entry
95 95 @changesets = @repository.latest_changesets(@path, @rev, Setting.repository_log_display_limit.to_i)
96 96 @properties = @repository.properties(@path, @rev)
97 97 end
98 98
99 99 def revisions
100 100 @changeset_count = @repository.changesets.count
101 101 @changeset_pages = Paginator.new self, @changeset_count,
102 102 per_page_option,
103 103 params['page']
104 104 @changesets = @repository.changesets.find(:all,
105 105 :limit => @changeset_pages.items_per_page,
106 106 :offset => @changeset_pages.current.offset,
107 107 :include => [:user, :repository])
108 108
109 109 respond_to do |format|
110 110 format.html { render :layout => false if request.xhr? }
111 111 format.atom { render_feed(@changesets, :title => "#{@project.name}: #{l(:label_revision_plural)}") }
112 112 end
113 113 end
114 114
115 115 def entry
116 116 @entry = @repository.entry(@path, @rev)
117 117 (show_error_not_found; return) unless @entry
118 118
119 119 # If the entry is a dir, show the browser
120 120 (show; return) if @entry.is_dir?
121 121
122 122 @content = @repository.cat(@path, @rev)
123 123 (show_error_not_found; return) unless @content
124 124 if 'raw' == params[:format] || @content.is_binary_data? || (@entry.size && @entry.size > Setting.file_max_size_displayed.to_i.kilobyte)
125 125 # Force the download
126 126 send_data @content, :filename => @path.split('/').last
127 127 else
128 128 # Prevent empty lines when displaying a file with Windows style eol
129 129 @content.gsub!("\r\n", "\n")
130 130 end
131 131 end
132 132
133 133 def annotate
134 134 @entry = @repository.entry(@path, @rev)
135 135 (show_error_not_found; return) unless @entry
136 136
137 137 @annotate = @repository.scm.annotate(@path, @rev)
138 138 (render_error l(:error_scm_annotate); return) if @annotate.nil? || @annotate.empty?
139 139 end
140 140
141 141 def revision
142 142 @changeset = @repository.find_changeset_by_name(@rev)
143 143 raise ChangesetNotFound unless @changeset
144 144
145 145 respond_to do |format|
146 146 format.html
147 147 format.js {render :layout => false}
148 148 end
149 149 rescue ChangesetNotFound
150 150 show_error_not_found
151 151 end
152 152
153 153 def diff
154 154 if params[:format] == 'diff'
155 155 @diff = @repository.diff(@path, @rev, @rev_to)
156 156 (show_error_not_found; return) unless @diff
157 157 filename = "changeset_r#{@rev}"
158 158 filename << "_r#{@rev_to}" if @rev_to
159 159 send_data @diff.join, :filename => "#{filename}.diff",
160 160 :type => 'text/x-patch',
161 161 :disposition => 'attachment'
162 162 else
163 163 @diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
164 164 @diff_type = 'inline' unless %w(inline sbs).include?(@diff_type)
165 165
166 166 # Save diff type as user preference
167 167 if User.current.logged? && @diff_type != User.current.pref[:diff_type]
168 168 User.current.pref[:diff_type] = @diff_type
169 169 User.current.preference.save
170 170 end
171 171
172 172 @cache_key = "repositories/diff/#{@repository.id}/" + Digest::MD5.hexdigest("#{@path}-#{@rev}-#{@rev_to}-#{@diff_type}")
173 173 unless read_fragment(@cache_key)
174 174 @diff = @repository.diff(@path, @rev, @rev_to)
175 175 show_error_not_found unless @diff
176 176 end
177 177
178 178 @changeset = @repository.find_changeset_by_name(@rev)
179 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 181 end
181 182 end
182
183
183 184 def stats
184 185 end
185 186
186 187 def graph
187 188 data = nil
188 189 case params[:graph]
189 190 when "commits_per_month"
190 191 data = graph_commits_per_month(@repository)
191 192 when "commits_per_author"
192 193 data = graph_commits_per_author(@repository)
193 194 end
194 195 if data
195 196 headers["Content-Type"] = "image/svg+xml"
196 197 send_data(data, :type => "image/svg+xml", :disposition => "inline")
197 198 else
198 199 render_404
199 200 end
200 201 end
201 202
202 203 private
203 204
204 205 REV_PARAM_RE = %r{\A[a-f0-9]*\Z}i
205 206
206 207 def find_repository
207 208 @project = Project.find(params[:id])
208 209 @repository = @project.repository
209 210 (render_404; return false) unless @repository
210 211 @path = params[:path].join('/') unless params[:path].nil?
211 212 @path ||= ''
212 213 @rev = params[:rev].blank? ? @repository.default_branch : params[:rev].strip
213 214 @rev_to = params[:rev_to]
214 215
215 216 unless @rev.to_s.match(REV_PARAM_RE) && @rev.to_s.match(REV_PARAM_RE)
216 217 if @repository.branches.blank?
217 218 raise InvalidRevisionParam
218 219 end
219 220 end
220 221 rescue ActiveRecord::RecordNotFound
221 222 render_404
222 223 rescue InvalidRevisionParam
223 224 show_error_not_found
224 225 end
225 226
226 227 def show_error_not_found
227 228 render_error l(:error_scm_not_found)
228 229 end
229 230
230 231 # Handler for Redmine::Scm::Adapters::CommandFailed exception
231 232 def show_error_command_failed(exception)
232 233 render_error l(:error_scm_command_failed, exception.message)
233 234 end
234 235
235 236 def graph_commits_per_month(repository)
236 237 @date_to = Date.today
237 238 @date_from = @date_to << 11
238 239 @date_from = Date.civil(@date_from.year, @date_from.month, 1)
239 240 commits_by_day = repository.changesets.count(:all, :group => :commit_date, :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
240 241 commits_by_month = [0] * 12
241 242 commits_by_day.each {|c| commits_by_month[c.first.to_date.months_ago] += c.last }
242 243
243 244 changes_by_day = repository.changes.count(:all, :group => :commit_date, :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
244 245 changes_by_month = [0] * 12
245 246 changes_by_day.each {|c| changes_by_month[c.first.to_date.months_ago] += c.last }
246 247
247 248 fields = []
248 249 12.times {|m| fields << month_name(((Date.today.month - 1 - m) % 12) + 1)}
249 250
250 251 graph = SVG::Graph::Bar.new(
251 252 :height => 300,
252 253 :width => 800,
253 254 :fields => fields.reverse,
254 255 :stack => :side,
255 256 :scale_integers => true,
256 257 :step_x_labels => 2,
257 258 :show_data_values => false,
258 259 :graph_title => l(:label_commits_per_month),
259 260 :show_graph_title => true
260 261 )
261 262
262 263 graph.add_data(
263 264 :data => commits_by_month[0..11].reverse,
264 265 :title => l(:label_revision_plural)
265 266 )
266 267
267 268 graph.add_data(
268 269 :data => changes_by_month[0..11].reverse,
269 270 :title => l(:label_change_plural)
270 271 )
271 272
272 273 graph.burn
273 274 end
274 275
275 276 def graph_commits_per_author(repository)
276 277 commits_by_author = repository.changesets.count(:all, :group => :committer)
277 278 commits_by_author.to_a.sort! {|x, y| x.last <=> y.last}
278 279
279 280 changes_by_author = repository.changes.count(:all, :group => :committer)
280 281 h = changes_by_author.inject({}) {|o, i| o[i.first] = i.last; o}
281 282
282 283 fields = commits_by_author.collect {|r| r.first}
283 284 commits_data = commits_by_author.collect {|r| r.last}
284 285 changes_data = commits_by_author.collect {|r| h[r.first] || 0}
285 286
286 287 fields = fields + [""]*(10 - fields.length) if fields.length<10
287 288 commits_data = commits_data + [0]*(10 - commits_data.length) if commits_data.length<10
288 289 changes_data = changes_data + [0]*(10 - changes_data.length) if changes_data.length<10
289 290
290 291 # Remove email adress in usernames
291 292 fields = fields.collect {|c| c.gsub(%r{<.+@.+>}, '') }
292 293
293 294 graph = SVG::Graph::BarHorizontal.new(
294 295 :height => 400,
295 296 :width => 800,
296 297 :fields => fields,
297 298 :stack => :side,
298 299 :scale_integers => true,
299 300 :show_data_values => false,
300 301 :rotate_y_labels => false,
301 302 :graph_title => l(:label_commits_per_author),
302 303 :show_graph_title => true
303 304 )
304 305
305 306 graph.add_data(
306 307 :data => commits_data,
307 308 :title => l(:label_revision_plural)
308 309 )
309 310
310 311 graph.add_data(
311 312 :data => changes_data,
312 313 :title => l(:label_change_plural)
313 314 )
314 315
315 316 graph.burn
316 317 end
317 318
318 319 end
319 320
320 321 class Date
321 322 def months_ago(date = Date.today)
322 323 (date.year - self.year)*12 + (date.month - self.month)
323 324 end
324 325
325 326 def weeks_ago(date = Date.today)
326 327 (date.year - self.year)*52 + (date.cweek - self.cweek)
327 328 end
328 329 end
329 330
330 331 class String
331 332 def with_leading_slash
332 333 starts_with?('/') ? self : "/#{self}"
333 334 end
334 335 end
@@ -1,215 +1,222
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Repository < ActiveRecord::Base
19 19 belongs_to :project
20 20 has_many :changesets, :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC"
21 21 has_many :changes, :through => :changesets
22 22
23 23 # Raw SQL to delete changesets and changes in the database
24 24 # has_many :changesets, :dependent => :destroy is too slow for big repositories
25 25 before_destroy :clear_changesets
26 26
27 27 # Checks if the SCM is enabled when creating a repository
28 28 validate_on_create { |r| r.errors.add(:type, :invalid) unless Setting.enabled_scm.include?(r.class.name.demodulize) }
29 29
30 30 # Removes leading and trailing whitespace
31 31 def url=(arg)
32 32 write_attribute(:url, arg ? arg.to_s.strip : nil)
33 33 end
34 34
35 35 # Removes leading and trailing whitespace
36 36 def root_url=(arg)
37 37 write_attribute(:root_url, arg ? arg.to_s.strip : nil)
38 38 end
39 39
40 40 def scm
41 41 @scm ||= self.scm_adapter.new url, root_url, login, password
42 42 update_attribute(:root_url, @scm.root_url) if root_url.blank?
43 43 @scm
44 44 end
45 45
46 46 def scm_name
47 47 self.class.scm_name
48 48 end
49 49
50 50 def supports_cat?
51 51 scm.supports_cat?
52 52 end
53 53
54 54 def supports_annotate?
55 55 scm.supports_annotate?
56 56 end
57 57
58 58 def entry(path=nil, identifier=nil)
59 59 scm.entry(path, identifier)
60 60 end
61 61
62 62 def entries(path=nil, identifier=nil)
63 63 scm.entries(path, identifier)
64 64 end
65 65
66 66 def branches
67 67 scm.branches
68 68 end
69 69
70 70 def tags
71 71 scm.tags
72 72 end
73 73
74 74 def default_branch
75 75 scm.default_branch
76 76 end
77 77
78 78 def properties(path, identifier=nil)
79 79 scm.properties(path, identifier)
80 80 end
81 81
82 82 def cat(path, identifier=nil)
83 83 scm.cat(path, identifier)
84 84 end
85 85
86 86 def diff(path, rev, rev_to)
87 87 scm.diff(path, rev, rev_to)
88 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 97 # Returns a path relative to the url of the repository
91 98 def relative_path(path)
92 99 path
93 100 end
94 101
95 102 # Finds and returns a revision with a number or the beginning of a hash
96 103 def find_changeset_by_name(name)
97 104 changesets.find(:first, :conditions => (name.match(/^\d*$/) ? ["revision = ?", name.to_s] : ["revision LIKE ?", name + '%']))
98 105 end
99 106
100 107 def latest_changeset
101 108 @latest_changeset ||= changesets.find(:first)
102 109 end
103 110
104 111 # Returns the latest changesets for +path+
105 112 # Default behaviour is to search in cached changesets
106 113 def latest_changesets(path, rev, limit=10)
107 114 if path.blank?
108 115 changesets.find(:all, :include => :user,
109 116 :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC",
110 117 :limit => limit)
111 118 else
112 119 changes.find(:all, :include => {:changeset => :user},
113 120 :conditions => ["path = ?", path.with_leading_slash],
114 121 :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC",
115 122 :limit => limit).collect(&:changeset)
116 123 end
117 124 end
118 125
119 126 def scan_changesets_for_issue_ids
120 127 self.changesets.each(&:scan_comment_for_issue_ids)
121 128 end
122 129
123 130 # Returns an array of committers usernames and associated user_id
124 131 def committers
125 132 @committers ||= Changeset.connection.select_rows("SELECT DISTINCT committer, user_id FROM #{Changeset.table_name} WHERE repository_id = #{id}")
126 133 end
127 134
128 135 # Maps committers username to a user ids
129 136 def committer_ids=(h)
130 137 if h.is_a?(Hash)
131 138 committers.each do |committer, user_id|
132 139 new_user_id = h[committer]
133 140 if new_user_id && (new_user_id.to_i != user_id.to_i)
134 141 new_user_id = (new_user_id.to_i > 0 ? new_user_id.to_i : nil)
135 142 Changeset.update_all("user_id = #{ new_user_id.nil? ? 'NULL' : new_user_id }", ["repository_id = ? AND committer = ?", id, committer])
136 143 end
137 144 end
138 145 @committers = nil
139 146 @found_committer_users = nil
140 147 true
141 148 else
142 149 false
143 150 end
144 151 end
145 152
146 153 # Returns the Redmine User corresponding to the given +committer+
147 154 # It will return nil if the committer is not yet mapped and if no User
148 155 # with the same username or email was found
149 156 def find_committer_user(committer)
150 157 unless committer.blank?
151 158 @found_committer_users ||= {}
152 159 return @found_committer_users[committer] if @found_committer_users.has_key?(committer)
153 160
154 161 user = nil
155 162 c = changesets.find(:first, :conditions => {:committer => committer}, :include => :user)
156 163 if c && c.user
157 164 user = c.user
158 165 elsif committer.strip =~ /^([^<]+)(<(.*)>)?$/
159 166 username, email = $1.strip, $3
160 167 u = User.find_by_login(username)
161 168 u ||= User.find_by_mail(email) unless email.blank?
162 169 user = u
163 170 end
164 171 @found_committer_users[committer] = user
165 172 user
166 173 end
167 174 end
168 175
169 176 # Fetches new changesets for all repositories of active projects
170 177 # Can be called periodically by an external script
171 178 # eg. ruby script/runner "Repository.fetch_changesets"
172 179 def self.fetch_changesets
173 180 Project.active.has_module(:repository).find(:all, :include => :repository).each do |project|
174 181 if project.repository
175 182 project.repository.fetch_changesets
176 183 end
177 184 end
178 185 end
179 186
180 187 # scan changeset comments to find related and fixed issues for all repositories
181 188 def self.scan_changesets_for_issue_ids
182 189 find(:all).each(&:scan_changesets_for_issue_ids)
183 190 end
184 191
185 192 def self.scm_name
186 193 'Abstract'
187 194 end
188 195
189 196 def self.available_scm
190 197 subclasses.collect {|klass| [klass.scm_name, klass.name]}
191 198 end
192 199
193 200 def self.factory(klass_name, *args)
194 201 klass = "Repository::#{klass_name}".constantize
195 202 klass.new(*args)
196 203 rescue
197 204 nil
198 205 end
199 206
200 207 private
201 208
202 209 def before_save
203 210 # Strips url and root_url
204 211 url.strip!
205 212 root_url.strip!
206 213 true
207 214 end
208 215
209 216 def clear_changesets
210 217 cs, ch, ci = Changeset.table_name, Change.table_name, "#{table_name_prefix}changesets_issues#{table_name_suffix}"
211 218 connection.delete("DELETE FROM #{ch} WHERE #{ch}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
212 219 connection.delete("DELETE FROM #{ci} WHERE #{ci}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
213 220 connection.delete("DELETE FROM #{cs} WHERE #{cs}.repository_id = #{id}")
214 221 end
215 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 3 <!-- Choose view type -->
4 4 <% form_tag({:path => to_path_param(@path)}, :method => 'get') do %>
5 5 <%= hidden_field_tag('rev', params[:rev]) if params[:rev] %>
6 6 <%= hidden_field_tag('rev_to', params[:rev_to]) if params[:rev_to] %>
7 7 <p><label><%= l(:label_view_diff) %></label>
8 8 <%= select_tag 'type', options_for_select([[l(:label_diff_inline), "inline"], [l(:label_diff_side_by_side), "sbs"]], @diff_type), :onchange => "if (this.value != '') {this.form.submit()}" %></p>
9 9 <% end %>
10 10
11 11 <% cache(@cache_key) do -%>
12 12 <%= render :partial => 'common/diff', :locals => {:diff => @diff, :diff_type => @diff_type} %>
13 13 <% end -%>
14 14
15 15 <% other_formats_links do |f| %>
16 16 <%= f.link_to 'Diff', :url => params, :caption => 'Unified diff' %>
17 17 <% end %>
18 18
19 19 <% html_title(with_leading_slash(@path), 'Diff') -%>
20 20
21 21 <% content_for :header_tags do %>
22 22 <%= stylesheet_link_tag "scm" %>
23 23 <% end %>
General Comments 0
You need to be logged in to leave comments. Login now