##// END OF EJS Templates
Move some RepositoriesController logic to Repository#stats_by_author (#13487)....
Jean-Baptiste Barth -
r12997:41bf39df3655
parent child
Show More
@@ -1,438 +1,439
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2014 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 require 'redmine/scm/adapters'
22 22
23 23 class ChangesetNotFound < Exception; end
24 24 class InvalidRevisionParam < Exception; end
25 25
26 26 class RepositoriesController < ApplicationController
27 27 menu_item :repository
28 28 menu_item :settings, :only => [:new, :create, :edit, :update, :destroy, :committers]
29 29 default_search_scope :changesets
30 30
31 31 before_filter :find_project_by_project_id, :only => [:new, :create]
32 32 before_filter :find_repository, :only => [:edit, :update, :destroy, :committers]
33 33 before_filter :find_project_repository, :except => [:new, :create, :edit, :update, :destroy, :committers]
34 34 before_filter :find_changeset, :only => [:revision, :add_related_issue, :remove_related_issue]
35 35 before_filter :authorize
36 36 accept_rss_auth :revisions
37 37
38 38 rescue_from Redmine::Scm::Adapters::CommandFailed, :with => :show_error_command_failed
39 39
40 40 def new
41 41 scm = params[:repository_scm] || (Redmine::Scm::Base.all & Setting.enabled_scm).first
42 42 @repository = Repository.factory(scm)
43 43 @repository.is_default = @project.repository.nil?
44 44 @repository.project = @project
45 45 end
46 46
47 47 def create
48 48 attrs = pickup_extra_info
49 49 @repository = Repository.factory(params[:repository_scm])
50 50 @repository.safe_attributes = params[:repository]
51 51 if attrs[:attrs_extra].keys.any?
52 52 @repository.merge_extra_info(attrs[:attrs_extra])
53 53 end
54 54 @repository.project = @project
55 55 if request.post? && @repository.save
56 56 redirect_to settings_project_path(@project, :tab => 'repositories')
57 57 else
58 58 render :action => 'new'
59 59 end
60 60 end
61 61
62 62 def edit
63 63 end
64 64
65 65 def update
66 66 attrs = pickup_extra_info
67 67 @repository.safe_attributes = attrs[:attrs]
68 68 if attrs[:attrs_extra].keys.any?
69 69 @repository.merge_extra_info(attrs[:attrs_extra])
70 70 end
71 71 @repository.project = @project
72 72 if request.put? && @repository.save
73 73 redirect_to settings_project_path(@project, :tab => 'repositories')
74 74 else
75 75 render :action => 'edit'
76 76 end
77 77 end
78 78
79 79 def pickup_extra_info
80 80 p = {}
81 81 p_extra = {}
82 82 params[:repository].each do |k, v|
83 83 if k =~ /^extra_/
84 84 p_extra[k] = v
85 85 else
86 86 p[k] = v
87 87 end
88 88 end
89 89 {:attrs => p, :attrs_extra => p_extra}
90 90 end
91 91 private :pickup_extra_info
92 92
93 93 def committers
94 94 @committers = @repository.committers
95 95 @users = @project.users
96 96 additional_user_ids = @committers.collect(&:last).collect(&:to_i) - @users.collect(&:id)
97 97 @users += User.where(:id => additional_user_ids).all unless additional_user_ids.empty?
98 98 @users.compact!
99 99 @users.sort!
100 100 if request.post? && params[:committers].is_a?(Hash)
101 101 # Build a hash with repository usernames as keys and corresponding user ids as values
102 102 @repository.committer_ids = params[:committers].values.inject({}) {|h, c| h[c.first] = c.last; h}
103 103 flash[:notice] = l(:notice_successful_update)
104 104 redirect_to settings_project_path(@project, :tab => 'repositories')
105 105 end
106 106 end
107 107
108 108 def destroy
109 109 @repository.destroy if request.delete?
110 110 redirect_to settings_project_path(@project, :tab => 'repositories')
111 111 end
112 112
113 113 def show
114 114 @repository.fetch_changesets if @project.active? && Setting.autofetch_changesets? && @path.empty?
115 115
116 116 @entries = @repository.entries(@path, @rev)
117 117 @changeset = @repository.find_changeset_by_name(@rev)
118 118 if request.xhr?
119 119 @entries ? render(:partial => 'dir_list_content') : render(:nothing => true)
120 120 else
121 121 (show_error_not_found; return) unless @entries
122 122 @changesets = @repository.latest_changesets(@path, @rev)
123 123 @properties = @repository.properties(@path, @rev)
124 124 @repositories = @project.repositories
125 125 render :action => 'show'
126 126 end
127 127 end
128 128
129 129 alias_method :browse, :show
130 130
131 131 def changes
132 132 @entry = @repository.entry(@path, @rev)
133 133 (show_error_not_found; return) unless @entry
134 134 @changesets = @repository.latest_changesets(@path, @rev, Setting.repository_log_display_limit.to_i)
135 135 @properties = @repository.properties(@path, @rev)
136 136 @changeset = @repository.find_changeset_by_name(@rev)
137 137 end
138 138
139 139 def revisions
140 140 @changeset_count = @repository.changesets.count
141 141 @changeset_pages = Paginator.new @changeset_count,
142 142 per_page_option,
143 143 params['page']
144 144 @changesets = @repository.changesets.
145 145 limit(@changeset_pages.per_page).
146 146 offset(@changeset_pages.offset).
147 147 includes(:user, :repository, :parents).
148 148 all
149 149
150 150 respond_to do |format|
151 151 format.html { render :layout => false if request.xhr? }
152 152 format.atom { render_feed(@changesets, :title => "#{@project.name}: #{l(:label_revision_plural)}") }
153 153 end
154 154 end
155 155
156 156 def raw
157 157 entry_and_raw(true)
158 158 end
159 159
160 160 def entry
161 161 entry_and_raw(false)
162 162 end
163 163
164 164 def entry_and_raw(is_raw)
165 165 @entry = @repository.entry(@path, @rev)
166 166 (show_error_not_found; return) unless @entry
167 167
168 168 # If the entry is a dir, show the browser
169 169 (show; return) if @entry.is_dir?
170 170
171 171 @content = @repository.cat(@path, @rev)
172 172 (show_error_not_found; return) unless @content
173 173 if is_raw ||
174 174 (@content.size && @content.size > Setting.file_max_size_displayed.to_i.kilobyte) ||
175 175 ! is_entry_text_data?(@content, @path)
176 176 # Force the download
177 177 send_opt = { :filename => filename_for_content_disposition(@path.split('/').last) }
178 178 send_type = Redmine::MimeType.of(@path)
179 179 send_opt[:type] = send_type.to_s if send_type
180 180 send_opt[:disposition] = (Redmine::MimeType.is_type?('image', @path) && !is_raw ? 'inline' : 'attachment')
181 181 send_data @content, send_opt
182 182 else
183 183 # Prevent empty lines when displaying a file with Windows style eol
184 184 # TODO: UTF-16
185 185 # Is this needs? AttachmentsController reads file simply.
186 186 @content.gsub!("\r\n", "\n")
187 187 @changeset = @repository.find_changeset_by_name(@rev)
188 188 end
189 189 end
190 190 private :entry_and_raw
191 191
192 192 def is_entry_text_data?(ent, path)
193 193 # UTF-16 contains "\x00".
194 194 # It is very strict that file contains less than 30% of ascii symbols
195 195 # in non Western Europe.
196 196 return true if Redmine::MimeType.is_type?('text', path)
197 197 # Ruby 1.8.6 has a bug of integer divisions.
198 198 # http://apidock.com/ruby/v1_8_6_287/String/is_binary_data%3F
199 199 return false if ent.is_binary_data?
200 200 true
201 201 end
202 202 private :is_entry_text_data?
203 203
204 204 def annotate
205 205 @entry = @repository.entry(@path, @rev)
206 206 (show_error_not_found; return) unless @entry
207 207
208 208 @annotate = @repository.scm.annotate(@path, @rev)
209 209 if @annotate.nil? || @annotate.empty?
210 210 (render_error l(:error_scm_annotate); return)
211 211 end
212 212 ann_buf_size = 0
213 213 @annotate.lines.each do |buf|
214 214 ann_buf_size += buf.size
215 215 end
216 216 if ann_buf_size > Setting.file_max_size_displayed.to_i.kilobyte
217 217 (render_error l(:error_scm_annotate_big_text_file); return)
218 218 end
219 219 @changeset = @repository.find_changeset_by_name(@rev)
220 220 end
221 221
222 222 def revision
223 223 respond_to do |format|
224 224 format.html
225 225 format.js {render :layout => false}
226 226 end
227 227 end
228 228
229 229 # Adds a related issue to a changeset
230 230 # POST /projects/:project_id/repository/(:repository_id/)revisions/:rev/issues
231 231 def add_related_issue
232 232 issue_id = params[:issue_id].to_s.sub(/^#/,'')
233 233 @issue = @changeset.find_referenced_issue_by_id(issue_id)
234 234 if @issue && (!@issue.visible? || @changeset.issues.include?(@issue))
235 235 @issue = nil
236 236 end
237 237
238 238 if @issue
239 239 @changeset.issues << @issue
240 240 end
241 241 end
242 242
243 243 # Removes a related issue from a changeset
244 244 # DELETE /projects/:project_id/repository/(:repository_id/)revisions/:rev/issues/:issue_id
245 245 def remove_related_issue
246 246 @issue = Issue.visible.find_by_id(params[:issue_id])
247 247 if @issue
248 248 @changeset.issues.delete(@issue)
249 249 end
250 250 end
251 251
252 252 def diff
253 253 if params[:format] == 'diff'
254 254 @diff = @repository.diff(@path, @rev, @rev_to)
255 255 (show_error_not_found; return) unless @diff
256 256 filename = "changeset_r#{@rev}"
257 257 filename << "_r#{@rev_to}" if @rev_to
258 258 send_data @diff.join, :filename => "#{filename}.diff",
259 259 :type => 'text/x-patch',
260 260 :disposition => 'attachment'
261 261 else
262 262 @diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
263 263 @diff_type = 'inline' unless %w(inline sbs).include?(@diff_type)
264 264
265 265 # Save diff type as user preference
266 266 if User.current.logged? && @diff_type != User.current.pref[:diff_type]
267 267 User.current.pref[:diff_type] = @diff_type
268 268 User.current.preference.save
269 269 end
270 270 @cache_key = "repositories/diff/#{@repository.id}/" +
271 271 Digest::MD5.hexdigest("#{@path}-#{@rev}-#{@rev_to}-#{@diff_type}-#{current_language}")
272 272 unless read_fragment(@cache_key)
273 273 @diff = @repository.diff(@path, @rev, @rev_to)
274 274 show_error_not_found unless @diff
275 275 end
276 276
277 277 @changeset = @repository.find_changeset_by_name(@rev)
278 278 @changeset_to = @rev_to ? @repository.find_changeset_by_name(@rev_to) : nil
279 279 @diff_format_revisions = @repository.diff_format_revisions(@changeset, @changeset_to)
280 280 end
281 281 end
282 282
283 283 def stats
284 284 end
285 285
286 286 def graph
287 287 data = nil
288 288 case params[:graph]
289 289 when "commits_per_month"
290 290 data = graph_commits_per_month(@repository)
291 291 when "commits_per_author"
292 292 data = graph_commits_per_author(@repository)
293 293 end
294 294 if data
295 295 headers["Content-Type"] = "image/svg+xml"
296 296 send_data(data, :type => "image/svg+xml", :disposition => "inline")
297 297 else
298 298 render_404
299 299 end
300 300 end
301 301
302 302 private
303 303
304 304 def find_repository
305 305 @repository = Repository.find(params[:id])
306 306 @project = @repository.project
307 307 rescue ActiveRecord::RecordNotFound
308 308 render_404
309 309 end
310 310
311 311 REV_PARAM_RE = %r{\A[a-f0-9]*\Z}i
312 312
313 313 def find_project_repository
314 314 @project = Project.find(params[:id])
315 315 if params[:repository_id].present?
316 316 @repository = @project.repositories.find_by_identifier_param(params[:repository_id])
317 317 else
318 318 @repository = @project.repository
319 319 end
320 320 (render_404; return false) unless @repository
321 321 @path = params[:path].is_a?(Array) ? params[:path].join('/') : params[:path].to_s
322 322 @rev = params[:rev].blank? ? @repository.default_branch : params[:rev].to_s.strip
323 323 @rev_to = params[:rev_to]
324 324
325 325 unless @rev.to_s.match(REV_PARAM_RE) && @rev_to.to_s.match(REV_PARAM_RE)
326 326 if @repository.branches.blank?
327 327 raise InvalidRevisionParam
328 328 end
329 329 end
330 330 rescue ActiveRecord::RecordNotFound
331 331 render_404
332 332 rescue InvalidRevisionParam
333 333 show_error_not_found
334 334 end
335 335
336 336 def find_changeset
337 337 if @rev.present?
338 338 @changeset = @repository.find_changeset_by_name(@rev)
339 339 end
340 340 show_error_not_found unless @changeset
341 341 end
342 342
343 343 def show_error_not_found
344 344 render_error :message => l(:error_scm_not_found), :status => 404
345 345 end
346 346
347 347 # Handler for Redmine::Scm::Adapters::CommandFailed exception
348 348 def show_error_command_failed(exception)
349 349 render_error l(:error_scm_command_failed, exception.message)
350 350 end
351 351
352 352 def graph_commits_per_month(repository)
353 353 @date_to = Date.today
354 354 @date_from = @date_to << 11
355 355 @date_from = Date.civil(@date_from.year, @date_from.month, 1)
356 356 commits_by_day = Changeset.
357 357 where("repository_id = ? AND commit_date BETWEEN ? AND ?", repository.id, @date_from, @date_to).
358 358 group(:commit_date).
359 359 count
360 360 commits_by_month = [0] * 12
361 361 commits_by_day.each {|c| commits_by_month[(@date_to.month - c.first.to_date.month) % 12] += c.last }
362 362
363 363 changes_by_day = Change.
364 364 joins(:changeset).
365 365 where("#{Changeset.table_name}.repository_id = ? AND #{Changeset.table_name}.commit_date BETWEEN ? AND ?", repository.id, @date_from, @date_to).
366 366 group(:commit_date).
367 367 count
368 368 changes_by_month = [0] * 12
369 369 changes_by_day.each {|c| changes_by_month[(@date_to.month - c.first.to_date.month) % 12] += c.last }
370 370
371 371 fields = []
372 372 12.times {|m| fields << month_name(((Date.today.month - 1 - m) % 12) + 1)}
373 373
374 374 graph = SVG::Graph::Bar.new(
375 375 :height => 300,
376 376 :width => 800,
377 377 :fields => fields.reverse,
378 378 :stack => :side,
379 379 :scale_integers => true,
380 380 :step_x_labels => 2,
381 381 :show_data_values => false,
382 382 :graph_title => l(:label_commits_per_month),
383 383 :show_graph_title => true
384 384 )
385 385
386 386 graph.add_data(
387 387 :data => commits_by_month[0..11].reverse,
388 388 :title => l(:label_revision_plural)
389 389 )
390 390
391 391 graph.add_data(
392 392 :data => changes_by_month[0..11].reverse,
393 393 :title => l(:label_change_plural)
394 394 )
395 395
396 396 graph.burn
397 397 end
398 398
399 399 def graph_commits_per_author(repository)
400 commits_by_author = Changeset.where("repository_id = ?", repository.id).group(:committer).count
401 commits_by_author.to_a.sort! {|x, y| x.last <=> y.last}
402
403 changes_by_author = Change.joins(:changeset).where("#{Changeset.table_name}.repository_id = ?", repository.id).group(:committer).count
404 h = changes_by_author.inject({}) {|o, i| o[i.first] = i.last; o}
405
406 fields = commits_by_author.collect {|r| r.first}
407 commits_data = commits_by_author.collect {|r| r.last}
408 changes_data = commits_by_author.collect {|r| h[r.first] || 0}
400 #data
401 stats = repository.stats_by_author
402 fields, commits_data, changes_data = [], [], []
403 stats.each do |name, hsh|
404 fields << name
405 commits_data << hsh[:commits_count]
406 changes_data << hsh[:changes_count]
407 end
409 408
409 #expand to 10 values if needed
410 410 fields = fields + [""]*(10 - fields.length) if fields.length<10
411 411 commits_data = commits_data + [0]*(10 - commits_data.length) if commits_data.length<10
412 412 changes_data = changes_data + [0]*(10 - changes_data.length) if changes_data.length<10
413 413
414 414 # Remove email address in usernames
415 415 fields = fields.collect {|c| c.gsub(%r{<.+@.+>}, '') }
416 416
417 #prepare graph
417 418 graph = SVG::Graph::BarHorizontal.new(
418 419 :height => 30 * commits_data.length,
419 420 :width => 800,
420 421 :fields => fields,
421 422 :stack => :side,
422 423 :scale_integers => true,
423 424 :show_data_values => false,
424 425 :rotate_y_labels => false,
425 426 :graph_title => l(:label_commits_per_author),
426 427 :show_graph_title => true
427 428 )
428 429 graph.add_data(
429 430 :data => commits_data,
430 431 :title => l(:label_revision_plural)
431 432 )
432 433 graph.add_data(
433 434 :data => changes_data,
434 435 :title => l(:label_change_plural)
435 436 )
436 437 graph.burn
437 438 end
438 439 end
@@ -1,447 +1,470
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2014 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 ScmFetchError < Exception; end
19 19
20 20 class Repository < ActiveRecord::Base
21 21 include Redmine::Ciphering
22 22 include Redmine::SafeAttributes
23 23
24 24 # Maximum length for repository identifiers
25 25 IDENTIFIER_MAX_LENGTH = 255
26 26
27 27 belongs_to :project
28 28 has_many :changesets, :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC"
29 29 has_many :filechanges, :class_name => 'Change', :through => :changesets
30 30
31 31 serialize :extra_info
32 32
33 33 before_save :check_default
34 34
35 35 # Raw SQL to delete changesets and changes in the database
36 36 # has_many :changesets, :dependent => :destroy is too slow for big repositories
37 37 before_destroy :clear_changesets
38 38
39 39 validates_length_of :password, :maximum => 255, :allow_nil => true
40 40 validates_length_of :identifier, :maximum => IDENTIFIER_MAX_LENGTH, :allow_blank => true
41 41 validates_presence_of :identifier, :unless => Proc.new { |r| r.is_default? || r.set_as_default? }
42 42 validates_uniqueness_of :identifier, :scope => :project_id, :allow_blank => true
43 43 validates_exclusion_of :identifier, :in => %w(browse show entry raw changes annotate diff statistics graph revisions revision)
44 44 # donwcase letters, digits, dashes, underscores but not digits only
45 45 validates_format_of :identifier, :with => /\A(?!\d+$)[a-z0-9\-_]*\z/, :allow_blank => true
46 46 # Checks if the SCM is enabled when creating a repository
47 47 validate :repo_create_validation, :on => :create
48 48
49 49 safe_attributes 'identifier',
50 50 'login',
51 51 'password',
52 52 'path_encoding',
53 53 'log_encoding',
54 54 'is_default'
55 55
56 56 safe_attributes 'url',
57 57 :if => lambda {|repository, user| repository.new_record?}
58 58
59 59 def repo_create_validation
60 60 unless Setting.enabled_scm.include?(self.class.name.demodulize)
61 61 errors.add(:type, :invalid)
62 62 end
63 63 end
64 64
65 65 def self.human_attribute_name(attribute_key_name, *args)
66 66 attr_name = attribute_key_name.to_s
67 67 if attr_name == "log_encoding"
68 68 attr_name = "commit_logs_encoding"
69 69 end
70 70 super(attr_name, *args)
71 71 end
72 72
73 73 # Removes leading and trailing whitespace
74 74 def url=(arg)
75 75 write_attribute(:url, arg ? arg.to_s.strip : nil)
76 76 end
77 77
78 78 # Removes leading and trailing whitespace
79 79 def root_url=(arg)
80 80 write_attribute(:root_url, arg ? arg.to_s.strip : nil)
81 81 end
82 82
83 83 def password
84 84 read_ciphered_attribute(:password)
85 85 end
86 86
87 87 def password=(arg)
88 88 write_ciphered_attribute(:password, arg)
89 89 end
90 90
91 91 def scm_adapter
92 92 self.class.scm_adapter_class
93 93 end
94 94
95 95 def scm
96 96 unless @scm
97 97 @scm = self.scm_adapter.new(url, root_url,
98 98 login, password, path_encoding)
99 99 if root_url.blank? && @scm.root_url.present?
100 100 update_attribute(:root_url, @scm.root_url)
101 101 end
102 102 end
103 103 @scm
104 104 end
105 105
106 106 def scm_name
107 107 self.class.scm_name
108 108 end
109 109
110 110 def name
111 111 if identifier.present?
112 112 identifier
113 113 elsif is_default?
114 114 l(:field_repository_is_default)
115 115 else
116 116 scm_name
117 117 end
118 118 end
119 119
120 120 def identifier=(identifier)
121 121 super unless identifier_frozen?
122 122 end
123 123
124 124 def identifier_frozen?
125 125 errors[:identifier].blank? && !(new_record? || identifier.blank?)
126 126 end
127 127
128 128 def identifier_param
129 129 if is_default?
130 130 nil
131 131 elsif identifier.present?
132 132 identifier
133 133 else
134 134 id.to_s
135 135 end
136 136 end
137 137
138 138 def <=>(repository)
139 139 if is_default?
140 140 -1
141 141 elsif repository.is_default?
142 142 1
143 143 else
144 144 identifier.to_s <=> repository.identifier.to_s
145 145 end
146 146 end
147 147
148 148 def self.find_by_identifier_param(param)
149 149 if param.to_s =~ /^\d+$/
150 150 find_by_id(param)
151 151 else
152 152 find_by_identifier(param)
153 153 end
154 154 end
155 155
156 156 # TODO: should return an empty hash instead of nil to avoid many ||{}
157 157 def extra_info
158 158 h = read_attribute(:extra_info)
159 159 h.is_a?(Hash) ? h : nil
160 160 end
161 161
162 162 def merge_extra_info(arg)
163 163 h = extra_info || {}
164 164 return h if arg.nil?
165 165 h.merge!(arg)
166 166 write_attribute(:extra_info, h)
167 167 end
168 168
169 169 def report_last_commit
170 170 true
171 171 end
172 172
173 173 def supports_cat?
174 174 scm.supports_cat?
175 175 end
176 176
177 177 def supports_annotate?
178 178 scm.supports_annotate?
179 179 end
180 180
181 181 def supports_all_revisions?
182 182 true
183 183 end
184 184
185 185 def supports_directory_revisions?
186 186 false
187 187 end
188 188
189 189 def supports_revision_graph?
190 190 false
191 191 end
192 192
193 193 def entry(path=nil, identifier=nil)
194 194 scm.entry(path, identifier)
195 195 end
196 196
197 197 def scm_entries(path=nil, identifier=nil)
198 198 scm.entries(path, identifier)
199 199 end
200 200 protected :scm_entries
201 201
202 202 def entries(path=nil, identifier=nil)
203 203 entries = scm_entries(path, identifier)
204 204 load_entries_changesets(entries)
205 205 entries
206 206 end
207 207
208 208 def branches
209 209 scm.branches
210 210 end
211 211
212 212 def tags
213 213 scm.tags
214 214 end
215 215
216 216 def default_branch
217 217 nil
218 218 end
219 219
220 220 def properties(path, identifier=nil)
221 221 scm.properties(path, identifier)
222 222 end
223 223
224 224 def cat(path, identifier=nil)
225 225 scm.cat(path, identifier)
226 226 end
227 227
228 228 def diff(path, rev, rev_to)
229 229 scm.diff(path, rev, rev_to)
230 230 end
231 231
232 232 def diff_format_revisions(cs, cs_to, sep=':')
233 233 text = ""
234 234 text << cs_to.format_identifier + sep if cs_to
235 235 text << cs.format_identifier if cs
236 236 text
237 237 end
238 238
239 239 # Returns a path relative to the url of the repository
240 240 def relative_path(path)
241 241 path
242 242 end
243 243
244 244 # Finds and returns a revision with a number or the beginning of a hash
245 245 def find_changeset_by_name(name)
246 246 return nil if name.blank?
247 247 s = name.to_s
248 248 if s.match(/^\d*$/)
249 249 changesets.where("revision = ?", s).first
250 250 else
251 251 changesets.where("revision LIKE ?", s + '%').first
252 252 end
253 253 end
254 254
255 255 def latest_changeset
256 256 @latest_changeset ||= changesets.first
257 257 end
258 258
259 259 # Returns the latest changesets for +path+
260 260 # Default behaviour is to search in cached changesets
261 261 def latest_changesets(path, rev, limit=10)
262 262 if path.blank?
263 263 changesets.
264 264 reorder("#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC").
265 265 limit(limit).
266 266 preload(:user).
267 267 all
268 268 else
269 269 filechanges.
270 270 where("path = ?", path.with_leading_slash).
271 271 reorder("#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC").
272 272 limit(limit).
273 273 preload(:changeset => :user).
274 274 collect(&:changeset)
275 275 end
276 276 end
277 277
278 278 def scan_changesets_for_issue_ids
279 279 self.changesets.each(&:scan_comment_for_issue_ids)
280 280 end
281 281
282 282 # Returns an array of committers usernames and associated user_id
283 283 def committers
284 284 @committers ||= Changeset.connection.select_rows(
285 285 "SELECT DISTINCT committer, user_id FROM #{Changeset.table_name} WHERE repository_id = #{id}")
286 286 end
287 287
288 288 # Maps committers username to a user ids
289 289 def committer_ids=(h)
290 290 if h.is_a?(Hash)
291 291 committers.each do |committer, user_id|
292 292 new_user_id = h[committer]
293 293 if new_user_id && (new_user_id.to_i != user_id.to_i)
294 294 new_user_id = (new_user_id.to_i > 0 ? new_user_id.to_i : nil)
295 295 Changeset.where(["repository_id = ? AND committer = ?", id, committer]).
296 296 update_all("user_id = #{new_user_id.nil? ? 'NULL' : new_user_id}")
297 297 end
298 298 end
299 299 @committers = nil
300 300 @found_committer_users = nil
301 301 true
302 302 else
303 303 false
304 304 end
305 305 end
306 306
307 307 # Returns the Redmine User corresponding to the given +committer+
308 308 # It will return nil if the committer is not yet mapped and if no User
309 309 # with the same username or email was found
310 310 def find_committer_user(committer)
311 311 unless committer.blank?
312 312 @found_committer_users ||= {}
313 313 return @found_committer_users[committer] if @found_committer_users.has_key?(committer)
314 314
315 315 user = nil
316 316 c = changesets.where(:committer => committer).includes(:user).first
317 317 if c && c.user
318 318 user = c.user
319 319 elsif committer.strip =~ /^([^<]+)(<(.*)>)?$/
320 320 username, email = $1.strip, $3
321 321 u = User.find_by_login(username)
322 322 u ||= User.find_by_mail(email) unless email.blank?
323 323 user = u
324 324 end
325 325 @found_committer_users[committer] = user
326 326 user
327 327 end
328 328 end
329 329
330 330 def repo_log_encoding
331 331 encoding = log_encoding.to_s.strip
332 332 encoding.blank? ? 'UTF-8' : encoding
333 333 end
334 334
335 335 # Fetches new changesets for all repositories of active projects
336 336 # Can be called periodically by an external script
337 337 # eg. ruby script/runner "Repository.fetch_changesets"
338 338 def self.fetch_changesets
339 339 Project.active.has_module(:repository).all.each do |project|
340 340 project.repositories.each do |repository|
341 341 begin
342 342 repository.fetch_changesets
343 343 rescue Redmine::Scm::Adapters::CommandFailed => e
344 344 logger.error "scm: error during fetching changesets: #{e.message}"
345 345 end
346 346 end
347 347 end
348 348 end
349 349
350 350 # scan changeset comments to find related and fixed issues for all repositories
351 351 def self.scan_changesets_for_issue_ids
352 352 all.each(&:scan_changesets_for_issue_ids)
353 353 end
354 354
355 355 def self.scm_name
356 356 'Abstract'
357 357 end
358 358
359 359 def self.available_scm
360 360 subclasses.collect {|klass| [klass.scm_name, klass.name]}
361 361 end
362 362
363 363 def self.factory(klass_name, *args)
364 364 klass = "Repository::#{klass_name}".constantize
365 365 klass.new(*args)
366 366 rescue
367 367 nil
368 368 end
369 369
370 370 def self.scm_adapter_class
371 371 nil
372 372 end
373 373
374 374 def self.scm_command
375 375 ret = ""
376 376 begin
377 377 ret = self.scm_adapter_class.client_command if self.scm_adapter_class
378 378 rescue Exception => e
379 379 logger.error "scm: error during get command: #{e.message}"
380 380 end
381 381 ret
382 382 end
383 383
384 384 def self.scm_version_string
385 385 ret = ""
386 386 begin
387 387 ret = self.scm_adapter_class.client_version_string if self.scm_adapter_class
388 388 rescue Exception => e
389 389 logger.error "scm: error during get version string: #{e.message}"
390 390 end
391 391 ret
392 392 end
393 393
394 394 def self.scm_available
395 395 ret = false
396 396 begin
397 397 ret = self.scm_adapter_class.client_available if self.scm_adapter_class
398 398 rescue Exception => e
399 399 logger.error "scm: error during get scm available: #{e.message}"
400 400 end
401 401 ret
402 402 end
403 403
404 404 def set_as_default?
405 405 new_record? && project && Repository.where(:project_id => project.id).empty?
406 406 end
407 407
408 # Returns a hash with statistics by author in the following form:
409 # {
410 # "John Smith" => { :commits => 45, :changes => 324 },
411 # "Bob" => { ... }
412 # }
413 #
414 # Notes:
415 # - this hash honnors the users mapping defined for the repository
416 def stats_by_author
417 commits_by_author = Changeset.where("repository_id = ?", id).group(:committer).count
418 commits_by_author.to_a.sort! {|x, y| x.last <=> y.last}
419
420 changes_by_author = Change.joins(:changeset).where("#{Changeset.table_name}.repository_id = ?", id).group(:committer).count
421 h = changes_by_author.inject({}) {|o, i| o[i.first] = i.last; o}
422
423 commits_by_author.inject({}) do |hash, (name, commits_count)|
424 hash[name] = {}
425 hash[name][:commits_count] = commits_count
426 hash[name][:changes_count] = h[name] || 0
427 hash
428 end
429 end
430
408 431 protected
409 432
410 433 def check_default
411 434 if !is_default? && set_as_default?
412 435 self.is_default = true
413 436 end
414 437 if is_default? && is_default_changed?
415 438 Repository.where(["project_id = ?", project_id]).update_all(["is_default = ?", false])
416 439 end
417 440 end
418 441
419 442 def load_entries_changesets(entries)
420 443 if entries
421 444 entries.each do |entry|
422 445 if entry.lastrev && entry.lastrev.identifier
423 446 entry.changeset = find_changeset_by_name(entry.lastrev.identifier)
424 447 end
425 448 end
426 449 end
427 450 end
428 451
429 452 private
430 453
431 454 # Deletes repository data
432 455 def clear_changesets
433 456 cs = Changeset.table_name
434 457 ch = Change.table_name
435 458 ci = "#{table_name_prefix}changesets_issues#{table_name_suffix}"
436 459 cp = "#{table_name_prefix}changeset_parents#{table_name_suffix}"
437 460
438 461 connection.delete("DELETE FROM #{ch} WHERE #{ch}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
439 462 connection.delete("DELETE FROM #{ci} WHERE #{ci}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
440 463 connection.delete("DELETE FROM #{cp} WHERE #{cp}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
441 464 connection.delete("DELETE FROM #{cs} WHERE #{cs}.repository_id = #{id}")
442 465 clear_extra_info_of_changesets
443 466 end
444 467
445 468 def clear_extra_info_of_changesets
446 469 end
447 470 end
@@ -1,398 +1,417
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2014 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../test_helper', __FILE__)
19 19
20 20 class RepositoryTest < ActiveSupport::TestCase
21 21 fixtures :projects,
22 22 :trackers,
23 23 :projects_trackers,
24 24 :enabled_modules,
25 25 :repositories,
26 26 :issues,
27 27 :issue_statuses,
28 28 :issue_categories,
29 29 :changesets,
30 30 :changes,
31 31 :users,
32 32 :members,
33 33 :member_roles,
34 34 :roles,
35 35 :enumerations
36 36
37 37 include Redmine::I18n
38 38
39 39 def setup
40 40 @repository = Project.find(1).repository
41 41 end
42 42
43 43 def test_blank_log_encoding_error_message
44 44 set_language_if_valid 'en'
45 45 repo = Repository::Bazaar.new(
46 46 :project => Project.find(3),
47 47 :url => "/test",
48 48 :log_encoding => ''
49 49 )
50 50 assert !repo.save
51 51 assert_include "Commit messages encoding can't be blank",
52 52 repo.errors.full_messages
53 53 end
54 54
55 55 def test_blank_log_encoding_error_message_fr
56 56 set_language_if_valid 'fr'
57 57 str = "Encodage des messages de commit doit \xc3\xaatre renseign\xc3\xa9(e)"
58 58 str.force_encoding('UTF-8') if str.respond_to?(:force_encoding)
59 59 repo = Repository::Bazaar.new(
60 60 :project => Project.find(3),
61 61 :url => "/test"
62 62 )
63 63 assert !repo.save
64 64 assert_include str, repo.errors.full_messages
65 65 end
66 66
67 67 def test_create
68 68 repository = Repository::Subversion.new(:project => Project.find(3))
69 69 assert !repository.save
70 70
71 71 repository.url = "svn://localhost"
72 72 assert repository.save
73 73 repository.reload
74 74
75 75 project = Project.find(3)
76 76 assert_equal repository, project.repository
77 77 end
78 78
79 79 def test_first_repository_should_be_set_as_default
80 80 repository1 = Repository::Subversion.new(
81 81 :project => Project.find(3),
82 82 :identifier => 'svn1',
83 83 :url => 'file:///svn1'
84 84 )
85 85 assert repository1.save
86 86 assert repository1.is_default?
87 87
88 88 repository2 = Repository::Subversion.new(
89 89 :project => Project.find(3),
90 90 :identifier => 'svn2',
91 91 :url => 'file:///svn2'
92 92 )
93 93 assert repository2.save
94 94 assert !repository2.is_default?
95 95
96 96 assert_equal repository1, Project.find(3).repository
97 97 assert_equal [repository1, repository2], Project.find(3).repositories.sort
98 98 end
99 99
100 100 def test_default_repository_should_be_one
101 101 assert_equal 0, Project.find(3).repositories.count
102 102 repository1 = Repository::Subversion.new(
103 103 :project => Project.find(3),
104 104 :identifier => 'svn1',
105 105 :url => 'file:///svn1'
106 106 )
107 107 assert repository1.save
108 108 assert repository1.is_default?
109 109
110 110 repository2 = Repository::Subversion.new(
111 111 :project => Project.find(3),
112 112 :identifier => 'svn2',
113 113 :url => 'file:///svn2',
114 114 :is_default => true
115 115 )
116 116 assert repository2.save
117 117 assert repository2.is_default?
118 118 repository1.reload
119 119 assert !repository1.is_default?
120 120
121 121 assert_equal repository2, Project.find(3).repository
122 122 assert_equal [repository2, repository1], Project.find(3).repositories.sort
123 123 end
124 124
125 125 def test_identifier_should_accept_letters_digits_dashes_and_underscores
126 126 r = Repository::Subversion.new(
127 127 :project_id => 3,
128 128 :identifier => 'svn-123_45',
129 129 :url => 'file:///svn'
130 130 )
131 131 assert r.save
132 132 end
133 133
134 134 def test_identifier_should_not_be_frozen_for_a_new_repository
135 135 assert_equal false, Repository.new.identifier_frozen?
136 136 end
137 137
138 138 def test_identifier_should_not_be_frozen_for_a_saved_repository_with_blank_identifier
139 139 Repository.where(:id => 10).update_all(["identifier = ''"])
140 140 assert_equal false, Repository.find(10).identifier_frozen?
141 141 end
142 142
143 143 def test_identifier_should_be_frozen_for_a_saved_repository_with_valid_identifier
144 144 Repository.where(:id => 10).update_all(["identifier = 'abc123'"])
145 145 assert_equal true, Repository.find(10).identifier_frozen?
146 146 end
147 147
148 148 def test_identifier_should_not_accept_change_if_frozen
149 149 r = Repository.new(:identifier => 'foo')
150 150 r.stubs(:identifier_frozen?).returns(true)
151 151
152 152 r.identifier = 'bar'
153 153 assert_equal 'foo', r.identifier
154 154 end
155 155
156 156 def test_identifier_should_accept_change_if_not_frozen
157 157 r = Repository.new(:identifier => 'foo')
158 158 r.stubs(:identifier_frozen?).returns(false)
159 159
160 160 r.identifier = 'bar'
161 161 assert_equal 'bar', r.identifier
162 162 end
163 163
164 164 def test_destroy
165 165 repository = Repository.find(10)
166 166 changesets = repository.changesets.count
167 167 changes = repository.filechanges.count
168 168
169 169 assert_difference 'Changeset.count', -changesets do
170 170 assert_difference 'Change.count', -changes do
171 171 Repository.find(10).destroy
172 172 end
173 173 end
174 174 end
175 175
176 176 def test_destroy_should_delete_parents_associations
177 177 changeset = Changeset.find(102)
178 178 changeset.parents = Changeset.where(:id => [100, 101]).all
179 179 assert_difference 'Changeset.connection.select_all("select * from changeset_parents").count', -2 do
180 180 Repository.find(10).destroy
181 181 end
182 182 end
183 183
184 184 def test_destroy_should_delete_issues_associations
185 185 changeset = Changeset.find(102)
186 186 changeset.issues = Issue.where(:id => [1, 2]).all
187 187 assert_difference 'Changeset.connection.select_all("select * from changesets_issues").count', -2 do
188 188 Repository.find(10).destroy
189 189 end
190 190 end
191 191
192 192 def test_should_not_create_with_disabled_scm
193 193 # disable Subversion
194 194 with_settings :enabled_scm => ['Darcs', 'Git'] do
195 195 repository = Repository::Subversion.new(
196 196 :project => Project.find(3), :url => "svn://localhost")
197 197 assert !repository.save
198 198 assert_include I18n.translate('activerecord.errors.messages.invalid'),
199 199 repository.errors[:type]
200 200 end
201 201 end
202 202
203 203 def test_scan_changesets_for_issue_ids
204 204 Setting.default_language = 'en'
205 205 Setting.commit_ref_keywords = 'refs , references, IssueID'
206 206 Setting.commit_update_keywords = [
207 207 {'keywords' => 'fixes , closes',
208 208 'status_id' => IssueStatus.where(:is_closed => true).first.id,
209 209 'done_ratio' => '90'}
210 210 ]
211 211 Setting.default_language = 'en'
212 212 ActionMailer::Base.deliveries.clear
213 213
214 214 # make sure issue 1 is not already closed
215 215 fixed_issue = Issue.find(1)
216 216 assert !fixed_issue.status.is_closed?
217 217 old_status = fixed_issue.status
218 218
219 219 with_settings :notified_events => %w(issue_added issue_updated) do
220 220 Repository.scan_changesets_for_issue_ids
221 221 end
222 222 assert_equal [101, 102], Issue.find(3).changeset_ids
223 223
224 224 # fixed issues
225 225 fixed_issue.reload
226 226 assert fixed_issue.status.is_closed?
227 227 assert_equal 90, fixed_issue.done_ratio
228 228 assert_equal [101], fixed_issue.changeset_ids
229 229
230 230 # issue change
231 231 journal = fixed_issue.journals.reorder('created_on desc').first
232 232 assert_equal User.find_by_login('dlopper'), journal.user
233 233 assert_equal 'Applied in changeset r2.', journal.notes
234 234
235 235 # 2 email notifications
236 236 assert_equal 2, ActionMailer::Base.deliveries.size
237 237 mail = ActionMailer::Base.deliveries.first
238 238 assert_not_nil mail
239 239 assert mail.subject.starts_with?(
240 240 "[#{fixed_issue.project.name} - #{fixed_issue.tracker.name} ##{fixed_issue.id}]")
241 241 assert_mail_body_match(
242 242 "Status changed from #{old_status} to #{fixed_issue.status}", mail)
243 243
244 244 # ignoring commits referencing an issue of another project
245 245 assert_equal [], Issue.find(4).changesets
246 246 end
247 247
248 248 def test_for_changeset_comments_strip
249 249 repository = Repository::Mercurial.create(
250 250 :project => Project.find( 4 ),
251 251 :url => '/foo/bar/baz' )
252 252 comment = <<-COMMENT
253 253 This is a loooooooooooooooooooooooooooong comment
254 254
255 255
256 256 COMMENT
257 257 changeset = Changeset.new(
258 258 :comments => comment, :commit_date => Time.now,
259 259 :revision => 0, :scmid => 'f39b7922fb3c',
260 260 :committer => 'foo <foo@example.com>',
261 261 :committed_on => Time.now, :repository => repository )
262 262 assert( changeset.save )
263 263 assert_not_equal( comment, changeset.comments )
264 264 assert_equal( 'This is a loooooooooooooooooooooooooooong comment',
265 265 changeset.comments )
266 266 end
267 267
268 268 def test_for_urls_strip_cvs
269 269 repository = Repository::Cvs.create(
270 270 :project => Project.find(4),
271 271 :url => ' :pserver:login:password@host:/path/to/the/repository',
272 272 :root_url => 'foo ',
273 273 :log_encoding => 'UTF-8')
274 274 assert repository.save
275 275 repository.reload
276 276 assert_equal ':pserver:login:password@host:/path/to/the/repository',
277 277 repository.url
278 278 assert_equal 'foo', repository.root_url
279 279 end
280 280
281 281 def test_for_urls_strip_subversion
282 282 repository = Repository::Subversion.create(
283 283 :project => Project.find(4),
284 284 :url => ' file:///dummy ')
285 285 assert repository.save
286 286 repository.reload
287 287 assert_equal 'file:///dummy', repository.url
288 288 end
289 289
290 290 def test_for_urls_strip_git
291 291 repository = Repository::Git.create(
292 292 :project => Project.find(4),
293 293 :url => ' c:\dummy ')
294 294 assert repository.save
295 295 repository.reload
296 296 assert_equal 'c:\dummy', repository.url
297 297 end
298 298
299 299 def test_manual_user_mapping
300 300 assert_no_difference "Changeset.where('user_id <> 2').count" do
301 301 c = Changeset.create!(
302 302 :repository => @repository,
303 303 :committer => 'foo',
304 304 :committed_on => Time.now,
305 305 :revision => 100,
306 306 :comments => 'Committed by foo.'
307 307 )
308 308 assert_nil c.user
309 309 @repository.committer_ids = {'foo' => '2'}
310 310 assert_equal User.find(2), c.reload.user
311 311 # committer is now mapped
312 312 c = Changeset.create!(
313 313 :repository => @repository,
314 314 :committer => 'foo',
315 315 :committed_on => Time.now,
316 316 :revision => 101,
317 317 :comments => 'Another commit by foo.'
318 318 )
319 319 assert_equal User.find(2), c.user
320 320 end
321 321 end
322 322
323 323 def test_auto_user_mapping_by_username
324 324 c = Changeset.create!(
325 325 :repository => @repository,
326 326 :committer => 'jsmith',
327 327 :committed_on => Time.now,
328 328 :revision => 100,
329 329 :comments => 'Committed by john.'
330 330 )
331 331 assert_equal User.find(2), c.user
332 332 end
333 333
334 334 def test_auto_user_mapping_by_email
335 335 c = Changeset.create!(
336 336 :repository => @repository,
337 337 :committer => 'john <jsmith@somenet.foo>',
338 338 :committed_on => Time.now,
339 339 :revision => 100,
340 340 :comments => 'Committed by john.'
341 341 )
342 342 assert_equal User.find(2), c.user
343 343 end
344 344
345 345 def test_filesystem_avaialbe
346 346 klass = Repository::Filesystem
347 347 assert klass.scm_adapter_class
348 348 assert_equal true, klass.scm_available
349 349 end
350 350
351 351 def test_extra_info_should_not_return_non_hash_value
352 352 repo = Repository.new
353 353 repo.extra_info = "foo"
354 354 assert_nil repo.extra_info
355 355 end
356 356
357 357 def test_merge_extra_info
358 358 repo = Repository::Subversion.new(:project => Project.find(3))
359 359 assert !repo.save
360 360 repo.url = "svn://localhost"
361 361 assert repo.save
362 362 repo.reload
363 363 project = Project.find(3)
364 364 assert_equal repo, project.repository
365 365 assert_nil repo.extra_info
366 366 h1 = {"test_1" => {"test_11" => "test_value_11"}}
367 367 repo.merge_extra_info(h1)
368 368 assert_equal h1, repo.extra_info
369 369 h2 = {"test_2" => {
370 370 "test_21" => "test_value_21",
371 371 "test_22" => "test_value_22",
372 372 }}
373 373 repo.merge_extra_info(h2)
374 374 assert_equal (h = {"test_11" => "test_value_11"}),
375 375 repo.extra_info["test_1"]
376 376 assert_equal "test_value_21",
377 377 repo.extra_info["test_2"]["test_21"]
378 378 h3 = {"test_2" => {
379 379 "test_23" => "test_value_23",
380 380 "test_24" => "test_value_24",
381 381 }}
382 382 repo.merge_extra_info(h3)
383 383 assert_equal (h = {"test_11" => "test_value_11"}),
384 384 repo.extra_info["test_1"]
385 385 assert_nil repo.extra_info["test_2"]["test_21"]
386 386 assert_equal "test_value_23",
387 387 repo.extra_info["test_2"]["test_23"]
388 388 end
389 389
390 390 def test_sort_should_not_raise_an_error_with_nil_identifiers
391 391 r1 = Repository.new
392 392 r2 = Repository.new
393 393
394 394 assert_nothing_raised do
395 395 [r1, r2].sort
396 396 end
397 397 end
398
399 def test_stats_by_author_reflect_changesets_and_changes
400 repository = Repository.find(10)
401
402 expected = {"dlopper"=>{:commits_count=>10, :changes_count=>3}}
403 assert_equal expected, repository.stats_by_author
404
405 set = Changeset.create!(
406 :repository => repository,
407 :committer => 'dlopper',
408 :committed_on => Time.now,
409 :revision => 101,
410 :comments => 'Another commit by foo.'
411 )
412 Change.create!(:changeset => set, :action => 'create', :path => '/path/to/file1')
413 Change.create!(:changeset => set, :action => 'create', :path => '/path/to/file2')
414 expected = {"dlopper"=>{:commits_count=>11, :changes_count=>5}}
415 assert_equal expected, repository.stats_by_author
416 end
398 417 end
General Comments 0
You need to be logged in to leave comments. Login now