##// END OF EJS Templates
backport r12453 AbstractAdapter change to 2.4-stable (#15756)...
Toshi MARUYAMA -
r12179:37a07789d1fa
parent child
Show More
@@ -1,447 +1,448
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 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 require 'redmine/scm/adapters'
19
20
20 if RUBY_VERSION < '1.9'
21 if RUBY_VERSION < '1.9'
21 require 'iconv'
22 require 'iconv'
22 end
23 end
23
24
24 module Redmine
25 module Redmine
25 module Scm
26 module Scm
26 module Adapters
27 module Adapters
27 class AbstractAdapter #:nodoc:
28 class AbstractAdapter #:nodoc:
28
29
29 # raised if scm command exited with error, e.g. unknown revision.
30 # raised if scm command exited with error, e.g. unknown revision.
30 class ScmCommandAborted < CommandFailed; end
31 class ScmCommandAborted < CommandFailed; end
31
32
32 class << self
33 class << self
33 def client_command
34 def client_command
34 ""
35 ""
35 end
36 end
36
37
37 def shell_quote_command
38 def shell_quote_command
38 if Redmine::Platform.mswin? && RUBY_PLATFORM == 'java'
39 if Redmine::Platform.mswin? && RUBY_PLATFORM == 'java'
39 client_command
40 client_command
40 else
41 else
41 shell_quote(client_command)
42 shell_quote(client_command)
42 end
43 end
43 end
44 end
44
45
45 # Returns the version of the scm client
46 # Returns the version of the scm client
46 # Eg: [1, 5, 0] or [] if unknown
47 # Eg: [1, 5, 0] or [] if unknown
47 def client_version
48 def client_version
48 []
49 []
49 end
50 end
50
51
51 # Returns the version string of the scm client
52 # Returns the version string of the scm client
52 # Eg: '1.5.0' or 'Unknown version' if unknown
53 # Eg: '1.5.0' or 'Unknown version' if unknown
53 def client_version_string
54 def client_version_string
54 v = client_version || 'Unknown version'
55 v = client_version || 'Unknown version'
55 v.is_a?(Array) ? v.join('.') : v.to_s
56 v.is_a?(Array) ? v.join('.') : v.to_s
56 end
57 end
57
58
58 # Returns true if the current client version is above
59 # Returns true if the current client version is above
59 # or equals the given one
60 # or equals the given one
60 # If option is :unknown is set to true, it will return
61 # If option is :unknown is set to true, it will return
61 # true if the client version is unknown
62 # true if the client version is unknown
62 def client_version_above?(v, options={})
63 def client_version_above?(v, options={})
63 ((client_version <=> v) >= 0) || (client_version.empty? && options[:unknown])
64 ((client_version <=> v) >= 0) || (client_version.empty? && options[:unknown])
64 end
65 end
65
66
66 def client_available
67 def client_available
67 true
68 true
68 end
69 end
69
70
70 def shell_quote(str)
71 def shell_quote(str)
71 if Redmine::Platform.mswin?
72 if Redmine::Platform.mswin?
72 '"' + str.gsub(/"/, '\\"') + '"'
73 '"' + str.gsub(/"/, '\\"') + '"'
73 else
74 else
74 "'" + str.gsub(/'/, "'\"'\"'") + "'"
75 "'" + str.gsub(/'/, "'\"'\"'") + "'"
75 end
76 end
76 end
77 end
77 end
78 end
78
79
79 def initialize(url, root_url=nil, login=nil, password=nil,
80 def initialize(url, root_url=nil, login=nil, password=nil,
80 path_encoding=nil)
81 path_encoding=nil)
81 @url = url
82 @url = url
82 @login = login if login && !login.empty?
83 @login = login if login && !login.empty?
83 @password = (password || "") if @login
84 @password = (password || "") if @login
84 @root_url = root_url.blank? ? retrieve_root_url : root_url
85 @root_url = root_url.blank? ? retrieve_root_url : root_url
85 end
86 end
86
87
87 def adapter_name
88 def adapter_name
88 'Abstract'
89 'Abstract'
89 end
90 end
90
91
91 def supports_cat?
92 def supports_cat?
92 true
93 true
93 end
94 end
94
95
95 def supports_annotate?
96 def supports_annotate?
96 respond_to?('annotate')
97 respond_to?('annotate')
97 end
98 end
98
99
99 def root_url
100 def root_url
100 @root_url
101 @root_url
101 end
102 end
102
103
103 def url
104 def url
104 @url
105 @url
105 end
106 end
106
107
107 def path_encoding
108 def path_encoding
108 nil
109 nil
109 end
110 end
110
111
111 # get info about the svn repository
112 # get info about the svn repository
112 def info
113 def info
113 return nil
114 return nil
114 end
115 end
115
116
116 # Returns the entry identified by path and revision identifier
117 # Returns the entry identified by path and revision identifier
117 # or nil if entry doesn't exist in the repository
118 # or nil if entry doesn't exist in the repository
118 def entry(path=nil, identifier=nil)
119 def entry(path=nil, identifier=nil)
119 parts = path.to_s.split(%r{[\/\\]}).select {|n| !n.blank?}
120 parts = path.to_s.split(%r{[\/\\]}).select {|n| !n.blank?}
120 search_path = parts[0..-2].join('/')
121 search_path = parts[0..-2].join('/')
121 search_name = parts[-1]
122 search_name = parts[-1]
122 if search_path.blank? && search_name.blank?
123 if search_path.blank? && search_name.blank?
123 # Root entry
124 # Root entry
124 Entry.new(:path => '', :kind => 'dir')
125 Entry.new(:path => '', :kind => 'dir')
125 else
126 else
126 # Search for the entry in the parent directory
127 # Search for the entry in the parent directory
127 es = entries(search_path, identifier)
128 es = entries(search_path, identifier)
128 es ? es.detect {|e| e.name == search_name} : nil
129 es ? es.detect {|e| e.name == search_name} : nil
129 end
130 end
130 end
131 end
131
132
132 # Returns an Entries collection
133 # Returns an Entries collection
133 # or nil if the given path doesn't exist in the repository
134 # or nil if the given path doesn't exist in the repository
134 def entries(path=nil, identifier=nil, options={})
135 def entries(path=nil, identifier=nil, options={})
135 return nil
136 return nil
136 end
137 end
137
138
138 def branches
139 def branches
139 return nil
140 return nil
140 end
141 end
141
142
142 def tags
143 def tags
143 return nil
144 return nil
144 end
145 end
145
146
146 def default_branch
147 def default_branch
147 return nil
148 return nil
148 end
149 end
149
150
150 def properties(path, identifier=nil)
151 def properties(path, identifier=nil)
151 return nil
152 return nil
152 end
153 end
153
154
154 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
155 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
155 return nil
156 return nil
156 end
157 end
157
158
158 def diff(path, identifier_from, identifier_to=nil)
159 def diff(path, identifier_from, identifier_to=nil)
159 return nil
160 return nil
160 end
161 end
161
162
162 def cat(path, identifier=nil)
163 def cat(path, identifier=nil)
163 return nil
164 return nil
164 end
165 end
165
166
166 def with_leading_slash(path)
167 def with_leading_slash(path)
167 path ||= ''
168 path ||= ''
168 (path[0,1]!="/") ? "/#{path}" : path
169 (path[0,1]!="/") ? "/#{path}" : path
169 end
170 end
170
171
171 def with_trailling_slash(path)
172 def with_trailling_slash(path)
172 path ||= ''
173 path ||= ''
173 (path[-1,1] == "/") ? path : "#{path}/"
174 (path[-1,1] == "/") ? path : "#{path}/"
174 end
175 end
175
176
176 def without_leading_slash(path)
177 def without_leading_slash(path)
177 path ||= ''
178 path ||= ''
178 path.gsub(%r{^/+}, '')
179 path.gsub(%r{^/+}, '')
179 end
180 end
180
181
181 def without_trailling_slash(path)
182 def without_trailling_slash(path)
182 path ||= ''
183 path ||= ''
183 (path[-1,1] == "/") ? path[0..-2] : path
184 (path[-1,1] == "/") ? path[0..-2] : path
184 end
185 end
185
186
186 def shell_quote(str)
187 def shell_quote(str)
187 self.class.shell_quote(str)
188 self.class.shell_quote(str)
188 end
189 end
189
190
190 private
191 private
191 def retrieve_root_url
192 def retrieve_root_url
192 info = self.info
193 info = self.info
193 info ? info.root_url : nil
194 info ? info.root_url : nil
194 end
195 end
195
196
196 def target(path, sq=true)
197 def target(path, sq=true)
197 path ||= ''
198 path ||= ''
198 base = path.match(/^\//) ? root_url : url
199 base = path.match(/^\//) ? root_url : url
199 str = "#{base}/#{path}".gsub(/[?<>\*]/, '')
200 str = "#{base}/#{path}".gsub(/[?<>\*]/, '')
200 if sq
201 if sq
201 str = shell_quote(str)
202 str = shell_quote(str)
202 end
203 end
203 str
204 str
204 end
205 end
205
206
206 def logger
207 def logger
207 self.class.logger
208 self.class.logger
208 end
209 end
209
210
210 def shellout(cmd, options = {}, &block)
211 def shellout(cmd, options = {}, &block)
211 self.class.shellout(cmd, options, &block)
212 self.class.shellout(cmd, options, &block)
212 end
213 end
213
214
214 def self.logger
215 def self.logger
215 Rails.logger
216 Rails.logger
216 end
217 end
217
218
218 # Path to the file where scm stderr output is logged
219 # Path to the file where scm stderr output is logged
219 # Returns nil if the log file is not writable
220 # Returns nil if the log file is not writable
220 def self.stderr_log_file
221 def self.stderr_log_file
221 if @stderr_log_file.nil?
222 if @stderr_log_file.nil?
222 writable = false
223 writable = false
223 path = Redmine::Configuration['scm_stderr_log_file'].presence
224 path = Redmine::Configuration['scm_stderr_log_file'].presence
224 path ||= Rails.root.join("log/#{Rails.env}.scm.stderr.log").to_s
225 path ||= Rails.root.join("log/#{Rails.env}.scm.stderr.log").to_s
225 if File.exists?(path)
226 if File.exists?(path)
226 if File.file?(path) && File.writable?(path)
227 if File.file?(path) && File.writable?(path)
227 writable = true
228 writable = true
228 else
229 else
229 logger.warn("SCM log file (#{path}) is not writable")
230 logger.warn("SCM log file (#{path}) is not writable")
230 end
231 end
231 else
232 else
232 begin
233 begin
233 File.open(path, "w") {}
234 File.open(path, "w") {}
234 writable = true
235 writable = true
235 rescue => e
236 rescue => e
236 logger.warn("SCM log file (#{path}) cannot be created: #{e.message}")
237 logger.warn("SCM log file (#{path}) cannot be created: #{e.message}")
237 end
238 end
238 end
239 end
239 @stderr_log_file = writable ? path : false
240 @stderr_log_file = writable ? path : false
240 end
241 end
241 @stderr_log_file || nil
242 @stderr_log_file || nil
242 end
243 end
243
244
244 def self.shellout(cmd, options = {}, &block)
245 def self.shellout(cmd, options = {}, &block)
245 if logger && logger.debug?
246 if logger && logger.debug?
246 logger.debug "Shelling out: #{strip_credential(cmd)}"
247 logger.debug "Shelling out: #{strip_credential(cmd)}"
247 # Capture stderr in a log file
248 # Capture stderr in a log file
248 if stderr_log_file
249 if stderr_log_file
249 cmd = "#{cmd} 2>>#{shell_quote(stderr_log_file)}"
250 cmd = "#{cmd} 2>>#{shell_quote(stderr_log_file)}"
250 end
251 end
251 end
252 end
252 begin
253 begin
253 mode = "r+"
254 mode = "r+"
254 IO.popen(cmd, mode) do |io|
255 IO.popen(cmd, mode) do |io|
255 io.set_encoding("ASCII-8BIT") if io.respond_to?(:set_encoding)
256 io.set_encoding("ASCII-8BIT") if io.respond_to?(:set_encoding)
256 io.close_write unless options[:write_stdin]
257 io.close_write unless options[:write_stdin]
257 block.call(io) if block_given?
258 block.call(io) if block_given?
258 end
259 end
259 ## If scm command does not exist,
260 ## If scm command does not exist,
260 ## Linux JRuby 1.6.2 (ruby-1.8.7-p330) raises java.io.IOException
261 ## Linux JRuby 1.6.2 (ruby-1.8.7-p330) raises java.io.IOException
261 ## in production environment.
262 ## in production environment.
262 # rescue Errno::ENOENT => e
263 # rescue Errno::ENOENT => e
263 rescue Exception => e
264 rescue Exception => e
264 msg = strip_credential(e.message)
265 msg = strip_credential(e.message)
265 # The command failed, log it and re-raise
266 # The command failed, log it and re-raise
266 logmsg = "SCM command failed, "
267 logmsg = "SCM command failed, "
267 logmsg += "make sure that your SCM command (e.g. svn) is "
268 logmsg += "make sure that your SCM command (e.g. svn) is "
268 logmsg += "in PATH (#{ENV['PATH']})\n"
269 logmsg += "in PATH (#{ENV['PATH']})\n"
269 logmsg += "You can configure your scm commands in config/configuration.yml.\n"
270 logmsg += "You can configure your scm commands in config/configuration.yml.\n"
270 logmsg += "#{strip_credential(cmd)}\n"
271 logmsg += "#{strip_credential(cmd)}\n"
271 logmsg += "with: #{msg}"
272 logmsg += "with: #{msg}"
272 logger.error(logmsg)
273 logger.error(logmsg)
273 raise CommandFailed.new(msg)
274 raise CommandFailed.new(msg)
274 end
275 end
275 end
276 end
276
277
277 # Hides username/password in a given command
278 # Hides username/password in a given command
278 def self.strip_credential(cmd)
279 def self.strip_credential(cmd)
279 q = (Redmine::Platform.mswin? ? '"' : "'")
280 q = (Redmine::Platform.mswin? ? '"' : "'")
280 cmd.to_s.gsub(/(\-\-(password|username))\s+(#{q}[^#{q}]+#{q}|[^#{q}]\S+)/, '\\1 xxxx')
281 cmd.to_s.gsub(/(\-\-(password|username))\s+(#{q}[^#{q}]+#{q}|[^#{q}]\S+)/, '\\1 xxxx')
281 end
282 end
282
283
283 def strip_credential(cmd)
284 def strip_credential(cmd)
284 self.class.strip_credential(cmd)
285 self.class.strip_credential(cmd)
285 end
286 end
286
287
287 def scm_iconv(to, from, str)
288 def scm_iconv(to, from, str)
288 return nil if str.nil?
289 return nil if str.nil?
289 return str if to == from
290 return str if to == from
290 if str.respond_to?(:force_encoding)
291 if str.respond_to?(:force_encoding)
291 str.force_encoding(from)
292 str.force_encoding(from)
292 begin
293 begin
293 str.encode(to)
294 str.encode(to)
294 rescue Exception => err
295 rescue Exception => err
295 logger.error("failed to convert from #{from} to #{to}. #{err}")
296 logger.error("failed to convert from #{from} to #{to}. #{err}")
296 nil
297 nil
297 end
298 end
298 else
299 else
299 begin
300 begin
300 Iconv.conv(to, from, str)
301 Iconv.conv(to, from, str)
301 rescue Iconv::Failure => err
302 rescue Iconv::Failure => err
302 logger.error("failed to convert from #{from} to #{to}. #{err}")
303 logger.error("failed to convert from #{from} to #{to}. #{err}")
303 nil
304 nil
304 end
305 end
305 end
306 end
306 end
307 end
307
308
308 def parse_xml(xml)
309 def parse_xml(xml)
309 if RUBY_PLATFORM == 'java'
310 if RUBY_PLATFORM == 'java'
310 xml = xml.sub(%r{<\?xml[^>]*\?>}, '')
311 xml = xml.sub(%r{<\?xml[^>]*\?>}, '')
311 end
312 end
312 ActiveSupport::XmlMini.parse(xml)
313 ActiveSupport::XmlMini.parse(xml)
313 end
314 end
314 end
315 end
315
316
316 class Entries < Array
317 class Entries < Array
317 def sort_by_name
318 def sort_by_name
318 dup.sort! {|x,y|
319 dup.sort! {|x,y|
319 if x.kind == y.kind
320 if x.kind == y.kind
320 x.name.to_s <=> y.name.to_s
321 x.name.to_s <=> y.name.to_s
321 else
322 else
322 x.kind <=> y.kind
323 x.kind <=> y.kind
323 end
324 end
324 }
325 }
325 end
326 end
326
327
327 def revisions
328 def revisions
328 revisions ||= Revisions.new(collect{|entry| entry.lastrev}.compact)
329 revisions ||= Revisions.new(collect{|entry| entry.lastrev}.compact)
329 end
330 end
330 end
331 end
331
332
332 class Info
333 class Info
333 attr_accessor :root_url, :lastrev
334 attr_accessor :root_url, :lastrev
334 def initialize(attributes={})
335 def initialize(attributes={})
335 self.root_url = attributes[:root_url] if attributes[:root_url]
336 self.root_url = attributes[:root_url] if attributes[:root_url]
336 self.lastrev = attributes[:lastrev]
337 self.lastrev = attributes[:lastrev]
337 end
338 end
338 end
339 end
339
340
340 class Entry
341 class Entry
341 attr_accessor :name, :path, :kind, :size, :lastrev, :changeset
342 attr_accessor :name, :path, :kind, :size, :lastrev, :changeset
342
343
343 def initialize(attributes={})
344 def initialize(attributes={})
344 self.name = attributes[:name] if attributes[:name]
345 self.name = attributes[:name] if attributes[:name]
345 self.path = attributes[:path] if attributes[:path]
346 self.path = attributes[:path] if attributes[:path]
346 self.kind = attributes[:kind] if attributes[:kind]
347 self.kind = attributes[:kind] if attributes[:kind]
347 self.size = attributes[:size].to_i if attributes[:size]
348 self.size = attributes[:size].to_i if attributes[:size]
348 self.lastrev = attributes[:lastrev]
349 self.lastrev = attributes[:lastrev]
349 end
350 end
350
351
351 def is_file?
352 def is_file?
352 'file' == self.kind
353 'file' == self.kind
353 end
354 end
354
355
355 def is_dir?
356 def is_dir?
356 'dir' == self.kind
357 'dir' == self.kind
357 end
358 end
358
359
359 def is_text?
360 def is_text?
360 Redmine::MimeType.is_type?('text', name)
361 Redmine::MimeType.is_type?('text', name)
361 end
362 end
362
363
363 def author
364 def author
364 if changeset
365 if changeset
365 changeset.author.to_s
366 changeset.author.to_s
366 elsif lastrev
367 elsif lastrev
367 Redmine::CodesetUtil.replace_invalid_utf8(lastrev.author.to_s.split('<').first)
368 Redmine::CodesetUtil.replace_invalid_utf8(lastrev.author.to_s.split('<').first)
368 end
369 end
369 end
370 end
370 end
371 end
371
372
372 class Revisions < Array
373 class Revisions < Array
373 def latest
374 def latest
374 sort {|x,y|
375 sort {|x,y|
375 unless x.time.nil? or y.time.nil?
376 unless x.time.nil? or y.time.nil?
376 x.time <=> y.time
377 x.time <=> y.time
377 else
378 else
378 0
379 0
379 end
380 end
380 }.last
381 }.last
381 end
382 end
382 end
383 end
383
384
384 class Revision
385 class Revision
385 attr_accessor :scmid, :name, :author, :time, :message,
386 attr_accessor :scmid, :name, :author, :time, :message,
386 :paths, :revision, :branch, :identifier,
387 :paths, :revision, :branch, :identifier,
387 :parents
388 :parents
388
389
389 def initialize(attributes={})
390 def initialize(attributes={})
390 self.identifier = attributes[:identifier]
391 self.identifier = attributes[:identifier]
391 self.scmid = attributes[:scmid]
392 self.scmid = attributes[:scmid]
392 self.name = attributes[:name] || self.identifier
393 self.name = attributes[:name] || self.identifier
393 self.author = attributes[:author]
394 self.author = attributes[:author]
394 self.time = attributes[:time]
395 self.time = attributes[:time]
395 self.message = attributes[:message] || ""
396 self.message = attributes[:message] || ""
396 self.paths = attributes[:paths]
397 self.paths = attributes[:paths]
397 self.revision = attributes[:revision]
398 self.revision = attributes[:revision]
398 self.branch = attributes[:branch]
399 self.branch = attributes[:branch]
399 self.parents = attributes[:parents]
400 self.parents = attributes[:parents]
400 end
401 end
401
402
402 # Returns the readable identifier.
403 # Returns the readable identifier.
403 def format_identifier
404 def format_identifier
404 self.identifier.to_s
405 self.identifier.to_s
405 end
406 end
406
407
407 def ==(other)
408 def ==(other)
408 if other.nil?
409 if other.nil?
409 false
410 false
410 elsif scmid.present?
411 elsif scmid.present?
411 scmid == other.scmid
412 scmid == other.scmid
412 elsif identifier.present?
413 elsif identifier.present?
413 identifier == other.identifier
414 identifier == other.identifier
414 elsif revision.present?
415 elsif revision.present?
415 revision == other.revision
416 revision == other.revision
416 end
417 end
417 end
418 end
418 end
419 end
419
420
420 class Annotate
421 class Annotate
421 attr_reader :lines, :revisions
422 attr_reader :lines, :revisions
422
423
423 def initialize
424 def initialize
424 @lines = []
425 @lines = []
425 @revisions = []
426 @revisions = []
426 end
427 end
427
428
428 def add_line(line, revision)
429 def add_line(line, revision)
429 @lines << line
430 @lines << line
430 @revisions << revision
431 @revisions << revision
431 end
432 end
432
433
433 def content
434 def content
434 content = lines.join("\n")
435 content = lines.join("\n")
435 end
436 end
436
437
437 def empty?
438 def empty?
438 lines.empty?
439 lines.empty?
439 end
440 end
440 end
441 end
441
442
442 class Branch < String
443 class Branch < String
443 attr_accessor :revision, :scmid
444 attr_accessor :revision, :scmid
444 end
445 end
445 end
446 end
446 end
447 end
447 end
448 end
General Comments 0
You need to be logged in to leave comments. Login now