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