comparison .svn/pristine/a8/a8fc14f9eb6b10ebe9da4200ebea010b2daee352.svn-base @ 1298:4f746d8966dd redmine_2.3_integration

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