##// END OF EJS Templates
scm: catch all exceptions at adapter shellout() to fork scm command (#8510, #6713, #4725, #5404)....
Toshi MARUYAMA -
r6110:34f838685bf7
parent child
Show More
@@ -1,367 +1,371
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 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 # Returns the version of the scm client
36 # Returns the version of the scm client
37 # Eg: [1, 5, 0] or [] if unknown
37 # Eg: [1, 5, 0] or [] if unknown
38 def client_version
38 def client_version
39 []
39 []
40 end
40 end
41
41
42 # Returns the version string of the scm client
42 # Returns the version string of the scm client
43 # Eg: '1.5.0' or 'Unknown version' if unknown
43 # Eg: '1.5.0' or 'Unknown version' if unknown
44 def client_version_string
44 def client_version_string
45 v = client_version || 'Unknown version'
45 v = client_version || 'Unknown version'
46 v.is_a?(Array) ? v.join('.') : v.to_s
46 v.is_a?(Array) ? v.join('.') : v.to_s
47 end
47 end
48
48
49 # Returns true if the current client version is above
49 # Returns true if the current client version is above
50 # or equals the given one
50 # or equals the given one
51 # If option is :unknown is set to true, it will return
51 # If option is :unknown is set to true, it will return
52 # true if the client version is unknown
52 # true if the client version is unknown
53 def client_version_above?(v, options={})
53 def client_version_above?(v, options={})
54 ((client_version <=> v) >= 0) || (client_version.empty? && options[:unknown])
54 ((client_version <=> v) >= 0) || (client_version.empty? && options[:unknown])
55 end
55 end
56
56
57 def client_available
57 def client_available
58 true
58 true
59 end
59 end
60
60
61 def shell_quote(str)
61 def shell_quote(str)
62 if Redmine::Platform.mswin?
62 if Redmine::Platform.mswin?
63 '"' + str.gsub(/"/, '\\"') + '"'
63 '"' + str.gsub(/"/, '\\"') + '"'
64 else
64 else
65 "'" + str.gsub(/'/, "'\"'\"'") + "'"
65 "'" + str.gsub(/'/, "'\"'\"'") + "'"
66 end
66 end
67 end
67 end
68 end
68 end
69
69
70 def initialize(url, root_url=nil, login=nil, password=nil,
70 def initialize(url, root_url=nil, login=nil, password=nil,
71 path_encoding=nil)
71 path_encoding=nil)
72 @url = url
72 @url = url
73 @login = login if login && !login.empty?
73 @login = login if login && !login.empty?
74 @password = (password || "") if @login
74 @password = (password || "") if @login
75 @root_url = root_url.blank? ? retrieve_root_url : root_url
75 @root_url = root_url.blank? ? retrieve_root_url : root_url
76 end
76 end
77
77
78 def adapter_name
78 def adapter_name
79 'Abstract'
79 'Abstract'
80 end
80 end
81
81
82 def supports_cat?
82 def supports_cat?
83 true
83 true
84 end
84 end
85
85
86 def supports_annotate?
86 def supports_annotate?
87 respond_to?('annotate')
87 respond_to?('annotate')
88 end
88 end
89
89
90 def root_url
90 def root_url
91 @root_url
91 @root_url
92 end
92 end
93
93
94 def url
94 def url
95 @url
95 @url
96 end
96 end
97
97
98 def path_encoding
98 def path_encoding
99 nil
99 nil
100 end
100 end
101
101
102 # get info about the svn repository
102 # get info about the svn repository
103 def info
103 def info
104 return nil
104 return nil
105 end
105 end
106
106
107 # Returns the entry identified by path and revision identifier
107 # Returns the entry identified by path and revision identifier
108 # or nil if entry doesn't exist in the repository
108 # or nil if entry doesn't exist in the repository
109 def entry(path=nil, identifier=nil)
109 def entry(path=nil, identifier=nil)
110 parts = path.to_s.split(%r{[\/\\]}).select {|n| !n.blank?}
110 parts = path.to_s.split(%r{[\/\\]}).select {|n| !n.blank?}
111 search_path = parts[0..-2].join('/')
111 search_path = parts[0..-2].join('/')
112 search_name = parts[-1]
112 search_name = parts[-1]
113 if search_path.blank? && search_name.blank?
113 if search_path.blank? && search_name.blank?
114 # Root entry
114 # Root entry
115 Entry.new(:path => '', :kind => 'dir')
115 Entry.new(:path => '', :kind => 'dir')
116 else
116 else
117 # Search for the entry in the parent directory
117 # Search for the entry in the parent directory
118 es = entries(search_path, identifier)
118 es = entries(search_path, identifier)
119 es ? es.detect {|e| e.name == search_name} : nil
119 es ? es.detect {|e| e.name == search_name} : nil
120 end
120 end
121 end
121 end
122
122
123 # Returns an Entries collection
123 # Returns an Entries collection
124 # or nil if the given path doesn't exist in the repository
124 # or nil if the given path doesn't exist in the repository
125 def entries(path=nil, identifier=nil, options={})
125 def entries(path=nil, identifier=nil, options={})
126 return nil
126 return nil
127 end
127 end
128
128
129 def branches
129 def branches
130 return nil
130 return nil
131 end
131 end
132
132
133 def tags
133 def tags
134 return nil
134 return nil
135 end
135 end
136
136
137 def default_branch
137 def default_branch
138 return nil
138 return nil
139 end
139 end
140
140
141 def properties(path, identifier=nil)
141 def properties(path, identifier=nil)
142 return nil
142 return nil
143 end
143 end
144
144
145 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
145 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
146 return nil
146 return nil
147 end
147 end
148
148
149 def diff(path, identifier_from, identifier_to=nil)
149 def diff(path, identifier_from, identifier_to=nil)
150 return nil
150 return nil
151 end
151 end
152
152
153 def cat(path, identifier=nil)
153 def cat(path, identifier=nil)
154 return nil
154 return nil
155 end
155 end
156
156
157 def with_leading_slash(path)
157 def with_leading_slash(path)
158 path ||= ''
158 path ||= ''
159 (path[0,1]!="/") ? "/#{path}" : path
159 (path[0,1]!="/") ? "/#{path}" : path
160 end
160 end
161
161
162 def with_trailling_slash(path)
162 def with_trailling_slash(path)
163 path ||= ''
163 path ||= ''
164 (path[-1,1] == "/") ? path : "#{path}/"
164 (path[-1,1] == "/") ? path : "#{path}/"
165 end
165 end
166
166
167 def without_leading_slash(path)
167 def without_leading_slash(path)
168 path ||= ''
168 path ||= ''
169 path.gsub(%r{^/+}, '')
169 path.gsub(%r{^/+}, '')
170 end
170 end
171
171
172 def without_trailling_slash(path)
172 def without_trailling_slash(path)
173 path ||= ''
173 path ||= ''
174 (path[-1,1] == "/") ? path[0..-2] : path
174 (path[-1,1] == "/") ? path[0..-2] : path
175 end
175 end
176
176
177 def shell_quote(str)
177 def shell_quote(str)
178 self.class.shell_quote(str)
178 self.class.shell_quote(str)
179 end
179 end
180
180
181 private
181 private
182 def retrieve_root_url
182 def retrieve_root_url
183 info = self.info
183 info = self.info
184 info ? info.root_url : nil
184 info ? info.root_url : nil
185 end
185 end
186
186
187 def target(path, sq=true)
187 def target(path, sq=true)
188 path ||= ''
188 path ||= ''
189 base = path.match(/^\//) ? root_url : url
189 base = path.match(/^\//) ? root_url : url
190 str = "#{base}/#{path}".gsub(/[?<>\*]/, '')
190 str = "#{base}/#{path}".gsub(/[?<>\*]/, '')
191 if sq
191 if sq
192 str = shell_quote(str)
192 str = shell_quote(str)
193 end
193 end
194 str
194 str
195 end
195 end
196
196
197 def logger
197 def logger
198 self.class.logger
198 self.class.logger
199 end
199 end
200
200
201 def shellout(cmd, &block)
201 def shellout(cmd, &block)
202 self.class.shellout(cmd, &block)
202 self.class.shellout(cmd, &block)
203 end
203 end
204
204
205 def self.logger
205 def self.logger
206 Rails.logger
206 Rails.logger
207 end
207 end
208
208
209 def self.shellout(cmd, &block)
209 def self.shellout(cmd, &block)
210 if logger && logger.debug?
210 if logger && logger.debug?
211 logger.debug "Shelling out: #{strip_credential(cmd)}"
211 logger.debug "Shelling out: #{strip_credential(cmd)}"
212 end
212 end
213 if Rails.env == 'development'
213 if Rails.env == 'development'
214 # Capture stderr when running in dev environment
214 # Capture stderr when running in dev environment
215 cmd = "#{cmd} 2>>#{Rails.root}/log/scm.stderr.log"
215 cmd = "#{cmd} 2>>#{Rails.root}/log/scm.stderr.log"
216 end
216 end
217 begin
217 begin
218 if RUBY_VERSION < '1.9'
218 if RUBY_VERSION < '1.9'
219 mode = "r+"
219 mode = "r+"
220 else
220 else
221 mode = "r+:ASCII-8BIT"
221 mode = "r+:ASCII-8BIT"
222 end
222 end
223 IO.popen(cmd, mode) do |io|
223 IO.popen(cmd, mode) do |io|
224 io.close_write
224 io.close_write
225 block.call(io) if block_given?
225 block.call(io) if block_given?
226 end
226 end
227 rescue Errno::ENOENT => e
227 ## If scm command does not exist,
228 ## Linux JRuby 1.6.2 (ruby-1.8.7-p330) raises java.io.IOException
229 ## in production environment.
230 # rescue Errno::ENOENT => e
231 rescue Exception => e
228 msg = strip_credential(e.message)
232 msg = strip_credential(e.message)
229 # The command failed, log it and re-raise
233 # The command failed, log it and re-raise
230 logmsg = "SCM command failed, "
234 logmsg = "SCM command failed, "
231 logmsg += "make sure that your SCM command (e.g. svn) is "
235 logmsg += "make sure that your SCM command (e.g. svn) is "
232 logmsg += "in PATH (#{ENV['PATH']})\n"
236 logmsg += "in PATH (#{ENV['PATH']})\n"
233 logmsg += "You can configure your scm commands in config/configuration.yml.\n"
237 logmsg += "You can configure your scm commands in config/configuration.yml.\n"
234 logmsg += "#{strip_credential(cmd)}\n"
238 logmsg += "#{strip_credential(cmd)}\n"
235 logmsg += "with: #{msg}"
239 logmsg += "with: #{msg}"
236 logger.error(logmsg)
240 logger.error(logmsg)
237 raise CommandFailed.new(msg)
241 raise CommandFailed.new(msg)
238 end
242 end
239 end
243 end
240
244
241 # Hides username/password in a given command
245 # Hides username/password in a given command
242 def self.strip_credential(cmd)
246 def self.strip_credential(cmd)
243 q = (Redmine::Platform.mswin? ? '"' : "'")
247 q = (Redmine::Platform.mswin? ? '"' : "'")
244 cmd.to_s.gsub(/(\-\-(password|username))\s+(#{q}[^#{q}]+#{q}|[^#{q}]\S+)/, '\\1 xxxx')
248 cmd.to_s.gsub(/(\-\-(password|username))\s+(#{q}[^#{q}]+#{q}|[^#{q}]\S+)/, '\\1 xxxx')
245 end
249 end
246
250
247 def strip_credential(cmd)
251 def strip_credential(cmd)
248 self.class.strip_credential(cmd)
252 self.class.strip_credential(cmd)
249 end
253 end
250
254
251 def scm_iconv(to, from, str)
255 def scm_iconv(to, from, str)
252 return nil if str.nil?
256 return nil if str.nil?
253 return str if to == from
257 return str if to == from
254 begin
258 begin
255 Iconv.conv(to, from, str)
259 Iconv.conv(to, from, str)
256 rescue Iconv::Failure => err
260 rescue Iconv::Failure => err
257 logger.error("failed to convert from #{from} to #{to}. #{err}")
261 logger.error("failed to convert from #{from} to #{to}. #{err}")
258 nil
262 nil
259 end
263 end
260 end
264 end
261 end
265 end
262
266
263 class Entries < Array
267 class Entries < Array
264 def sort_by_name
268 def sort_by_name
265 sort {|x,y|
269 sort {|x,y|
266 if x.kind == y.kind
270 if x.kind == y.kind
267 x.name.to_s <=> y.name.to_s
271 x.name.to_s <=> y.name.to_s
268 else
272 else
269 x.kind <=> y.kind
273 x.kind <=> y.kind
270 end
274 end
271 }
275 }
272 end
276 end
273
277
274 def revisions
278 def revisions
275 revisions ||= Revisions.new(collect{|entry| entry.lastrev}.compact)
279 revisions ||= Revisions.new(collect{|entry| entry.lastrev}.compact)
276 end
280 end
277 end
281 end
278
282
279 class Info
283 class Info
280 attr_accessor :root_url, :lastrev
284 attr_accessor :root_url, :lastrev
281 def initialize(attributes={})
285 def initialize(attributes={})
282 self.root_url = attributes[:root_url] if attributes[:root_url]
286 self.root_url = attributes[:root_url] if attributes[:root_url]
283 self.lastrev = attributes[:lastrev]
287 self.lastrev = attributes[:lastrev]
284 end
288 end
285 end
289 end
286
290
287 class Entry
291 class Entry
288 attr_accessor :name, :path, :kind, :size, :lastrev
292 attr_accessor :name, :path, :kind, :size, :lastrev
289 def initialize(attributes={})
293 def initialize(attributes={})
290 self.name = attributes[:name] if attributes[:name]
294 self.name = attributes[:name] if attributes[:name]
291 self.path = attributes[:path] if attributes[:path]
295 self.path = attributes[:path] if attributes[:path]
292 self.kind = attributes[:kind] if attributes[:kind]
296 self.kind = attributes[:kind] if attributes[:kind]
293 self.size = attributes[:size].to_i if attributes[:size]
297 self.size = attributes[:size].to_i if attributes[:size]
294 self.lastrev = attributes[:lastrev]
298 self.lastrev = attributes[:lastrev]
295 end
299 end
296
300
297 def is_file?
301 def is_file?
298 'file' == self.kind
302 'file' == self.kind
299 end
303 end
300
304
301 def is_dir?
305 def is_dir?
302 'dir' == self.kind
306 'dir' == self.kind
303 end
307 end
304
308
305 def is_text?
309 def is_text?
306 Redmine::MimeType.is_type?('text', name)
310 Redmine::MimeType.is_type?('text', name)
307 end
311 end
308 end
312 end
309
313
310 class Revisions < Array
314 class Revisions < Array
311 def latest
315 def latest
312 sort {|x,y|
316 sort {|x,y|
313 unless x.time.nil? or y.time.nil?
317 unless x.time.nil? or y.time.nil?
314 x.time <=> y.time
318 x.time <=> y.time
315 else
319 else
316 0
320 0
317 end
321 end
318 }.last
322 }.last
319 end
323 end
320 end
324 end
321
325
322 class Revision
326 class Revision
323 attr_accessor :scmid, :name, :author, :time, :message,
327 attr_accessor :scmid, :name, :author, :time, :message,
324 :paths, :revision, :branch, :identifier
328 :paths, :revision, :branch, :identifier
325
329
326 def initialize(attributes={})
330 def initialize(attributes={})
327 self.identifier = attributes[:identifier]
331 self.identifier = attributes[:identifier]
328 self.scmid = attributes[:scmid]
332 self.scmid = attributes[:scmid]
329 self.name = attributes[:name] || self.identifier
333 self.name = attributes[:name] || self.identifier
330 self.author = attributes[:author]
334 self.author = attributes[:author]
331 self.time = attributes[:time]
335 self.time = attributes[:time]
332 self.message = attributes[:message] || ""
336 self.message = attributes[:message] || ""
333 self.paths = attributes[:paths]
337 self.paths = attributes[:paths]
334 self.revision = attributes[:revision]
338 self.revision = attributes[:revision]
335 self.branch = attributes[:branch]
339 self.branch = attributes[:branch]
336 end
340 end
337
341
338 # Returns the readable identifier.
342 # Returns the readable identifier.
339 def format_identifier
343 def format_identifier
340 self.identifier.to_s
344 self.identifier.to_s
341 end
345 end
342 end
346 end
343
347
344 class Annotate
348 class Annotate
345 attr_reader :lines, :revisions
349 attr_reader :lines, :revisions
346
350
347 def initialize
351 def initialize
348 @lines = []
352 @lines = []
349 @revisions = []
353 @revisions = []
350 end
354 end
351
355
352 def add_line(line, revision)
356 def add_line(line, revision)
353 @lines << line
357 @lines << line
354 @revisions << revision
358 @revisions << revision
355 end
359 end
356
360
357 def content
361 def content
358 content = lines.join("\n")
362 content = lines.join("\n")
359 end
363 end
360
364
361 def empty?
365 def empty?
362 lines.empty?
366 lines.empty?
363 end
367 end
364 end
368 end
365 end
369 end
366 end
370 end
367 end
371 end
General Comments 0
You need to be logged in to leave comments. Login now