##// END OF EJS Templates
scm: split Repository#entries (#14361)...
Toshi MARUYAMA -
r12479:d45bf0a83eb7
parent child
Show More
@@ -1,436 +1,441
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 43 validates_exclusion_of :identifier, :in => %w(show entry raw changes annotate diff show stats graph)
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 def merge_extra_info(arg)
157 157 h = extra_info || {}
158 158 return h if arg.nil?
159 159 h.merge!(arg)
160 160 write_attribute(:extra_info, h)
161 161 end
162 162
163 163 def report_last_commit
164 164 true
165 165 end
166 166
167 167 def supports_cat?
168 168 scm.supports_cat?
169 169 end
170 170
171 171 def supports_annotate?
172 172 scm.supports_annotate?
173 173 end
174 174
175 175 def supports_all_revisions?
176 176 true
177 177 end
178 178
179 179 def supports_directory_revisions?
180 180 false
181 181 end
182 182
183 183 def supports_revision_graph?
184 184 false
185 185 end
186 186
187 187 def entry(path=nil, identifier=nil)
188 188 scm.entry(path, identifier)
189 189 end
190 190
191 def scm_entries(path=nil, identifier=nil)
192 scm.entries(path, identifier)
193 end
194 protected :scm_entries
195
191 196 def entries(path=nil, identifier=nil)
192 entries = scm.entries(path, identifier)
197 entries = scm_entries(path, identifier)
193 198 load_entries_changesets(entries)
194 199 entries
195 200 end
196 201
197 202 def branches
198 203 scm.branches
199 204 end
200 205
201 206 def tags
202 207 scm.tags
203 208 end
204 209
205 210 def default_branch
206 211 nil
207 212 end
208 213
209 214 def properties(path, identifier=nil)
210 215 scm.properties(path, identifier)
211 216 end
212 217
213 218 def cat(path, identifier=nil)
214 219 scm.cat(path, identifier)
215 220 end
216 221
217 222 def diff(path, rev, rev_to)
218 223 scm.diff(path, rev, rev_to)
219 224 end
220 225
221 226 def diff_format_revisions(cs, cs_to, sep=':')
222 227 text = ""
223 228 text << cs_to.format_identifier + sep if cs_to
224 229 text << cs.format_identifier if cs
225 230 text
226 231 end
227 232
228 233 # Returns a path relative to the url of the repository
229 234 def relative_path(path)
230 235 path
231 236 end
232 237
233 238 # Finds and returns a revision with a number or the beginning of a hash
234 239 def find_changeset_by_name(name)
235 240 return nil if name.blank?
236 241 s = name.to_s
237 242 if s.match(/^\d*$/)
238 243 changesets.where("revision = ?", s).first
239 244 else
240 245 changesets.where("revision LIKE ?", s + '%').first
241 246 end
242 247 end
243 248
244 249 def latest_changeset
245 250 @latest_changeset ||= changesets.first
246 251 end
247 252
248 253 # Returns the latest changesets for +path+
249 254 # Default behaviour is to search in cached changesets
250 255 def latest_changesets(path, rev, limit=10)
251 256 if path.blank?
252 257 changesets.
253 258 reorder("#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC").
254 259 limit(limit).
255 260 preload(:user).
256 261 all
257 262 else
258 263 filechanges.
259 264 where("path = ?", path.with_leading_slash).
260 265 reorder("#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC").
261 266 limit(limit).
262 267 preload(:changeset => :user).
263 268 collect(&:changeset)
264 269 end
265 270 end
266 271
267 272 def scan_changesets_for_issue_ids
268 273 self.changesets.each(&:scan_comment_for_issue_ids)
269 274 end
270 275
271 276 # Returns an array of committers usernames and associated user_id
272 277 def committers
273 278 @committers ||= Changeset.connection.select_rows(
274 279 "SELECT DISTINCT committer, user_id FROM #{Changeset.table_name} WHERE repository_id = #{id}")
275 280 end
276 281
277 282 # Maps committers username to a user ids
278 283 def committer_ids=(h)
279 284 if h.is_a?(Hash)
280 285 committers.each do |committer, user_id|
281 286 new_user_id = h[committer]
282 287 if new_user_id && (new_user_id.to_i != user_id.to_i)
283 288 new_user_id = (new_user_id.to_i > 0 ? new_user_id.to_i : nil)
284 289 Changeset.where(["repository_id = ? AND committer = ?", id, committer]).
285 290 update_all("user_id = #{new_user_id.nil? ? 'NULL' : new_user_id}")
286 291 end
287 292 end
288 293 @committers = nil
289 294 @found_committer_users = nil
290 295 true
291 296 else
292 297 false
293 298 end
294 299 end
295 300
296 301 # Returns the Redmine User corresponding to the given +committer+
297 302 # It will return nil if the committer is not yet mapped and if no User
298 303 # with the same username or email was found
299 304 def find_committer_user(committer)
300 305 unless committer.blank?
301 306 @found_committer_users ||= {}
302 307 return @found_committer_users[committer] if @found_committer_users.has_key?(committer)
303 308
304 309 user = nil
305 310 c = changesets.where(:committer => committer).includes(:user).first
306 311 if c && c.user
307 312 user = c.user
308 313 elsif committer.strip =~ /^([^<]+)(<(.*)>)?$/
309 314 username, email = $1.strip, $3
310 315 u = User.find_by_login(username)
311 316 u ||= User.find_by_mail(email) unless email.blank?
312 317 user = u
313 318 end
314 319 @found_committer_users[committer] = user
315 320 user
316 321 end
317 322 end
318 323
319 324 def repo_log_encoding
320 325 encoding = log_encoding.to_s.strip
321 326 encoding.blank? ? 'UTF-8' : encoding
322 327 end
323 328
324 329 # Fetches new changesets for all repositories of active projects
325 330 # Can be called periodically by an external script
326 331 # eg. ruby script/runner "Repository.fetch_changesets"
327 332 def self.fetch_changesets
328 333 Project.active.has_module(:repository).all.each do |project|
329 334 project.repositories.each do |repository|
330 335 begin
331 336 repository.fetch_changesets
332 337 rescue Redmine::Scm::Adapters::CommandFailed => e
333 338 logger.error "scm: error during fetching changesets: #{e.message}"
334 339 end
335 340 end
336 341 end
337 342 end
338 343
339 344 # scan changeset comments to find related and fixed issues for all repositories
340 345 def self.scan_changesets_for_issue_ids
341 346 all.each(&:scan_changesets_for_issue_ids)
342 347 end
343 348
344 349 def self.scm_name
345 350 'Abstract'
346 351 end
347 352
348 353 def self.available_scm
349 354 subclasses.collect {|klass| [klass.scm_name, klass.name]}
350 355 end
351 356
352 357 def self.factory(klass_name, *args)
353 358 klass = "Repository::#{klass_name}".constantize
354 359 klass.new(*args)
355 360 rescue
356 361 nil
357 362 end
358 363
359 364 def self.scm_adapter_class
360 365 nil
361 366 end
362 367
363 368 def self.scm_command
364 369 ret = ""
365 370 begin
366 371 ret = self.scm_adapter_class.client_command if self.scm_adapter_class
367 372 rescue Exception => e
368 373 logger.error "scm: error during get command: #{e.message}"
369 374 end
370 375 ret
371 376 end
372 377
373 378 def self.scm_version_string
374 379 ret = ""
375 380 begin
376 381 ret = self.scm_adapter_class.client_version_string if self.scm_adapter_class
377 382 rescue Exception => e
378 383 logger.error "scm: error during get version string: #{e.message}"
379 384 end
380 385 ret
381 386 end
382 387
383 388 def self.scm_available
384 389 ret = false
385 390 begin
386 391 ret = self.scm_adapter_class.client_available if self.scm_adapter_class
387 392 rescue Exception => e
388 393 logger.error "scm: error during get scm available: #{e.message}"
389 394 end
390 395 ret
391 396 end
392 397
393 398 def set_as_default?
394 399 new_record? && project && Repository.where(:project_id => project.id).empty?
395 400 end
396 401
397 402 protected
398 403
399 404 def check_default
400 405 if !is_default? && set_as_default?
401 406 self.is_default = true
402 407 end
403 408 if is_default? && is_default_changed?
404 409 Repository.where(["project_id = ?", project_id]).update_all(["is_default = ?", false])
405 410 end
406 411 end
407 412
408 413 def load_entries_changesets(entries)
409 414 if entries
410 415 entries.each do |entry|
411 416 if entry.lastrev && entry.lastrev.identifier
412 417 entry.changeset = find_changeset_by_name(entry.lastrev.identifier)
413 418 end
414 419 end
415 420 end
416 421 end
417 422
418 423 private
419 424
420 425 # Deletes repository data
421 426 def clear_changesets
422 427 cs = Changeset.table_name
423 428 ch = Change.table_name
424 429 ci = "#{table_name_prefix}changesets_issues#{table_name_suffix}"
425 430 cp = "#{table_name_prefix}changeset_parents#{table_name_suffix}"
426 431
427 432 connection.delete("DELETE FROM #{ch} WHERE #{ch}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
428 433 connection.delete("DELETE FROM #{ci} WHERE #{ci}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
429 434 connection.delete("DELETE FROM #{cp} WHERE #{cp}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
430 435 connection.delete("DELETE FROM #{cs} WHERE #{cs}.repository_id = #{id}")
431 436 clear_extra_info_of_changesets
432 437 end
433 438
434 439 def clear_extra_info_of_changesets
435 440 end
436 441 end
@@ -1,124 +1,124
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 require 'redmine/scm/adapters/bazaar_adapter'
19 19
20 20 class Repository::Bazaar < Repository
21 21 attr_protected :root_url
22 22 validates_presence_of :url, :log_encoding
23 23
24 24 def self.human_attribute_name(attribute_key_name, *args)
25 25 attr_name = attribute_key_name.to_s
26 26 if attr_name == "url"
27 27 attr_name = "path_to_repository"
28 28 end
29 29 super(attr_name, *args)
30 30 end
31 31
32 32 def self.scm_adapter_class
33 33 Redmine::Scm::Adapters::BazaarAdapter
34 34 end
35 35
36 36 def self.scm_name
37 37 'Bazaar'
38 38 end
39 39
40 40 def entry(path=nil, identifier=nil)
41 41 scm.bzr_path_encodig = log_encoding
42 42 scm.entry(path, identifier)
43 43 end
44 44
45 45 def cat(path, identifier=nil)
46 46 scm.bzr_path_encodig = log_encoding
47 47 scm.cat(path, identifier)
48 48 end
49 49
50 50 def annotate(path, identifier=nil)
51 51 scm.bzr_path_encodig = log_encoding
52 52 scm.annotate(path, identifier)
53 53 end
54 54
55 55 def diff(path, rev, rev_to)
56 56 scm.bzr_path_encodig = log_encoding
57 57 scm.diff(path, rev, rev_to)
58 58 end
59 59
60 def entries(path=nil, identifier=nil)
60 def scm_entries(path=nil, identifier=nil)
61 61 scm.bzr_path_encodig = log_encoding
62 62 entries = scm.entries(path, identifier)
63 63 if entries
64 64 entries.each do |e|
65 65 next if e.lastrev.revision.blank?
66 66 # Set the filesize unless browsing a specific revision
67 67 if identifier.nil? && e.is_file?
68 68 full_path = File.join(root_url, e.path)
69 69 e.size = File.stat(full_path).size if File.file?(full_path)
70 70 end
71 71 c = Change.
72 72 includes(:changeset).
73 73 where("#{Change.table_name}.revision = ? and #{Changeset.table_name}.repository_id = ?", e.lastrev.revision, id).
74 74 order("#{Changeset.table_name}.revision DESC").
75 75 first
76 76 if c
77 77 e.lastrev.identifier = c.changeset.revision
78 78 e.lastrev.name = c.changeset.revision
79 79 e.lastrev.author = c.changeset.committer
80 80 end
81 81 end
82 82 end
83 load_entries_changesets(entries)
84 83 entries
85 84 end
85 protected :scm_entries
86 86
87 87 def fetch_changesets
88 88 scm.bzr_path_encodig = log_encoding
89 89 scm_info = scm.info
90 90 if scm_info
91 91 # latest revision found in database
92 92 db_revision = latest_changeset ? latest_changeset.revision.to_i : 0
93 93 # latest revision in the repository
94 94 scm_revision = scm_info.lastrev.identifier.to_i
95 95 if db_revision < scm_revision
96 96 logger.debug "Fetching changesets for repository #{url}" if logger && logger.debug?
97 97 identifier_from = db_revision + 1
98 98 while (identifier_from <= scm_revision)
99 99 # loads changesets by batches of 200
100 100 identifier_to = [identifier_from + 199, scm_revision].min
101 101 revisions = scm.revisions('', identifier_to, identifier_from)
102 102 transaction do
103 103 revisions.reverse_each do |revision|
104 104 changeset = Changeset.create(:repository => self,
105 105 :revision => revision.identifier,
106 106 :committer => revision.author,
107 107 :committed_on => revision.time,
108 108 :scmid => revision.scmid,
109 109 :comments => revision.message)
110 110
111 111 revision.paths.each do |change|
112 112 Change.create(:changeset => changeset,
113 113 :action => change[:action],
114 114 :path => change[:path],
115 115 :revision => change[:revision])
116 116 end
117 117 end
118 118 end unless revisions.nil?
119 119 identifier_from = identifier_to + 1
120 120 end
121 121 end
122 122 end
123 123 end
124 124 end
@@ -1,205 +1,205
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 require 'redmine/scm/adapters/cvs_adapter'
19 19 require 'digest/sha1'
20 20
21 21 class Repository::Cvs < Repository
22 22 validates_presence_of :url, :root_url, :log_encoding
23 23
24 24 safe_attributes 'root_url',
25 25 :if => lambda {|repository, user| repository.new_record?}
26 26
27 27 def self.human_attribute_name(attribute_key_name, *args)
28 28 attr_name = attribute_key_name.to_s
29 29 if attr_name == "root_url"
30 30 attr_name = "cvsroot"
31 31 elsif attr_name == "url"
32 32 attr_name = "cvs_module"
33 33 end
34 34 super(attr_name, *args)
35 35 end
36 36
37 37 def self.scm_adapter_class
38 38 Redmine::Scm::Adapters::CvsAdapter
39 39 end
40 40
41 41 def self.scm_name
42 42 'CVS'
43 43 end
44 44
45 45 def entry(path=nil, identifier=nil)
46 46 rev = identifier.nil? ? nil : changesets.find_by_revision(identifier)
47 47 scm.entry(path, rev.nil? ? nil : rev.committed_on)
48 48 end
49 49
50 def entries(path=nil, identifier=nil)
50 def scm_entries(path=nil, identifier=nil)
51 51 rev = nil
52 52 if ! identifier.nil?
53 53 rev = changesets.find_by_revision(identifier)
54 54 return nil if rev.nil?
55 55 end
56 56 entries = scm.entries(path, rev.nil? ? nil : rev.committed_on)
57 57 if entries
58 58 entries.each() do |entry|
59 59 if ( ! entry.lastrev.nil? ) && ( ! entry.lastrev.revision.nil? )
60 60 change = filechanges.find_by_revision_and_path(
61 61 entry.lastrev.revision,
62 62 scm.with_leading_slash(entry.path) )
63 63 if change
64 64 entry.lastrev.identifier = change.changeset.revision
65 65 entry.lastrev.revision = change.changeset.revision
66 66 entry.lastrev.author = change.changeset.committer
67 67 # entry.lastrev.branch = change.branch
68 68 end
69 69 end
70 70 end
71 71 end
72 load_entries_changesets(entries)
73 72 entries
74 73 end
74 protected :scm_entries
75 75
76 76 def cat(path, identifier=nil)
77 77 rev = nil
78 78 if ! identifier.nil?
79 79 rev = changesets.find_by_revision(identifier)
80 80 return nil if rev.nil?
81 81 end
82 82 scm.cat(path, rev.nil? ? nil : rev.committed_on)
83 83 end
84 84
85 85 def annotate(path, identifier=nil)
86 86 rev = nil
87 87 if ! identifier.nil?
88 88 rev = changesets.find_by_revision(identifier)
89 89 return nil if rev.nil?
90 90 end
91 91 scm.annotate(path, rev.nil? ? nil : rev.committed_on)
92 92 end
93 93
94 94 def diff(path, rev, rev_to)
95 95 # convert rev to revision. CVS can't handle changesets here
96 96 diff=[]
97 97 changeset_from = changesets.find_by_revision(rev)
98 98 if rev_to.to_i > 0
99 99 changeset_to = changesets.find_by_revision(rev_to)
100 100 end
101 101 changeset_from.filechanges.each() do |change_from|
102 102 revision_from = nil
103 103 revision_to = nil
104 104 if path.nil? || (change_from.path.starts_with? scm.with_leading_slash(path))
105 105 revision_from = change_from.revision
106 106 end
107 107 if revision_from
108 108 if changeset_to
109 109 changeset_to.filechanges.each() do |change_to|
110 110 revision_to = change_to.revision if change_to.path == change_from.path
111 111 end
112 112 end
113 113 unless revision_to
114 114 revision_to = scm.get_previous_revision(revision_from)
115 115 end
116 116 file_diff = scm.diff(change_from.path, revision_from, revision_to)
117 117 diff = diff + file_diff unless file_diff.nil?
118 118 end
119 119 end
120 120 return diff
121 121 end
122 122
123 123 def fetch_changesets
124 124 # some nifty bits to introduce a commit-id with cvs
125 125 # natively cvs doesn't provide any kind of changesets,
126 126 # there is only a revision per file.
127 127 # we now take a guess using the author, the commitlog and the commit-date.
128 128
129 129 # last one is the next step to take. the commit-date is not equal for all
130 130 # commits in one changeset. cvs update the commit-date when the *,v file was touched. so
131 131 # we use a small delta here, to merge all changes belonging to _one_ changeset
132 132 time_delta = 10.seconds
133 133 fetch_since = latest_changeset ? latest_changeset.committed_on : nil
134 134 transaction do
135 135 tmp_rev_num = 1
136 136 scm.revisions('', fetch_since, nil, :log_encoding => repo_log_encoding) do |revision|
137 137 # only add the change to the database, if it doen't exists. the cvs log
138 138 # is not exclusive at all.
139 139 tmp_time = revision.time.clone
140 140 unless filechanges.find_by_path_and_revision(
141 141 scm.with_leading_slash(revision.paths[0][:path]),
142 142 revision.paths[0][:revision]
143 143 )
144 144 cmt = Changeset.normalize_comments(revision.message, repo_log_encoding)
145 145 author_utf8 = Changeset.to_utf8(revision.author, repo_log_encoding)
146 146 cs = changesets.where(
147 147 :committed_on => tmp_time - time_delta .. tmp_time + time_delta,
148 148 :committer => author_utf8,
149 149 :comments => cmt
150 150 ).first
151 151 # create a new changeset....
152 152 unless cs
153 153 # we use a temporaray revision number here (just for inserting)
154 154 # later on, we calculate a continous positive number
155 155 tmp_time2 = tmp_time.clone.gmtime
156 156 branch = revision.paths[0][:branch]
157 157 scmid = branch + "-" + tmp_time2.strftime("%Y%m%d-%H%M%S")
158 158 cs = Changeset.create(:repository => self,
159 159 :revision => "tmp#{tmp_rev_num}",
160 160 :scmid => scmid,
161 161 :committer => revision.author,
162 162 :committed_on => tmp_time,
163 163 :comments => revision.message)
164 164 tmp_rev_num += 1
165 165 end
166 166 # convert CVS-File-States to internal Action-abbrevations
167 167 # default action is (M)odified
168 168 action = "M"
169 169 if revision.paths[0][:action] == "Exp" && revision.paths[0][:revision] == "1.1"
170 170 action = "A" # add-action always at first revision (= 1.1)
171 171 elsif revision.paths[0][:action] == "dead"
172 172 action = "D" # dead-state is similar to Delete
173 173 end
174 174 Change.create(
175 175 :changeset => cs,
176 176 :action => action,
177 177 :path => scm.with_leading_slash(revision.paths[0][:path]),
178 178 :revision => revision.paths[0][:revision],
179 179 :branch => revision.paths[0][:branch]
180 180 )
181 181 end
182 182 end
183 183
184 184 # Renumber new changesets in chronological order
185 185 Changeset.
186 186 order('committed_on ASC, id ASC').
187 187 where("repository_id = ? AND revision LIKE 'tmp%'", id).
188 188 each do |changeset|
189 189 changeset.update_attribute :revision, next_revision_number
190 190 end
191 191 end # transaction
192 192 @current_revision_number = nil
193 193 end
194 194
195 195 private
196 196
197 197 # Returns the next revision number to assign to a CVS changeset
198 198 def next_revision_number
199 199 # Need to retrieve existing revision numbers to sort them as integers
200 200 sql = "SELECT revision FROM #{Changeset.table_name} "
201 201 sql << "WHERE repository_id = #{id} AND revision NOT LIKE 'tmp%'"
202 202 @current_revision_number ||= (connection.select_values(sql).collect(&:to_i).max || 0)
203 203 @current_revision_number += 1
204 204 end
205 205 end
@@ -1,114 +1,114
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 require 'redmine/scm/adapters/darcs_adapter'
19 19
20 20 class Repository::Darcs < Repository
21 21 validates_presence_of :url, :log_encoding
22 22
23 23 def self.human_attribute_name(attribute_key_name, *args)
24 24 attr_name = attribute_key_name.to_s
25 25 if attr_name == "url"
26 26 attr_name = "path_to_repository"
27 27 end
28 28 super(attr_name, *args)
29 29 end
30 30
31 31 def self.scm_adapter_class
32 32 Redmine::Scm::Adapters::DarcsAdapter
33 33 end
34 34
35 35 def self.scm_name
36 36 'Darcs'
37 37 end
38 38
39 39 def supports_directory_revisions?
40 40 true
41 41 end
42 42
43 43 def entry(path=nil, identifier=nil)
44 44 patch = identifier.nil? ? nil : changesets.find_by_revision(identifier)
45 45 scm.entry(path, patch.nil? ? nil : patch.scmid)
46 46 end
47 47
48 def entries(path=nil, identifier=nil)
48 def scm_entries(path=nil, identifier=nil)
49 49 patch = nil
50 50 if ! identifier.nil?
51 51 patch = changesets.find_by_revision(identifier)
52 52 return nil if patch.nil?
53 53 end
54 54 entries = scm.entries(path, patch.nil? ? nil : patch.scmid)
55 55 if entries
56 56 entries.each do |entry|
57 57 # Search the DB for the entry's last change
58 58 if entry.lastrev && !entry.lastrev.scmid.blank?
59 59 changeset = changesets.find_by_scmid(entry.lastrev.scmid)
60 60 end
61 61 if changeset
62 62 entry.lastrev.identifier = changeset.revision
63 63 entry.lastrev.name = changeset.revision
64 64 entry.lastrev.time = changeset.committed_on
65 65 entry.lastrev.author = changeset.committer
66 66 end
67 67 end
68 68 end
69 load_entries_changesets(entries)
70 69 entries
71 70 end
71 protected :scm_entries
72 72
73 73 def cat(path, identifier=nil)
74 74 patch = identifier.nil? ? nil : changesets.find_by_revision(identifier.to_s)
75 75 scm.cat(path, patch.nil? ? nil : patch.scmid)
76 76 end
77 77
78 78 def diff(path, rev, rev_to)
79 79 patch_from = changesets.find_by_revision(rev)
80 80 return nil if patch_from.nil?
81 81 patch_to = changesets.find_by_revision(rev_to) if rev_to
82 82 if path.blank?
83 83 path = patch_from.filechanges.collect{|change| change.path}.join(' ')
84 84 end
85 85 patch_from ? scm.diff(path, patch_from.scmid, patch_to ? patch_to.scmid : nil) : nil
86 86 end
87 87
88 88 def fetch_changesets
89 89 scm_info = scm.info
90 90 if scm_info
91 91 db_last_id = latest_changeset ? latest_changeset.scmid : nil
92 92 next_rev = latest_changeset ? latest_changeset.revision.to_i + 1 : 1
93 93 # latest revision in the repository
94 94 scm_revision = scm_info.lastrev.scmid
95 95 unless changesets.find_by_scmid(scm_revision)
96 96 revisions = scm.revisions('', db_last_id, nil, :with_path => true)
97 97 transaction do
98 98 revisions.reverse_each do |revision|
99 99 changeset = Changeset.create(:repository => self,
100 100 :revision => next_rev,
101 101 :scmid => revision.scmid,
102 102 :committer => revision.author,
103 103 :committed_on => revision.time,
104 104 :comments => revision.message)
105 105 revision.paths.each do |change|
106 106 changeset.create_change(change)
107 107 end
108 108 next_rev += 1
109 109 end if revisions
110 110 end
111 111 end
112 112 end
113 113 end
114 114 end
@@ -1,258 +1,257
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2014 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 def entries(path=nil, identifier=nil)
97 entries = scm.entries(path, identifier, :report_last_commit => extra_report_last_commit)
98 load_entries_changesets(entries)
99 entries
96 def scm_entries(path=nil, identifier=nil)
97 scm.entries(path, identifier, :report_last_commit => extra_report_last_commit)
100 98 end
99 protected :scm_entries
101 100
102 101 # With SCMs that have a sequential commit numbering,
103 102 # such as Subversion and Mercurial,
104 103 # Redmine is able to be clever and only fetch changesets
105 104 # going forward from the most recent one it knows about.
106 105 #
107 106 # However, Git does not have a sequential commit numbering.
108 107 #
109 108 # In order to fetch only new adding revisions,
110 109 # Redmine needs to save "heads".
111 110 #
112 111 # In Git and Mercurial, revisions are not in date order.
113 112 # Redmine Mercurial fixed issues.
114 113 # * Redmine Takes Too Long On Large Mercurial Repository
115 114 # http://www.redmine.org/issues/3449
116 115 # * Sorting for changesets might go wrong on Mercurial repos
117 116 # http://www.redmine.org/issues/3567
118 117 #
119 118 # Database revision column is text, so Redmine can not sort by revision.
120 119 # Mercurial has revision number, and revision number guarantees revision order.
121 120 # Redmine Mercurial model stored revisions ordered by database id to database.
122 121 # So, Redmine Mercurial model can use correct ordering revisions.
123 122 #
124 123 # Redmine Mercurial adapter uses "hg log -r 0:tip --limit 10"
125 124 # to get limited revisions from old to new.
126 125 # But, Git 1.7.3.4 does not support --reverse with -n or --skip.
127 126 #
128 127 # The repository can still be fully reloaded by calling #clear_changesets
129 128 # before fetching changesets (eg. for offline resync)
130 129 def fetch_changesets
131 130 scm_brs = branches
132 131 return if scm_brs.nil? || scm_brs.empty?
133 132
134 133 h1 = extra_info || {}
135 134 h = h1.dup
136 135 repo_heads = scm_brs.map{ |br| br.scmid }
137 136 h["heads"] ||= []
138 137 prev_db_heads = h["heads"].dup
139 138 if prev_db_heads.empty?
140 139 prev_db_heads += heads_from_branches_hash
141 140 end
142 141 return if prev_db_heads.sort == repo_heads.sort
143 142
144 143 h["db_consistent"] ||= {}
145 144 if changesets.count == 0
146 145 h["db_consistent"]["ordering"] = 1
147 146 merge_extra_info(h)
148 147 self.save
149 148 elsif ! h["db_consistent"].has_key?("ordering")
150 149 h["db_consistent"]["ordering"] = 0
151 150 merge_extra_info(h)
152 151 self.save
153 152 end
154 153 save_revisions(prev_db_heads, repo_heads)
155 154 end
156 155
157 156 def save_revisions(prev_db_heads, repo_heads)
158 157 h = {}
159 158 opts = {}
160 159 opts[:reverse] = true
161 160 opts[:excludes] = prev_db_heads
162 161 opts[:includes] = repo_heads
163 162
164 163 revisions = scm.revisions('', nil, nil, opts)
165 164 return if revisions.blank?
166 165
167 166 # Make the search for existing revisions in the database in a more sufficient manner
168 167 #
169 168 # Git branch is the reference to the specific revision.
170 169 # Git can *delete* remote branch and *re-push* branch.
171 170 #
172 171 # $ git push remote :branch
173 172 # $ git push remote branch
174 173 #
175 174 # After deleting branch, revisions remain in repository until "git gc".
176 175 # On git 1.7.2.3, default pruning date is 2 weeks.
177 176 # So, "git log --not deleted_branch_head_revision" return code is 0.
178 177 #
179 178 # After re-pushing branch, "git log" returns revisions which are saved in database.
180 179 # So, Redmine needs to scan revisions and database every time.
181 180 #
182 181 # This is replacing the one-after-one queries.
183 182 # Find all revisions, that are in the database, and then remove them
184 183 # from the revision array.
185 184 # Then later we won't need any conditions for db existence.
186 185 # Query for several revisions at once, and remove them
187 186 # from the revisions array, if they are there.
188 187 # Do this in chunks, to avoid eventual memory problems
189 188 # (in case of tens of thousands of commits).
190 189 # If there are no revisions (because the original code's algorithm filtered them),
191 190 # then this part will be stepped over.
192 191 # We make queries, just if there is any revision.
193 192 limit = 100
194 193 offset = 0
195 194 revisions_copy = revisions.clone # revisions will change
196 195 while offset < revisions_copy.size
197 196 scmids = revisions_copy.slice(offset, limit).map{|x| x.scmid}
198 197 recent_changesets_slice = changesets.where(:scmid => scmids)
199 198 # Subtract revisions that redmine already knows about
200 199 recent_revisions = recent_changesets_slice.map{|c| c.scmid}
201 200 revisions.reject!{|r| recent_revisions.include?(r.scmid)}
202 201 offset += limit
203 202 end
204 203 revisions.each do |rev|
205 204 transaction do
206 205 # There is no search in the db for this revision, because above we ensured,
207 206 # that it's not in the db.
208 207 save_revision(rev)
209 208 end
210 209 end
211 210 h["heads"] = repo_heads.dup
212 211 merge_extra_info(h)
213 212 self.save
214 213 end
215 214 private :save_revisions
216 215
217 216 def save_revision(rev)
218 217 parents = (rev.parents || []).collect{|rp| find_changeset_by_name(rp)}.compact
219 218 changeset = Changeset.create(
220 219 :repository => self,
221 220 :revision => rev.identifier,
222 221 :scmid => rev.scmid,
223 222 :committer => rev.author,
224 223 :committed_on => rev.time,
225 224 :comments => rev.message,
226 225 :parents => parents
227 226 )
228 227 unless changeset.new_record?
229 228 rev.paths.each { |change| changeset.create_change(change) }
230 229 end
231 230 changeset
232 231 end
233 232 private :save_revision
234 233
235 234 def heads_from_branches_hash
236 235 h1 = extra_info || {}
237 236 h = h1.dup
238 237 h["branches"] ||= {}
239 238 h['branches'].map{|br, hs| hs['last_scmid']}
240 239 end
241 240
242 241 def latest_changesets(path,rev,limit=10)
243 242 revisions = scm.revisions(path, nil, rev, :limit => limit, :all => false)
244 243 return [] if revisions.nil? || revisions.empty?
245 244 changesets.where(:scmid => revisions.map {|c| c.scmid}).all
246 245 end
247 246
248 247 def clear_extra_info_of_changesets
249 248 return if extra_info.nil?
250 249 v = extra_info["extra_report_last_commit"]
251 250 write_attribute(:extra_info, nil)
252 251 h = {}
253 252 h["extra_report_last_commit"] = v
254 253 merge_extra_info(h)
255 254 self.save
256 255 end
257 256 private :clear_extra_info_of_changesets
258 257 end
General Comments 0
You need to be logged in to leave comments. Login now