##// END OF EJS Templates
Merged r15820 (#23758)....
Jean-Philippe Lang -
r15440:8b60a4ba0607
parent child
Show More
@@ -1,514 +1,514
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2016 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class ScmFetchError < Exception; end
19 19
20 20 class Repository < ActiveRecord::Base
21 21 include Redmine::Ciphering
22 22 include Redmine::SafeAttributes
23 23
24 24 # Maximum length for repository identifiers
25 25 IDENTIFIER_MAX_LENGTH = 255
26 26
27 27 belongs_to :project
28 28 has_many :changesets, lambda{order("#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC")}
29 29 has_many :filechanges, :class_name => 'Change', :through => :changesets
30 30
31 31 serialize :extra_info
32 32
33 33 before_validation :normalize_identifier
34 34 before_save :check_default
35 35
36 36 # Raw SQL to delete changesets and changes in the database
37 37 # has_many :changesets, :dependent => :destroy is too slow for big repositories
38 38 before_destroy :clear_changesets
39 39
40 40 validates_length_of :password, :maximum => 255, :allow_nil => true
41 41 validates_length_of :identifier, :maximum => IDENTIFIER_MAX_LENGTH, :allow_blank => true
42 42 validates_uniqueness_of :identifier, :scope => :project_id
43 43 validates_exclusion_of :identifier, :in => %w(browse show entry raw changes annotate diff statistics graph revisions revision)
44 44 # donwcase letters, digits, dashes, underscores but not digits only
45 45 validates_format_of :identifier, :with => /\A(?!\d+$)[a-z0-9\-_]*\z/, :allow_blank => true
46 46 # Checks if the SCM is enabled when creating a repository
47 47 validate :repo_create_validation, :on => :create
48 48 validate :validate_repository_path
49 49 attr_protected :id
50 50
51 51 safe_attributes 'identifier',
52 52 'login',
53 53 'password',
54 54 'path_encoding',
55 55 'log_encoding',
56 56 'is_default'
57 57
58 58 safe_attributes 'url',
59 59 :if => lambda {|repository, user| repository.new_record?}
60 60
61 61 def repo_create_validation
62 62 unless Setting.enabled_scm.include?(self.class.name.demodulize)
63 63 errors.add(:type, :invalid)
64 64 end
65 65 end
66 66
67 67 def self.human_attribute_name(attribute_key_name, *args)
68 68 attr_name = attribute_key_name.to_s
69 69 if attr_name == "log_encoding"
70 70 attr_name = "commit_logs_encoding"
71 71 end
72 72 super(attr_name, *args)
73 73 end
74 74
75 75 # Removes leading and trailing whitespace
76 76 def url=(arg)
77 77 write_attribute(:url, arg ? arg.to_s.strip : nil)
78 78 end
79 79
80 80 # Removes leading and trailing whitespace
81 81 def root_url=(arg)
82 82 write_attribute(:root_url, arg ? arg.to_s.strip : nil)
83 83 end
84 84
85 85 def password
86 86 read_ciphered_attribute(:password)
87 87 end
88 88
89 89 def password=(arg)
90 90 write_ciphered_attribute(:password, arg)
91 91 end
92 92
93 93 def scm_adapter
94 94 self.class.scm_adapter_class
95 95 end
96 96
97 97 def scm
98 98 unless @scm
99 99 @scm = self.scm_adapter.new(url, root_url,
100 100 login, password, path_encoding)
101 101 if root_url.blank? && @scm.root_url.present?
102 102 update_attribute(:root_url, @scm.root_url)
103 103 end
104 104 end
105 105 @scm
106 106 end
107 107
108 108 def scm_name
109 109 self.class.scm_name
110 110 end
111 111
112 112 def name
113 113 if identifier.present?
114 114 identifier
115 115 elsif is_default?
116 116 l(:field_repository_is_default)
117 117 else
118 118 scm_name
119 119 end
120 120 end
121 121
122 122 def identifier=(identifier)
123 123 super unless identifier_frozen?
124 124 end
125 125
126 126 def identifier_frozen?
127 127 errors[:identifier].blank? && !(new_record? || identifier.blank?)
128 128 end
129 129
130 130 def identifier_param
131 131 if is_default?
132 132 nil
133 133 elsif identifier.present?
134 134 identifier
135 135 else
136 136 id.to_s
137 137 end
138 138 end
139 139
140 140 def <=>(repository)
141 141 if is_default?
142 142 -1
143 143 elsif repository.is_default?
144 144 1
145 145 else
146 146 identifier.to_s <=> repository.identifier.to_s
147 147 end
148 148 end
149 149
150 150 def self.find_by_identifier_param(param)
151 151 if param.to_s =~ /^\d+$/
152 152 find_by_id(param)
153 153 else
154 154 find_by_identifier(param)
155 155 end
156 156 end
157 157
158 158 # TODO: should return an empty hash instead of nil to avoid many ||{}
159 159 def extra_info
160 160 h = read_attribute(:extra_info)
161 161 h.is_a?(Hash) ? h : nil
162 162 end
163 163
164 164 def merge_extra_info(arg)
165 165 h = extra_info || {}
166 166 return h if arg.nil?
167 167 h.merge!(arg)
168 168 write_attribute(:extra_info, h)
169 169 end
170 170
171 171 def report_last_commit
172 172 true
173 173 end
174 174
175 175 def supports_cat?
176 176 scm.supports_cat?
177 177 end
178 178
179 179 def supports_annotate?
180 180 scm.supports_annotate?
181 181 end
182 182
183 183 def supports_all_revisions?
184 184 true
185 185 end
186 186
187 187 def supports_directory_revisions?
188 188 false
189 189 end
190 190
191 191 def supports_revision_graph?
192 192 false
193 193 end
194 194
195 195 def entry(path=nil, identifier=nil)
196 196 scm.entry(path, identifier)
197 197 end
198 198
199 199 def scm_entries(path=nil, identifier=nil)
200 200 scm.entries(path, identifier)
201 201 end
202 202 protected :scm_entries
203 203
204 204 def entries(path=nil, identifier=nil)
205 205 entries = scm_entries(path, identifier)
206 206 load_entries_changesets(entries)
207 207 entries
208 208 end
209 209
210 210 def branches
211 211 scm.branches
212 212 end
213 213
214 214 def tags
215 215 scm.tags
216 216 end
217 217
218 218 def default_branch
219 219 nil
220 220 end
221 221
222 222 def properties(path, identifier=nil)
223 223 scm.properties(path, identifier)
224 224 end
225 225
226 226 def cat(path, identifier=nil)
227 227 scm.cat(path, identifier)
228 228 end
229 229
230 230 def diff(path, rev, rev_to)
231 231 scm.diff(path, rev, rev_to)
232 232 end
233 233
234 234 def diff_format_revisions(cs, cs_to, sep=':')
235 235 text = ""
236 236 text << cs_to.format_identifier + sep if cs_to
237 237 text << cs.format_identifier if cs
238 238 text
239 239 end
240 240
241 241 # Returns a path relative to the url of the repository
242 242 def relative_path(path)
243 243 path
244 244 end
245 245
246 246 # Finds and returns a revision with a number or the beginning of a hash
247 247 def find_changeset_by_name(name)
248 248 return nil if name.blank?
249 249 s = name.to_s
250 250 if s.match(/^\d*$/)
251 251 changesets.where("revision = ?", s).first
252 252 else
253 253 changesets.where("revision LIKE ?", s + '%').first
254 254 end
255 255 end
256 256
257 257 def latest_changeset
258 258 @latest_changeset ||= changesets.first
259 259 end
260 260
261 261 # Returns the latest changesets for +path+
262 262 # Default behaviour is to search in cached changesets
263 263 def latest_changesets(path, rev, limit=10)
264 264 if path.blank?
265 265 changesets.
266 266 reorder("#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC").
267 267 limit(limit).
268 268 preload(:user).
269 269 to_a
270 270 else
271 271 filechanges.
272 272 where("path = ?", path.with_leading_slash).
273 273 reorder("#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC").
274 274 limit(limit).
275 275 preload(:changeset => :user).
276 276 collect(&:changeset)
277 277 end
278 278 end
279 279
280 280 def scan_changesets_for_issue_ids
281 281 self.changesets.each(&:scan_comment_for_issue_ids)
282 282 end
283 283
284 284 # Returns an array of committers usernames and associated user_id
285 285 def committers
286 286 @committers ||= Changeset.where(:repository_id => id).uniq.pluck(:committer, :user_id)
287 287 end
288 288
289 289 # Maps committers username to a user ids
290 290 def committer_ids=(h)
291 291 if h.is_a?(Hash)
292 292 committers.each do |committer, user_id|
293 293 new_user_id = h[committer]
294 294 if new_user_id && (new_user_id.to_i != user_id.to_i)
295 295 new_user_id = (new_user_id.to_i > 0 ? new_user_id.to_i : nil)
296 296 Changeset.where(["repository_id = ? AND committer = ?", id, committer]).
297 297 update_all("user_id = #{new_user_id.nil? ? 'NULL' : new_user_id}")
298 298 end
299 299 end
300 300 @committers = nil
301 301 @found_committer_users = nil
302 302 true
303 303 else
304 304 false
305 305 end
306 306 end
307 307
308 308 # Returns the Redmine User corresponding to the given +committer+
309 309 # It will return nil if the committer is not yet mapped and if no User
310 310 # with the same username or email was found
311 311 def find_committer_user(committer)
312 312 unless committer.blank?
313 313 @found_committer_users ||= {}
314 314 return @found_committer_users[committer] if @found_committer_users.has_key?(committer)
315 315
316 316 user = nil
317 317 c = changesets.where(:committer => committer).
318 318 includes(:user).references(:user).first
319 319 if c && c.user
320 320 user = c.user
321 321 elsif committer.strip =~ /^([^<]+)(<(.*)>)?$/
322 322 username, email = $1.strip, $3
323 323 u = User.find_by_login(username)
324 324 u ||= User.find_by_mail(email) unless email.blank?
325 325 user = u
326 326 end
327 327 @found_committer_users[committer] = user
328 328 user
329 329 end
330 330 end
331 331
332 332 def repo_log_encoding
333 333 encoding = log_encoding.to_s.strip
334 334 encoding.blank? ? 'UTF-8' : encoding
335 335 end
336 336
337 337 # Fetches new changesets for all repositories of active projects
338 338 # Can be called periodically by an external script
339 339 # eg. ruby script/runner "Repository.fetch_changesets"
340 340 def self.fetch_changesets
341 341 Project.active.has_module(:repository).all.each do |project|
342 342 project.repositories.each do |repository|
343 343 begin
344 344 repository.fetch_changesets
345 345 rescue Redmine::Scm::Adapters::CommandFailed => e
346 346 logger.error "scm: error during fetching changesets: #{e.message}"
347 347 end
348 348 end
349 349 end
350 350 end
351 351
352 352 # scan changeset comments to find related and fixed issues for all repositories
353 353 def self.scan_changesets_for_issue_ids
354 354 all.each(&:scan_changesets_for_issue_ids)
355 355 end
356 356
357 357 def self.scm_name
358 358 'Abstract'
359 359 end
360 360
361 361 def self.available_scm
362 362 subclasses.collect {|klass| [klass.scm_name, klass.name]}
363 363 end
364 364
365 365 def self.factory(klass_name, *args)
366 366 repository_class(klass_name).new(*args) rescue nil
367 367 end
368 368
369 369 def self.repository_class(class_name)
370 class_name = class_name.to_s.classify
370 class_name = class_name.to_s.camelize
371 371 if Redmine::Scm::Base.all.include?(class_name)
372 372 "Repository::#{class_name}".constantize
373 373 end
374 374 end
375 375
376 376 def self.scm_adapter_class
377 377 nil
378 378 end
379 379
380 380 def self.scm_command
381 381 ret = ""
382 382 begin
383 383 ret = self.scm_adapter_class.client_command if self.scm_adapter_class
384 384 rescue Exception => e
385 385 logger.error "scm: error during get command: #{e.message}"
386 386 end
387 387 ret
388 388 end
389 389
390 390 def self.scm_version_string
391 391 ret = ""
392 392 begin
393 393 ret = self.scm_adapter_class.client_version_string if self.scm_adapter_class
394 394 rescue Exception => e
395 395 logger.error "scm: error during get version string: #{e.message}"
396 396 end
397 397 ret
398 398 end
399 399
400 400 def self.scm_available
401 401 ret = false
402 402 begin
403 403 ret = self.scm_adapter_class.client_available if self.scm_adapter_class
404 404 rescue Exception => e
405 405 logger.error "scm: error during get scm available: #{e.message}"
406 406 end
407 407 ret
408 408 end
409 409
410 410 def set_as_default?
411 411 new_record? && project && Repository.where(:project_id => project.id).empty?
412 412 end
413 413
414 414 # Returns a hash with statistics by author in the following form:
415 415 # {
416 416 # "John Smith" => { :commits => 45, :changes => 324 },
417 417 # "Bob" => { ... }
418 418 # }
419 419 #
420 420 # Notes:
421 421 # - this hash honnors the users mapping defined for the repository
422 422 def stats_by_author
423 423 commits = Changeset.where("repository_id = ?", id).select("committer, user_id, count(*) as count").group("committer, user_id")
424 424
425 425 #TODO: restore ordering ; this line probably never worked
426 426 #commits.to_a.sort! {|x, y| x.last <=> y.last}
427 427
428 428 changes = Change.joins(:changeset).where("#{Changeset.table_name}.repository_id = ?", id).select("committer, user_id, count(*) as count").group("committer, user_id")
429 429
430 430 user_ids = changesets.map(&:user_id).compact.uniq
431 431 authors_names = User.where(:id => user_ids).inject({}) do |memo, user|
432 432 memo[user.id] = user.to_s
433 433 memo
434 434 end
435 435
436 436 (commits + changes).inject({}) do |hash, element|
437 437 mapped_name = element.committer
438 438 if username = authors_names[element.user_id.to_i]
439 439 mapped_name = username
440 440 end
441 441 hash[mapped_name] ||= { :commits_count => 0, :changes_count => 0 }
442 442 if element.is_a?(Changeset)
443 443 hash[mapped_name][:commits_count] += element.count.to_i
444 444 else
445 445 hash[mapped_name][:changes_count] += element.count.to_i
446 446 end
447 447 hash
448 448 end
449 449 end
450 450
451 451 # Returns a scope of changesets that come from the same commit as the given changeset
452 452 # in different repositories that point to the same backend
453 453 def same_commits_in_scope(scope, changeset)
454 454 scope = scope.joins(:repository).where(:repositories => {:url => url, :root_url => root_url, :type => type})
455 455 if changeset.scmid.present?
456 456 scope = scope.where(:scmid => changeset.scmid)
457 457 else
458 458 scope = scope.where(:revision => changeset.revision)
459 459 end
460 460 scope
461 461 end
462 462
463 463 protected
464 464
465 465 # Validates repository url based against an optional regular expression
466 466 # that can be set in the Redmine configuration file.
467 467 def validate_repository_path(attribute=:url)
468 468 regexp = Redmine::Configuration["scm_#{scm_name.to_s.downcase}_path_regexp"]
469 469 if changes[attribute] && regexp.present?
470 470 regexp = regexp.to_s.strip.gsub('%project%') {Regexp.escape(project.try(:identifier).to_s)}
471 471 unless send(attribute).to_s.match(Regexp.new("\\A#{regexp}\\z"))
472 472 errors.add(attribute, :invalid)
473 473 end
474 474 end
475 475 end
476 476
477 477 def normalize_identifier
478 478 self.identifier = identifier.to_s.strip
479 479 end
480 480
481 481 def check_default
482 482 if !is_default? && set_as_default?
483 483 self.is_default = true
484 484 end
485 485 if is_default? && is_default_changed?
486 486 Repository.where(["project_id = ?", project_id]).update_all(["is_default = ?", false])
487 487 end
488 488 end
489 489
490 490 def load_entries_changesets(entries)
491 491 if entries
492 492 entries.each do |entry|
493 493 if entry.lastrev && entry.lastrev.identifier
494 494 entry.changeset = find_changeset_by_name(entry.lastrev.identifier)
495 495 end
496 496 end
497 497 end
498 498 end
499 499
500 500 private
501 501
502 502 # Deletes repository data
503 503 def clear_changesets
504 504 cs = Changeset.table_name
505 505 ch = Change.table_name
506 506 ci = "#{table_name_prefix}changesets_issues#{table_name_suffix}"
507 507 cp = "#{table_name_prefix}changeset_parents#{table_name_suffix}"
508 508
509 509 self.class.connection.delete("DELETE FROM #{ch} WHERE #{ch}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
510 510 self.class.connection.delete("DELETE FROM #{ci} WHERE #{ci}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
511 511 self.class.connection.delete("DELETE FROM #{cp} WHERE #{cp}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
512 512 self.class.connection.delete("DELETE FROM #{cs} WHERE #{cs}.repository_id = #{id}")
513 513 end
514 514 end
General Comments 0
You need to be logged in to leave comments. Login now