##// END OF EJS Templates
Merged r12848 (#16032)....
Jean-Philippe Lang -
r12578:287bcacd81d9
parent child
Show More
@@ -1,437 +1,443
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class ScmFetchError < Exception; end
18 class ScmFetchError < Exception; end
19
19
20 class Repository < ActiveRecord::Base
20 class Repository < ActiveRecord::Base
21 include Redmine::Ciphering
21 include Redmine::Ciphering
22 include Redmine::SafeAttributes
22 include Redmine::SafeAttributes
23
23
24 # Maximum length for repository identifiers
24 # Maximum length for repository identifiers
25 IDENTIFIER_MAX_LENGTH = 255
25 IDENTIFIER_MAX_LENGTH = 255
26
26
27 belongs_to :project
27 belongs_to :project
28 has_many :changesets, :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC"
28 has_many :changesets, :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC"
29 has_many :filechanges, :class_name => 'Change', :through => :changesets
29 has_many :filechanges, :class_name => 'Change', :through => :changesets
30
30
31 serialize :extra_info
31 serialize :extra_info
32
32
33 before_save :check_default
33 before_save :check_default
34
34
35 # Raw SQL to delete changesets and changes in the database
35 # Raw SQL to delete changesets and changes in the database
36 # has_many :changesets, :dependent => :destroy is too slow for big repositories
36 # has_many :changesets, :dependent => :destroy is too slow for big repositories
37 before_destroy :clear_changesets
37 before_destroy :clear_changesets
38
38
39 validates_length_of :password, :maximum => 255, :allow_nil => true
39 validates_length_of :password, :maximum => 255, :allow_nil => true
40 validates_length_of :identifier, :maximum => IDENTIFIER_MAX_LENGTH, :allow_blank => true
40 validates_length_of :identifier, :maximum => IDENTIFIER_MAX_LENGTH, :allow_blank => true
41 validates_presence_of :identifier, :unless => Proc.new { |r| r.is_default? || r.set_as_default? }
41 validates_presence_of :identifier, :unless => Proc.new { |r| r.is_default? || r.set_as_default? }
42 validates_uniqueness_of :identifier, :scope => :project_id, :allow_blank => true
42 validates_uniqueness_of :identifier, :scope => :project_id, :allow_blank => true
43 validates_exclusion_of :identifier, :in => %w(show entry raw changes annotate diff show stats graph)
43 validates_exclusion_of :identifier, :in => %w(show entry raw changes annotate diff show stats graph)
44 # donwcase letters, digits, dashes, underscores but not digits only
44 # donwcase letters, digits, dashes, underscores but not digits only
45 validates_format_of :identifier, :with => /\A(?!\d+$)[a-z0-9\-_]*\z/, :allow_blank => true
45 validates_format_of :identifier, :with => /\A(?!\d+$)[a-z0-9\-_]*\z/, :allow_blank => true
46 # Checks if the SCM is enabled when creating a repository
46 # Checks if the SCM is enabled when creating a repository
47 validate :repo_create_validation, :on => :create
47 validate :repo_create_validation, :on => :create
48
48
49 safe_attributes 'identifier',
49 safe_attributes 'identifier',
50 'login',
50 'login',
51 'password',
51 'password',
52 'path_encoding',
52 'path_encoding',
53 'log_encoding',
53 'log_encoding',
54 'is_default'
54 'is_default'
55
55
56 safe_attributes 'url',
56 safe_attributes 'url',
57 :if => lambda {|repository, user| repository.new_record?}
57 :if => lambda {|repository, user| repository.new_record?}
58
58
59 def repo_create_validation
59 def repo_create_validation
60 unless Setting.enabled_scm.include?(self.class.name.demodulize)
60 unless Setting.enabled_scm.include?(self.class.name.demodulize)
61 errors.add(:type, :invalid)
61 errors.add(:type, :invalid)
62 end
62 end
63 end
63 end
64
64
65 def self.human_attribute_name(attribute_key_name, *args)
65 def self.human_attribute_name(attribute_key_name, *args)
66 attr_name = attribute_key_name.to_s
66 attr_name = attribute_key_name.to_s
67 if attr_name == "log_encoding"
67 if attr_name == "log_encoding"
68 attr_name = "commit_logs_encoding"
68 attr_name = "commit_logs_encoding"
69 end
69 end
70 super(attr_name, *args)
70 super(attr_name, *args)
71 end
71 end
72
72
73 # Removes leading and trailing whitespace
73 # Removes leading and trailing whitespace
74 def url=(arg)
74 def url=(arg)
75 write_attribute(:url, arg ? arg.to_s.strip : nil)
75 write_attribute(:url, arg ? arg.to_s.strip : nil)
76 end
76 end
77
77
78 # Removes leading and trailing whitespace
78 # Removes leading and trailing whitespace
79 def root_url=(arg)
79 def root_url=(arg)
80 write_attribute(:root_url, arg ? arg.to_s.strip : nil)
80 write_attribute(:root_url, arg ? arg.to_s.strip : nil)
81 end
81 end
82
82
83 def password
83 def password
84 read_ciphered_attribute(:password)
84 read_ciphered_attribute(:password)
85 end
85 end
86
86
87 def password=(arg)
87 def password=(arg)
88 write_ciphered_attribute(:password, arg)
88 write_ciphered_attribute(:password, arg)
89 end
89 end
90
90
91 def scm_adapter
91 def scm_adapter
92 self.class.scm_adapter_class
92 self.class.scm_adapter_class
93 end
93 end
94
94
95 def scm
95 def scm
96 unless @scm
96 unless @scm
97 @scm = self.scm_adapter.new(url, root_url,
97 @scm = self.scm_adapter.new(url, root_url,
98 login, password, path_encoding)
98 login, password, path_encoding)
99 if root_url.blank? && @scm.root_url.present?
99 if root_url.blank? && @scm.root_url.present?
100 update_attribute(:root_url, @scm.root_url)
100 update_attribute(:root_url, @scm.root_url)
101 end
101 end
102 end
102 end
103 @scm
103 @scm
104 end
104 end
105
105
106 def scm_name
106 def scm_name
107 self.class.scm_name
107 self.class.scm_name
108 end
108 end
109
109
110 def name
110 def name
111 if identifier.present?
111 if identifier.present?
112 identifier
112 identifier
113 elsif is_default?
113 elsif is_default?
114 l(:field_repository_is_default)
114 l(:field_repository_is_default)
115 else
115 else
116 scm_name
116 scm_name
117 end
117 end
118 end
118 end
119
119
120 def identifier=(identifier)
120 def identifier=(identifier)
121 super unless identifier_frozen?
121 super unless identifier_frozen?
122 end
122 end
123
123
124 def identifier_frozen?
124 def identifier_frozen?
125 errors[:identifier].blank? && !(new_record? || identifier.blank?)
125 errors[:identifier].blank? && !(new_record? || identifier.blank?)
126 end
126 end
127
127
128 def identifier_param
128 def identifier_param
129 if is_default?
129 if is_default?
130 nil
130 nil
131 elsif identifier.present?
131 elsif identifier.present?
132 identifier
132 identifier
133 else
133 else
134 id.to_s
134 id.to_s
135 end
135 end
136 end
136 end
137
137
138 def <=>(repository)
138 def <=>(repository)
139 if is_default?
139 if is_default?
140 -1
140 -1
141 elsif repository.is_default?
141 elsif repository.is_default?
142 1
142 1
143 else
143 else
144 identifier.to_s <=> repository.identifier.to_s
144 identifier.to_s <=> repository.identifier.to_s
145 end
145 end
146 end
146 end
147
147
148 def self.find_by_identifier_param(param)
148 def self.find_by_identifier_param(param)
149 if param.to_s =~ /^\d+$/
149 if param.to_s =~ /^\d+$/
150 find_by_id(param)
150 find_by_id(param)
151 else
151 else
152 find_by_identifier(param)
152 find_by_identifier(param)
153 end
153 end
154 end
154 end
155
155
156 # TODO: should return an empty hash instead of nil to avoid many ||{}
157 def extra_info
158 h = read_attribute(:extra_info)
159 h.is_a?(Hash) ? h : nil
160 end
161
156 def merge_extra_info(arg)
162 def merge_extra_info(arg)
157 h = extra_info || {}
163 h = extra_info || {}
158 return h if arg.nil?
164 return h if arg.nil?
159 h.merge!(arg)
165 h.merge!(arg)
160 write_attribute(:extra_info, h)
166 write_attribute(:extra_info, h)
161 end
167 end
162
168
163 def report_last_commit
169 def report_last_commit
164 true
170 true
165 end
171 end
166
172
167 def supports_cat?
173 def supports_cat?
168 scm.supports_cat?
174 scm.supports_cat?
169 end
175 end
170
176
171 def supports_annotate?
177 def supports_annotate?
172 scm.supports_annotate?
178 scm.supports_annotate?
173 end
179 end
174
180
175 def supports_all_revisions?
181 def supports_all_revisions?
176 true
182 true
177 end
183 end
178
184
179 def supports_directory_revisions?
185 def supports_directory_revisions?
180 false
186 false
181 end
187 end
182
188
183 def supports_revision_graph?
189 def supports_revision_graph?
184 false
190 false
185 end
191 end
186
192
187 def entry(path=nil, identifier=nil)
193 def entry(path=nil, identifier=nil)
188 scm.entry(path, identifier)
194 scm.entry(path, identifier)
189 end
195 end
190
196
191 def entries(path=nil, identifier=nil)
197 def entries(path=nil, identifier=nil)
192 entries = scm.entries(path, identifier)
198 entries = scm.entries(path, identifier)
193 load_entries_changesets(entries)
199 load_entries_changesets(entries)
194 entries
200 entries
195 end
201 end
196
202
197 def branches
203 def branches
198 scm.branches
204 scm.branches
199 end
205 end
200
206
201 def tags
207 def tags
202 scm.tags
208 scm.tags
203 end
209 end
204
210
205 def default_branch
211 def default_branch
206 nil
212 nil
207 end
213 end
208
214
209 def properties(path, identifier=nil)
215 def properties(path, identifier=nil)
210 scm.properties(path, identifier)
216 scm.properties(path, identifier)
211 end
217 end
212
218
213 def cat(path, identifier=nil)
219 def cat(path, identifier=nil)
214 scm.cat(path, identifier)
220 scm.cat(path, identifier)
215 end
221 end
216
222
217 def diff(path, rev, rev_to)
223 def diff(path, rev, rev_to)
218 scm.diff(path, rev, rev_to)
224 scm.diff(path, rev, rev_to)
219 end
225 end
220
226
221 def diff_format_revisions(cs, cs_to, sep=':')
227 def diff_format_revisions(cs, cs_to, sep=':')
222 text = ""
228 text = ""
223 text << cs_to.format_identifier + sep if cs_to
229 text << cs_to.format_identifier + sep if cs_to
224 text << cs.format_identifier if cs
230 text << cs.format_identifier if cs
225 text
231 text
226 end
232 end
227
233
228 # Returns a path relative to the url of the repository
234 # Returns a path relative to the url of the repository
229 def relative_path(path)
235 def relative_path(path)
230 path
236 path
231 end
237 end
232
238
233 # Finds and returns a revision with a number or the beginning of a hash
239 # Finds and returns a revision with a number or the beginning of a hash
234 def find_changeset_by_name(name)
240 def find_changeset_by_name(name)
235 return nil if name.blank?
241 return nil if name.blank?
236 s = name.to_s
242 s = name.to_s
237 if s.match(/^\d*$/)
243 if s.match(/^\d*$/)
238 changesets.where("revision = ?", s).first
244 changesets.where("revision = ?", s).first
239 else
245 else
240 changesets.where("revision LIKE ?", s + '%').first
246 changesets.where("revision LIKE ?", s + '%').first
241 end
247 end
242 end
248 end
243
249
244 def latest_changeset
250 def latest_changeset
245 @latest_changeset ||= changesets.first
251 @latest_changeset ||= changesets.first
246 end
252 end
247
253
248 # Returns the latest changesets for +path+
254 # Returns the latest changesets for +path+
249 # Default behaviour is to search in cached changesets
255 # Default behaviour is to search in cached changesets
250 def latest_changesets(path, rev, limit=10)
256 def latest_changesets(path, rev, limit=10)
251 if path.blank?
257 if path.blank?
252 changesets.
258 changesets.
253 reorder("#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC").
259 reorder("#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC").
254 limit(limit).
260 limit(limit).
255 preload(:user).
261 preload(:user).
256 all
262 all
257 else
263 else
258 filechanges.
264 filechanges.
259 where("path = ?", path.with_leading_slash).
265 where("path = ?", path.with_leading_slash).
260 reorder("#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC").
266 reorder("#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC").
261 limit(limit).
267 limit(limit).
262 preload(:changeset => :user).
268 preload(:changeset => :user).
263 collect(&:changeset)
269 collect(&:changeset)
264 end
270 end
265 end
271 end
266
272
267 def scan_changesets_for_issue_ids
273 def scan_changesets_for_issue_ids
268 self.changesets.each(&:scan_comment_for_issue_ids)
274 self.changesets.each(&:scan_comment_for_issue_ids)
269 end
275 end
270
276
271 # Returns an array of committers usernames and associated user_id
277 # Returns an array of committers usernames and associated user_id
272 def committers
278 def committers
273 @committers ||= Changeset.connection.select_rows(
279 @committers ||= Changeset.connection.select_rows(
274 "SELECT DISTINCT committer, user_id FROM #{Changeset.table_name} WHERE repository_id = #{id}")
280 "SELECT DISTINCT committer, user_id FROM #{Changeset.table_name} WHERE repository_id = #{id}")
275 end
281 end
276
282
277 # Maps committers username to a user ids
283 # Maps committers username to a user ids
278 def committer_ids=(h)
284 def committer_ids=(h)
279 if h.is_a?(Hash)
285 if h.is_a?(Hash)
280 committers.each do |committer, user_id|
286 committers.each do |committer, user_id|
281 new_user_id = h[committer]
287 new_user_id = h[committer]
282 if new_user_id && (new_user_id.to_i != user_id.to_i)
288 if new_user_id && (new_user_id.to_i != user_id.to_i)
283 new_user_id = (new_user_id.to_i > 0 ? new_user_id.to_i : nil)
289 new_user_id = (new_user_id.to_i > 0 ? new_user_id.to_i : nil)
284 Changeset.update_all(
290 Changeset.update_all(
285 "user_id = #{ new_user_id.nil? ? 'NULL' : new_user_id }",
291 "user_id = #{ new_user_id.nil? ? 'NULL' : new_user_id }",
286 ["repository_id = ? AND committer = ?", id, committer])
292 ["repository_id = ? AND committer = ?", id, committer])
287 end
293 end
288 end
294 end
289 @committers = nil
295 @committers = nil
290 @found_committer_users = nil
296 @found_committer_users = nil
291 true
297 true
292 else
298 else
293 false
299 false
294 end
300 end
295 end
301 end
296
302
297 # Returns the Redmine User corresponding to the given +committer+
303 # Returns the Redmine User corresponding to the given +committer+
298 # It will return nil if the committer is not yet mapped and if no User
304 # It will return nil if the committer is not yet mapped and if no User
299 # with the same username or email was found
305 # with the same username or email was found
300 def find_committer_user(committer)
306 def find_committer_user(committer)
301 unless committer.blank?
307 unless committer.blank?
302 @found_committer_users ||= {}
308 @found_committer_users ||= {}
303 return @found_committer_users[committer] if @found_committer_users.has_key?(committer)
309 return @found_committer_users[committer] if @found_committer_users.has_key?(committer)
304
310
305 user = nil
311 user = nil
306 c = changesets.where(:committer => committer).includes(:user).first
312 c = changesets.where(:committer => committer).includes(:user).first
307 if c && c.user
313 if c && c.user
308 user = c.user
314 user = c.user
309 elsif committer.strip =~ /^([^<]+)(<(.*)>)?$/
315 elsif committer.strip =~ /^([^<]+)(<(.*)>)?$/
310 username, email = $1.strip, $3
316 username, email = $1.strip, $3
311 u = User.find_by_login(username)
317 u = User.find_by_login(username)
312 u ||= User.find_by_mail(email) unless email.blank?
318 u ||= User.find_by_mail(email) unless email.blank?
313 user = u
319 user = u
314 end
320 end
315 @found_committer_users[committer] = user
321 @found_committer_users[committer] = user
316 user
322 user
317 end
323 end
318 end
324 end
319
325
320 def repo_log_encoding
326 def repo_log_encoding
321 encoding = log_encoding.to_s.strip
327 encoding = log_encoding.to_s.strip
322 encoding.blank? ? 'UTF-8' : encoding
328 encoding.blank? ? 'UTF-8' : encoding
323 end
329 end
324
330
325 # Fetches new changesets for all repositories of active projects
331 # Fetches new changesets for all repositories of active projects
326 # Can be called periodically by an external script
332 # Can be called periodically by an external script
327 # eg. ruby script/runner "Repository.fetch_changesets"
333 # eg. ruby script/runner "Repository.fetch_changesets"
328 def self.fetch_changesets
334 def self.fetch_changesets
329 Project.active.has_module(:repository).all.each do |project|
335 Project.active.has_module(:repository).all.each do |project|
330 project.repositories.each do |repository|
336 project.repositories.each do |repository|
331 begin
337 begin
332 repository.fetch_changesets
338 repository.fetch_changesets
333 rescue Redmine::Scm::Adapters::CommandFailed => e
339 rescue Redmine::Scm::Adapters::CommandFailed => e
334 logger.error "scm: error during fetching changesets: #{e.message}"
340 logger.error "scm: error during fetching changesets: #{e.message}"
335 end
341 end
336 end
342 end
337 end
343 end
338 end
344 end
339
345
340 # scan changeset comments to find related and fixed issues for all repositories
346 # scan changeset comments to find related and fixed issues for all repositories
341 def self.scan_changesets_for_issue_ids
347 def self.scan_changesets_for_issue_ids
342 all.each(&:scan_changesets_for_issue_ids)
348 all.each(&:scan_changesets_for_issue_ids)
343 end
349 end
344
350
345 def self.scm_name
351 def self.scm_name
346 'Abstract'
352 'Abstract'
347 end
353 end
348
354
349 def self.available_scm
355 def self.available_scm
350 subclasses.collect {|klass| [klass.scm_name, klass.name]}
356 subclasses.collect {|klass| [klass.scm_name, klass.name]}
351 end
357 end
352
358
353 def self.factory(klass_name, *args)
359 def self.factory(klass_name, *args)
354 klass = "Repository::#{klass_name}".constantize
360 klass = "Repository::#{klass_name}".constantize
355 klass.new(*args)
361 klass.new(*args)
356 rescue
362 rescue
357 nil
363 nil
358 end
364 end
359
365
360 def self.scm_adapter_class
366 def self.scm_adapter_class
361 nil
367 nil
362 end
368 end
363
369
364 def self.scm_command
370 def self.scm_command
365 ret = ""
371 ret = ""
366 begin
372 begin
367 ret = self.scm_adapter_class.client_command if self.scm_adapter_class
373 ret = self.scm_adapter_class.client_command if self.scm_adapter_class
368 rescue Exception => e
374 rescue Exception => e
369 logger.error "scm: error during get command: #{e.message}"
375 logger.error "scm: error during get command: #{e.message}"
370 end
376 end
371 ret
377 ret
372 end
378 end
373
379
374 def self.scm_version_string
380 def self.scm_version_string
375 ret = ""
381 ret = ""
376 begin
382 begin
377 ret = self.scm_adapter_class.client_version_string if self.scm_adapter_class
383 ret = self.scm_adapter_class.client_version_string if self.scm_adapter_class
378 rescue Exception => e
384 rescue Exception => e
379 logger.error "scm: error during get version string: #{e.message}"
385 logger.error "scm: error during get version string: #{e.message}"
380 end
386 end
381 ret
387 ret
382 end
388 end
383
389
384 def self.scm_available
390 def self.scm_available
385 ret = false
391 ret = false
386 begin
392 begin
387 ret = self.scm_adapter_class.client_available if self.scm_adapter_class
393 ret = self.scm_adapter_class.client_available if self.scm_adapter_class
388 rescue Exception => e
394 rescue Exception => e
389 logger.error "scm: error during get scm available: #{e.message}"
395 logger.error "scm: error during get scm available: #{e.message}"
390 end
396 end
391 ret
397 ret
392 end
398 end
393
399
394 def set_as_default?
400 def set_as_default?
395 new_record? && project && Repository.where(:project_id => project.id).empty?
401 new_record? && project && Repository.where(:project_id => project.id).empty?
396 end
402 end
397
403
398 protected
404 protected
399
405
400 def check_default
406 def check_default
401 if !is_default? && set_as_default?
407 if !is_default? && set_as_default?
402 self.is_default = true
408 self.is_default = true
403 end
409 end
404 if is_default? && is_default_changed?
410 if is_default? && is_default_changed?
405 Repository.update_all(["is_default = ?", false], ["project_id = ?", project_id])
411 Repository.update_all(["is_default = ?", false], ["project_id = ?", project_id])
406 end
412 end
407 end
413 end
408
414
409 def load_entries_changesets(entries)
415 def load_entries_changesets(entries)
410 if entries
416 if entries
411 entries.each do |entry|
417 entries.each do |entry|
412 if entry.lastrev && entry.lastrev.identifier
418 if entry.lastrev && entry.lastrev.identifier
413 entry.changeset = find_changeset_by_name(entry.lastrev.identifier)
419 entry.changeset = find_changeset_by_name(entry.lastrev.identifier)
414 end
420 end
415 end
421 end
416 end
422 end
417 end
423 end
418
424
419 private
425 private
420
426
421 # Deletes repository data
427 # Deletes repository data
422 def clear_changesets
428 def clear_changesets
423 cs = Changeset.table_name
429 cs = Changeset.table_name
424 ch = Change.table_name
430 ch = Change.table_name
425 ci = "#{table_name_prefix}changesets_issues#{table_name_suffix}"
431 ci = "#{table_name_prefix}changesets_issues#{table_name_suffix}"
426 cp = "#{table_name_prefix}changeset_parents#{table_name_suffix}"
432 cp = "#{table_name_prefix}changeset_parents#{table_name_suffix}"
427
433
428 connection.delete("DELETE FROM #{ch} WHERE #{ch}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
434 connection.delete("DELETE FROM #{ch} WHERE #{ch}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
429 connection.delete("DELETE FROM #{ci} WHERE #{ci}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
435 connection.delete("DELETE FROM #{ci} WHERE #{ci}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
430 connection.delete("DELETE FROM #{cp} WHERE #{cp}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
436 connection.delete("DELETE FROM #{cp} WHERE #{cp}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
431 connection.delete("DELETE FROM #{cs} WHERE #{cs}.repository_id = #{id}")
437 connection.delete("DELETE FROM #{cs} WHERE #{cs}.repository_id = #{id}")
432 clear_extra_info_of_changesets
438 clear_extra_info_of_changesets
433 end
439 end
434
440
435 def clear_extra_info_of_changesets
441 def clear_extra_info_of_changesets
436 end
442 end
437 end
443 end
@@ -1,370 +1,376
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 class RepositoryTest < ActiveSupport::TestCase
20 class RepositoryTest < ActiveSupport::TestCase
21 fixtures :projects,
21 fixtures :projects,
22 :trackers,
22 :trackers,
23 :projects_trackers,
23 :projects_trackers,
24 :enabled_modules,
24 :enabled_modules,
25 :repositories,
25 :repositories,
26 :issues,
26 :issues,
27 :issue_statuses,
27 :issue_statuses,
28 :issue_categories,
28 :issue_categories,
29 :changesets,
29 :changesets,
30 :changes,
30 :changes,
31 :users,
31 :users,
32 :members,
32 :members,
33 :member_roles,
33 :member_roles,
34 :roles,
34 :roles,
35 :enumerations
35 :enumerations
36
36
37 include Redmine::I18n
37 include Redmine::I18n
38
38
39 def setup
39 def setup
40 @repository = Project.find(1).repository
40 @repository = Project.find(1).repository
41 end
41 end
42
42
43 def test_blank_log_encoding_error_message
43 def test_blank_log_encoding_error_message
44 set_language_if_valid 'en'
44 set_language_if_valid 'en'
45 repo = Repository::Bazaar.new(
45 repo = Repository::Bazaar.new(
46 :project => Project.find(3),
46 :project => Project.find(3),
47 :url => "/test",
47 :url => "/test",
48 :log_encoding => ''
48 :log_encoding => ''
49 )
49 )
50 assert !repo.save
50 assert !repo.save
51 assert_include "Commit messages encoding can't be blank",
51 assert_include "Commit messages encoding can't be blank",
52 repo.errors.full_messages
52 repo.errors.full_messages
53 end
53 end
54
54
55 def test_blank_log_encoding_error_message_fr
55 def test_blank_log_encoding_error_message_fr
56 set_language_if_valid 'fr'
56 set_language_if_valid 'fr'
57 str = "Encodage des messages de commit doit \xc3\xaatre renseign\xc3\xa9(e)"
57 str = "Encodage des messages de commit doit \xc3\xaatre renseign\xc3\xa9(e)"
58 str.force_encoding('UTF-8') if str.respond_to?(:force_encoding)
58 str.force_encoding('UTF-8') if str.respond_to?(:force_encoding)
59 repo = Repository::Bazaar.new(
59 repo = Repository::Bazaar.new(
60 :project => Project.find(3),
60 :project => Project.find(3),
61 :url => "/test"
61 :url => "/test"
62 )
62 )
63 assert !repo.save
63 assert !repo.save
64 assert_include str, repo.errors.full_messages
64 assert_include str, repo.errors.full_messages
65 end
65 end
66
66
67 def test_create
67 def test_create
68 repository = Repository::Subversion.new(:project => Project.find(3))
68 repository = Repository::Subversion.new(:project => Project.find(3))
69 assert !repository.save
69 assert !repository.save
70
70
71 repository.url = "svn://localhost"
71 repository.url = "svn://localhost"
72 assert repository.save
72 assert repository.save
73 repository.reload
73 repository.reload
74
74
75 project = Project.find(3)
75 project = Project.find(3)
76 assert_equal repository, project.repository
76 assert_equal repository, project.repository
77 end
77 end
78
78
79 def test_first_repository_should_be_set_as_default
79 def test_first_repository_should_be_set_as_default
80 repository1 = Repository::Subversion.new(
80 repository1 = Repository::Subversion.new(
81 :project => Project.find(3),
81 :project => Project.find(3),
82 :identifier => 'svn1',
82 :identifier => 'svn1',
83 :url => 'file:///svn1'
83 :url => 'file:///svn1'
84 )
84 )
85 assert repository1.save
85 assert repository1.save
86 assert repository1.is_default?
86 assert repository1.is_default?
87
87
88 repository2 = Repository::Subversion.new(
88 repository2 = Repository::Subversion.new(
89 :project => Project.find(3),
89 :project => Project.find(3),
90 :identifier => 'svn2',
90 :identifier => 'svn2',
91 :url => 'file:///svn2'
91 :url => 'file:///svn2'
92 )
92 )
93 assert repository2.save
93 assert repository2.save
94 assert !repository2.is_default?
94 assert !repository2.is_default?
95
95
96 assert_equal repository1, Project.find(3).repository
96 assert_equal repository1, Project.find(3).repository
97 assert_equal [repository1, repository2], Project.find(3).repositories.sort
97 assert_equal [repository1, repository2], Project.find(3).repositories.sort
98 end
98 end
99
99
100 def test_identifier_should_accept_letters_digits_dashes_and_underscores
100 def test_identifier_should_accept_letters_digits_dashes_and_underscores
101 r = Repository::Subversion.new(
101 r = Repository::Subversion.new(
102 :project_id => 3,
102 :project_id => 3,
103 :identifier => 'svn-123_45',
103 :identifier => 'svn-123_45',
104 :url => 'file:///svn'
104 :url => 'file:///svn'
105 )
105 )
106 assert r.save
106 assert r.save
107 end
107 end
108
108
109 def test_identifier_should_not_be_frozen_for_a_new_repository
109 def test_identifier_should_not_be_frozen_for_a_new_repository
110 assert_equal false, Repository.new.identifier_frozen?
110 assert_equal false, Repository.new.identifier_frozen?
111 end
111 end
112
112
113 def test_identifier_should_not_be_frozen_for_a_saved_repository_with_blank_identifier
113 def test_identifier_should_not_be_frozen_for_a_saved_repository_with_blank_identifier
114 Repository.update_all(["identifier = ''"], "id = 10")
114 Repository.update_all(["identifier = ''"], "id = 10")
115
115
116 assert_equal false, Repository.find(10).identifier_frozen?
116 assert_equal false, Repository.find(10).identifier_frozen?
117 end
117 end
118
118
119 def test_identifier_should_be_frozen_for_a_saved_repository_with_valid_identifier
119 def test_identifier_should_be_frozen_for_a_saved_repository_with_valid_identifier
120 Repository.update_all(["identifier = 'abc123'"], "id = 10")
120 Repository.update_all(["identifier = 'abc123'"], "id = 10")
121
121
122 assert_equal true, Repository.find(10).identifier_frozen?
122 assert_equal true, Repository.find(10).identifier_frozen?
123 end
123 end
124
124
125 def test_identifier_should_not_accept_change_if_frozen
125 def test_identifier_should_not_accept_change_if_frozen
126 r = Repository.new(:identifier => 'foo')
126 r = Repository.new(:identifier => 'foo')
127 r.stubs(:identifier_frozen?).returns(true)
127 r.stubs(:identifier_frozen?).returns(true)
128
128
129 r.identifier = 'bar'
129 r.identifier = 'bar'
130 assert_equal 'foo', r.identifier
130 assert_equal 'foo', r.identifier
131 end
131 end
132
132
133 def test_identifier_should_accept_change_if_not_frozen
133 def test_identifier_should_accept_change_if_not_frozen
134 r = Repository.new(:identifier => 'foo')
134 r = Repository.new(:identifier => 'foo')
135 r.stubs(:identifier_frozen?).returns(false)
135 r.stubs(:identifier_frozen?).returns(false)
136
136
137 r.identifier = 'bar'
137 r.identifier = 'bar'
138 assert_equal 'bar', r.identifier
138 assert_equal 'bar', r.identifier
139 end
139 end
140
140
141 def test_destroy
141 def test_destroy
142 repository = Repository.find(10)
142 repository = Repository.find(10)
143 changesets = repository.changesets.count
143 changesets = repository.changesets.count
144 changes = repository.filechanges.count
144 changes = repository.filechanges.count
145
145
146 assert_difference 'Changeset.count', -changesets do
146 assert_difference 'Changeset.count', -changesets do
147 assert_difference 'Change.count', -changes do
147 assert_difference 'Change.count', -changes do
148 Repository.find(10).destroy
148 Repository.find(10).destroy
149 end
149 end
150 end
150 end
151 end
151 end
152
152
153 def test_destroy_should_delete_parents_associations
153 def test_destroy_should_delete_parents_associations
154 changeset = Changeset.find(102)
154 changeset = Changeset.find(102)
155 changeset.parents = Changeset.find_all_by_id([100, 101])
155 changeset.parents = Changeset.find_all_by_id([100, 101])
156
156
157 assert_difference 'Changeset.connection.select_all("select * from changeset_parents").size', -2 do
157 assert_difference 'Changeset.connection.select_all("select * from changeset_parents").size', -2 do
158 Repository.find(10).destroy
158 Repository.find(10).destroy
159 end
159 end
160 end
160 end
161
161
162 def test_destroy_should_delete_issues_associations
162 def test_destroy_should_delete_issues_associations
163 changeset = Changeset.find(102)
163 changeset = Changeset.find(102)
164 changeset.issues = Issue.find_all_by_id([1, 2])
164 changeset.issues = Issue.find_all_by_id([1, 2])
165
165
166 assert_difference 'Changeset.connection.select_all("select * from changesets_issues").size', -2 do
166 assert_difference 'Changeset.connection.select_all("select * from changesets_issues").size', -2 do
167 Repository.find(10).destroy
167 Repository.find(10).destroy
168 end
168 end
169 end
169 end
170
170
171 def test_should_not_create_with_disabled_scm
171 def test_should_not_create_with_disabled_scm
172 # disable Subversion
172 # disable Subversion
173 with_settings :enabled_scm => ['Darcs', 'Git'] do
173 with_settings :enabled_scm => ['Darcs', 'Git'] do
174 repository = Repository::Subversion.new(
174 repository = Repository::Subversion.new(
175 :project => Project.find(3), :url => "svn://localhost")
175 :project => Project.find(3), :url => "svn://localhost")
176 assert !repository.save
176 assert !repository.save
177 assert_include I18n.translate('activerecord.errors.messages.invalid'),
177 assert_include I18n.translate('activerecord.errors.messages.invalid'),
178 repository.errors[:type]
178 repository.errors[:type]
179 end
179 end
180 end
180 end
181
181
182 def test_scan_changesets_for_issue_ids
182 def test_scan_changesets_for_issue_ids
183 Setting.default_language = 'en'
183 Setting.default_language = 'en'
184
184
185 Setting.commit_ref_keywords = 'refs , references, IssueID'
185 Setting.commit_ref_keywords = 'refs , references, IssueID'
186 Setting.commit_update_keywords = [
186 Setting.commit_update_keywords = [
187 {'keywords' => 'fixes , closes', 'status_id' => IssueStatus.where(:is_closed => true).first.id, 'done_ratio' => '90'}
187 {'keywords' => 'fixes , closes', 'status_id' => IssueStatus.where(:is_closed => true).first.id, 'done_ratio' => '90'}
188 ]
188 ]
189 Setting.default_language = 'en'
189 Setting.default_language = 'en'
190 ActionMailer::Base.deliveries.clear
190 ActionMailer::Base.deliveries.clear
191
191
192 # make sure issue 1 is not already closed
192 # make sure issue 1 is not already closed
193 fixed_issue = Issue.find(1)
193 fixed_issue = Issue.find(1)
194 assert !fixed_issue.status.is_closed?
194 assert !fixed_issue.status.is_closed?
195 old_status = fixed_issue.status
195 old_status = fixed_issue.status
196
196
197 with_settings :notified_events => %w(issue_added issue_updated) do
197 with_settings :notified_events => %w(issue_added issue_updated) do
198 Repository.scan_changesets_for_issue_ids
198 Repository.scan_changesets_for_issue_ids
199 end
199 end
200 assert_equal [101, 102], Issue.find(3).changeset_ids
200 assert_equal [101, 102], Issue.find(3).changeset_ids
201
201
202 # fixed issues
202 # fixed issues
203 fixed_issue.reload
203 fixed_issue.reload
204 assert fixed_issue.status.is_closed?
204 assert fixed_issue.status.is_closed?
205 assert_equal 90, fixed_issue.done_ratio
205 assert_equal 90, fixed_issue.done_ratio
206 assert_equal [101], fixed_issue.changeset_ids
206 assert_equal [101], fixed_issue.changeset_ids
207
207
208 # issue change
208 # issue change
209 journal = fixed_issue.journals.reorder('created_on desc').first
209 journal = fixed_issue.journals.reorder('created_on desc').first
210 assert_equal User.find_by_login('dlopper'), journal.user
210 assert_equal User.find_by_login('dlopper'), journal.user
211 assert_equal 'Applied in changeset r2.', journal.notes
211 assert_equal 'Applied in changeset r2.', journal.notes
212
212
213 # 2 email notifications
213 # 2 email notifications
214 assert_equal 2, ActionMailer::Base.deliveries.size
214 assert_equal 2, ActionMailer::Base.deliveries.size
215 mail = ActionMailer::Base.deliveries.first
215 mail = ActionMailer::Base.deliveries.first
216 assert_not_nil mail
216 assert_not_nil mail
217 assert mail.subject.starts_with?(
217 assert mail.subject.starts_with?(
218 "[#{fixed_issue.project.name} - #{fixed_issue.tracker.name} ##{fixed_issue.id}]")
218 "[#{fixed_issue.project.name} - #{fixed_issue.tracker.name} ##{fixed_issue.id}]")
219 assert_mail_body_match(
219 assert_mail_body_match(
220 "Status changed from #{old_status} to #{fixed_issue.status}", mail)
220 "Status changed from #{old_status} to #{fixed_issue.status}", mail)
221
221
222 # ignoring commits referencing an issue of another project
222 # ignoring commits referencing an issue of another project
223 assert_equal [], Issue.find(4).changesets
223 assert_equal [], Issue.find(4).changesets
224 end
224 end
225
225
226 def test_for_changeset_comments_strip
226 def test_for_changeset_comments_strip
227 repository = Repository::Mercurial.create(
227 repository = Repository::Mercurial.create(
228 :project => Project.find( 4 ),
228 :project => Project.find( 4 ),
229 :url => '/foo/bar/baz' )
229 :url => '/foo/bar/baz' )
230 comment = <<-COMMENT
230 comment = <<-COMMENT
231 This is a loooooooooooooooooooooooooooong comment
231 This is a loooooooooooooooooooooooooooong comment
232
232
233
233
234 COMMENT
234 COMMENT
235 changeset = Changeset.new(
235 changeset = Changeset.new(
236 :comments => comment, :commit_date => Time.now,
236 :comments => comment, :commit_date => Time.now,
237 :revision => 0, :scmid => 'f39b7922fb3c',
237 :revision => 0, :scmid => 'f39b7922fb3c',
238 :committer => 'foo <foo@example.com>',
238 :committer => 'foo <foo@example.com>',
239 :committed_on => Time.now, :repository => repository )
239 :committed_on => Time.now, :repository => repository )
240 assert( changeset.save )
240 assert( changeset.save )
241 assert_not_equal( comment, changeset.comments )
241 assert_not_equal( comment, changeset.comments )
242 assert_equal( 'This is a loooooooooooooooooooooooooooong comment',
242 assert_equal( 'This is a loooooooooooooooooooooooooooong comment',
243 changeset.comments )
243 changeset.comments )
244 end
244 end
245
245
246 def test_for_urls_strip_cvs
246 def test_for_urls_strip_cvs
247 repository = Repository::Cvs.create(
247 repository = Repository::Cvs.create(
248 :project => Project.find(4),
248 :project => Project.find(4),
249 :url => ' :pserver:login:password@host:/path/to/the/repository',
249 :url => ' :pserver:login:password@host:/path/to/the/repository',
250 :root_url => 'foo ',
250 :root_url => 'foo ',
251 :log_encoding => 'UTF-8')
251 :log_encoding => 'UTF-8')
252 assert repository.save
252 assert repository.save
253 repository.reload
253 repository.reload
254 assert_equal ':pserver:login:password@host:/path/to/the/repository',
254 assert_equal ':pserver:login:password@host:/path/to/the/repository',
255 repository.url
255 repository.url
256 assert_equal 'foo', repository.root_url
256 assert_equal 'foo', repository.root_url
257 end
257 end
258
258
259 def test_for_urls_strip_subversion
259 def test_for_urls_strip_subversion
260 repository = Repository::Subversion.create(
260 repository = Repository::Subversion.create(
261 :project => Project.find(4),
261 :project => Project.find(4),
262 :url => ' file:///dummy ')
262 :url => ' file:///dummy ')
263 assert repository.save
263 assert repository.save
264 repository.reload
264 repository.reload
265 assert_equal 'file:///dummy', repository.url
265 assert_equal 'file:///dummy', repository.url
266 end
266 end
267
267
268 def test_for_urls_strip_git
268 def test_for_urls_strip_git
269 repository = Repository::Git.create(
269 repository = Repository::Git.create(
270 :project => Project.find(4),
270 :project => Project.find(4),
271 :url => ' c:\dummy ')
271 :url => ' c:\dummy ')
272 assert repository.save
272 assert repository.save
273 repository.reload
273 repository.reload
274 assert_equal 'c:\dummy', repository.url
274 assert_equal 'c:\dummy', repository.url
275 end
275 end
276
276
277 def test_manual_user_mapping
277 def test_manual_user_mapping
278 assert_no_difference "Changeset.where('user_id <> 2').count" do
278 assert_no_difference "Changeset.where('user_id <> 2').count" do
279 c = Changeset.create!(
279 c = Changeset.create!(
280 :repository => @repository,
280 :repository => @repository,
281 :committer => 'foo',
281 :committer => 'foo',
282 :committed_on => Time.now,
282 :committed_on => Time.now,
283 :revision => 100,
283 :revision => 100,
284 :comments => 'Committed by foo.'
284 :comments => 'Committed by foo.'
285 )
285 )
286 assert_nil c.user
286 assert_nil c.user
287 @repository.committer_ids = {'foo' => '2'}
287 @repository.committer_ids = {'foo' => '2'}
288 assert_equal User.find(2), c.reload.user
288 assert_equal User.find(2), c.reload.user
289 # committer is now mapped
289 # committer is now mapped
290 c = Changeset.create!(
290 c = Changeset.create!(
291 :repository => @repository,
291 :repository => @repository,
292 :committer => 'foo',
292 :committer => 'foo',
293 :committed_on => Time.now,
293 :committed_on => Time.now,
294 :revision => 101,
294 :revision => 101,
295 :comments => 'Another commit by foo.'
295 :comments => 'Another commit by foo.'
296 )
296 )
297 assert_equal User.find(2), c.user
297 assert_equal User.find(2), c.user
298 end
298 end
299 end
299 end
300
300
301 def test_auto_user_mapping_by_username
301 def test_auto_user_mapping_by_username
302 c = Changeset.create!(
302 c = Changeset.create!(
303 :repository => @repository,
303 :repository => @repository,
304 :committer => 'jsmith',
304 :committer => 'jsmith',
305 :committed_on => Time.now,
305 :committed_on => Time.now,
306 :revision => 100,
306 :revision => 100,
307 :comments => 'Committed by john.'
307 :comments => 'Committed by john.'
308 )
308 )
309 assert_equal User.find(2), c.user
309 assert_equal User.find(2), c.user
310 end
310 end
311
311
312 def test_auto_user_mapping_by_email
312 def test_auto_user_mapping_by_email
313 c = Changeset.create!(
313 c = Changeset.create!(
314 :repository => @repository,
314 :repository => @repository,
315 :committer => 'john <jsmith@somenet.foo>',
315 :committer => 'john <jsmith@somenet.foo>',
316 :committed_on => Time.now,
316 :committed_on => Time.now,
317 :revision => 100,
317 :revision => 100,
318 :comments => 'Committed by john.'
318 :comments => 'Committed by john.'
319 )
319 )
320 assert_equal User.find(2), c.user
320 assert_equal User.find(2), c.user
321 end
321 end
322
322
323 def test_filesystem_avaialbe
323 def test_filesystem_avaialbe
324 klass = Repository::Filesystem
324 klass = Repository::Filesystem
325 assert klass.scm_adapter_class
325 assert klass.scm_adapter_class
326 assert_equal true, klass.scm_available
326 assert_equal true, klass.scm_available
327 end
327 end
328
328
329 def test_extra_info_should_not_return_non_hash_value
330 repo = Repository.new
331 repo.extra_info = "foo"
332 assert_nil repo.extra_info
333 end
334
329 def test_merge_extra_info
335 def test_merge_extra_info
330 repo = Repository::Subversion.new(:project => Project.find(3))
336 repo = Repository::Subversion.new(:project => Project.find(3))
331 assert !repo.save
337 assert !repo.save
332 repo.url = "svn://localhost"
338 repo.url = "svn://localhost"
333 assert repo.save
339 assert repo.save
334 repo.reload
340 repo.reload
335 project = Project.find(3)
341 project = Project.find(3)
336 assert_equal repo, project.repository
342 assert_equal repo, project.repository
337 assert_nil repo.extra_info
343 assert_nil repo.extra_info
338 h1 = {"test_1" => {"test_11" => "test_value_11"}}
344 h1 = {"test_1" => {"test_11" => "test_value_11"}}
339 repo.merge_extra_info(h1)
345 repo.merge_extra_info(h1)
340 assert_equal h1, repo.extra_info
346 assert_equal h1, repo.extra_info
341 h2 = {"test_2" => {
347 h2 = {"test_2" => {
342 "test_21" => "test_value_21",
348 "test_21" => "test_value_21",
343 "test_22" => "test_value_22",
349 "test_22" => "test_value_22",
344 }}
350 }}
345 repo.merge_extra_info(h2)
351 repo.merge_extra_info(h2)
346 assert_equal (h = {"test_11" => "test_value_11"}),
352 assert_equal (h = {"test_11" => "test_value_11"}),
347 repo.extra_info["test_1"]
353 repo.extra_info["test_1"]
348 assert_equal "test_value_21",
354 assert_equal "test_value_21",
349 repo.extra_info["test_2"]["test_21"]
355 repo.extra_info["test_2"]["test_21"]
350 h3 = {"test_2" => {
356 h3 = {"test_2" => {
351 "test_23" => "test_value_23",
357 "test_23" => "test_value_23",
352 "test_24" => "test_value_24",
358 "test_24" => "test_value_24",
353 }}
359 }}
354 repo.merge_extra_info(h3)
360 repo.merge_extra_info(h3)
355 assert_equal (h = {"test_11" => "test_value_11"}),
361 assert_equal (h = {"test_11" => "test_value_11"}),
356 repo.extra_info["test_1"]
362 repo.extra_info["test_1"]
357 assert_nil repo.extra_info["test_2"]["test_21"]
363 assert_nil repo.extra_info["test_2"]["test_21"]
358 assert_equal "test_value_23",
364 assert_equal "test_value_23",
359 repo.extra_info["test_2"]["test_23"]
365 repo.extra_info["test_2"]["test_23"]
360 end
366 end
361
367
362 def test_sort_should_not_raise_an_error_with_nil_identifiers
368 def test_sort_should_not_raise_an_error_with_nil_identifiers
363 r1 = Repository.new
369 r1 = Repository.new
364 r2 = Repository.new
370 r2 = Repository.new
365
371
366 assert_nothing_raised do
372 assert_nothing_raised do
367 [r1, r2].sort
373 [r1, r2].sort
368 end
374 end
369 end
375 end
370 end
376 end
General Comments 0
You need to be logged in to leave comments. Login now