##// END OF EJS Templates
Merged r13127 (#16564)....
Jean-Philippe Lang -
r12879:23b322772d42
parent child
Show More
@@ -1,447 +1,447
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2014 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 41 validates_presence_of :identifier, :unless => Proc.new { |r| r.is_default? || r.set_as_default? }
42 42 validates_uniqueness_of :identifier, :scope => :project_id, :allow_blank => true
43 validates_exclusion_of :identifier, :in => %w(show entry raw changes annotate diff show stats graph)
43 validates_exclusion_of :identifier, :in => %w(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
49 49 safe_attributes 'identifier',
50 50 'login',
51 51 'password',
52 52 'path_encoding',
53 53 'log_encoding',
54 54 'is_default'
55 55
56 56 safe_attributes 'url',
57 57 :if => lambda {|repository, user| repository.new_record?}
58 58
59 59 def repo_create_validation
60 60 unless Setting.enabled_scm.include?(self.class.name.demodulize)
61 61 errors.add(:type, :invalid)
62 62 end
63 63 end
64 64
65 65 def self.human_attribute_name(attribute_key_name, *args)
66 66 attr_name = attribute_key_name.to_s
67 67 if attr_name == "log_encoding"
68 68 attr_name = "commit_logs_encoding"
69 69 end
70 70 super(attr_name, *args)
71 71 end
72 72
73 73 # Removes leading and trailing whitespace
74 74 def url=(arg)
75 75 write_attribute(:url, arg ? arg.to_s.strip : nil)
76 76 end
77 77
78 78 # Removes leading and trailing whitespace
79 79 def root_url=(arg)
80 80 write_attribute(:root_url, arg ? arg.to_s.strip : nil)
81 81 end
82 82
83 83 def password
84 84 read_ciphered_attribute(:password)
85 85 end
86 86
87 87 def password=(arg)
88 88 write_ciphered_attribute(:password, arg)
89 89 end
90 90
91 91 def scm_adapter
92 92 self.class.scm_adapter_class
93 93 end
94 94
95 95 def scm
96 96 unless @scm
97 97 @scm = self.scm_adapter.new(url, root_url,
98 98 login, password, path_encoding)
99 99 if root_url.blank? && @scm.root_url.present?
100 100 update_attribute(:root_url, @scm.root_url)
101 101 end
102 102 end
103 103 @scm
104 104 end
105 105
106 106 def scm_name
107 107 self.class.scm_name
108 108 end
109 109
110 110 def name
111 111 if identifier.present?
112 112 identifier
113 113 elsif is_default?
114 114 l(:field_repository_is_default)
115 115 else
116 116 scm_name
117 117 end
118 118 end
119 119
120 120 def identifier=(identifier)
121 121 super unless identifier_frozen?
122 122 end
123 123
124 124 def identifier_frozen?
125 125 errors[:identifier].blank? && !(new_record? || identifier.blank?)
126 126 end
127 127
128 128 def identifier_param
129 129 if is_default?
130 130 nil
131 131 elsif identifier.present?
132 132 identifier
133 133 else
134 134 id.to_s
135 135 end
136 136 end
137 137
138 138 def <=>(repository)
139 139 if is_default?
140 140 -1
141 141 elsif repository.is_default?
142 142 1
143 143 else
144 144 identifier.to_s <=> repository.identifier.to_s
145 145 end
146 146 end
147 147
148 148 def self.find_by_identifier_param(param)
149 149 if param.to_s =~ /^\d+$/
150 150 find_by_id(param)
151 151 else
152 152 find_by_identifier(param)
153 153 end
154 154 end
155 155
156 156 # TODO: should return an empty hash instead of nil to avoid many ||{}
157 157 def extra_info
158 158 h = read_attribute(:extra_info)
159 159 h.is_a?(Hash) ? h : nil
160 160 end
161 161
162 162 def merge_extra_info(arg)
163 163 h = extra_info || {}
164 164 return h if arg.nil?
165 165 h.merge!(arg)
166 166 write_attribute(:extra_info, h)
167 167 end
168 168
169 169 def report_last_commit
170 170 true
171 171 end
172 172
173 173 def supports_cat?
174 174 scm.supports_cat?
175 175 end
176 176
177 177 def supports_annotate?
178 178 scm.supports_annotate?
179 179 end
180 180
181 181 def supports_all_revisions?
182 182 true
183 183 end
184 184
185 185 def supports_directory_revisions?
186 186 false
187 187 end
188 188
189 189 def supports_revision_graph?
190 190 false
191 191 end
192 192
193 193 def entry(path=nil, identifier=nil)
194 194 scm.entry(path, identifier)
195 195 end
196 196
197 197 def scm_entries(path=nil, identifier=nil)
198 198 scm.entries(path, identifier)
199 199 end
200 200 protected :scm_entries
201 201
202 202 def entries(path=nil, identifier=nil)
203 203 entries = scm_entries(path, identifier)
204 204 load_entries_changesets(entries)
205 205 entries
206 206 end
207 207
208 208 def branches
209 209 scm.branches
210 210 end
211 211
212 212 def tags
213 213 scm.tags
214 214 end
215 215
216 216 def default_branch
217 217 nil
218 218 end
219 219
220 220 def properties(path, identifier=nil)
221 221 scm.properties(path, identifier)
222 222 end
223 223
224 224 def cat(path, identifier=nil)
225 225 scm.cat(path, identifier)
226 226 end
227 227
228 228 def diff(path, rev, rev_to)
229 229 scm.diff(path, rev, rev_to)
230 230 end
231 231
232 232 def diff_format_revisions(cs, cs_to, sep=':')
233 233 text = ""
234 234 text << cs_to.format_identifier + sep if cs_to
235 235 text << cs.format_identifier if cs
236 236 text
237 237 end
238 238
239 239 # Returns a path relative to the url of the repository
240 240 def relative_path(path)
241 241 path
242 242 end
243 243
244 244 # Finds and returns a revision with a number or the beginning of a hash
245 245 def find_changeset_by_name(name)
246 246 return nil if name.blank?
247 247 s = name.to_s
248 248 if s.match(/^\d*$/)
249 249 changesets.where("revision = ?", s).first
250 250 else
251 251 changesets.where("revision LIKE ?", s + '%').first
252 252 end
253 253 end
254 254
255 255 def latest_changeset
256 256 @latest_changeset ||= changesets.first
257 257 end
258 258
259 259 # Returns the latest changesets for +path+
260 260 # Default behaviour is to search in cached changesets
261 261 def latest_changesets(path, rev, limit=10)
262 262 if path.blank?
263 263 changesets.
264 264 reorder("#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC").
265 265 limit(limit).
266 266 preload(:user).
267 267 all
268 268 else
269 269 filechanges.
270 270 where("path = ?", path.with_leading_slash).
271 271 reorder("#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC").
272 272 limit(limit).
273 273 preload(:changeset => :user).
274 274 collect(&:changeset)
275 275 end
276 276 end
277 277
278 278 def scan_changesets_for_issue_ids
279 279 self.changesets.each(&:scan_comment_for_issue_ids)
280 280 end
281 281
282 282 # Returns an array of committers usernames and associated user_id
283 283 def committers
284 284 @committers ||= Changeset.connection.select_rows(
285 285 "SELECT DISTINCT committer, user_id FROM #{Changeset.table_name} WHERE repository_id = #{id}")
286 286 end
287 287
288 288 # Maps committers username to a user ids
289 289 def committer_ids=(h)
290 290 if h.is_a?(Hash)
291 291 committers.each do |committer, user_id|
292 292 new_user_id = h[committer]
293 293 if new_user_id && (new_user_id.to_i != user_id.to_i)
294 294 new_user_id = (new_user_id.to_i > 0 ? new_user_id.to_i : nil)
295 295 Changeset.where(["repository_id = ? AND committer = ?", id, committer]).
296 296 update_all("user_id = #{new_user_id.nil? ? 'NULL' : new_user_id}")
297 297 end
298 298 end
299 299 @committers = nil
300 300 @found_committer_users = nil
301 301 true
302 302 else
303 303 false
304 304 end
305 305 end
306 306
307 307 # Returns the Redmine User corresponding to the given +committer+
308 308 # It will return nil if the committer is not yet mapped and if no User
309 309 # with the same username or email was found
310 310 def find_committer_user(committer)
311 311 unless committer.blank?
312 312 @found_committer_users ||= {}
313 313 return @found_committer_users[committer] if @found_committer_users.has_key?(committer)
314 314
315 315 user = nil
316 316 c = changesets.where(:committer => committer).includes(:user).first
317 317 if c && c.user
318 318 user = c.user
319 319 elsif committer.strip =~ /^([^<]+)(<(.*)>)?$/
320 320 username, email = $1.strip, $3
321 321 u = User.find_by_login(username)
322 322 u ||= User.find_by_mail(email) unless email.blank?
323 323 user = u
324 324 end
325 325 @found_committer_users[committer] = user
326 326 user
327 327 end
328 328 end
329 329
330 330 def repo_log_encoding
331 331 encoding = log_encoding.to_s.strip
332 332 encoding.blank? ? 'UTF-8' : encoding
333 333 end
334 334
335 335 # Fetches new changesets for all repositories of active projects
336 336 # Can be called periodically by an external script
337 337 # eg. ruby script/runner "Repository.fetch_changesets"
338 338 def self.fetch_changesets
339 339 Project.active.has_module(:repository).all.each do |project|
340 340 project.repositories.each do |repository|
341 341 begin
342 342 repository.fetch_changesets
343 343 rescue Redmine::Scm::Adapters::CommandFailed => e
344 344 logger.error "scm: error during fetching changesets: #{e.message}"
345 345 end
346 346 end
347 347 end
348 348 end
349 349
350 350 # scan changeset comments to find related and fixed issues for all repositories
351 351 def self.scan_changesets_for_issue_ids
352 352 all.each(&:scan_changesets_for_issue_ids)
353 353 end
354 354
355 355 def self.scm_name
356 356 'Abstract'
357 357 end
358 358
359 359 def self.available_scm
360 360 subclasses.collect {|klass| [klass.scm_name, klass.name]}
361 361 end
362 362
363 363 def self.factory(klass_name, *args)
364 364 klass = "Repository::#{klass_name}".constantize
365 365 klass.new(*args)
366 366 rescue
367 367 nil
368 368 end
369 369
370 370 def self.scm_adapter_class
371 371 nil
372 372 end
373 373
374 374 def self.scm_command
375 375 ret = ""
376 376 begin
377 377 ret = self.scm_adapter_class.client_command if self.scm_adapter_class
378 378 rescue Exception => e
379 379 logger.error "scm: error during get command: #{e.message}"
380 380 end
381 381 ret
382 382 end
383 383
384 384 def self.scm_version_string
385 385 ret = ""
386 386 begin
387 387 ret = self.scm_adapter_class.client_version_string if self.scm_adapter_class
388 388 rescue Exception => e
389 389 logger.error "scm: error during get version string: #{e.message}"
390 390 end
391 391 ret
392 392 end
393 393
394 394 def self.scm_available
395 395 ret = false
396 396 begin
397 397 ret = self.scm_adapter_class.client_available if self.scm_adapter_class
398 398 rescue Exception => e
399 399 logger.error "scm: error during get scm available: #{e.message}"
400 400 end
401 401 ret
402 402 end
403 403
404 404 def set_as_default?
405 405 new_record? && project && Repository.where(:project_id => project.id).empty?
406 406 end
407 407
408 408 protected
409 409
410 410 def check_default
411 411 if !is_default? && set_as_default?
412 412 self.is_default = true
413 413 end
414 414 if is_default? && is_default_changed?
415 415 Repository.where(["project_id = ?", project_id]).update_all(["is_default = ?", false])
416 416 end
417 417 end
418 418
419 419 def load_entries_changesets(entries)
420 420 if entries
421 421 entries.each do |entry|
422 422 if entry.lastrev && entry.lastrev.identifier
423 423 entry.changeset = find_changeset_by_name(entry.lastrev.identifier)
424 424 end
425 425 end
426 426 end
427 427 end
428 428
429 429 private
430 430
431 431 # Deletes repository data
432 432 def clear_changesets
433 433 cs = Changeset.table_name
434 434 ch = Change.table_name
435 435 ci = "#{table_name_prefix}changesets_issues#{table_name_suffix}"
436 436 cp = "#{table_name_prefix}changeset_parents#{table_name_suffix}"
437 437
438 438 connection.delete("DELETE FROM #{ch} WHERE #{ch}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
439 439 connection.delete("DELETE FROM #{ci} WHERE #{ci}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
440 440 connection.delete("DELETE FROM #{cp} WHERE #{cp}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
441 441 connection.delete("DELETE FROM #{cs} WHERE #{cs}.repository_id = #{id}")
442 442 clear_extra_info_of_changesets
443 443 end
444 444
445 445 def clear_extra_info_of_changesets
446 446 end
447 447 end
General Comments 0
You need to be logged in to leave comments. Login now