##// END OF EJS Templates
Honnor committers/users mapping in repository statistics (#13487)....
Jean-Baptiste Barth -
r12999:8c945fb7914b
parent child
Show More
@@ -1,470 +1,471
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 408 # Returns a hash with statistics by author in the following form:
409 409 # {
410 410 # "John Smith" => { :commits => 45, :changes => 324 },
411 411 # "Bob" => { ... }
412 412 # }
413 413 #
414 414 # Notes:
415 415 # - this hash honnors the users mapping defined for the repository
416 416 def stats_by_author
417 417 commits_by_author = Changeset.where("repository_id = ?", id).group(:committer).count
418 418 commits_by_author.to_a.sort! {|x, y| x.last <=> y.last}
419 419
420 420 changes_by_author = Change.joins(:changeset).where("#{Changeset.table_name}.repository_id = ?", id).group(:committer).count
421 421 h = changes_by_author.inject({}) {|o, i| o[i.first] = i.last; o}
422 422
423 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
424 mapped_name = (find_committer_user(name) || name).to_s
425 hash[mapped_name] ||= { :commits_count => 0, :changes_count => 0 }
426 hash[mapped_name][:commits_count] += commits_count
427 hash[mapped_name][:changes_count] += h[name] || 0
427 428 hash
428 429 end
429 430 end
430 431
431 432 protected
432 433
433 434 def check_default
434 435 if !is_default? && set_as_default?
435 436 self.is_default = true
436 437 end
437 438 if is_default? && is_default_changed?
438 439 Repository.where(["project_id = ?", project_id]).update_all(["is_default = ?", false])
439 440 end
440 441 end
441 442
442 443 def load_entries_changesets(entries)
443 444 if entries
444 445 entries.each do |entry|
445 446 if entry.lastrev && entry.lastrev.identifier
446 447 entry.changeset = find_changeset_by_name(entry.lastrev.identifier)
447 448 end
448 449 end
449 450 end
450 451 end
451 452
452 453 private
453 454
454 455 # Deletes repository data
455 456 def clear_changesets
456 457 cs = Changeset.table_name
457 458 ch = Change.table_name
458 459 ci = "#{table_name_prefix}changesets_issues#{table_name_suffix}"
459 460 cp = "#{table_name_prefix}changeset_parents#{table_name_suffix}"
460 461
461 462 connection.delete("DELETE FROM #{ch} WHERE #{ch}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
462 463 connection.delete("DELETE FROM #{ci} WHERE #{ci}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
463 464 connection.delete("DELETE FROM #{cp} WHERE #{cp}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
464 465 connection.delete("DELETE FROM #{cs} WHERE #{cs}.repository_id = #{id}")
465 466 clear_extra_info_of_changesets
466 467 end
467 468
468 469 def clear_extra_info_of_changesets
469 470 end
470 471 end
@@ -1,417 +1,463
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 398
399 399 def test_stats_by_author_reflect_changesets_and_changes
400 400 repository = Repository.find(10)
401 401
402 expected = {"dlopper"=>{:commits_count=>10, :changes_count=>3}}
402 expected = {"Dave Lopper"=>{:commits_count=>10, :changes_count=>3}}
403 403 assert_equal expected, repository.stats_by_author
404 404
405 405 set = Changeset.create!(
406 406 :repository => repository,
407 407 :committer => 'dlopper',
408 408 :committed_on => Time.now,
409 409 :revision => 101,
410 410 :comments => 'Another commit by foo.'
411 411 )
412 412 Change.create!(:changeset => set, :action => 'A', :path => '/path/to/file1')
413 413 Change.create!(:changeset => set, :action => 'A', :path => '/path/to/file2')
414 expected = {"dlopper"=>{:commits_count=>11, :changes_count=>5}}
414 expected = {"Dave Lopper"=>{:commits_count=>11, :changes_count=>5}}
415 assert_equal expected, repository.stats_by_author
416 end
417
418 def test_stats_by_author_honnor_committers
419 # in fact it is really tested above, but let's have a dedicated test
420 # to ensure things are dynamically linked to Users
421 User.find_by_login("dlopper").update_attribute(:firstname, "Dave's")
422 repository = Repository.find(10)
423 expected = {"Dave's Lopper"=>{:commits_count=>10, :changes_count=>3}}
424 assert_equal expected, repository.stats_by_author
425 end
426
427 def test_stats_by_author_doesnt_drop_unmapped_users
428 repository = Repository.find(10)
429 Changeset.create!(
430 :repository => repository,
431 :committer => 'unnamed <foo@bar.net>',
432 :committed_on => Time.now,
433 :revision => 101,
434 :comments => 'Another commit by foo.'
435 )
436
437 assert repository.stats_by_author.has_key?("unnamed <foo@bar.net>")
438 end
439
440 def test_stats_by_author_merge_correctly
441 # as we honnor users->committer map and it's not injective,
442 # we must be sure merges happen correctly and stats are not
443 # wiped out when two source counts map to the same user.
444 #
445 # Here we have Changeset's with committer="dlopper" and others
446 # with committer="dlopper <dlopper@somefoo.net>"
447 repository = Repository.find(10)
448
449 expected = {"Dave Lopper"=>{:commits_count=>10, :changes_count=>3}}
450 assert_equal expected, repository.stats_by_author
451
452 set = Changeset.create!(
453 :repository => repository,
454 :committer => 'dlopper <dlopper@somefoo.net>',
455 :committed_on => Time.now,
456 :revision => 101,
457 :comments => 'Another commit by foo.'
458 )
459
460 expected = {"Dave Lopper"=>{:commits_count=>11, :changes_count=>3}}
415 461 assert_equal expected, repository.stats_by_author
416 462 end
417 463 end
General Comments 0
You need to be logged in to leave comments. Login now