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