##// END OF EJS Templates
Merged r14142 and r14143 (#19400)....
Jean-Philippe Lang -
r13765:c6c304edf1e3
parent child
Show More
@@ -1,509 +1,509
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_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_uniqueness_of :identifier, :scope => :project_id, :allow_blank => true
41 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)
42 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
43 # donwcase letters, digits, dashes, underscores but not digits only
44 validates_format_of :identifier, :with => /\A(?!\d+$)[a-z0-9\-_]*\z/, :allow_blank => true
44 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
45 # Checks if the SCM is enabled when creating a repository
46 validate :repo_create_validation, :on => :create
46 validate :repo_create_validation, :on => :create
47 validate :validate_repository_path
47 validate :validate_repository_path
48 attr_protected :id
48 attr_protected :id
49
49
50 safe_attributes 'identifier',
50 safe_attributes 'identifier',
51 'login',
51 'login',
52 'password',
52 'password',
53 'path_encoding',
53 'path_encoding',
54 'log_encoding',
54 'log_encoding',
55 'is_default'
55 'is_default'
56
56
57 safe_attributes 'url',
57 safe_attributes 'url',
58 :if => lambda {|repository, user| repository.new_record?}
58 :if => lambda {|repository, user| repository.new_record?}
59
59
60 def repo_create_validation
60 def repo_create_validation
61 unless Setting.enabled_scm.include?(self.class.name.demodulize)
61 unless Setting.enabled_scm.include?(self.class.name.demodulize)
62 errors.add(:type, :invalid)
62 errors.add(:type, :invalid)
63 end
63 end
64 end
64 end
65
65
66 def self.human_attribute_name(attribute_key_name, *args)
66 def self.human_attribute_name(attribute_key_name, *args)
67 attr_name = attribute_key_name.to_s
67 attr_name = attribute_key_name.to_s
68 if attr_name == "log_encoding"
68 if attr_name == "log_encoding"
69 attr_name = "commit_logs_encoding"
69 attr_name = "commit_logs_encoding"
70 end
70 end
71 super(attr_name, *args)
71 super(attr_name, *args)
72 end
72 end
73
73
74 # Removes leading and trailing whitespace
74 # Removes leading and trailing whitespace
75 def url=(arg)
75 def url=(arg)
76 write_attribute(:url, arg ? arg.to_s.strip : nil)
76 write_attribute(:url, arg ? arg.to_s.strip : nil)
77 end
77 end
78
78
79 # Removes leading and trailing whitespace
79 # Removes leading and trailing whitespace
80 def root_url=(arg)
80 def root_url=(arg)
81 write_attribute(:root_url, arg ? arg.to_s.strip : nil)
81 write_attribute(:root_url, arg ? arg.to_s.strip : nil)
82 end
82 end
83
83
84 def password
84 def password
85 read_ciphered_attribute(:password)
85 read_ciphered_attribute(:password)
86 end
86 end
87
87
88 def password=(arg)
88 def password=(arg)
89 write_ciphered_attribute(:password, arg)
89 write_ciphered_attribute(:password, arg)
90 end
90 end
91
91
92 def scm_adapter
92 def scm_adapter
93 self.class.scm_adapter_class
93 self.class.scm_adapter_class
94 end
94 end
95
95
96 def scm
96 def scm
97 unless @scm
97 unless @scm
98 @scm = self.scm_adapter.new(url, root_url,
98 @scm = self.scm_adapter.new(url, root_url,
99 login, password, path_encoding)
99 login, password, path_encoding)
100 if root_url.blank? && @scm.root_url.present?
100 if root_url.blank? && @scm.root_url.present?
101 update_attribute(:root_url, @scm.root_url)
101 update_attribute(:root_url, @scm.root_url)
102 end
102 end
103 end
103 end
104 @scm
104 @scm
105 end
105 end
106
106
107 def scm_name
107 def scm_name
108 self.class.scm_name
108 self.class.scm_name
109 end
109 end
110
110
111 def name
111 def name
112 if identifier.present?
112 if identifier.present?
113 identifier
113 identifier
114 elsif is_default?
114 elsif is_default?
115 l(:field_repository_is_default)
115 l(:field_repository_is_default)
116 else
116 else
117 scm_name
117 scm_name
118 end
118 end
119 end
119 end
120
120
121 def identifier=(identifier)
121 def identifier=(identifier)
122 super unless identifier_frozen?
122 super unless identifier_frozen?
123 end
123 end
124
124
125 def identifier_frozen?
125 def identifier_frozen?
126 errors[:identifier].blank? && !(new_record? || identifier.blank?)
126 errors[:identifier].blank? && !(new_record? || identifier.blank?)
127 end
127 end
128
128
129 def identifier_param
129 def identifier_param
130 if is_default?
130 if is_default?
131 nil
131 nil
132 elsif identifier.present?
132 elsif identifier.present?
133 identifier
133 identifier
134 else
134 else
135 id.to_s
135 id.to_s
136 end
136 end
137 end
137 end
138
138
139 def <=>(repository)
139 def <=>(repository)
140 if is_default?
140 if is_default?
141 -1
141 -1
142 elsif repository.is_default?
142 elsif repository.is_default?
143 1
143 1
144 else
144 else
145 identifier.to_s <=> repository.identifier.to_s
145 identifier.to_s <=> repository.identifier.to_s
146 end
146 end
147 end
147 end
148
148
149 def self.find_by_identifier_param(param)
149 def self.find_by_identifier_param(param)
150 if param.to_s =~ /^\d+$/
150 if param.to_s =~ /^\d+$/
151 find_by_id(param)
151 find_by_id(param)
152 else
152 else
153 find_by_identifier(param)
153 find_by_identifier(param)
154 end
154 end
155 end
155 end
156
156
157 # TODO: should return an empty hash instead of nil to avoid many ||{}
157 # TODO: should return an empty hash instead of nil to avoid many ||{}
158 def extra_info
158 def extra_info
159 h = read_attribute(:extra_info)
159 h = read_attribute(:extra_info)
160 h.is_a?(Hash) ? h : nil
160 h.is_a?(Hash) ? h : nil
161 end
161 end
162
162
163 def merge_extra_info(arg)
163 def merge_extra_info(arg)
164 h = extra_info || {}
164 h = extra_info || {}
165 return h if arg.nil?
165 return h if arg.nil?
166 h.merge!(arg)
166 h.merge!(arg)
167 write_attribute(:extra_info, h)
167 write_attribute(:extra_info, h)
168 end
168 end
169
169
170 def report_last_commit
170 def report_last_commit
171 true
171 true
172 end
172 end
173
173
174 def supports_cat?
174 def supports_cat?
175 scm.supports_cat?
175 scm.supports_cat?
176 end
176 end
177
177
178 def supports_annotate?
178 def supports_annotate?
179 scm.supports_annotate?
179 scm.supports_annotate?
180 end
180 end
181
181
182 def supports_all_revisions?
182 def supports_all_revisions?
183 true
183 true
184 end
184 end
185
185
186 def supports_directory_revisions?
186 def supports_directory_revisions?
187 false
187 false
188 end
188 end
189
189
190 def supports_revision_graph?
190 def supports_revision_graph?
191 false
191 false
192 end
192 end
193
193
194 def entry(path=nil, identifier=nil)
194 def entry(path=nil, identifier=nil)
195 scm.entry(path, identifier)
195 scm.entry(path, identifier)
196 end
196 end
197
197
198 def scm_entries(path=nil, identifier=nil)
198 def scm_entries(path=nil, identifier=nil)
199 scm.entries(path, identifier)
199 scm.entries(path, identifier)
200 end
200 end
201 protected :scm_entries
201 protected :scm_entries
202
202
203 def entries(path=nil, identifier=nil)
203 def entries(path=nil, identifier=nil)
204 entries = scm_entries(path, identifier)
204 entries = scm_entries(path, identifier)
205 load_entries_changesets(entries)
205 load_entries_changesets(entries)
206 entries
206 entries
207 end
207 end
208
208
209 def branches
209 def branches
210 scm.branches
210 scm.branches
211 end
211 end
212
212
213 def tags
213 def tags
214 scm.tags
214 scm.tags
215 end
215 end
216
216
217 def default_branch
217 def default_branch
218 nil
218 nil
219 end
219 end
220
220
221 def properties(path, identifier=nil)
221 def properties(path, identifier=nil)
222 scm.properties(path, identifier)
222 scm.properties(path, identifier)
223 end
223 end
224
224
225 def cat(path, identifier=nil)
225 def cat(path, identifier=nil)
226 scm.cat(path, identifier)
226 scm.cat(path, identifier)
227 end
227 end
228
228
229 def diff(path, rev, rev_to)
229 def diff(path, rev, rev_to)
230 scm.diff(path, rev, rev_to)
230 scm.diff(path, rev, rev_to)
231 end
231 end
232
232
233 def diff_format_revisions(cs, cs_to, sep=':')
233 def diff_format_revisions(cs, cs_to, sep=':')
234 text = ""
234 text = ""
235 text << cs_to.format_identifier + sep if cs_to
235 text << cs_to.format_identifier + sep if cs_to
236 text << cs.format_identifier if cs
236 text << cs.format_identifier if cs
237 text
237 text
238 end
238 end
239
239
240 # Returns a path relative to the url of the repository
240 # Returns a path relative to the url of the repository
241 def relative_path(path)
241 def relative_path(path)
242 path
242 path
243 end
243 end
244
244
245 # Finds and returns a revision with a number or the beginning of a hash
245 # Finds and returns a revision with a number or the beginning of a hash
246 def find_changeset_by_name(name)
246 def find_changeset_by_name(name)
247 return nil if name.blank?
247 return nil if name.blank?
248 s = name.to_s
248 s = name.to_s
249 if s.match(/^\d*$/)
249 if s.match(/^\d*$/)
250 changesets.where("revision = ?", s).first
250 changesets.where("revision = ?", s).first
251 else
251 else
252 changesets.where("revision LIKE ?", s + '%').first
252 changesets.where("revision LIKE ?", s + '%').first
253 end
253 end
254 end
254 end
255
255
256 def latest_changeset
256 def latest_changeset
257 @latest_changeset ||= changesets.first
257 @latest_changeset ||= changesets.first
258 end
258 end
259
259
260 # Returns the latest changesets for +path+
260 # Returns the latest changesets for +path+
261 # Default behaviour is to search in cached changesets
261 # Default behaviour is to search in cached changesets
262 def latest_changesets(path, rev, limit=10)
262 def latest_changesets(path, rev, limit=10)
263 if path.blank?
263 if path.blank?
264 changesets.
264 changesets.
265 reorder("#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC").
265 reorder("#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC").
266 limit(limit).
266 limit(limit).
267 preload(:user).
267 preload(:user).
268 to_a
268 to_a
269 else
269 else
270 filechanges.
270 filechanges.
271 where("path = ?", path.with_leading_slash).
271 where("path = ?", path.with_leading_slash).
272 reorder("#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC").
272 reorder("#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC").
273 limit(limit).
273 limit(limit).
274 preload(:changeset => :user).
274 preload(:changeset => :user).
275 collect(&:changeset)
275 collect(&:changeset)
276 end
276 end
277 end
277 end
278
278
279 def scan_changesets_for_issue_ids
279 def scan_changesets_for_issue_ids
280 self.changesets.each(&:scan_comment_for_issue_ids)
280 self.changesets.each(&:scan_comment_for_issue_ids)
281 end
281 end
282
282
283 # Returns an array of committers usernames and associated user_id
283 # Returns an array of committers usernames and associated user_id
284 def committers
284 def committers
285 @committers ||= Changeset.where(:repository_id => id).uniq.pluck(:committer, :user_id)
285 @committers ||= Changeset.where(:repository_id => id).uniq.pluck(:committer, :user_id)
286 end
286 end
287
287
288 # Maps committers username to a user ids
288 # Maps committers username to a user ids
289 def committer_ids=(h)
289 def committer_ids=(h)
290 if h.is_a?(Hash)
290 if h.is_a?(Hash)
291 committers.each do |committer, user_id|
291 committers.each do |committer, user_id|
292 new_user_id = h[committer]
292 new_user_id = h[committer]
293 if new_user_id && (new_user_id.to_i != user_id.to_i)
293 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)
294 new_user_id = (new_user_id.to_i > 0 ? new_user_id.to_i : nil)
295 Changeset.where(["repository_id = ? AND committer = ?", id, committer]).
295 Changeset.where(["repository_id = ? AND committer = ?", id, committer]).
296 update_all("user_id = #{new_user_id.nil? ? 'NULL' : new_user_id}")
296 update_all("user_id = #{new_user_id.nil? ? 'NULL' : new_user_id}")
297 end
297 end
298 end
298 end
299 @committers = nil
299 @committers = nil
300 @found_committer_users = nil
300 @found_committer_users = nil
301 true
301 true
302 else
302 else
303 false
303 false
304 end
304 end
305 end
305 end
306
306
307 # Returns the Redmine User corresponding to the given +committer+
307 # 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
308 # It will return nil if the committer is not yet mapped and if no User
309 # with the same username or email was found
309 # with the same username or email was found
310 def find_committer_user(committer)
310 def find_committer_user(committer)
311 unless committer.blank?
311 unless committer.blank?
312 @found_committer_users ||= {}
312 @found_committer_users ||= {}
313 return @found_committer_users[committer] if @found_committer_users.has_key?(committer)
313 return @found_committer_users[committer] if @found_committer_users.has_key?(committer)
314
314
315 user = nil
315 user = nil
316 c = changesets.where(:committer => committer).
316 c = changesets.where(:committer => committer).
317 includes(:user).references(:user).first
317 includes(:user).references(:user).first
318 if c && c.user
318 if c && c.user
319 user = c.user
319 user = c.user
320 elsif committer.strip =~ /^([^<]+)(<(.*)>)?$/
320 elsif committer.strip =~ /^([^<]+)(<(.*)>)?$/
321 username, email = $1.strip, $3
321 username, email = $1.strip, $3
322 u = User.find_by_login(username)
322 u = User.find_by_login(username)
323 u ||= User.find_by_mail(email) unless email.blank?
323 u ||= User.find_by_mail(email) unless email.blank?
324 user = u
324 user = u
325 end
325 end
326 @found_committer_users[committer] = user
326 @found_committer_users[committer] = user
327 user
327 user
328 end
328 end
329 end
329 end
330
330
331 def repo_log_encoding
331 def repo_log_encoding
332 encoding = log_encoding.to_s.strip
332 encoding = log_encoding.to_s.strip
333 encoding.blank? ? 'UTF-8' : encoding
333 encoding.blank? ? 'UTF-8' : encoding
334 end
334 end
335
335
336 # Fetches new changesets for all repositories of active projects
336 # Fetches new changesets for all repositories of active projects
337 # Can be called periodically by an external script
337 # Can be called periodically by an external script
338 # eg. ruby script/runner "Repository.fetch_changesets"
338 # eg. ruby script/runner "Repository.fetch_changesets"
339 def self.fetch_changesets
339 def self.fetch_changesets
340 Project.active.has_module(:repository).all.each do |project|
340 Project.active.has_module(:repository).all.each do |project|
341 project.repositories.each do |repository|
341 project.repositories.each do |repository|
342 begin
342 begin
343 repository.fetch_changesets
343 repository.fetch_changesets
344 rescue Redmine::Scm::Adapters::CommandFailed => e
344 rescue Redmine::Scm::Adapters::CommandFailed => e
345 logger.error "scm: error during fetching changesets: #{e.message}"
345 logger.error "scm: error during fetching changesets: #{e.message}"
346 end
346 end
347 end
347 end
348 end
348 end
349 end
349 end
350
350
351 # scan changeset comments to find related and fixed issues for all repositories
351 # scan changeset comments to find related and fixed issues for all repositories
352 def self.scan_changesets_for_issue_ids
352 def self.scan_changesets_for_issue_ids
353 all.each(&:scan_changesets_for_issue_ids)
353 all.each(&:scan_changesets_for_issue_ids)
354 end
354 end
355
355
356 def self.scm_name
356 def self.scm_name
357 'Abstract'
357 'Abstract'
358 end
358 end
359
359
360 def self.available_scm
360 def self.available_scm
361 subclasses.collect {|klass| [klass.scm_name, klass.name]}
361 subclasses.collect {|klass| [klass.scm_name, klass.name]}
362 end
362 end
363
363
364 def self.factory(klass_name, *args)
364 def self.factory(klass_name, *args)
365 klass = "Repository::#{klass_name}".constantize
365 klass = "Repository::#{klass_name}".constantize
366 klass.new(*args)
366 klass.new(*args)
367 rescue
367 rescue
368 nil
368 nil
369 end
369 end
370
370
371 def self.scm_adapter_class
371 def self.scm_adapter_class
372 nil
372 nil
373 end
373 end
374
374
375 def self.scm_command
375 def self.scm_command
376 ret = ""
376 ret = ""
377 begin
377 begin
378 ret = self.scm_adapter_class.client_command if self.scm_adapter_class
378 ret = self.scm_adapter_class.client_command if self.scm_adapter_class
379 rescue Exception => e
379 rescue Exception => e
380 logger.error "scm: error during get command: #{e.message}"
380 logger.error "scm: error during get command: #{e.message}"
381 end
381 end
382 ret
382 ret
383 end
383 end
384
384
385 def self.scm_version_string
385 def self.scm_version_string
386 ret = ""
386 ret = ""
387 begin
387 begin
388 ret = self.scm_adapter_class.client_version_string if self.scm_adapter_class
388 ret = self.scm_adapter_class.client_version_string if self.scm_adapter_class
389 rescue Exception => e
389 rescue Exception => e
390 logger.error "scm: error during get version string: #{e.message}"
390 logger.error "scm: error during get version string: #{e.message}"
391 end
391 end
392 ret
392 ret
393 end
393 end
394
394
395 def self.scm_available
395 def self.scm_available
396 ret = false
396 ret = false
397 begin
397 begin
398 ret = self.scm_adapter_class.client_available if self.scm_adapter_class
398 ret = self.scm_adapter_class.client_available if self.scm_adapter_class
399 rescue Exception => e
399 rescue Exception => e
400 logger.error "scm: error during get scm available: #{e.message}"
400 logger.error "scm: error during get scm available: #{e.message}"
401 end
401 end
402 ret
402 ret
403 end
403 end
404
404
405 def set_as_default?
405 def set_as_default?
406 new_record? && project && Repository.where(:project_id => project.id).empty?
406 new_record? && project && Repository.where(:project_id => project.id).empty?
407 end
407 end
408
408
409 # Returns a hash with statistics by author in the following form:
409 # Returns a hash with statistics by author in the following form:
410 # {
410 # {
411 # "John Smith" => { :commits => 45, :changes => 324 },
411 # "John Smith" => { :commits => 45, :changes => 324 },
412 # "Bob" => { ... }
412 # "Bob" => { ... }
413 # }
413 # }
414 #
414 #
415 # Notes:
415 # Notes:
416 # - this hash honnors the users mapping defined for the repository
416 # - this hash honnors the users mapping defined for the repository
417 def stats_by_author
417 def stats_by_author
418 commits = Changeset.where("repository_id = ?", id).select("committer, user_id, count(*) as count").group("committer, user_id")
418 commits = Changeset.where("repository_id = ?", id).select("committer, user_id, count(*) as count").group("committer, user_id")
419
419
420 #TODO: restore ordering ; this line probably never worked
420 #TODO: restore ordering ; this line probably never worked
421 #commits.to_a.sort! {|x, y| x.last <=> y.last}
421 #commits.to_a.sort! {|x, y| x.last <=> y.last}
422
422
423 changes = Change.joins(:changeset).where("#{Changeset.table_name}.repository_id = ?", id).select("committer, user_id, count(*) as count").group("committer, user_id")
423 changes = Change.joins(:changeset).where("#{Changeset.table_name}.repository_id = ?", id).select("committer, user_id, count(*) as count").group("committer, user_id")
424
424
425 user_ids = changesets.map(&:user_id).compact.uniq
425 user_ids = changesets.map(&:user_id).compact.uniq
426 authors_names = User.where(:id => user_ids).inject({}) do |memo, user|
426 authors_names = User.where(:id => user_ids).inject({}) do |memo, user|
427 memo[user.id] = user.to_s
427 memo[user.id] = user.to_s
428 memo
428 memo
429 end
429 end
430
430
431 (commits + changes).inject({}) do |hash, element|
431 (commits + changes).inject({}) do |hash, element|
432 mapped_name = element.committer
432 mapped_name = element.committer
433 if username = authors_names[element.user_id.to_i]
433 if username = authors_names[element.user_id.to_i]
434 mapped_name = username
434 mapped_name = username
435 end
435 end
436 hash[mapped_name] ||= { :commits_count => 0, :changes_count => 0 }
436 hash[mapped_name] ||= { :commits_count => 0, :changes_count => 0 }
437 if element.is_a?(Changeset)
437 if element.is_a?(Changeset)
438 hash[mapped_name][:commits_count] += element.count.to_i
438 hash[mapped_name][:commits_count] += element.count.to_i
439 else
439 else
440 hash[mapped_name][:changes_count] += element.count.to_i
440 hash[mapped_name][:changes_count] += element.count.to_i
441 end
441 end
442 hash
442 hash
443 end
443 end
444 end
444 end
445
445
446 # Returns a scope of changesets that come from the same commit as the given changeset
446 # 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
447 # in different repositories that point to the same backend
448 def same_commits_in_scope(scope, changeset)
448 def same_commits_in_scope(scope, changeset)
449 scope = scope.joins(:repository).where(:repositories => {:url => url, :root_url => root_url, :type => type})
449 scope = scope.joins(:repository).where(:repositories => {:url => url, :root_url => root_url, :type => type})
450 if changeset.scmid.present?
450 if changeset.scmid.present?
451 scope = scope.where(:scmid => changeset.scmid)
451 scope = scope.where(:scmid => changeset.scmid)
452 else
452 else
453 scope = scope.where(:revision => changeset.revision)
453 scope = scope.where(:revision => changeset.revision)
454 end
454 end
455 scope
455 scope
456 end
456 end
457
457
458 protected
458 protected
459
459
460 # Validates repository url based against an optional regular expression
460 # Validates repository url based against an optional regular expression
461 # that can be set in the Redmine configuration file.
461 # that can be set in the Redmine configuration file.
462 def validate_repository_path(attribute=:url)
462 def validate_repository_path(attribute=:url)
463 regexp = Redmine::Configuration["scm_#{scm_name.to_s.downcase}_path_regexp"]
463 regexp = Redmine::Configuration["scm_#{scm_name.to_s.downcase}_path_regexp"]
464 if changes[attribute] && regexp.present?
464 if changes[attribute] && regexp.present?
465 regexp = regexp.to_s.strip.gsub('%project%') {Regexp.escape(project.try(:identifier).to_s)}
465 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"))
466 unless send(attribute).to_s.match(Regexp.new("\\A#{regexp}\\z"))
467 errors.add(attribute, :invalid)
467 errors.add(attribute, :invalid)
468 end
468 end
469 end
469 end
470 end
470 end
471
471
472 def check_default
472 def check_default
473 if !is_default? && set_as_default?
473 if !is_default? && set_as_default?
474 self.is_default = true
474 self.is_default = true
475 end
475 end
476 if is_default? && is_default_changed?
476 if is_default? && is_default_changed?
477 Repository.where(["project_id = ?", project_id]).update_all(["is_default = ?", false])
477 Repository.where(["project_id = ?", project_id]).update_all(["is_default = ?", false])
478 end
478 end
479 end
479 end
480
480
481 def load_entries_changesets(entries)
481 def load_entries_changesets(entries)
482 if entries
482 if entries
483 entries.each do |entry|
483 entries.each do |entry|
484 if entry.lastrev && entry.lastrev.identifier
484 if entry.lastrev && entry.lastrev.identifier
485 entry.changeset = find_changeset_by_name(entry.lastrev.identifier)
485 entry.changeset = find_changeset_by_name(entry.lastrev.identifier)
486 end
486 end
487 end
487 end
488 end
488 end
489 end
489 end
490
490
491 private
491 private
492
492
493 # Deletes repository data
493 # Deletes repository data
494 def clear_changesets
494 def clear_changesets
495 cs = Changeset.table_name
495 cs = Changeset.table_name
496 ch = Change.table_name
496 ch = Change.table_name
497 ci = "#{table_name_prefix}changesets_issues#{table_name_suffix}"
497 ci = "#{table_name_prefix}changesets_issues#{table_name_suffix}"
498 cp = "#{table_name_prefix}changeset_parents#{table_name_suffix}"
498 cp = "#{table_name_prefix}changeset_parents#{table_name_suffix}"
499
499
500 self.class.connection.delete("DELETE FROM #{ch} WHERE #{ch}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
500 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})")
501 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})")
502 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}")
503 self.class.connection.delete("DELETE FROM #{cs} WHERE #{cs}.repository_id = #{id}")
504 clear_extra_info_of_changesets
504 clear_extra_info_of_changesets
505 end
505 end
506
506
507 def clear_extra_info_of_changesets
507 def clear_extra_info_of_changesets
508 end
508 end
509 end
509 end
@@ -1,257 +1,257
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 # Copyright (C) 2007 Patrick Aljord patcito@Ε‹mail.com
3 # Copyright (C) 2007 Patrick Aljord patcito@Ε‹mail.com
4 #
4 #
5 # This program is free software; you can redistribute it and/or
5 # This program is free software; you can redistribute it and/or
6 # modify it under the terms of the GNU General Public License
6 # modify it under the terms of the GNU General Public License
7 # as published by the Free Software Foundation; either version 2
7 # as published by the Free Software Foundation; either version 2
8 # of the License, or (at your option) any later version.
8 # of the License, or (at your option) any later version.
9 #
9 #
10 # This program is distributed in the hope that it will be useful,
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
13 # GNU General Public License for more details.
14 #
14 #
15 # You should have received a copy of the GNU General Public License
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
18
19 require 'redmine/scm/adapters/git_adapter'
19 require 'redmine/scm/adapters/git_adapter'
20
20
21 class Repository::Git < Repository
21 class Repository::Git < Repository
22 attr_protected :root_url
22 attr_protected :root_url
23 validates_presence_of :url
23 validates_presence_of :url
24
24
25 def self.human_attribute_name(attribute_key_name, *args)
25 def self.human_attribute_name(attribute_key_name, *args)
26 attr_name = attribute_key_name.to_s
26 attr_name = attribute_key_name.to_s
27 if attr_name == "url"
27 if attr_name == "url"
28 attr_name = "path_to_repository"
28 attr_name = "path_to_repository"
29 end
29 end
30 super(attr_name, *args)
30 super(attr_name, *args)
31 end
31 end
32
32
33 def self.scm_adapter_class
33 def self.scm_adapter_class
34 Redmine::Scm::Adapters::GitAdapter
34 Redmine::Scm::Adapters::GitAdapter
35 end
35 end
36
36
37 def self.scm_name
37 def self.scm_name
38 'Git'
38 'Git'
39 end
39 end
40
40
41 def report_last_commit
41 def report_last_commit
42 extra_report_last_commit
42 extra_report_last_commit
43 end
43 end
44
44
45 def extra_report_last_commit
45 def extra_report_last_commit
46 return false if extra_info.nil?
46 return false if extra_info.nil?
47 v = extra_info["extra_report_last_commit"]
47 v = extra_info["extra_report_last_commit"]
48 return false if v.nil?
48 return false if v.nil?
49 v.to_s != '0'
49 v.to_s != '0'
50 end
50 end
51
51
52 def supports_directory_revisions?
52 def supports_directory_revisions?
53 true
53 true
54 end
54 end
55
55
56 def supports_revision_graph?
56 def supports_revision_graph?
57 true
57 true
58 end
58 end
59
59
60 def repo_log_encoding
60 def repo_log_encoding
61 'UTF-8'
61 'UTF-8'
62 end
62 end
63
63
64 # Returns the identifier for the given git changeset
64 # Returns the identifier for the given git changeset
65 def self.changeset_identifier(changeset)
65 def self.changeset_identifier(changeset)
66 changeset.scmid
66 changeset.scmid
67 end
67 end
68
68
69 # Returns the readable identifier for the given git changeset
69 # Returns the readable identifier for the given git changeset
70 def self.format_changeset_identifier(changeset)
70 def self.format_changeset_identifier(changeset)
71 changeset.revision[0, 8]
71 changeset.revision[0, 8]
72 end
72 end
73
73
74 def branches
74 def branches
75 scm.branches
75 scm.branches
76 end
76 end
77
77
78 def tags
78 def tags
79 scm.tags
79 scm.tags
80 end
80 end
81
81
82 def default_branch
82 def default_branch
83 scm.default_branch
83 scm.default_branch
84 rescue Exception => e
84 rescue Exception => e
85 logger.error "git: error during get default branch: #{e.message}"
85 logger.error "git: error during get default branch: #{e.message}"
86 nil
86 nil
87 end
87 end
88
88
89 def find_changeset_by_name(name)
89 def find_changeset_by_name(name)
90 if name.present?
90 if name.present?
91 changesets.where(:revision => name.to_s).first ||
91 changesets.where(:revision => name.to_s).first ||
92 changesets.where('scmid LIKE ?', "#{name}%").first
92 changesets.where('scmid LIKE ?', "#{name}%").first
93 end
93 end
94 end
94 end
95
95
96 def scm_entries(path=nil, identifier=nil)
96 def scm_entries(path=nil, identifier=nil)
97 scm.entries(path, identifier, :report_last_commit => extra_report_last_commit)
97 scm.entries(path, identifier, :report_last_commit => extra_report_last_commit)
98 end
98 end
99 protected :scm_entries
99 protected :scm_entries
100
100
101 # With SCMs that have a sequential commit numbering,
101 # With SCMs that have a sequential commit numbering,
102 # such as Subversion and Mercurial,
102 # such as Subversion and Mercurial,
103 # Redmine is able to be clever and only fetch changesets
103 # Redmine is able to be clever and only fetch changesets
104 # going forward from the most recent one it knows about.
104 # going forward from the most recent one it knows about.
105 #
105 #
106 # However, Git does not have a sequential commit numbering.
106 # However, Git does not have a sequential commit numbering.
107 #
107 #
108 # In order to fetch only new adding revisions,
108 # In order to fetch only new adding revisions,
109 # Redmine needs to save "heads".
109 # Redmine needs to save "heads".
110 #
110 #
111 # In Git and Mercurial, revisions are not in date order.
111 # In Git and Mercurial, revisions are not in date order.
112 # Redmine Mercurial fixed issues.
112 # Redmine Mercurial fixed issues.
113 # * Redmine Takes Too Long On Large Mercurial Repository
113 # * Redmine Takes Too Long On Large Mercurial Repository
114 # http://www.redmine.org/issues/3449
114 # http://www.redmine.org/issues/3449
115 # * Sorting for changesets might go wrong on Mercurial repos
115 # * Sorting for changesets might go wrong on Mercurial repos
116 # http://www.redmine.org/issues/3567
116 # http://www.redmine.org/issues/3567
117 #
117 #
118 # Database revision column is text, so Redmine can not sort by revision.
118 # Database revision column is text, so Redmine can not sort by revision.
119 # Mercurial has revision number, and revision number guarantees revision order.
119 # Mercurial has revision number, and revision number guarantees revision order.
120 # Redmine Mercurial model stored revisions ordered by database id to database.
120 # Redmine Mercurial model stored revisions ordered by database id to database.
121 # So, Redmine Mercurial model can use correct ordering revisions.
121 # So, Redmine Mercurial model can use correct ordering revisions.
122 #
122 #
123 # Redmine Mercurial adapter uses "hg log -r 0:tip --limit 10"
123 # Redmine Mercurial adapter uses "hg log -r 0:tip --limit 10"
124 # to get limited revisions from old to new.
124 # to get limited revisions from old to new.
125 # But, Git 1.7.3.4 does not support --reverse with -n or --skip.
125 # But, Git 1.7.3.4 does not support --reverse with -n or --skip.
126 #
126 #
127 # The repository can still be fully reloaded by calling #clear_changesets
127 # The repository can still be fully reloaded by calling #clear_changesets
128 # before fetching changesets (eg. for offline resync)
128 # before fetching changesets (eg. for offline resync)
129 def fetch_changesets
129 def fetch_changesets
130 scm_brs = branches
130 scm_brs = branches
131 return if scm_brs.nil? || scm_brs.empty?
131 return if scm_brs.nil? || scm_brs.empty?
132
132
133 h1 = extra_info || {}
133 h1 = extra_info || {}
134 h = h1.dup
134 h = h1.dup
135 repo_heads = scm_brs.map{ |br| br.scmid }
135 repo_heads = scm_brs.map{ |br| br.scmid }
136 h["heads"] ||= []
136 h["heads"] ||= []
137 prev_db_heads = h["heads"].dup
137 prev_db_heads = h["heads"].dup
138 if prev_db_heads.empty?
138 if prev_db_heads.empty?
139 prev_db_heads += heads_from_branches_hash
139 prev_db_heads += heads_from_branches_hash
140 end
140 end
141 return if prev_db_heads.sort == repo_heads.sort
141 return if prev_db_heads.sort == repo_heads.sort
142
142
143 h["db_consistent"] ||= {}
143 h["db_consistent"] ||= {}
144 if changesets.count == 0
144 if changesets.count == 0
145 h["db_consistent"]["ordering"] = 1
145 h["db_consistent"]["ordering"] = 1
146 merge_extra_info(h)
146 merge_extra_info(h)
147 self.save
147 self.save
148 elsif ! h["db_consistent"].has_key?("ordering")
148 elsif ! h["db_consistent"].has_key?("ordering")
149 h["db_consistent"]["ordering"] = 0
149 h["db_consistent"]["ordering"] = 0
150 merge_extra_info(h)
150 merge_extra_info(h)
151 self.save
151 self.save
152 end
152 end
153 save_revisions(prev_db_heads, repo_heads)
153 save_revisions(prev_db_heads, repo_heads)
154 end
154 end
155
155
156 def save_revisions(prev_db_heads, repo_heads)
156 def save_revisions(prev_db_heads, repo_heads)
157 h = {}
157 h = {}
158 opts = {}
158 opts = {}
159 opts[:reverse] = true
159 opts[:reverse] = true
160 opts[:excludes] = prev_db_heads
160 opts[:excludes] = prev_db_heads
161 opts[:includes] = repo_heads
161 opts[:includes] = repo_heads
162
162
163 revisions = scm.revisions('', nil, nil, opts)
163 revisions = scm.revisions('', nil, nil, opts)
164 return if revisions.blank?
164 return if revisions.blank?
165
165
166 # Make the search for existing revisions in the database in a more sufficient manner
166 # Make the search for existing revisions in the database in a more sufficient manner
167 #
167 #
168 # Git branch is the reference to the specific revision.
168 # Git branch is the reference to the specific revision.
169 # Git can *delete* remote branch and *re-push* branch.
169 # Git can *delete* remote branch and *re-push* branch.
170 #
170 #
171 # $ git push remote :branch
171 # $ git push remote :branch
172 # $ git push remote branch
172 # $ git push remote branch
173 #
173 #
174 # After deleting branch, revisions remain in repository until "git gc".
174 # After deleting branch, revisions remain in repository until "git gc".
175 # On git 1.7.2.3, default pruning date is 2 weeks.
175 # On git 1.7.2.3, default pruning date is 2 weeks.
176 # So, "git log --not deleted_branch_head_revision" return code is 0.
176 # So, "git log --not deleted_branch_head_revision" return code is 0.
177 #
177 #
178 # After re-pushing branch, "git log" returns revisions which are saved in database.
178 # After re-pushing branch, "git log" returns revisions which are saved in database.
179 # So, Redmine needs to scan revisions and database every time.
179 # So, Redmine needs to scan revisions and database every time.
180 #
180 #
181 # This is replacing the one-after-one queries.
181 # This is replacing the one-after-one queries.
182 # Find all revisions, that are in the database, and then remove them
182 # Find all revisions, that are in the database, and then remove them
183 # from the revision array.
183 # from the revision array.
184 # Then later we won't need any conditions for db existence.
184 # Then later we won't need any conditions for db existence.
185 # Query for several revisions at once, and remove them
185 # Query for several revisions at once, and remove them
186 # from the revisions array, if they are there.
186 # from the revisions array, if they are there.
187 # Do this in chunks, to avoid eventual memory problems
187 # Do this in chunks, to avoid eventual memory problems
188 # (in case of tens of thousands of commits).
188 # (in case of tens of thousands of commits).
189 # If there are no revisions (because the original code's algorithm filtered them),
189 # If there are no revisions (because the original code's algorithm filtered them),
190 # then this part will be stepped over.
190 # then this part will be stepped over.
191 # We make queries, just if there is any revision.
191 # We make queries, just if there is any revision.
192 limit = 100
192 limit = 100
193 offset = 0
193 offset = 0
194 revisions_copy = revisions.clone # revisions will change
194 revisions_copy = revisions.clone # revisions will change
195 while offset < revisions_copy.size
195 while offset < revisions_copy.size
196 scmids = revisions_copy.slice(offset, limit).map{|x| x.scmid}
196 scmids = revisions_copy.slice(offset, limit).map{|x| x.scmid}
197 recent_changesets_slice = changesets.where(:scmid => scmids)
197 recent_changesets_slice = changesets.where(:scmid => scmids)
198 # Subtract revisions that redmine already knows about
198 # Subtract revisions that redmine already knows about
199 recent_revisions = recent_changesets_slice.map{|c| c.scmid}
199 recent_revisions = recent_changesets_slice.map{|c| c.scmid}
200 revisions.reject!{|r| recent_revisions.include?(r.scmid)}
200 revisions.reject!{|r| recent_revisions.include?(r.scmid)}
201 offset += limit
201 offset += limit
202 end
202 end
203 revisions.each do |rev|
203 revisions.each do |rev|
204 transaction do
204 transaction do
205 # There is no search in the db for this revision, because above we ensured,
205 # There is no search in the db for this revision, because above we ensured,
206 # that it's not in the db.
206 # that it's not in the db.
207 save_revision(rev)
207 save_revision(rev)
208 end
208 end
209 end
209 end
210 h["heads"] = repo_heads.dup
210 h["heads"] = repo_heads.dup
211 merge_extra_info(h)
211 merge_extra_info(h)
212 self.save
212 save(:validate => false)
213 end
213 end
214 private :save_revisions
214 private :save_revisions
215
215
216 def save_revision(rev)
216 def save_revision(rev)
217 parents = (rev.parents || []).collect{|rp| find_changeset_by_name(rp)}.compact
217 parents = (rev.parents || []).collect{|rp| find_changeset_by_name(rp)}.compact
218 changeset = Changeset.create(
218 changeset = Changeset.create(
219 :repository => self,
219 :repository => self,
220 :revision => rev.identifier,
220 :revision => rev.identifier,
221 :scmid => rev.scmid,
221 :scmid => rev.scmid,
222 :committer => rev.author,
222 :committer => rev.author,
223 :committed_on => rev.time,
223 :committed_on => rev.time,
224 :comments => rev.message,
224 :comments => rev.message,
225 :parents => parents
225 :parents => parents
226 )
226 )
227 unless changeset.new_record?
227 unless changeset.new_record?
228 rev.paths.each { |change| changeset.create_change(change) }
228 rev.paths.each { |change| changeset.create_change(change) }
229 end
229 end
230 changeset
230 changeset
231 end
231 end
232 private :save_revision
232 private :save_revision
233
233
234 def heads_from_branches_hash
234 def heads_from_branches_hash
235 h1 = extra_info || {}
235 h1 = extra_info || {}
236 h = h1.dup
236 h = h1.dup
237 h["branches"] ||= {}
237 h["branches"] ||= {}
238 h['branches'].map{|br, hs| hs['last_scmid']}
238 h['branches'].map{|br, hs| hs['last_scmid']}
239 end
239 end
240
240
241 def latest_changesets(path,rev,limit=10)
241 def latest_changesets(path,rev,limit=10)
242 revisions = scm.revisions(path, nil, rev, :limit => limit, :all => false)
242 revisions = scm.revisions(path, nil, rev, :limit => limit, :all => false)
243 return [] if revisions.nil? || revisions.empty?
243 return [] if revisions.nil? || revisions.empty?
244 changesets.where(:scmid => revisions.map {|c| c.scmid}).to_a
244 changesets.where(:scmid => revisions.map {|c| c.scmid}).to_a
245 end
245 end
246
246
247 def clear_extra_info_of_changesets
247 def clear_extra_info_of_changesets
248 return if extra_info.nil?
248 return if extra_info.nil?
249 v = extra_info["extra_report_last_commit"]
249 v = extra_info["extra_report_last_commit"]
250 write_attribute(:extra_info, nil)
250 write_attribute(:extra_info, nil)
251 h = {}
251 h = {}
252 h["extra_report_last_commit"] = v
252 h["extra_report_last_commit"] = v
253 merge_extra_info(h)
253 merge_extra_info(h)
254 self.save
254 save(:validate => false)
255 end
255 end
256 private :clear_extra_info_of_changesets
256 private :clear_extra_info_of_changesets
257 end
257 end
@@ -1,469 +1,487
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
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')
82 assert r.save
83 end
84
85 def test_2_repositories_with_same_identifier_should_not_be_valid
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')
88 assert !r.save
89 end
90
91 def test_2_repositories_with_blank_identifier_should_not_be_valid
92 Repository::Subversion.create!(:project_id => 3, :identifier => '', :url => 'file:///foo')
93 r = Repository::Subversion.new(:project_id => 3, :identifier => '', :url => 'file:///bar')
94 assert !r.save
95 end
96
79 def test_first_repository_should_be_set_as_default
97 def test_first_repository_should_be_set_as_default
80 repository1 = Repository::Subversion.new(
98 repository1 = Repository::Subversion.new(
81 :project => Project.find(3),
99 :project => Project.find(3),
82 :identifier => 'svn1',
100 :identifier => 'svn1',
83 :url => 'file:///svn1'
101 :url => 'file:///svn1'
84 )
102 )
85 assert repository1.save
103 assert repository1.save
86 assert repository1.is_default?
104 assert repository1.is_default?
87
105
88 repository2 = Repository::Subversion.new(
106 repository2 = Repository::Subversion.new(
89 :project => Project.find(3),
107 :project => Project.find(3),
90 :identifier => 'svn2',
108 :identifier => 'svn2',
91 :url => 'file:///svn2'
109 :url => 'file:///svn2'
92 )
110 )
93 assert repository2.save
111 assert repository2.save
94 assert !repository2.is_default?
112 assert !repository2.is_default?
95
113
96 assert_equal repository1, Project.find(3).repository
114 assert_equal repository1, Project.find(3).repository
97 assert_equal [repository1, repository2], Project.find(3).repositories.sort
115 assert_equal [repository1, repository2], Project.find(3).repositories.sort
98 end
116 end
99
117
100 def test_default_repository_should_be_one
118 def test_default_repository_should_be_one
101 assert_equal 0, Project.find(3).repositories.count
119 assert_equal 0, Project.find(3).repositories.count
102 repository1 = Repository::Subversion.new(
120 repository1 = Repository::Subversion.new(
103 :project => Project.find(3),
121 :project => Project.find(3),
104 :identifier => 'svn1',
122 :identifier => 'svn1',
105 :url => 'file:///svn1'
123 :url => 'file:///svn1'
106 )
124 )
107 assert repository1.save
125 assert repository1.save
108 assert repository1.is_default?
126 assert repository1.is_default?
109
127
110 repository2 = Repository::Subversion.new(
128 repository2 = Repository::Subversion.new(
111 :project => Project.find(3),
129 :project => Project.find(3),
112 :identifier => 'svn2',
130 :identifier => 'svn2',
113 :url => 'file:///svn2',
131 :url => 'file:///svn2',
114 :is_default => true
132 :is_default => true
115 )
133 )
116 assert repository2.save
134 assert repository2.save
117 assert repository2.is_default?
135 assert repository2.is_default?
118 repository1.reload
136 repository1.reload
119 assert !repository1.is_default?
137 assert !repository1.is_default?
120
138
121 assert_equal repository2, Project.find(3).repository
139 assert_equal repository2, Project.find(3).repository
122 assert_equal [repository2, repository1], Project.find(3).repositories.sort
140 assert_equal [repository2, repository1], Project.find(3).repositories.sort
123 end
141 end
124
142
125 def test_identifier_should_accept_letters_digits_dashes_and_underscores
143 def test_identifier_should_accept_letters_digits_dashes_and_underscores
126 r = Repository::Subversion.new(
144 r = Repository::Subversion.new(
127 :project_id => 3,
145 :project_id => 3,
128 :identifier => 'svn-123_45',
146 :identifier => 'svn-123_45',
129 :url => 'file:///svn'
147 :url => 'file:///svn'
130 )
148 )
131 assert r.save
149 assert r.save
132 end
150 end
133
151
134 def test_identifier_should_not_be_frozen_for_a_new_repository
152 def test_identifier_should_not_be_frozen_for_a_new_repository
135 assert_equal false, Repository.new.identifier_frozen?
153 assert_equal false, Repository.new.identifier_frozen?
136 end
154 end
137
155
138 def test_identifier_should_not_be_frozen_for_a_saved_repository_with_blank_identifier
156 def test_identifier_should_not_be_frozen_for_a_saved_repository_with_blank_identifier
139 Repository.where(:id => 10).update_all(["identifier = ''"])
157 Repository.where(:id => 10).update_all(["identifier = ''"])
140 assert_equal false, Repository.find(10).identifier_frozen?
158 assert_equal false, Repository.find(10).identifier_frozen?
141 end
159 end
142
160
143 def test_identifier_should_be_frozen_for_a_saved_repository_with_valid_identifier
161 def test_identifier_should_be_frozen_for_a_saved_repository_with_valid_identifier
144 Repository.where(:id => 10).update_all(["identifier = 'abc123'"])
162 Repository.where(:id => 10).update_all(["identifier = 'abc123'"])
145 assert_equal true, Repository.find(10).identifier_frozen?
163 assert_equal true, Repository.find(10).identifier_frozen?
146 end
164 end
147
165
148 def test_identifier_should_not_accept_change_if_frozen
166 def test_identifier_should_not_accept_change_if_frozen
149 r = Repository.new(:identifier => 'foo')
167 r = Repository.new(:identifier => 'foo')
150 r.stubs(:identifier_frozen?).returns(true)
168 r.stubs(:identifier_frozen?).returns(true)
151
169
152 r.identifier = 'bar'
170 r.identifier = 'bar'
153 assert_equal 'foo', r.identifier
171 assert_equal 'foo', r.identifier
154 end
172 end
155
173
156 def test_identifier_should_accept_change_if_not_frozen
174 def test_identifier_should_accept_change_if_not_frozen
157 r = Repository.new(:identifier => 'foo')
175 r = Repository.new(:identifier => 'foo')
158 r.stubs(:identifier_frozen?).returns(false)
176 r.stubs(:identifier_frozen?).returns(false)
159
177
160 r.identifier = 'bar'
178 r.identifier = 'bar'
161 assert_equal 'bar', r.identifier
179 assert_equal 'bar', r.identifier
162 end
180 end
163
181
164 def test_destroy
182 def test_destroy
165 repository = Repository.find(10)
183 repository = Repository.find(10)
166 changesets = repository.changesets.count
184 changesets = repository.changesets.count
167 changes = repository.filechanges.count
185 changes = repository.filechanges.count
168
186
169 assert_difference 'Changeset.count', -changesets do
187 assert_difference 'Changeset.count', -changesets do
170 assert_difference 'Change.count', -changes do
188 assert_difference 'Change.count', -changes do
171 Repository.find(10).destroy
189 Repository.find(10).destroy
172 end
190 end
173 end
191 end
174 end
192 end
175
193
176 def test_destroy_should_delete_parents_associations
194 def test_destroy_should_delete_parents_associations
177 changeset = Changeset.find(102)
195 changeset = Changeset.find(102)
178 changeset.parents = Changeset.where(:id => [100, 101]).to_a
196 changeset.parents = Changeset.where(:id => [100, 101]).to_a
179 assert_difference 'Changeset.connection.select_all("select * from changeset_parents").count', -2 do
197 assert_difference 'Changeset.connection.select_all("select * from changeset_parents").count', -2 do
180 Repository.find(10).destroy
198 Repository.find(10).destroy
181 end
199 end
182 end
200 end
183
201
184 def test_destroy_should_delete_issues_associations
202 def test_destroy_should_delete_issues_associations
185 changeset = Changeset.find(102)
203 changeset = Changeset.find(102)
186 changeset.issues = Issue.where(:id => [1, 2]).to_a
204 changeset.issues = Issue.where(:id => [1, 2]).to_a
187 assert_difference 'Changeset.connection.select_all("select * from changesets_issues").count', -2 do
205 assert_difference 'Changeset.connection.select_all("select * from changesets_issues").count', -2 do
188 Repository.find(10).destroy
206 Repository.find(10).destroy
189 end
207 end
190 end
208 end
191
209
192 def test_should_not_create_with_disabled_scm
210 def test_should_not_create_with_disabled_scm
193 # disable Subversion
211 # disable Subversion
194 with_settings :enabled_scm => ['Darcs', 'Git'] do
212 with_settings :enabled_scm => ['Darcs', 'Git'] do
195 repository = Repository::Subversion.new(
213 repository = Repository::Subversion.new(
196 :project => Project.find(3), :url => "svn://localhost")
214 :project => Project.find(3), :url => "svn://localhost")
197 assert !repository.save
215 assert !repository.save
198 assert_include I18n.translate('activerecord.errors.messages.invalid'),
216 assert_include I18n.translate('activerecord.errors.messages.invalid'),
199 repository.errors[:type]
217 repository.errors[:type]
200 end
218 end
201 end
219 end
202
220
203 def test_scan_changesets_for_issue_ids
221 def test_scan_changesets_for_issue_ids
204 Setting.default_language = 'en'
222 Setting.default_language = 'en'
205 Setting.commit_ref_keywords = 'refs , references, IssueID'
223 Setting.commit_ref_keywords = 'refs , references, IssueID'
206 Setting.commit_update_keywords = [
224 Setting.commit_update_keywords = [
207 {'keywords' => 'fixes , closes',
225 {'keywords' => 'fixes , closes',
208 'status_id' => IssueStatus.where(:is_closed => true).first.id,
226 'status_id' => IssueStatus.where(:is_closed => true).first.id,
209 'done_ratio' => '90'}
227 'done_ratio' => '90'}
210 ]
228 ]
211 Setting.default_language = 'en'
229 Setting.default_language = 'en'
212 ActionMailer::Base.deliveries.clear
230 ActionMailer::Base.deliveries.clear
213
231
214 # make sure issue 1 is not already closed
232 # make sure issue 1 is not already closed
215 fixed_issue = Issue.find(1)
233 fixed_issue = Issue.find(1)
216 assert !fixed_issue.closed?
234 assert !fixed_issue.closed?
217 old_status = fixed_issue.status
235 old_status = fixed_issue.status
218
236
219 with_settings :notified_events => %w(issue_added issue_updated) do
237 with_settings :notified_events => %w(issue_added issue_updated) do
220 Repository.scan_changesets_for_issue_ids
238 Repository.scan_changesets_for_issue_ids
221 end
239 end
222 assert_equal [101, 102], Issue.find(3).changeset_ids
240 assert_equal [101, 102], Issue.find(3).changeset_ids
223
241
224 # fixed issues
242 # fixed issues
225 fixed_issue.reload
243 fixed_issue.reload
226 assert fixed_issue.closed?
244 assert fixed_issue.closed?
227 assert_equal 90, fixed_issue.done_ratio
245 assert_equal 90, fixed_issue.done_ratio
228 assert_equal [101], fixed_issue.changeset_ids
246 assert_equal [101], fixed_issue.changeset_ids
229
247
230 # issue change
248 # issue change
231 journal = fixed_issue.journals.reorder('created_on desc').first
249 journal = fixed_issue.journals.reorder('created_on desc').first
232 assert_equal User.find_by_login('dlopper'), journal.user
250 assert_equal User.find_by_login('dlopper'), journal.user
233 assert_equal 'Applied in changeset r2.', journal.notes
251 assert_equal 'Applied in changeset r2.', journal.notes
234
252
235 # 2 email notifications
253 # 2 email notifications
236 assert_equal 2, ActionMailer::Base.deliveries.size
254 assert_equal 2, ActionMailer::Base.deliveries.size
237 mail = ActionMailer::Base.deliveries.first
255 mail = ActionMailer::Base.deliveries.first
238 assert_not_nil mail
256 assert_not_nil mail
239 assert mail.subject.starts_with?(
257 assert mail.subject.starts_with?(
240 "[#{fixed_issue.project.name} - #{fixed_issue.tracker.name} ##{fixed_issue.id}]")
258 "[#{fixed_issue.project.name} - #{fixed_issue.tracker.name} ##{fixed_issue.id}]")
241 assert_mail_body_match(
259 assert_mail_body_match(
242 "Status changed from #{old_status} to #{fixed_issue.status}", mail)
260 "Status changed from #{old_status} to #{fixed_issue.status}", mail)
243
261
244 # ignoring commits referencing an issue of another project
262 # ignoring commits referencing an issue of another project
245 assert_equal [], Issue.find(4).changesets
263 assert_equal [], Issue.find(4).changesets
246 end
264 end
247
265
248 def test_for_changeset_comments_strip
266 def test_for_changeset_comments_strip
249 repository = Repository::Mercurial.create(
267 repository = Repository::Mercurial.create(
250 :project => Project.find( 4 ),
268 :project => Project.find( 4 ),
251 :url => '/foo/bar/baz' )
269 :url => '/foo/bar/baz' )
252 comment = <<-COMMENT
270 comment = <<-COMMENT
253 This is a loooooooooooooooooooooooooooong comment
271 This is a loooooooooooooooooooooooooooong comment
254
272
255
273
256 COMMENT
274 COMMENT
257 changeset = Changeset.new(
275 changeset = Changeset.new(
258 :comments => comment, :commit_date => Time.now,
276 :comments => comment, :commit_date => Time.now,
259 :revision => 0, :scmid => 'f39b7922fb3c',
277 :revision => 0, :scmid => 'f39b7922fb3c',
260 :committer => 'foo <foo@example.com>',
278 :committer => 'foo <foo@example.com>',
261 :committed_on => Time.now, :repository => repository )
279 :committed_on => Time.now, :repository => repository )
262 assert( changeset.save )
280 assert( changeset.save )
263 assert_not_equal( comment, changeset.comments )
281 assert_not_equal( comment, changeset.comments )
264 assert_equal( 'This is a loooooooooooooooooooooooooooong comment',
282 assert_equal( 'This is a loooooooooooooooooooooooooooong comment',
265 changeset.comments )
283 changeset.comments )
266 end
284 end
267
285
268 def test_for_urls_strip_cvs
286 def test_for_urls_strip_cvs
269 repository = Repository::Cvs.create(
287 repository = Repository::Cvs.create(
270 :project => Project.find(4),
288 :project => Project.find(4),
271 :url => ' :pserver:login:password@host:/path/to/the/repository',
289 :url => ' :pserver:login:password@host:/path/to/the/repository',
272 :root_url => 'foo ',
290 :root_url => 'foo ',
273 :log_encoding => 'UTF-8')
291 :log_encoding => 'UTF-8')
274 assert repository.save
292 assert repository.save
275 repository.reload
293 repository.reload
276 assert_equal ':pserver:login:password@host:/path/to/the/repository',
294 assert_equal ':pserver:login:password@host:/path/to/the/repository',
277 repository.url
295 repository.url
278 assert_equal 'foo', repository.root_url
296 assert_equal 'foo', repository.root_url
279 end
297 end
280
298
281 def test_for_urls_strip_subversion
299 def test_for_urls_strip_subversion
282 repository = Repository::Subversion.create(
300 repository = Repository::Subversion.create(
283 :project => Project.find(4),
301 :project => Project.find(4),
284 :url => ' file:///dummy ')
302 :url => ' file:///dummy ')
285 assert repository.save
303 assert repository.save
286 repository.reload
304 repository.reload
287 assert_equal 'file:///dummy', repository.url
305 assert_equal 'file:///dummy', repository.url
288 end
306 end
289
307
290 def test_for_urls_strip_git
308 def test_for_urls_strip_git
291 repository = Repository::Git.create(
309 repository = Repository::Git.create(
292 :project => Project.find(4),
310 :project => Project.find(4),
293 :url => ' c:\dummy ')
311 :url => ' c:\dummy ')
294 assert repository.save
312 assert repository.save
295 repository.reload
313 repository.reload
296 assert_equal 'c:\dummy', repository.url
314 assert_equal 'c:\dummy', repository.url
297 end
315 end
298
316
299 def test_manual_user_mapping
317 def test_manual_user_mapping
300 assert_no_difference "Changeset.where('user_id <> 2').count" do
318 assert_no_difference "Changeset.where('user_id <> 2').count" do
301 c = Changeset.create!(
319 c = Changeset.create!(
302 :repository => @repository,
320 :repository => @repository,
303 :committer => 'foo',
321 :committer => 'foo',
304 :committed_on => Time.now,
322 :committed_on => Time.now,
305 :revision => 100,
323 :revision => 100,
306 :comments => 'Committed by foo.'
324 :comments => 'Committed by foo.'
307 )
325 )
308 assert_nil c.user
326 assert_nil c.user
309 @repository.committer_ids = {'foo' => '2'}
327 @repository.committer_ids = {'foo' => '2'}
310 assert_equal User.find(2), c.reload.user
328 assert_equal User.find(2), c.reload.user
311 # committer is now mapped
329 # committer is now mapped
312 c = Changeset.create!(
330 c = Changeset.create!(
313 :repository => @repository,
331 :repository => @repository,
314 :committer => 'foo',
332 :committer => 'foo',
315 :committed_on => Time.now,
333 :committed_on => Time.now,
316 :revision => 101,
334 :revision => 101,
317 :comments => 'Another commit by foo.'
335 :comments => 'Another commit by foo.'
318 )
336 )
319 assert_equal User.find(2), c.user
337 assert_equal User.find(2), c.user
320 end
338 end
321 end
339 end
322
340
323 def test_auto_user_mapping_by_username
341 def test_auto_user_mapping_by_username
324 c = Changeset.create!(
342 c = Changeset.create!(
325 :repository => @repository,
343 :repository => @repository,
326 :committer => 'jsmith',
344 :committer => 'jsmith',
327 :committed_on => Time.now,
345 :committed_on => Time.now,
328 :revision => 100,
346 :revision => 100,
329 :comments => 'Committed by john.'
347 :comments => 'Committed by john.'
330 )
348 )
331 assert_equal User.find(2), c.user
349 assert_equal User.find(2), c.user
332 end
350 end
333
351
334 def test_auto_user_mapping_by_email
352 def test_auto_user_mapping_by_email
335 c = Changeset.create!(
353 c = Changeset.create!(
336 :repository => @repository,
354 :repository => @repository,
337 :committer => 'john <jsmith@somenet.foo>',
355 :committer => 'john <jsmith@somenet.foo>',
338 :committed_on => Time.now,
356 :committed_on => Time.now,
339 :revision => 100,
357 :revision => 100,
340 :comments => 'Committed by john.'
358 :comments => 'Committed by john.'
341 )
359 )
342 assert_equal User.find(2), c.user
360 assert_equal User.find(2), c.user
343 end
361 end
344
362
345 def test_filesystem_avaialbe
363 def test_filesystem_avaialbe
346 klass = Repository::Filesystem
364 klass = Repository::Filesystem
347 assert klass.scm_adapter_class
365 assert klass.scm_adapter_class
348 assert_equal true, klass.scm_available
366 assert_equal true, klass.scm_available
349 end
367 end
350
368
351 def test_extra_info_should_not_return_non_hash_value
369 def test_extra_info_should_not_return_non_hash_value
352 repo = Repository.new
370 repo = Repository.new
353 repo.extra_info = "foo"
371 repo.extra_info = "foo"
354 assert_nil repo.extra_info
372 assert_nil repo.extra_info
355 end
373 end
356
374
357 def test_merge_extra_info
375 def test_merge_extra_info
358 repo = Repository::Subversion.new(:project => Project.find(3))
376 repo = Repository::Subversion.new(:project => Project.find(3))
359 assert !repo.save
377 assert !repo.save
360 repo.url = "svn://localhost"
378 repo.url = "svn://localhost"
361 assert repo.save
379 assert repo.save
362 repo.reload
380 repo.reload
363 project = Project.find(3)
381 project = Project.find(3)
364 assert_equal repo, project.repository
382 assert_equal repo, project.repository
365 assert_nil repo.extra_info
383 assert_nil repo.extra_info
366 h1 = {"test_1" => {"test_11" => "test_value_11"}}
384 h1 = {"test_1" => {"test_11" => "test_value_11"}}
367 repo.merge_extra_info(h1)
385 repo.merge_extra_info(h1)
368 assert_equal h1, repo.extra_info
386 assert_equal h1, repo.extra_info
369 h2 = {"test_2" => {
387 h2 = {"test_2" => {
370 "test_21" => "test_value_21",
388 "test_21" => "test_value_21",
371 "test_22" => "test_value_22",
389 "test_22" => "test_value_22",
372 }}
390 }}
373 repo.merge_extra_info(h2)
391 repo.merge_extra_info(h2)
374 assert_equal (h = {"test_11" => "test_value_11"}),
392 assert_equal (h = {"test_11" => "test_value_11"}),
375 repo.extra_info["test_1"]
393 repo.extra_info["test_1"]
376 assert_equal "test_value_21",
394 assert_equal "test_value_21",
377 repo.extra_info["test_2"]["test_21"]
395 repo.extra_info["test_2"]["test_21"]
378 h3 = {"test_2" => {
396 h3 = {"test_2" => {
379 "test_23" => "test_value_23",
397 "test_23" => "test_value_23",
380 "test_24" => "test_value_24",
398 "test_24" => "test_value_24",
381 }}
399 }}
382 repo.merge_extra_info(h3)
400 repo.merge_extra_info(h3)
383 assert_equal (h = {"test_11" => "test_value_11"}),
401 assert_equal (h = {"test_11" => "test_value_11"}),
384 repo.extra_info["test_1"]
402 repo.extra_info["test_1"]
385 assert_nil repo.extra_info["test_2"]["test_21"]
403 assert_nil repo.extra_info["test_2"]["test_21"]
386 assert_equal "test_value_23",
404 assert_equal "test_value_23",
387 repo.extra_info["test_2"]["test_23"]
405 repo.extra_info["test_2"]["test_23"]
388 end
406 end
389
407
390 def test_sort_should_not_raise_an_error_with_nil_identifiers
408 def test_sort_should_not_raise_an_error_with_nil_identifiers
391 r1 = Repository.new
409 r1 = Repository.new
392 r2 = Repository.new
410 r2 = Repository.new
393
411
394 assert_nothing_raised do
412 assert_nothing_raised do
395 [r1, r2].sort
413 [r1, r2].sort
396 end
414 end
397 end
415 end
398
416
399 def test_stats_by_author_reflect_changesets_and_changes
417 def test_stats_by_author_reflect_changesets_and_changes
400 repository = Repository.find(10)
418 repository = Repository.find(10)
401
419
402 expected = {"Dave Lopper"=>{:commits_count=>10, :changes_count=>3}}
420 expected = {"Dave Lopper"=>{:commits_count=>10, :changes_count=>3}}
403 assert_equal expected, repository.stats_by_author
421 assert_equal expected, repository.stats_by_author
404
422
405 set = Changeset.create!(
423 set = Changeset.create!(
406 :repository => repository,
424 :repository => repository,
407 :committer => 'dlopper',
425 :committer => 'dlopper',
408 :committed_on => Time.now,
426 :committed_on => Time.now,
409 :revision => 101,
427 :revision => 101,
410 :comments => 'Another commit by foo.'
428 :comments => 'Another commit by foo.'
411 )
429 )
412 Change.create!(:changeset => set, :action => 'A', :path => '/path/to/file1')
430 Change.create!(:changeset => set, :action => 'A', :path => '/path/to/file1')
413 Change.create!(:changeset => set, :action => 'A', :path => '/path/to/file2')
431 Change.create!(:changeset => set, :action => 'A', :path => '/path/to/file2')
414 expected = {"Dave Lopper"=>{:commits_count=>11, :changes_count=>5}}
432 expected = {"Dave Lopper"=>{:commits_count=>11, :changes_count=>5}}
415 assert_equal expected, repository.stats_by_author
433 assert_equal expected, repository.stats_by_author
416 end
434 end
417
435
418 def test_stats_by_author_honnor_committers
436 def test_stats_by_author_honnor_committers
419 # in fact it is really tested above, but let's have a dedicated test
437 # in fact it is really tested above, but let's have a dedicated test
420 # to ensure things are dynamically linked to Users
438 # to ensure things are dynamically linked to Users
421 User.find_by_login("dlopper").update_attribute(:firstname, "Dave's")
439 User.find_by_login("dlopper").update_attribute(:firstname, "Dave's")
422 repository = Repository.find(10)
440 repository = Repository.find(10)
423 expected = {"Dave's Lopper"=>{:commits_count=>10, :changes_count=>3}}
441 expected = {"Dave's Lopper"=>{:commits_count=>10, :changes_count=>3}}
424 assert_equal expected, repository.stats_by_author
442 assert_equal expected, repository.stats_by_author
425 end
443 end
426
444
427 def test_stats_by_author_doesnt_drop_unmapped_users
445 def test_stats_by_author_doesnt_drop_unmapped_users
428 repository = Repository.find(10)
446 repository = Repository.find(10)
429 Changeset.create!(
447 Changeset.create!(
430 :repository => repository,
448 :repository => repository,
431 :committer => 'unnamed <foo@bar.net>',
449 :committer => 'unnamed <foo@bar.net>',
432 :committed_on => Time.now,
450 :committed_on => Time.now,
433 :revision => 101,
451 :revision => 101,
434 :comments => 'Another commit by foo.'
452 :comments => 'Another commit by foo.'
435 )
453 )
436
454
437 assert repository.stats_by_author.has_key?("unnamed <foo@bar.net>")
455 assert repository.stats_by_author.has_key?("unnamed <foo@bar.net>")
438 end
456 end
439
457
440 def test_stats_by_author_merge_correctly
458 def test_stats_by_author_merge_correctly
441 # as we honnor users->committer map and it's not injective,
459 # as we honnor users->committer map and it's not injective,
442 # we must be sure merges happen correctly and stats are not
460 # we must be sure merges happen correctly and stats are not
443 # wiped out when two source counts map to the same user.
461 # wiped out when two source counts map to the same user.
444 #
462 #
445 # Here we have Changeset's with committer="dlopper" and others
463 # Here we have Changeset's with committer="dlopper" and others
446 # with committer="dlopper <dlopper@somefoo.net>"
464 # with committer="dlopper <dlopper@somefoo.net>"
447 repository = Repository.find(10)
465 repository = Repository.find(10)
448
466
449 expected = {"Dave Lopper"=>{:commits_count=>10, :changes_count=>3}}
467 expected = {"Dave Lopper"=>{:commits_count=>10, :changes_count=>3}}
450 assert_equal expected, repository.stats_by_author
468 assert_equal expected, repository.stats_by_author
451
469
452 set = Changeset.create!(
470 set = Changeset.create!(
453 :repository => repository,
471 :repository => repository,
454 :committer => 'dlopper <dlopper@somefoo.net>',
472 :committer => 'dlopper <dlopper@somefoo.net>',
455 :committed_on => Time.now,
473 :committed_on => Time.now,
456 :revision => 101,
474 :revision => 101,
457 :comments => 'Another commit by foo.'
475 :comments => 'Another commit by foo.'
458 )
476 )
459
477
460 expected = {"Dave Lopper"=>{:commits_count=>11, :changes_count=>3}}
478 expected = {"Dave Lopper"=>{:commits_count=>11, :changes_count=>3}}
461 assert_equal expected, repository.stats_by_author
479 assert_equal expected, repository.stats_by_author
462 end
480 end
463
481
464 def test_fetch_changesets
482 def test_fetch_changesets
465 # 2 repositories in fixtures
483 # 2 repositories in fixtures
466 Repository::Subversion.any_instance.expects(:fetch_changesets).twice.returns(true)
484 Repository::Subversion.any_instance.expects(:fetch_changesets).twice.returns(true)
467 Repository.fetch_changesets
485 Repository.fetch_changesets
468 end
486 end
469 end
487 end
General Comments 0
You need to be logged in to leave comments. Login now