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