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