##// END OF EJS Templates
Adds a method to load changesets for repository entries....
Jean-Philippe Lang -
r9622:7c105ec9e93b
parent child
Show More
@@ -1,397 +1,409
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 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
23 23 belongs_to :project
24 24 has_many :changesets, :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC"
25 25 has_many :filechanges, :class_name => 'Change', :through => :changesets
26 26
27 27 serialize :extra_info
28 28
29 29 before_save :check_default
30 30
31 31 # Raw SQL to delete changesets and changes in the database
32 32 # has_many :changesets, :dependent => :destroy is too slow for big repositories
33 33 before_destroy :clear_changesets
34 34
35 35 validates_length_of :password, :maximum => 255, :allow_nil => true
36 36 validates_length_of :identifier, :maximum => 255, :allow_blank => true
37 37 validates_presence_of :identifier, :unless => Proc.new { |r| r.is_default? || r.set_as_default? }
38 38 validates_uniqueness_of :identifier, :scope => :project_id, :allow_blank => true
39 39 validates_exclusion_of :identifier, :in => %w(show entry raw changes annotate diff show stats graph)
40 40 # donwcase letters, digits, dashes but not digits only
41 41 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :allow_blank => true
42 42 # Checks if the SCM is enabled when creating a repository
43 43 validate :repo_create_validation, :on => :create
44 44
45 45 def repo_create_validation
46 46 unless Setting.enabled_scm.include?(self.class.name.demodulize)
47 47 errors.add(:type, :invalid)
48 48 end
49 49 end
50 50
51 51 def self.human_attribute_name(attribute_key_name, *args)
52 52 attr_name = attribute_key_name.to_s
53 53 if attr_name == "log_encoding"
54 54 attr_name = "commit_logs_encoding"
55 55 end
56 56 super(attr_name, *args)
57 57 end
58 58
59 59 # Removes leading and trailing whitespace
60 60 def url=(arg)
61 61 write_attribute(:url, arg ? arg.to_s.strip : nil)
62 62 end
63 63
64 64 # Removes leading and trailing whitespace
65 65 def root_url=(arg)
66 66 write_attribute(:root_url, arg ? arg.to_s.strip : nil)
67 67 end
68 68
69 69 def password
70 70 read_ciphered_attribute(:password)
71 71 end
72 72
73 73 def password=(arg)
74 74 write_ciphered_attribute(:password, arg)
75 75 end
76 76
77 77 def scm_adapter
78 78 self.class.scm_adapter_class
79 79 end
80 80
81 81 def scm
82 82 unless @scm
83 83 @scm = self.scm_adapter.new(url, root_url,
84 84 login, password, path_encoding)
85 85 if root_url.blank? && @scm.root_url.present?
86 86 update_attribute(:root_url, @scm.root_url)
87 87 end
88 88 end
89 89 @scm
90 90 end
91 91
92 92 def scm_name
93 93 self.class.scm_name
94 94 end
95 95
96 96 def name
97 97 if identifier.present?
98 98 identifier
99 99 elsif is_default?
100 100 l(:field_repository_is_default)
101 101 else
102 102 scm_name
103 103 end
104 104 end
105 105
106 106 def identifier_param
107 107 if is_default?
108 108 nil
109 109 elsif identifier.present?
110 110 identifier
111 111 else
112 112 id.to_s
113 113 end
114 114 end
115 115
116 116 def <=>(repository)
117 117 if is_default?
118 118 -1
119 119 elsif repository.is_default?
120 120 1
121 121 else
122 122 identifier.to_s <=> repository.identifier.to_s
123 123 end
124 124 end
125 125
126 126 def self.find_by_identifier_param(param)
127 127 if param.to_s =~ /^\d+$/
128 128 find_by_id(param)
129 129 else
130 130 find_by_identifier(param)
131 131 end
132 132 end
133 133
134 134 def merge_extra_info(arg)
135 135 h = extra_info || {}
136 136 return h if arg.nil?
137 137 h.merge!(arg)
138 138 write_attribute(:extra_info, h)
139 139 end
140 140
141 141 def report_last_commit
142 142 true
143 143 end
144 144
145 145 def supports_cat?
146 146 scm.supports_cat?
147 147 end
148 148
149 149 def supports_annotate?
150 150 scm.supports_annotate?
151 151 end
152 152
153 153 def supports_all_revisions?
154 154 true
155 155 end
156 156
157 157 def supports_directory_revisions?
158 158 false
159 159 end
160 160
161 161 def supports_revision_graph?
162 162 false
163 163 end
164 164
165 165 def entry(path=nil, identifier=nil)
166 166 scm.entry(path, identifier)
167 167 end
168 168
169 169 def entries(path=nil, identifier=nil)
170 scm.entries(path, identifier)
170 entries = scm.entries(path, identifier)
171 load_entries_changesets(entries)
172 entries
171 173 end
172 174
173 175 def branches
174 176 scm.branches
175 177 end
176 178
177 179 def tags
178 180 scm.tags
179 181 end
180 182
181 183 def default_branch
182 184 nil
183 185 end
184 186
185 187 def properties(path, identifier=nil)
186 188 scm.properties(path, identifier)
187 189 end
188 190
189 191 def cat(path, identifier=nil)
190 192 scm.cat(path, identifier)
191 193 end
192 194
193 195 def diff(path, rev, rev_to)
194 196 scm.diff(path, rev, rev_to)
195 197 end
196 198
197 199 def diff_format_revisions(cs, cs_to, sep=':')
198 200 text = ""
199 201 text << cs_to.format_identifier + sep if cs_to
200 202 text << cs.format_identifier if cs
201 203 text
202 204 end
203 205
204 206 # Returns a path relative to the url of the repository
205 207 def relative_path(path)
206 208 path
207 209 end
208 210
209 211 # Finds and returns a revision with a number or the beginning of a hash
210 212 def find_changeset_by_name(name)
211 213 return nil if name.blank?
212 214 s = name.to_s
213 215 changesets.find(:first, :conditions => (s.match(/^\d*$/) ?
214 216 ["revision = ?", s] : ["revision LIKE ?", s + '%']))
215 217 end
216 218
217 219 def latest_changeset
218 220 @latest_changeset ||= changesets.find(:first)
219 221 end
220 222
221 223 # Returns the latest changesets for +path+
222 224 # Default behaviour is to search in cached changesets
223 225 def latest_changesets(path, rev, limit=10)
224 226 if path.blank?
225 227 changesets.find(
226 228 :all,
227 229 :include => :user,
228 230 :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC",
229 231 :limit => limit)
230 232 else
231 233 filechanges.find(
232 234 :all,
233 235 :include => {:changeset => :user},
234 236 :conditions => ["path = ?", path.with_leading_slash],
235 237 :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC",
236 238 :limit => limit
237 239 ).collect(&:changeset)
238 240 end
239 241 end
240 242
241 243 def scan_changesets_for_issue_ids
242 244 self.changesets.each(&:scan_comment_for_issue_ids)
243 245 end
244 246
245 247 # Returns an array of committers usernames and associated user_id
246 248 def committers
247 249 @committers ||= Changeset.connection.select_rows(
248 250 "SELECT DISTINCT committer, user_id FROM #{Changeset.table_name} WHERE repository_id = #{id}")
249 251 end
250 252
251 253 # Maps committers username to a user ids
252 254 def committer_ids=(h)
253 255 if h.is_a?(Hash)
254 256 committers.each do |committer, user_id|
255 257 new_user_id = h[committer]
256 258 if new_user_id && (new_user_id.to_i != user_id.to_i)
257 259 new_user_id = (new_user_id.to_i > 0 ? new_user_id.to_i : nil)
258 260 Changeset.update_all(
259 261 "user_id = #{ new_user_id.nil? ? 'NULL' : new_user_id }",
260 262 ["repository_id = ? AND committer = ?", id, committer])
261 263 end
262 264 end
263 265 @committers = nil
264 266 @found_committer_users = nil
265 267 true
266 268 else
267 269 false
268 270 end
269 271 end
270 272
271 273 # Returns the Redmine User corresponding to the given +committer+
272 274 # It will return nil if the committer is not yet mapped and if no User
273 275 # with the same username or email was found
274 276 def find_committer_user(committer)
275 277 unless committer.blank?
276 278 @found_committer_users ||= {}
277 279 return @found_committer_users[committer] if @found_committer_users.has_key?(committer)
278 280
279 281 user = nil
280 282 c = changesets.find(:first, :conditions => {:committer => committer}, :include => :user)
281 283 if c && c.user
282 284 user = c.user
283 285 elsif committer.strip =~ /^([^<]+)(<(.*)>)?$/
284 286 username, email = $1.strip, $3
285 287 u = User.find_by_login(username)
286 288 u ||= User.find_by_mail(email) unless email.blank?
287 289 user = u
288 290 end
289 291 @found_committer_users[committer] = user
290 292 user
291 293 end
292 294 end
293 295
294 296 def repo_log_encoding
295 297 encoding = log_encoding.to_s.strip
296 298 encoding.blank? ? 'UTF-8' : encoding
297 299 end
298 300
299 301 # Fetches new changesets for all repositories of active projects
300 302 # Can be called periodically by an external script
301 303 # eg. ruby script/runner "Repository.fetch_changesets"
302 304 def self.fetch_changesets
303 305 Project.active.has_module(:repository).all.each do |project|
304 306 project.repositories.each do |repository|
305 307 begin
306 308 repository.fetch_changesets
307 309 rescue Redmine::Scm::Adapters::CommandFailed => e
308 310 logger.error "scm: error during fetching changesets: #{e.message}"
309 311 end
310 312 end
311 313 end
312 314 end
313 315
314 316 # scan changeset comments to find related and fixed issues for all repositories
315 317 def self.scan_changesets_for_issue_ids
316 318 find(:all).each(&:scan_changesets_for_issue_ids)
317 319 end
318 320
319 321 def self.scm_name
320 322 'Abstract'
321 323 end
322 324
323 325 def self.available_scm
324 326 subclasses.collect {|klass| [klass.scm_name, klass.name]}
325 327 end
326 328
327 329 def self.factory(klass_name, *args)
328 330 klass = "Repository::#{klass_name}".constantize
329 331 klass.new(*args)
330 332 rescue
331 333 nil
332 334 end
333 335
334 336 def self.scm_adapter_class
335 337 nil
336 338 end
337 339
338 340 def self.scm_command
339 341 ret = ""
340 342 begin
341 343 ret = self.scm_adapter_class.client_command if self.scm_adapter_class
342 344 rescue Exception => e
343 345 logger.error "scm: error during get command: #{e.message}"
344 346 end
345 347 ret
346 348 end
347 349
348 350 def self.scm_version_string
349 351 ret = ""
350 352 begin
351 353 ret = self.scm_adapter_class.client_version_string if self.scm_adapter_class
352 354 rescue Exception => e
353 355 logger.error "scm: error during get version string: #{e.message}"
354 356 end
355 357 ret
356 358 end
357 359
358 360 def self.scm_available
359 361 ret = false
360 362 begin
361 363 ret = self.scm_adapter_class.client_available if self.scm_adapter_class
362 364 rescue Exception => e
363 365 logger.error "scm: error during get scm available: #{e.message}"
364 366 end
365 367 ret
366 368 end
367 369
368 370 def set_as_default?
369 371 new_record? && project && !Repository.first(:conditions => {:project_id => project.id})
370 372 end
371 373
372 374 protected
373 375
374 376 def check_default
375 377 if !is_default? && set_as_default?
376 378 self.is_default = true
377 379 end
378 380 if is_default? && is_default_changed?
379 381 Repository.update_all(["is_default = ?", false], ["project_id = ?", project_id])
380 382 end
381 383 end
382 384
385 def load_entries_changesets(entries)
386 if entries
387 entries.each do |entry|
388 if entry.lastrev && entry.lastrev.identifier
389 entry.changeset = find_changeset_by_name(entry.lastrev.identifier)
390 end
391 end
392 end
393 end
394
383 395 private
384 396
385 397 # Deletes repository data
386 398 def clear_changesets
387 399 cs = Changeset.table_name
388 400 ch = Change.table_name
389 401 ci = "#{table_name_prefix}changesets_issues#{table_name_suffix}"
390 402 cp = "#{table_name_prefix}changeset_parents#{table_name_suffix}"
391 403
392 404 connection.delete("DELETE FROM #{ch} WHERE #{ch}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
393 405 connection.delete("DELETE FROM #{ci} WHERE #{ci}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
394 406 connection.delete("DELETE FROM #{cp} WHERE #{cp}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
395 407 connection.delete("DELETE FROM #{cs} WHERE #{cs}.repository_id = #{id}")
396 408 end
397 409 end
@@ -1,104 +1,106
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 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 entries(path=nil, identifier=nil)
41 41 entries = scm.entries(path, identifier)
42 42 if entries
43 43 entries.each do |e|
44 44 next if e.lastrev.revision.blank?
45 45 # Set the filesize unless browsing a specific revision
46 46 if identifier.nil? && e.is_file?
47 47 full_path = File.join(root_url, e.path)
48 48 e.size = File.stat(full_path).size if File.file?(full_path)
49 49 end
50 50 c = Change.find(
51 51 :first,
52 52 :include => :changeset,
53 53 :conditions => [
54 54 "#{Change.table_name}.revision = ? and #{Changeset.table_name}.repository_id = ?",
55 55 e.lastrev.revision,
56 56 id
57 57 ],
58 58 :order => "#{Changeset.table_name}.revision DESC")
59 59 if c
60 60 e.lastrev.identifier = c.changeset.revision
61 61 e.lastrev.name = c.changeset.revision
62 62 e.lastrev.author = c.changeset.committer
63 63 end
64 64 end
65 65 end
66 load_entries_changesets(entries)
67 entries
66 68 end
67 69
68 70 def fetch_changesets
69 71 scm_info = scm.info
70 72 if scm_info
71 73 # latest revision found in database
72 74 db_revision = latest_changeset ? latest_changeset.revision.to_i : 0
73 75 # latest revision in the repository
74 76 scm_revision = scm_info.lastrev.identifier.to_i
75 77 if db_revision < scm_revision
76 78 logger.debug "Fetching changesets for repository #{url}" if logger && logger.debug?
77 79 identifier_from = db_revision + 1
78 80 while (identifier_from <= scm_revision)
79 81 # loads changesets by batches of 200
80 82 identifier_to = [identifier_from + 199, scm_revision].min
81 83 revisions = scm.revisions('', identifier_to, identifier_from, :with_paths => true)
82 84 transaction do
83 85 revisions.reverse_each do |revision|
84 86 changeset = Changeset.create(:repository => self,
85 87 :revision => revision.identifier,
86 88 :committer => revision.author,
87 89 :committed_on => revision.time,
88 90 :scmid => revision.scmid,
89 91 :comments => revision.message)
90 92
91 93 revision.paths.each do |change|
92 94 Change.create(:changeset => changeset,
93 95 :action => change[:action],
94 96 :path => change[:path],
95 97 :revision => change[:revision])
96 98 end
97 99 end
98 100 end unless revisions.nil?
99 101 identifier_from = identifier_to + 1
100 102 end
101 103 end
102 104 end
103 105 end
104 106 end
@@ -1,204 +1,205
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 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 def self.human_attribute_name(attribute_key_name, *args)
25 25 attr_name = attribute_key_name.to_s
26 26 if attr_name == "root_url"
27 27 attr_name = "cvsroot"
28 28 elsif attr_name == "url"
29 29 attr_name = "cvs_module"
30 30 end
31 31 super(attr_name, *args)
32 32 end
33 33
34 34 def self.scm_adapter_class
35 35 Redmine::Scm::Adapters::CvsAdapter
36 36 end
37 37
38 38 def self.scm_name
39 39 'CVS'
40 40 end
41 41
42 42 def entry(path=nil, identifier=nil)
43 43 rev = identifier.nil? ? nil : changesets.find_by_revision(identifier)
44 44 scm.entry(path, rev.nil? ? nil : rev.committed_on)
45 45 end
46 46
47 47 def entries(path=nil, identifier=nil)
48 48 rev = nil
49 49 if ! identifier.nil?
50 50 rev = changesets.find_by_revision(identifier)
51 51 return nil if rev.nil?
52 52 end
53 53 entries = scm.entries(path, rev.nil? ? nil : rev.committed_on)
54 54 if entries
55 55 entries.each() do |entry|
56 56 if ( ! entry.lastrev.nil? ) && ( ! entry.lastrev.revision.nil? )
57 57 change = filechanges.find_by_revision_and_path(
58 58 entry.lastrev.revision,
59 59 scm.with_leading_slash(entry.path) )
60 60 if change
61 61 entry.lastrev.identifier = change.changeset.revision
62 62 entry.lastrev.revision = change.changeset.revision
63 63 entry.lastrev.author = change.changeset.committer
64 64 # entry.lastrev.branch = change.branch
65 65 end
66 66 end
67 67 end
68 68 end
69 load_entries_changesets(entries)
69 70 entries
70 71 end
71 72
72 73 def cat(path, identifier=nil)
73 74 rev = nil
74 75 if ! identifier.nil?
75 76 rev = changesets.find_by_revision(identifier)
76 77 return nil if rev.nil?
77 78 end
78 79 scm.cat(path, rev.nil? ? nil : rev.committed_on)
79 80 end
80 81
81 82 def annotate(path, identifier=nil)
82 83 rev = nil
83 84 if ! identifier.nil?
84 85 rev = changesets.find_by_revision(identifier)
85 86 return nil if rev.nil?
86 87 end
87 88 scm.annotate(path, rev.nil? ? nil : rev.committed_on)
88 89 end
89 90
90 91 def diff(path, rev, rev_to)
91 92 # convert rev to revision. CVS can't handle changesets here
92 93 diff=[]
93 94 changeset_from = changesets.find_by_revision(rev)
94 95 if rev_to.to_i > 0
95 96 changeset_to = changesets.find_by_revision(rev_to)
96 97 end
97 98 changeset_from.filechanges.each() do |change_from|
98 99 revision_from = nil
99 100 revision_to = nil
100 101 if path.nil? || (change_from.path.starts_with? scm.with_leading_slash(path))
101 102 revision_from = change_from.revision
102 103 end
103 104 if revision_from
104 105 if changeset_to
105 106 changeset_to.filechanges.each() do |change_to|
106 107 revision_to = change_to.revision if change_to.path == change_from.path
107 108 end
108 109 end
109 110 unless revision_to
110 111 revision_to = scm.get_previous_revision(revision_from)
111 112 end
112 113 file_diff = scm.diff(change_from.path, revision_from, revision_to)
113 114 diff = diff + file_diff unless file_diff.nil?
114 115 end
115 116 end
116 117 return diff
117 118 end
118 119
119 120 def fetch_changesets
120 121 # some nifty bits to introduce a commit-id with cvs
121 122 # natively cvs doesn't provide any kind of changesets,
122 123 # there is only a revision per file.
123 124 # we now take a guess using the author, the commitlog and the commit-date.
124 125
125 126 # last one is the next step to take. the commit-date is not equal for all
126 127 # commits in one changeset. cvs update the commit-date when the *,v file was touched. so
127 128 # we use a small delta here, to merge all changes belonging to _one_ changeset
128 129 time_delta = 10.seconds
129 130 fetch_since = latest_changeset ? latest_changeset.committed_on : nil
130 131 transaction do
131 132 tmp_rev_num = 1
132 133 scm.revisions('', fetch_since, nil, :log_encoding => repo_log_encoding) do |revision|
133 134 # only add the change to the database, if it doen't exists. the cvs log
134 135 # is not exclusive at all.
135 136 tmp_time = revision.time.clone
136 137 unless filechanges.find_by_path_and_revision(
137 138 scm.with_leading_slash(revision.paths[0][:path]),
138 139 revision.paths[0][:revision]
139 140 )
140 141 cmt = Changeset.normalize_comments(revision.message, repo_log_encoding)
141 142 author_utf8 = Changeset.to_utf8(revision.author, repo_log_encoding)
142 143 cs = changesets.find(
143 144 :first,
144 145 :conditions => {
145 146 :committed_on => tmp_time - time_delta .. tmp_time + time_delta,
146 147 :committer => author_utf8,
147 148 :comments => cmt
148 149 }
149 150 )
150 151 # create a new changeset....
151 152 unless cs
152 153 # we use a temporaray revision number here (just for inserting)
153 154 # later on, we calculate a continous positive number
154 155 tmp_time2 = tmp_time.clone.gmtime
155 156 branch = revision.paths[0][:branch]
156 157 scmid = branch + "-" + tmp_time2.strftime("%Y%m%d-%H%M%S")
157 158 cs = Changeset.create(:repository => self,
158 159 :revision => "tmp#{tmp_rev_num}",
159 160 :scmid => scmid,
160 161 :committer => revision.author,
161 162 :committed_on => tmp_time,
162 163 :comments => revision.message)
163 164 tmp_rev_num += 1
164 165 end
165 166 # convert CVS-File-States to internal Action-abbrevations
166 167 # default action is (M)odified
167 168 action = "M"
168 169 if revision.paths[0][:action] == "Exp" && revision.paths[0][:revision] == "1.1"
169 170 action = "A" # add-action always at first revision (= 1.1)
170 171 elsif revision.paths[0][:action] == "dead"
171 172 action = "D" # dead-state is similar to Delete
172 173 end
173 174 Change.create(
174 175 :changeset => cs,
175 176 :action => action,
176 177 :path => scm.with_leading_slash(revision.paths[0][:path]),
177 178 :revision => revision.paths[0][:revision],
178 179 :branch => revision.paths[0][:branch]
179 180 )
180 181 end
181 182 end
182 183
183 184 # Renumber new changesets in chronological order
184 185 Changeset.all(
185 186 :order => 'committed_on ASC, id ASC',
186 187 :conditions => ["repository_id = ? AND revision LIKE 'tmp%'", id]
187 188 ).each do |changeset|
188 189 changeset.update_attribute :revision, next_revision_number
189 190 end
190 191 end # transaction
191 192 @current_revision_number = nil
192 193 end
193 194
194 195 private
195 196
196 197 # Returns the next revision number to assign to a CVS changeset
197 198 def next_revision_number
198 199 # Need to retrieve existing revision numbers to sort them as integers
199 200 sql = "SELECT revision FROM #{Changeset.table_name} "
200 201 sql << "WHERE repository_id = #{id} AND revision NOT LIKE 'tmp%'"
201 202 @current_revision_number ||= (connection.select_values(sql).collect(&:to_i).max || 0)
202 203 @current_revision_number += 1
203 204 end
204 205 end
@@ -1,113 +1,114
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 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 48 def 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)
69 70 entries
70 71 end
71 72
72 73 def cat(path, identifier=nil)
73 74 patch = identifier.nil? ? nil : changesets.find_by_revision(identifier.to_s)
74 75 scm.cat(path, patch.nil? ? nil : patch.scmid)
75 76 end
76 77
77 78 def diff(path, rev, rev_to)
78 79 patch_from = changesets.find_by_revision(rev)
79 80 return nil if patch_from.nil?
80 81 patch_to = changesets.find_by_revision(rev_to) if rev_to
81 82 if path.blank?
82 83 path = patch_from.filechanges.collect{|change| change.path}.join(' ')
83 84 end
84 85 patch_from ? scm.diff(path, patch_from.scmid, patch_to ? patch_to.scmid : nil) : nil
85 86 end
86 87
87 88 def fetch_changesets
88 89 scm_info = scm.info
89 90 if scm_info
90 91 db_last_id = latest_changeset ? latest_changeset.scmid : nil
91 92 next_rev = latest_changeset ? latest_changeset.revision.to_i + 1 : 1
92 93 # latest revision in the repository
93 94 scm_revision = scm_info.lastrev.scmid
94 95 unless changesets.find_by_scmid(scm_revision)
95 96 revisions = scm.revisions('', db_last_id, nil, :with_path => true)
96 97 transaction do
97 98 revisions.reverse_each do |revision|
98 99 changeset = Changeset.create(:repository => self,
99 100 :revision => next_rev,
100 101 :scmid => revision.scmid,
101 102 :committer => revision.author,
102 103 :committed_on => revision.time,
103 104 :comments => revision.message)
104 105 revision.paths.each do |change|
105 106 changeset.create_change(change)
106 107 end
107 108 next_rev += 1
108 109 end if revisions
109 110 end
110 111 end
111 112 end
112 113 end
113 114 end
@@ -1,54 +1,50
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 3 #
4 4 # FileSystem adapter
5 5 # File written by Paul Rivier, at Demotera.
6 6 #
7 7 # This program is free software; you can redistribute it and/or
8 8 # modify it under the terms of the GNU General Public License
9 9 # as published by the Free Software Foundation; either version 2
10 10 # of the License, or (at your option) any later version.
11 11 #
12 12 # This program is distributed in the hope that it will be useful,
13 13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 15 # GNU General Public License for more details.
16 16 #
17 17 # You should have received a copy of the GNU General Public License
18 18 # along with this program; if not, write to the Free Software
19 19 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 20
21 21 require 'redmine/scm/adapters/filesystem_adapter'
22 22
23 23 class Repository::Filesystem < Repository
24 24 attr_protected :root_url
25 25 validates_presence_of :url
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 == "url"
30 30 attr_name = "root_directory"
31 31 end
32 32 super(attr_name, *args)
33 33 end
34 34
35 35 def self.scm_adapter_class
36 36 Redmine::Scm::Adapters::FilesystemAdapter
37 37 end
38 38
39 39 def self.scm_name
40 40 'Filesystem'
41 41 end
42 42
43 43 def supports_all_revisions?
44 44 false
45 45 end
46 46
47 def entries(path=nil, identifier=nil)
48 scm.entries(path, identifier)
49 end
50
51 47 def fetch_changesets
52 48 nil
53 49 end
54 50 end
@@ -1,258 +1,258
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 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 return nil if name.nil? || name.empty?
91 91 e = changesets.find(:first, :conditions => ['revision = ?', name.to_s])
92 92 return e if e
93 93 changesets.find(:first, :conditions => ['scmid LIKE ?', "#{name}%"])
94 94 end
95 95
96 96 def entries(path=nil, identifier=nil)
97 scm.entries(path,
98 identifier,
99 options = {:report_last_commit => extra_report_last_commit})
97 entries = scm.entries(path, identifier, :report_last_commit => extra_report_last_commit)
98 load_entries_changesets(entries)
99 entries
100 100 end
101 101
102 102 # With SCMs that have a sequential commit numbering,
103 103 # such as Subversion and Mercurial,
104 104 # Redmine is able to be clever and only fetch changesets
105 105 # going forward from the most recent one it knows about.
106 106 #
107 107 # However, Git does not have a sequential commit numbering.
108 108 #
109 109 # In order to fetch only new adding revisions,
110 110 # Redmine needs to save "heads".
111 111 #
112 112 # In Git and Mercurial, revisions are not in date order.
113 113 # Redmine Mercurial fixed issues.
114 114 # * Redmine Takes Too Long On Large Mercurial Repository
115 115 # http://www.redmine.org/issues/3449
116 116 # * Sorting for changesets might go wrong on Mercurial repos
117 117 # http://www.redmine.org/issues/3567
118 118 #
119 119 # Database revision column is text, so Redmine can not sort by revision.
120 120 # Mercurial has revision number, and revision number guarantees revision order.
121 121 # Redmine Mercurial model stored revisions ordered by database id to database.
122 122 # So, Redmine Mercurial model can use correct ordering revisions.
123 123 #
124 124 # Redmine Mercurial adapter uses "hg log -r 0:tip --limit 10"
125 125 # to get limited revisions from old to new.
126 126 # But, Git 1.7.3.4 does not support --reverse with -n or --skip.
127 127 #
128 128 # The repository can still be fully reloaded by calling #clear_changesets
129 129 # before fetching changesets (eg. for offline resync)
130 130 def fetch_changesets
131 131 scm_brs = branches
132 132 return if scm_brs.nil? || scm_brs.empty?
133 133
134 134 h1 = extra_info || {}
135 135 h = h1.dup
136 136 repo_heads = scm_brs.map{ |br| br.scmid }
137 137 h["heads"] ||= []
138 138 prev_db_heads = h["heads"].dup
139 139 if prev_db_heads.empty?
140 140 prev_db_heads += heads_from_branches_hash
141 141 end
142 142 return if prev_db_heads.sort == repo_heads.sort
143 143
144 144 h["db_consistent"] ||= {}
145 145 if changesets.count == 0
146 146 h["db_consistent"]["ordering"] = 1
147 147 merge_extra_info(h)
148 148 self.save
149 149 elsif ! h["db_consistent"].has_key?("ordering")
150 150 h["db_consistent"]["ordering"] = 0
151 151 merge_extra_info(h)
152 152 self.save
153 153 end
154 154 save_revisions(prev_db_heads, repo_heads)
155 155 end
156 156
157 157 def save_revisions(prev_db_heads, repo_heads)
158 158 h = {}
159 159 opts = {}
160 160 opts[:reverse] = true
161 161 opts[:excludes] = prev_db_heads
162 162 opts[:includes] = repo_heads
163 163
164 164 revisions = scm.revisions('', nil, nil, opts)
165 165 return if revisions.blank?
166 166
167 167 # Make the search for existing revisions in the database in a more sufficient manner
168 168 #
169 169 # Git branch is the reference to the specific revision.
170 170 # Git can *delete* remote branch and *re-push* branch.
171 171 #
172 172 # $ git push remote :branch
173 173 # $ git push remote branch
174 174 #
175 175 # After deleting branch, revisions remain in repository until "git gc".
176 176 # On git 1.7.2.3, default pruning date is 2 weeks.
177 177 # So, "git log --not deleted_branch_head_revision" return code is 0.
178 178 #
179 179 # After re-pushing branch, "git log" returns revisions which are saved in database.
180 180 # So, Redmine needs to scan revisions and database every time.
181 181 #
182 182 # This is replacing the one-after-one queries.
183 183 # Find all revisions, that are in the database, and then remove them 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 from the revisions array, if they are there.
186 186 # Do this in chunks, to avoid eventual memory problems (in case of tens of thousands of commits).
187 187 # If there are no revisions (because the original code's algorithm filtered them),
188 188 # then this part will be stepped over.
189 189 # We make queries, just if there is any revision.
190 190 limit = 100
191 191 offset = 0
192 192 revisions_copy = revisions.clone # revisions will change
193 193 while offset < revisions_copy.size
194 194 recent_changesets_slice = changesets.find(
195 195 :all,
196 196 :conditions => [
197 197 'scmid IN (?)',
198 198 revisions_copy.slice(offset, limit).map{|x| x.scmid}
199 199 ]
200 200 )
201 201 # Subtract revisions that redmine already knows about
202 202 recent_revisions = recent_changesets_slice.map{|c| c.scmid}
203 203 revisions.reject!{|r| recent_revisions.include?(r.scmid)}
204 204 offset += limit
205 205 end
206 206
207 207 revisions.each do |rev|
208 208 transaction do
209 209 # There is no search in the db for this revision, because above we ensured,
210 210 # that it's not in the db.
211 211 save_revision(rev)
212 212 end
213 213 end
214 214 h["heads"] = repo_heads.dup
215 215 merge_extra_info(h)
216 216 self.save
217 217 end
218 218 private :save_revisions
219 219
220 220 def save_revision(rev)
221 221 parents = (rev.parents || []).collect{|rp| find_changeset_by_name(rp)}.compact
222 222 changeset = Changeset.create(
223 223 :repository => self,
224 224 :revision => rev.identifier,
225 225 :scmid => rev.scmid,
226 226 :committer => rev.author,
227 227 :committed_on => rev.time,
228 228 :comments => rev.message,
229 229 :parents => parents
230 230 )
231 231 unless changeset.new_record?
232 232 rev.paths.each { |change| changeset.create_change(change) }
233 233 end
234 234 changeset
235 235 end
236 236 private :save_revision
237 237
238 238 def heads_from_branches_hash
239 239 h1 = extra_info || {}
240 240 h = h1.dup
241 241 h["branches"] ||= {}
242 242 h['branches'].map{|br, hs| hs['last_scmid']}
243 243 end
244 244
245 245 def latest_changesets(path,rev,limit=10)
246 246 revisions = scm.revisions(path, nil, rev, :limit => limit, :all => false)
247 247 return [] if revisions.nil? || revisions.empty?
248 248
249 249 changesets.find(
250 250 :all,
251 251 :conditions => [
252 252 "scmid IN (?)",
253 253 revisions.map!{|c| c.scmid}
254 254 ],
255 255 :order => 'committed_on DESC'
256 256 )
257 257 end
258 258 end
@@ -1,40 +1,39
1 1 <% @entries.each do |entry| %>
2 2 <% tr_id = Digest::MD5.hexdigest(entry.path)
3 3 depth = params[:depth].to_i %>
4 4 <% ent_path = Redmine::CodesetUtil.replace_invalid_utf8(entry.path) %>
5 5 <% ent_name = Redmine::CodesetUtil.replace_invalid_utf8(entry.name) %>
6 6 <tr id="<%= tr_id %>" class="<%= h params[:parent_id] %> entry <%= entry.kind %>">
7 7 <td style="padding-left: <%=18 * depth%>px;" class="<%=
8 8 @repository.report_last_commit ? "filename" : "filename_no_report" %>";>
9 9 <% if entry.is_dir? %>
10 10 <span class="expander" onclick="<%= remote_function(
11 11 :url => {
12 12 :action => 'show',
13 13 :id => @project,
14 14 :repository_id => @repository.identifier_param,
15 15 :path => to_path_param(ent_path),
16 16 :rev => @rev,
17 17 :depth => (depth + 1),
18 18 :parent_id => tr_id
19 19 },
20 20 :method => :get,
21 21 :update => { :success => tr_id },
22 22 :position => :after,
23 23 :success => "scmEntryLoaded('#{tr_id}')",
24 24 :condition => "scmEntryClick('#{tr_id}')"
25 25 ) %>">&nbsp</span>
26 26 <% end %>
27 27 <%= link_to h(ent_name),
28 28 {:action => (entry.is_dir? ? 'show' : 'changes'), :id => @project, :repository_id => @repository.identifier_param, :path => to_path_param(ent_path), :rev => @rev},
29 29 :class => (entry.is_dir? ? 'icon icon-folder' : "icon icon-file #{Redmine::MimeType.css_class_of(ent_name)}")%>
30 30 </td>
31 31 <td class="size"><%= (entry.size ? number_to_human_size(entry.size) : "?") unless entry.is_dir? %></td>
32 <% changeset = @repository.find_changeset_by_name(entry.lastrev.identifier) if entry.lastrev && entry.lastrev.identifier %>
33 32 <% if @repository.report_last_commit %>
34 <td class="revision"><%= link_to_revision(changeset, @repository) if changeset %></td>
33 <td class="revision"><%= link_to_revision(entry.changeset, @repository) if entry.changeset %></td>
35 34 <td class="age"><%= distance_of_time_in_words(entry.lastrev.time, Time.now) if entry.lastrev && entry.lastrev.time %></td>
36 <td class="author"><%= changeset.nil? ? h(Redmine::CodesetUtil.replace_invalid_utf8(entry.lastrev.author.to_s.split('<').first)) : h(changeset.author) if entry.lastrev %></td>
37 <td class="comments"><%=h truncate(changeset.comments, :length => 50) unless changeset.nil? %></td>
35 <td class="author"><%= entry.changeset.nil? ? h(Redmine::CodesetUtil.replace_invalid_utf8(entry.lastrev.author.to_s.split('<').first)) : h(entry.changeset.author) if entry.lastrev %></td>
36 <td class="comments"><%=h truncate(entry.changeset.comments, :length => 50) if entry.changeset %></td>
38 37 <% end %>
39 38 </tr>
40 39 <% end %>
@@ -1,389 +1,390
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 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 'cgi'
19 19
20 20 module Redmine
21 21 module Scm
22 22 module Adapters
23 23 class CommandFailed < StandardError #:nodoc:
24 24 end
25 25
26 26 class AbstractAdapter #:nodoc:
27 27
28 28 # raised if scm command exited with error, e.g. unknown revision.
29 29 class ScmCommandAborted < CommandFailed; end
30 30
31 31 class << self
32 32 def client_command
33 33 ""
34 34 end
35 35
36 36 def shell_quote_command
37 37 if Redmine::Platform.mswin? && RUBY_PLATFORM == 'java'
38 38 client_command
39 39 else
40 40 shell_quote(client_command)
41 41 end
42 42 end
43 43
44 44 # Returns the version of the scm client
45 45 # Eg: [1, 5, 0] or [] if unknown
46 46 def client_version
47 47 []
48 48 end
49 49
50 50 # Returns the version string of the scm client
51 51 # Eg: '1.5.0' or 'Unknown version' if unknown
52 52 def client_version_string
53 53 v = client_version || 'Unknown version'
54 54 v.is_a?(Array) ? v.join('.') : v.to_s
55 55 end
56 56
57 57 # Returns true if the current client version is above
58 58 # or equals the given one
59 59 # If option is :unknown is set to true, it will return
60 60 # true if the client version is unknown
61 61 def client_version_above?(v, options={})
62 62 ((client_version <=> v) >= 0) || (client_version.empty? && options[:unknown])
63 63 end
64 64
65 65 def client_available
66 66 true
67 67 end
68 68
69 69 def shell_quote(str)
70 70 if Redmine::Platform.mswin?
71 71 '"' + str.gsub(/"/, '\\"') + '"'
72 72 else
73 73 "'" + str.gsub(/'/, "'\"'\"'") + "'"
74 74 end
75 75 end
76 76 end
77 77
78 78 def initialize(url, root_url=nil, login=nil, password=nil,
79 79 path_encoding=nil)
80 80 @url = url
81 81 @login = login if login && !login.empty?
82 82 @password = (password || "") if @login
83 83 @root_url = root_url.blank? ? retrieve_root_url : root_url
84 84 end
85 85
86 86 def adapter_name
87 87 'Abstract'
88 88 end
89 89
90 90 def supports_cat?
91 91 true
92 92 end
93 93
94 94 def supports_annotate?
95 95 respond_to?('annotate')
96 96 end
97 97
98 98 def root_url
99 99 @root_url
100 100 end
101 101
102 102 def url
103 103 @url
104 104 end
105 105
106 106 def path_encoding
107 107 nil
108 108 end
109 109
110 110 # get info about the svn repository
111 111 def info
112 112 return nil
113 113 end
114 114
115 115 # Returns the entry identified by path and revision identifier
116 116 # or nil if entry doesn't exist in the repository
117 117 def entry(path=nil, identifier=nil)
118 118 parts = path.to_s.split(%r{[\/\\]}).select {|n| !n.blank?}
119 119 search_path = parts[0..-2].join('/')
120 120 search_name = parts[-1]
121 121 if search_path.blank? && search_name.blank?
122 122 # Root entry
123 123 Entry.new(:path => '', :kind => 'dir')
124 124 else
125 125 # Search for the entry in the parent directory
126 126 es = entries(search_path, identifier)
127 127 es ? es.detect {|e| e.name == search_name} : nil
128 128 end
129 129 end
130 130
131 131 # Returns an Entries collection
132 132 # or nil if the given path doesn't exist in the repository
133 133 def entries(path=nil, identifier=nil, options={})
134 134 return nil
135 135 end
136 136
137 137 def branches
138 138 return nil
139 139 end
140 140
141 141 def tags
142 142 return nil
143 143 end
144 144
145 145 def default_branch
146 146 return nil
147 147 end
148 148
149 149 def properties(path, identifier=nil)
150 150 return nil
151 151 end
152 152
153 153 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
154 154 return nil
155 155 end
156 156
157 157 def diff(path, identifier_from, identifier_to=nil)
158 158 return nil
159 159 end
160 160
161 161 def cat(path, identifier=nil)
162 162 return nil
163 163 end
164 164
165 165 def with_leading_slash(path)
166 166 path ||= ''
167 167 (path[0,1]!="/") ? "/#{path}" : path
168 168 end
169 169
170 170 def with_trailling_slash(path)
171 171 path ||= ''
172 172 (path[-1,1] == "/") ? path : "#{path}/"
173 173 end
174 174
175 175 def without_leading_slash(path)
176 176 path ||= ''
177 177 path.gsub(%r{^/+}, '')
178 178 end
179 179
180 180 def without_trailling_slash(path)
181 181 path ||= ''
182 182 (path[-1,1] == "/") ? path[0..-2] : path
183 183 end
184 184
185 185 def shell_quote(str)
186 186 self.class.shell_quote(str)
187 187 end
188 188
189 189 private
190 190 def retrieve_root_url
191 191 info = self.info
192 192 info ? info.root_url : nil
193 193 end
194 194
195 195 def target(path, sq=true)
196 196 path ||= ''
197 197 base = path.match(/^\//) ? root_url : url
198 198 str = "#{base}/#{path}".gsub(/[?<>\*]/, '')
199 199 if sq
200 200 str = shell_quote(str)
201 201 end
202 202 str
203 203 end
204 204
205 205 def logger
206 206 self.class.logger
207 207 end
208 208
209 209 def shellout(cmd, options = {}, &block)
210 210 self.class.shellout(cmd, options, &block)
211 211 end
212 212
213 213 def self.logger
214 214 Rails.logger
215 215 end
216 216
217 217 def self.shellout(cmd, options = {}, &block)
218 218 if logger && logger.debug?
219 219 logger.debug "Shelling out: #{strip_credential(cmd)}"
220 220 end
221 221 if Rails.env == 'development'
222 222 # Capture stderr when running in dev environment
223 223 cmd = "#{cmd} 2>>#{shell_quote(Rails.root.join('log/scm.stderr.log').to_s)}"
224 224 end
225 225 begin
226 226 mode = "r+"
227 227 IO.popen(cmd, mode) do |io|
228 228 io.set_encoding("ASCII-8BIT") if io.respond_to?(:set_encoding)
229 229 io.close_write unless options[:write_stdin]
230 230 block.call(io) if block_given?
231 231 end
232 232 ## If scm command does not exist,
233 233 ## Linux JRuby 1.6.2 (ruby-1.8.7-p330) raises java.io.IOException
234 234 ## in production environment.
235 235 # rescue Errno::ENOENT => e
236 236 rescue Exception => e
237 237 msg = strip_credential(e.message)
238 238 # The command failed, log it and re-raise
239 239 logmsg = "SCM command failed, "
240 240 logmsg += "make sure that your SCM command (e.g. svn) is "
241 241 logmsg += "in PATH (#{ENV['PATH']})\n"
242 242 logmsg += "You can configure your scm commands in config/configuration.yml.\n"
243 243 logmsg += "#{strip_credential(cmd)}\n"
244 244 logmsg += "with: #{msg}"
245 245 logger.error(logmsg)
246 246 raise CommandFailed.new(msg)
247 247 end
248 248 end
249 249
250 250 # Hides username/password in a given command
251 251 def self.strip_credential(cmd)
252 252 q = (Redmine::Platform.mswin? ? '"' : "'")
253 253 cmd.to_s.gsub(/(\-\-(password|username))\s+(#{q}[^#{q}]+#{q}|[^#{q}]\S+)/, '\\1 xxxx')
254 254 end
255 255
256 256 def strip_credential(cmd)
257 257 self.class.strip_credential(cmd)
258 258 end
259 259
260 260 def scm_iconv(to, from, str)
261 261 return nil if str.nil?
262 262 return str if to == from
263 263 begin
264 264 Iconv.conv(to, from, str)
265 265 rescue Iconv::Failure => err
266 266 logger.error("failed to convert from #{from} to #{to}. #{err}")
267 267 nil
268 268 end
269 269 end
270 270
271 271 def parse_xml(xml)
272 272 if RUBY_PLATFORM == 'java'
273 273 xml = xml.sub(%r{<\?xml[^>]*\?>}, '')
274 274 end
275 275 ActiveSupport::XmlMini.parse(xml)
276 276 end
277 277 end
278 278
279 279 class Entries < Array
280 280 def sort_by_name
281 281 dup.sort! {|x,y|
282 282 if x.kind == y.kind
283 283 x.name.to_s <=> y.name.to_s
284 284 else
285 285 x.kind <=> y.kind
286 286 end
287 287 }
288 288 end
289 289
290 290 def revisions
291 291 revisions ||= Revisions.new(collect{|entry| entry.lastrev}.compact)
292 292 end
293 293 end
294 294
295 295 class Info
296 296 attr_accessor :root_url, :lastrev
297 297 def initialize(attributes={})
298 298 self.root_url = attributes[:root_url] if attributes[:root_url]
299 299 self.lastrev = attributes[:lastrev]
300 300 end
301 301 end
302 302
303 303 class Entry
304 attr_accessor :name, :path, :kind, :size, :lastrev
304 attr_accessor :name, :path, :kind, :size, :lastrev, :changeset
305
305 306 def initialize(attributes={})
306 307 self.name = attributes[:name] if attributes[:name]
307 308 self.path = attributes[:path] if attributes[:path]
308 309 self.kind = attributes[:kind] if attributes[:kind]
309 310 self.size = attributes[:size].to_i if attributes[:size]
310 311 self.lastrev = attributes[:lastrev]
311 312 end
312 313
313 314 def is_file?
314 315 'file' == self.kind
315 316 end
316 317
317 318 def is_dir?
318 319 'dir' == self.kind
319 320 end
320 321
321 322 def is_text?
322 323 Redmine::MimeType.is_type?('text', name)
323 324 end
324 325 end
325 326
326 327 class Revisions < Array
327 328 def latest
328 329 sort {|x,y|
329 330 unless x.time.nil? or y.time.nil?
330 331 x.time <=> y.time
331 332 else
332 333 0
333 334 end
334 335 }.last
335 336 end
336 337 end
337 338
338 339 class Revision
339 340 attr_accessor :scmid, :name, :author, :time, :message,
340 341 :paths, :revision, :branch, :identifier,
341 342 :parents
342 343
343 344 def initialize(attributes={})
344 345 self.identifier = attributes[:identifier]
345 346 self.scmid = attributes[:scmid]
346 347 self.name = attributes[:name] || self.identifier
347 348 self.author = attributes[:author]
348 349 self.time = attributes[:time]
349 350 self.message = attributes[:message] || ""
350 351 self.paths = attributes[:paths]
351 352 self.revision = attributes[:revision]
352 353 self.branch = attributes[:branch]
353 354 self.parents = attributes[:parents]
354 355 end
355 356
356 357 # Returns the readable identifier.
357 358 def format_identifier
358 359 self.identifier.to_s
359 360 end
360 361 end
361 362
362 363 class Annotate
363 364 attr_reader :lines, :revisions
364 365
365 366 def initialize
366 367 @lines = []
367 368 @revisions = []
368 369 end
369 370
370 371 def add_line(line, revision)
371 372 @lines << line
372 373 @revisions << revision
373 374 end
374 375
375 376 def content
376 377 content = lines.join("\n")
377 378 end
378 379
379 380 def empty?
380 381 lines.empty?
381 382 end
382 383 end
383 384
384 385 class Branch < String
385 386 attr_accessor :revision, :scmid
386 387 end
387 388 end
388 389 end
389 390 end
General Comments 0
You need to be logged in to leave comments. Login now