##// END OF EJS Templates
Fixed that 2 repositories can be created with blank/nil identifier (#19400)....
Jean-Philippe Lang -
r13774:5cd29b1a6b04
parent child
Show More
@@ -1,505 +1,510
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 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, lambda{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 before_validation :normalize_identifier
33 34 before_save :check_default
34 35
35 36 # Raw SQL to delete changesets and changes in the database
36 37 # has_many :changesets, :dependent => :destroy is too slow for big repositories
37 38 before_destroy :clear_changesets
38 39
39 40 validates_length_of :password, :maximum => 255, :allow_nil => true
40 41 validates_length_of :identifier, :maximum => IDENTIFIER_MAX_LENGTH, :allow_blank => true
41 42 validates_uniqueness_of :identifier, :scope => :project_id
42 43 validates_exclusion_of :identifier, :in => %w(browse show entry raw changes annotate diff statistics graph revisions revision)
43 44 # donwcase letters, digits, dashes, underscores but not digits only
44 45 validates_format_of :identifier, :with => /\A(?!\d+$)[a-z0-9\-_]*\z/, :allow_blank => true
45 46 # Checks if the SCM is enabled when creating a repository
46 47 validate :repo_create_validation, :on => :create
47 48 validate :validate_repository_path
48 49 attr_protected :id
49 50
50 51 safe_attributes 'identifier',
51 52 'login',
52 53 'password',
53 54 'path_encoding',
54 55 'log_encoding',
55 56 'is_default'
56 57
57 58 safe_attributes 'url',
58 59 :if => lambda {|repository, user| repository.new_record?}
59 60
60 61 def repo_create_validation
61 62 unless Setting.enabled_scm.include?(self.class.name.demodulize)
62 63 errors.add(:type, :invalid)
63 64 end
64 65 end
65 66
66 67 def self.human_attribute_name(attribute_key_name, *args)
67 68 attr_name = attribute_key_name.to_s
68 69 if attr_name == "log_encoding"
69 70 attr_name = "commit_logs_encoding"
70 71 end
71 72 super(attr_name, *args)
72 73 end
73 74
74 75 # Removes leading and trailing whitespace
75 76 def url=(arg)
76 77 write_attribute(:url, arg ? arg.to_s.strip : nil)
77 78 end
78 79
79 80 # Removes leading and trailing whitespace
80 81 def root_url=(arg)
81 82 write_attribute(:root_url, arg ? arg.to_s.strip : nil)
82 83 end
83 84
84 85 def password
85 86 read_ciphered_attribute(:password)
86 87 end
87 88
88 89 def password=(arg)
89 90 write_ciphered_attribute(:password, arg)
90 91 end
91 92
92 93 def scm_adapter
93 94 self.class.scm_adapter_class
94 95 end
95 96
96 97 def scm
97 98 unless @scm
98 99 @scm = self.scm_adapter.new(url, root_url,
99 100 login, password, path_encoding)
100 101 if root_url.blank? && @scm.root_url.present?
101 102 update_attribute(:root_url, @scm.root_url)
102 103 end
103 104 end
104 105 @scm
105 106 end
106 107
107 108 def scm_name
108 109 self.class.scm_name
109 110 end
110 111
111 112 def name
112 113 if identifier.present?
113 114 identifier
114 115 elsif is_default?
115 116 l(:field_repository_is_default)
116 117 else
117 118 scm_name
118 119 end
119 120 end
120 121
121 122 def identifier=(identifier)
122 123 super unless identifier_frozen?
123 124 end
124 125
125 126 def identifier_frozen?
126 127 errors[:identifier].blank? && !(new_record? || identifier.blank?)
127 128 end
128 129
129 130 def identifier_param
130 131 if is_default?
131 132 nil
132 133 elsif identifier.present?
133 134 identifier
134 135 else
135 136 id.to_s
136 137 end
137 138 end
138 139
139 140 def <=>(repository)
140 141 if is_default?
141 142 -1
142 143 elsif repository.is_default?
143 144 1
144 145 else
145 146 identifier.to_s <=> repository.identifier.to_s
146 147 end
147 148 end
148 149
149 150 def self.find_by_identifier_param(param)
150 151 if param.to_s =~ /^\d+$/
151 152 find_by_id(param)
152 153 else
153 154 find_by_identifier(param)
154 155 end
155 156 end
156 157
157 158 # TODO: should return an empty hash instead of nil to avoid many ||{}
158 159 def extra_info
159 160 h = read_attribute(:extra_info)
160 161 h.is_a?(Hash) ? h : nil
161 162 end
162 163
163 164 def merge_extra_info(arg)
164 165 h = extra_info || {}
165 166 return h if arg.nil?
166 167 h.merge!(arg)
167 168 write_attribute(:extra_info, h)
168 169 end
169 170
170 171 def report_last_commit
171 172 true
172 173 end
173 174
174 175 def supports_cat?
175 176 scm.supports_cat?
176 177 end
177 178
178 179 def supports_annotate?
179 180 scm.supports_annotate?
180 181 end
181 182
182 183 def supports_all_revisions?
183 184 true
184 185 end
185 186
186 187 def supports_directory_revisions?
187 188 false
188 189 end
189 190
190 191 def supports_revision_graph?
191 192 false
192 193 end
193 194
194 195 def entry(path=nil, identifier=nil)
195 196 scm.entry(path, identifier)
196 197 end
197 198
198 199 def scm_entries(path=nil, identifier=nil)
199 200 scm.entries(path, identifier)
200 201 end
201 202 protected :scm_entries
202 203
203 204 def entries(path=nil, identifier=nil)
204 205 entries = scm_entries(path, identifier)
205 206 load_entries_changesets(entries)
206 207 entries
207 208 end
208 209
209 210 def branches
210 211 scm.branches
211 212 end
212 213
213 214 def tags
214 215 scm.tags
215 216 end
216 217
217 218 def default_branch
218 219 nil
219 220 end
220 221
221 222 def properties(path, identifier=nil)
222 223 scm.properties(path, identifier)
223 224 end
224 225
225 226 def cat(path, identifier=nil)
226 227 scm.cat(path, identifier)
227 228 end
228 229
229 230 def diff(path, rev, rev_to)
230 231 scm.diff(path, rev, rev_to)
231 232 end
232 233
233 234 def diff_format_revisions(cs, cs_to, sep=':')
234 235 text = ""
235 236 text << cs_to.format_identifier + sep if cs_to
236 237 text << cs.format_identifier if cs
237 238 text
238 239 end
239 240
240 241 # Returns a path relative to the url of the repository
241 242 def relative_path(path)
242 243 path
243 244 end
244 245
245 246 # Finds and returns a revision with a number or the beginning of a hash
246 247 def find_changeset_by_name(name)
247 248 return nil if name.blank?
248 249 s = name.to_s
249 250 if s.match(/^\d*$/)
250 251 changesets.where("revision = ?", s).first
251 252 else
252 253 changesets.where("revision LIKE ?", s + '%').first
253 254 end
254 255 end
255 256
256 257 def latest_changeset
257 258 @latest_changeset ||= changesets.first
258 259 end
259 260
260 261 # Returns the latest changesets for +path+
261 262 # Default behaviour is to search in cached changesets
262 263 def latest_changesets(path, rev, limit=10)
263 264 if path.blank?
264 265 changesets.
265 266 reorder("#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC").
266 267 limit(limit).
267 268 preload(:user).
268 269 to_a
269 270 else
270 271 filechanges.
271 272 where("path = ?", path.with_leading_slash).
272 273 reorder("#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC").
273 274 limit(limit).
274 275 preload(:changeset => :user).
275 276 collect(&:changeset)
276 277 end
277 278 end
278 279
279 280 def scan_changesets_for_issue_ids
280 281 self.changesets.each(&:scan_comment_for_issue_ids)
281 282 end
282 283
283 284 # Returns an array of committers usernames and associated user_id
284 285 def committers
285 286 @committers ||= Changeset.where(:repository_id => id).uniq.pluck(:committer, :user_id)
286 287 end
287 288
288 289 # Maps committers username to a user ids
289 290 def committer_ids=(h)
290 291 if h.is_a?(Hash)
291 292 committers.each do |committer, user_id|
292 293 new_user_id = h[committer]
293 294 if new_user_id && (new_user_id.to_i != user_id.to_i)
294 295 new_user_id = (new_user_id.to_i > 0 ? new_user_id.to_i : nil)
295 296 Changeset.where(["repository_id = ? AND committer = ?", id, committer]).
296 297 update_all("user_id = #{new_user_id.nil? ? 'NULL' : new_user_id}")
297 298 end
298 299 end
299 300 @committers = nil
300 301 @found_committer_users = nil
301 302 true
302 303 else
303 304 false
304 305 end
305 306 end
306 307
307 308 # Returns the Redmine User corresponding to the given +committer+
308 309 # It will return nil if the committer is not yet mapped and if no User
309 310 # with the same username or email was found
310 311 def find_committer_user(committer)
311 312 unless committer.blank?
312 313 @found_committer_users ||= {}
313 314 return @found_committer_users[committer] if @found_committer_users.has_key?(committer)
314 315
315 316 user = nil
316 317 c = changesets.where(:committer => committer).
317 318 includes(:user).references(:user).first
318 319 if c && c.user
319 320 user = c.user
320 321 elsif committer.strip =~ /^([^<]+)(<(.*)>)?$/
321 322 username, email = $1.strip, $3
322 323 u = User.find_by_login(username)
323 324 u ||= User.find_by_mail(email) unless email.blank?
324 325 user = u
325 326 end
326 327 @found_committer_users[committer] = user
327 328 user
328 329 end
329 330 end
330 331
331 332 def repo_log_encoding
332 333 encoding = log_encoding.to_s.strip
333 334 encoding.blank? ? 'UTF-8' : encoding
334 335 end
335 336
336 337 # Fetches new changesets for all repositories of active projects
337 338 # Can be called periodically by an external script
338 339 # eg. ruby script/runner "Repository.fetch_changesets"
339 340 def self.fetch_changesets
340 341 Project.active.has_module(:repository).all.each do |project|
341 342 project.repositories.each do |repository|
342 343 begin
343 344 repository.fetch_changesets
344 345 rescue Redmine::Scm::Adapters::CommandFailed => e
345 346 logger.error "scm: error during fetching changesets: #{e.message}"
346 347 end
347 348 end
348 349 end
349 350 end
350 351
351 352 # scan changeset comments to find related and fixed issues for all repositories
352 353 def self.scan_changesets_for_issue_ids
353 354 all.each(&:scan_changesets_for_issue_ids)
354 355 end
355 356
356 357 def self.scm_name
357 358 'Abstract'
358 359 end
359 360
360 361 def self.available_scm
361 362 subclasses.collect {|klass| [klass.scm_name, klass.name]}
362 363 end
363 364
364 365 def self.factory(klass_name, *args)
365 366 klass = "Repository::#{klass_name}".constantize
366 367 klass.new(*args)
367 368 rescue
368 369 nil
369 370 end
370 371
371 372 def self.scm_adapter_class
372 373 nil
373 374 end
374 375
375 376 def self.scm_command
376 377 ret = ""
377 378 begin
378 379 ret = self.scm_adapter_class.client_command if self.scm_adapter_class
379 380 rescue Exception => e
380 381 logger.error "scm: error during get command: #{e.message}"
381 382 end
382 383 ret
383 384 end
384 385
385 386 def self.scm_version_string
386 387 ret = ""
387 388 begin
388 389 ret = self.scm_adapter_class.client_version_string if self.scm_adapter_class
389 390 rescue Exception => e
390 391 logger.error "scm: error during get version string: #{e.message}"
391 392 end
392 393 ret
393 394 end
394 395
395 396 def self.scm_available
396 397 ret = false
397 398 begin
398 399 ret = self.scm_adapter_class.client_available if self.scm_adapter_class
399 400 rescue Exception => e
400 401 logger.error "scm: error during get scm available: #{e.message}"
401 402 end
402 403 ret
403 404 end
404 405
405 406 def set_as_default?
406 407 new_record? && project && Repository.where(:project_id => project.id).empty?
407 408 end
408 409
409 410 # Returns a hash with statistics by author in the following form:
410 411 # {
411 412 # "John Smith" => { :commits => 45, :changes => 324 },
412 413 # "Bob" => { ... }
413 414 # }
414 415 #
415 416 # Notes:
416 417 # - this hash honnors the users mapping defined for the repository
417 418 def stats_by_author
418 419 commits = Changeset.where("repository_id = ?", id).select("committer, user_id, count(*) as count").group("committer, user_id")
419 420
420 421 #TODO: restore ordering ; this line probably never worked
421 422 #commits.to_a.sort! {|x, y| x.last <=> y.last}
422 423
423 424 changes = Change.joins(:changeset).where("#{Changeset.table_name}.repository_id = ?", id).select("committer, user_id, count(*) as count").group("committer, user_id")
424 425
425 426 user_ids = changesets.map(&:user_id).compact.uniq
426 427 authors_names = User.where(:id => user_ids).inject({}) do |memo, user|
427 428 memo[user.id] = user.to_s
428 429 memo
429 430 end
430 431
431 432 (commits + changes).inject({}) do |hash, element|
432 433 mapped_name = element.committer
433 434 if username = authors_names[element.user_id.to_i]
434 435 mapped_name = username
435 436 end
436 437 hash[mapped_name] ||= { :commits_count => 0, :changes_count => 0 }
437 438 if element.is_a?(Changeset)
438 439 hash[mapped_name][:commits_count] += element.count.to_i
439 440 else
440 441 hash[mapped_name][:changes_count] += element.count.to_i
441 442 end
442 443 hash
443 444 end
444 445 end
445 446
446 447 # Returns a scope of changesets that come from the same commit as the given changeset
447 448 # in different repositories that point to the same backend
448 449 def same_commits_in_scope(scope, changeset)
449 450 scope = scope.joins(:repository).where(:repositories => {:url => url, :root_url => root_url, :type => type})
450 451 if changeset.scmid.present?
451 452 scope = scope.where(:scmid => changeset.scmid)
452 453 else
453 454 scope = scope.where(:revision => changeset.revision)
454 455 end
455 456 scope
456 457 end
457 458
458 459 protected
459 460
460 461 # Validates repository url based against an optional regular expression
461 462 # that can be set in the Redmine configuration file.
462 463 def validate_repository_path(attribute=:url)
463 464 regexp = Redmine::Configuration["scm_#{scm_name.to_s.downcase}_path_regexp"]
464 465 if changes[attribute] && regexp.present?
465 466 regexp = regexp.to_s.strip.gsub('%project%') {Regexp.escape(project.try(:identifier).to_s)}
466 467 unless send(attribute).to_s.match(Regexp.new("\\A#{regexp}\\z"))
467 468 errors.add(attribute, :invalid)
468 469 end
469 470 end
470 471 end
471 472
473 def normalize_identifier
474 self.identifier = identifier.to_s.strip
475 end
476
472 477 def check_default
473 478 if !is_default? && set_as_default?
474 479 self.is_default = true
475 480 end
476 481 if is_default? && is_default_changed?
477 482 Repository.where(["project_id = ?", project_id]).update_all(["is_default = ?", false])
478 483 end
479 484 end
480 485
481 486 def load_entries_changesets(entries)
482 487 if entries
483 488 entries.each do |entry|
484 489 if entry.lastrev && entry.lastrev.identifier
485 490 entry.changeset = find_changeset_by_name(entry.lastrev.identifier)
486 491 end
487 492 end
488 493 end
489 494 end
490 495
491 496 private
492 497
493 498 # Deletes repository data
494 499 def clear_changesets
495 500 cs = Changeset.table_name
496 501 ch = Change.table_name
497 502 ci = "#{table_name_prefix}changesets_issues#{table_name_suffix}"
498 503 cp = "#{table_name_prefix}changeset_parents#{table_name_suffix}"
499 504
500 505 self.class.connection.delete("DELETE FROM #{ch} WHERE #{ch}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
501 506 self.class.connection.delete("DELETE FROM #{ci} WHERE #{ci}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
502 507 self.class.connection.delete("DELETE FROM #{cp} WHERE #{cp}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
503 508 self.class.connection.delete("DELETE FROM #{cs} WHERE #{cs}.repository_id = #{id}")
504 509 end
505 510 end
@@ -1,487 +1,499
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 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 :email_addresses,
33 33 :members,
34 34 :member_roles,
35 35 :roles,
36 36 :enumerations
37 37
38 38 include Redmine::I18n
39 39
40 40 def setup
41 41 @repository = Project.find(1).repository
42 42 end
43 43
44 44 def test_blank_log_encoding_error_message
45 45 set_language_if_valid 'en'
46 46 repo = Repository::Bazaar.new(
47 47 :project => Project.find(3),
48 48 :url => "/test",
49 49 :log_encoding => ''
50 50 )
51 51 assert !repo.save
52 52 assert_include "Commit messages encoding cannot be blank",
53 53 repo.errors.full_messages
54 54 end
55 55
56 56 def test_blank_log_encoding_error_message_fr
57 57 set_language_if_valid 'fr'
58 58 str = "Encodage des messages de commit doit \xc3\xaatre renseign\xc3\xa9(e)".force_encoding('UTF-8')
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_2_repositories_with_same_identifier_in_different_projects_should_be_valid
80 80 Repository::Subversion.create!(:project_id => 2, :identifier => 'foo', :url => 'file:///foo')
81 81 r = Repository::Subversion.new(:project_id => 3, :identifier => 'foo', :url => 'file:///bar')
82 82 assert r.save
83 83 end
84 84
85 85 def test_2_repositories_with_same_identifier_should_not_be_valid
86 86 Repository::Subversion.create!(:project_id => 3, :identifier => 'foo', :url => 'file:///foo')
87 87 r = Repository::Subversion.new(:project_id => 3, :identifier => 'foo', :url => 'file:///bar')
88 88 assert !r.save
89 89 end
90 90
91 91 def test_2_repositories_with_blank_identifier_should_not_be_valid
92 92 Repository::Subversion.create!(:project_id => 3, :identifier => '', :url => 'file:///foo')
93 93 r = Repository::Subversion.new(:project_id => 3, :identifier => '', :url => 'file:///bar')
94 94 assert !r.save
95 95 end
96 96
97 def test_2_repositories_with_blank_identifier_and_one_as_default_should_not_be_valid
98 Repository::Subversion.create!(:project_id => 3, :identifier => '', :url => 'file:///foo', :is_default => true)
99 r = Repository::Subversion.new(:project_id => 3, :identifier => '', :url => 'file:///bar')
100 assert !r.save
101 end
102
103 def test_2_repositories_with_blank_and_nil_identifier_should_not_be_valid
104 Repository::Subversion.create!(:project_id => 3, :identifier => nil, :url => 'file:///foo')
105 r = Repository::Subversion.new(:project_id => 3, :identifier => '', :url => 'file:///bar')
106 assert !r.save
107 end
108
97 109 def test_first_repository_should_be_set_as_default
98 110 repository1 = Repository::Subversion.new(
99 111 :project => Project.find(3),
100 112 :identifier => 'svn1',
101 113 :url => 'file:///svn1'
102 114 )
103 115 assert repository1.save
104 116 assert repository1.is_default?
105 117
106 118 repository2 = Repository::Subversion.new(
107 119 :project => Project.find(3),
108 120 :identifier => 'svn2',
109 121 :url => 'file:///svn2'
110 122 )
111 123 assert repository2.save
112 124 assert !repository2.is_default?
113 125
114 126 assert_equal repository1, Project.find(3).repository
115 127 assert_equal [repository1, repository2], Project.find(3).repositories.sort
116 128 end
117 129
118 130 def test_default_repository_should_be_one
119 131 assert_equal 0, Project.find(3).repositories.count
120 132 repository1 = Repository::Subversion.new(
121 133 :project => Project.find(3),
122 134 :identifier => 'svn1',
123 135 :url => 'file:///svn1'
124 136 )
125 137 assert repository1.save
126 138 assert repository1.is_default?
127 139
128 140 repository2 = Repository::Subversion.new(
129 141 :project => Project.find(3),
130 142 :identifier => 'svn2',
131 143 :url => 'file:///svn2',
132 144 :is_default => true
133 145 )
134 146 assert repository2.save
135 147 assert repository2.is_default?
136 148 repository1.reload
137 149 assert !repository1.is_default?
138 150
139 151 assert_equal repository2, Project.find(3).repository
140 152 assert_equal [repository2, repository1], Project.find(3).repositories.sort
141 153 end
142 154
143 155 def test_identifier_should_accept_letters_digits_dashes_and_underscores
144 156 r = Repository::Subversion.new(
145 157 :project_id => 3,
146 158 :identifier => 'svn-123_45',
147 159 :url => 'file:///svn'
148 160 )
149 161 assert r.save
150 162 end
151 163
152 164 def test_identifier_should_not_be_frozen_for_a_new_repository
153 165 assert_equal false, Repository.new.identifier_frozen?
154 166 end
155 167
156 168 def test_identifier_should_not_be_frozen_for_a_saved_repository_with_blank_identifier
157 169 Repository.where(:id => 10).update_all(["identifier = ''"])
158 170 assert_equal false, Repository.find(10).identifier_frozen?
159 171 end
160 172
161 173 def test_identifier_should_be_frozen_for_a_saved_repository_with_valid_identifier
162 174 Repository.where(:id => 10).update_all(["identifier = 'abc123'"])
163 175 assert_equal true, Repository.find(10).identifier_frozen?
164 176 end
165 177
166 178 def test_identifier_should_not_accept_change_if_frozen
167 179 r = Repository.new(:identifier => 'foo')
168 180 r.stubs(:identifier_frozen?).returns(true)
169 181
170 182 r.identifier = 'bar'
171 183 assert_equal 'foo', r.identifier
172 184 end
173 185
174 186 def test_identifier_should_accept_change_if_not_frozen
175 187 r = Repository.new(:identifier => 'foo')
176 188 r.stubs(:identifier_frozen?).returns(false)
177 189
178 190 r.identifier = 'bar'
179 191 assert_equal 'bar', r.identifier
180 192 end
181 193
182 194 def test_destroy
183 195 repository = Repository.find(10)
184 196 changesets = repository.changesets.count
185 197 changes = repository.filechanges.count
186 198
187 199 assert_difference 'Changeset.count', -changesets do
188 200 assert_difference 'Change.count', -changes do
189 201 Repository.find(10).destroy
190 202 end
191 203 end
192 204 end
193 205
194 206 def test_destroy_should_delete_parents_associations
195 207 changeset = Changeset.find(102)
196 208 changeset.parents = Changeset.where(:id => [100, 101]).to_a
197 209 assert_difference 'Changeset.connection.select_all("select * from changeset_parents").count', -2 do
198 210 Repository.find(10).destroy
199 211 end
200 212 end
201 213
202 214 def test_destroy_should_delete_issues_associations
203 215 changeset = Changeset.find(102)
204 216 changeset.issues = Issue.where(:id => [1, 2]).to_a
205 217 assert_difference 'Changeset.connection.select_all("select * from changesets_issues").count', -2 do
206 218 Repository.find(10).destroy
207 219 end
208 220 end
209 221
210 222 def test_should_not_create_with_disabled_scm
211 223 # disable Subversion
212 224 with_settings :enabled_scm => ['Darcs', 'Git'] do
213 225 repository = Repository::Subversion.new(
214 226 :project => Project.find(3), :url => "svn://localhost")
215 227 assert !repository.save
216 228 assert_include I18n.translate('activerecord.errors.messages.invalid'),
217 229 repository.errors[:type]
218 230 end
219 231 end
220 232
221 233 def test_scan_changesets_for_issue_ids
222 234 Setting.default_language = 'en'
223 235 Setting.commit_ref_keywords = 'refs , references, IssueID'
224 236 Setting.commit_update_keywords = [
225 237 {'keywords' => 'fixes , closes',
226 238 'status_id' => IssueStatus.where(:is_closed => true).first.id,
227 239 'done_ratio' => '90'}
228 240 ]
229 241 Setting.default_language = 'en'
230 242 ActionMailer::Base.deliveries.clear
231 243
232 244 # make sure issue 1 is not already closed
233 245 fixed_issue = Issue.find(1)
234 246 assert !fixed_issue.closed?
235 247 old_status = fixed_issue.status
236 248
237 249 with_settings :notified_events => %w(issue_added issue_updated) do
238 250 Repository.scan_changesets_for_issue_ids
239 251 end
240 252 assert_equal [101, 102], Issue.find(3).changeset_ids
241 253
242 254 # fixed issues
243 255 fixed_issue.reload
244 256 assert fixed_issue.closed?
245 257 assert_equal 90, fixed_issue.done_ratio
246 258 assert_equal [101], fixed_issue.changeset_ids
247 259
248 260 # issue change
249 261 journal = fixed_issue.journals.reorder('created_on desc').first
250 262 assert_equal User.find_by_login('dlopper'), journal.user
251 263 assert_equal 'Applied in changeset r2.', journal.notes
252 264
253 265 # 2 email notifications
254 266 assert_equal 2, ActionMailer::Base.deliveries.size
255 267 mail = ActionMailer::Base.deliveries.first
256 268 assert_not_nil mail
257 269 assert mail.subject.starts_with?(
258 270 "[#{fixed_issue.project.name} - #{fixed_issue.tracker.name} ##{fixed_issue.id}]")
259 271 assert_mail_body_match(
260 272 "Status changed from #{old_status} to #{fixed_issue.status}", mail)
261 273
262 274 # ignoring commits referencing an issue of another project
263 275 assert_equal [], Issue.find(4).changesets
264 276 end
265 277
266 278 def test_for_changeset_comments_strip
267 279 repository = Repository::Mercurial.create(
268 280 :project => Project.find( 4 ),
269 281 :url => '/foo/bar/baz' )
270 282 comment = <<-COMMENT
271 283 This is a loooooooooooooooooooooooooooong comment
272 284
273 285
274 286 COMMENT
275 287 changeset = Changeset.new(
276 288 :comments => comment, :commit_date => Time.now,
277 289 :revision => 0, :scmid => 'f39b7922fb3c',
278 290 :committer => 'foo <foo@example.com>',
279 291 :committed_on => Time.now, :repository => repository )
280 292 assert( changeset.save )
281 293 assert_not_equal( comment, changeset.comments )
282 294 assert_equal( 'This is a loooooooooooooooooooooooooooong comment',
283 295 changeset.comments )
284 296 end
285 297
286 298 def test_for_urls_strip_cvs
287 299 repository = Repository::Cvs.create(
288 300 :project => Project.find(4),
289 301 :url => ' :pserver:login:password@host:/path/to/the/repository',
290 302 :root_url => 'foo ',
291 303 :log_encoding => 'UTF-8')
292 304 assert repository.save
293 305 repository.reload
294 306 assert_equal ':pserver:login:password@host:/path/to/the/repository',
295 307 repository.url
296 308 assert_equal 'foo', repository.root_url
297 309 end
298 310
299 311 def test_for_urls_strip_subversion
300 312 repository = Repository::Subversion.create(
301 313 :project => Project.find(4),
302 314 :url => ' file:///dummy ')
303 315 assert repository.save
304 316 repository.reload
305 317 assert_equal 'file:///dummy', repository.url
306 318 end
307 319
308 320 def test_for_urls_strip_git
309 321 repository = Repository::Git.create(
310 322 :project => Project.find(4),
311 323 :url => ' c:\dummy ')
312 324 assert repository.save
313 325 repository.reload
314 326 assert_equal 'c:\dummy', repository.url
315 327 end
316 328
317 329 def test_manual_user_mapping
318 330 assert_no_difference "Changeset.where('user_id <> 2').count" do
319 331 c = Changeset.create!(
320 332 :repository => @repository,
321 333 :committer => 'foo',
322 334 :committed_on => Time.now,
323 335 :revision => 100,
324 336 :comments => 'Committed by foo.'
325 337 )
326 338 assert_nil c.user
327 339 @repository.committer_ids = {'foo' => '2'}
328 340 assert_equal User.find(2), c.reload.user
329 341 # committer is now mapped
330 342 c = Changeset.create!(
331 343 :repository => @repository,
332 344 :committer => 'foo',
333 345 :committed_on => Time.now,
334 346 :revision => 101,
335 347 :comments => 'Another commit by foo.'
336 348 )
337 349 assert_equal User.find(2), c.user
338 350 end
339 351 end
340 352
341 353 def test_auto_user_mapping_by_username
342 354 c = Changeset.create!(
343 355 :repository => @repository,
344 356 :committer => 'jsmith',
345 357 :committed_on => Time.now,
346 358 :revision => 100,
347 359 :comments => 'Committed by john.'
348 360 )
349 361 assert_equal User.find(2), c.user
350 362 end
351 363
352 364 def test_auto_user_mapping_by_email
353 365 c = Changeset.create!(
354 366 :repository => @repository,
355 367 :committer => 'john <jsmith@somenet.foo>',
356 368 :committed_on => Time.now,
357 369 :revision => 100,
358 370 :comments => 'Committed by john.'
359 371 )
360 372 assert_equal User.find(2), c.user
361 373 end
362 374
363 375 def test_filesystem_avaialbe
364 376 klass = Repository::Filesystem
365 377 assert klass.scm_adapter_class
366 378 assert_equal true, klass.scm_available
367 379 end
368 380
369 381 def test_extra_info_should_not_return_non_hash_value
370 382 repo = Repository.new
371 383 repo.extra_info = "foo"
372 384 assert_nil repo.extra_info
373 385 end
374 386
375 387 def test_merge_extra_info
376 388 repo = Repository::Subversion.new(:project => Project.find(3))
377 389 assert !repo.save
378 390 repo.url = "svn://localhost"
379 391 assert repo.save
380 392 repo.reload
381 393 project = Project.find(3)
382 394 assert_equal repo, project.repository
383 395 assert_nil repo.extra_info
384 396 h1 = {"test_1" => {"test_11" => "test_value_11"}}
385 397 repo.merge_extra_info(h1)
386 398 assert_equal h1, repo.extra_info
387 399 h2 = {"test_2" => {
388 400 "test_21" => "test_value_21",
389 401 "test_22" => "test_value_22",
390 402 }}
391 403 repo.merge_extra_info(h2)
392 404 assert_equal (h = {"test_11" => "test_value_11"}),
393 405 repo.extra_info["test_1"]
394 406 assert_equal "test_value_21",
395 407 repo.extra_info["test_2"]["test_21"]
396 408 h3 = {"test_2" => {
397 409 "test_23" => "test_value_23",
398 410 "test_24" => "test_value_24",
399 411 }}
400 412 repo.merge_extra_info(h3)
401 413 assert_equal (h = {"test_11" => "test_value_11"}),
402 414 repo.extra_info["test_1"]
403 415 assert_nil repo.extra_info["test_2"]["test_21"]
404 416 assert_equal "test_value_23",
405 417 repo.extra_info["test_2"]["test_23"]
406 418 end
407 419
408 420 def test_sort_should_not_raise_an_error_with_nil_identifiers
409 421 r1 = Repository.new
410 422 r2 = Repository.new
411 423
412 424 assert_nothing_raised do
413 425 [r1, r2].sort
414 426 end
415 427 end
416 428
417 429 def test_stats_by_author_reflect_changesets_and_changes
418 430 repository = Repository.find(10)
419 431
420 432 expected = {"Dave Lopper"=>{:commits_count=>10, :changes_count=>3}}
421 433 assert_equal expected, repository.stats_by_author
422 434
423 435 set = Changeset.create!(
424 436 :repository => repository,
425 437 :committer => 'dlopper',
426 438 :committed_on => Time.now,
427 439 :revision => 101,
428 440 :comments => 'Another commit by foo.'
429 441 )
430 442 Change.create!(:changeset => set, :action => 'A', :path => '/path/to/file1')
431 443 Change.create!(:changeset => set, :action => 'A', :path => '/path/to/file2')
432 444 expected = {"Dave Lopper"=>{:commits_count=>11, :changes_count=>5}}
433 445 assert_equal expected, repository.stats_by_author
434 446 end
435 447
436 448 def test_stats_by_author_honnor_committers
437 449 # in fact it is really tested above, but let's have a dedicated test
438 450 # to ensure things are dynamically linked to Users
439 451 User.find_by_login("dlopper").update_attribute(:firstname, "Dave's")
440 452 repository = Repository.find(10)
441 453 expected = {"Dave's Lopper"=>{:commits_count=>10, :changes_count=>3}}
442 454 assert_equal expected, repository.stats_by_author
443 455 end
444 456
445 457 def test_stats_by_author_doesnt_drop_unmapped_users
446 458 repository = Repository.find(10)
447 459 Changeset.create!(
448 460 :repository => repository,
449 461 :committer => 'unnamed <foo@bar.net>',
450 462 :committed_on => Time.now,
451 463 :revision => 101,
452 464 :comments => 'Another commit by foo.'
453 465 )
454 466
455 467 assert repository.stats_by_author.has_key?("unnamed <foo@bar.net>")
456 468 end
457 469
458 470 def test_stats_by_author_merge_correctly
459 471 # as we honnor users->committer map and it's not injective,
460 472 # we must be sure merges happen correctly and stats are not
461 473 # wiped out when two source counts map to the same user.
462 474 #
463 475 # Here we have Changeset's with committer="dlopper" and others
464 476 # with committer="dlopper <dlopper@somefoo.net>"
465 477 repository = Repository.find(10)
466 478
467 479 expected = {"Dave Lopper"=>{:commits_count=>10, :changes_count=>3}}
468 480 assert_equal expected, repository.stats_by_author
469 481
470 482 set = Changeset.create!(
471 483 :repository => repository,
472 484 :committer => 'dlopper <dlopper@somefoo.net>',
473 485 :committed_on => Time.now,
474 486 :revision => 101,
475 487 :comments => 'Another commit by foo.'
476 488 )
477 489
478 490 expected = {"Dave Lopper"=>{:commits_count=>11, :changes_count=>3}}
479 491 assert_equal expected, repository.stats_by_author
480 492 end
481 493
482 494 def test_fetch_changesets
483 495 # 2 repositories in fixtures
484 496 Repository::Subversion.any_instance.expects(:fetch_changesets).twice.returns(true)
485 497 Repository.fetch_changesets
486 498 end
487 499 end
General Comments 0
You need to be logged in to leave comments. Login now