##// END OF EJS Templates
Removes the XML declaration that breaks the parser with JRuby....
Jean-Philippe Lang -
r9352:cb16661d365c
parent child
Show More
@@ -1,382 +1,389
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 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 module Redmine
21 21 module Scm
22 22 module Adapters
23 23 class CommandFailed < StandardError #:nodoc:
24 24 end
25 25
26 26 class AbstractAdapter #:nodoc:
27 27
28 28 # raised if scm command exited with error, e.g. unknown revision.
29 29 class ScmCommandAborted < CommandFailed; end
30 30
31 31 class << self
32 32 def client_command
33 33 ""
34 34 end
35 35
36 36 def shell_quote_command
37 37 if Redmine::Platform.mswin? && RUBY_PLATFORM == 'java'
38 38 client_command
39 39 else
40 40 shell_quote(client_command)
41 41 end
42 42 end
43 43
44 44 # Returns the version of the scm client
45 45 # Eg: [1, 5, 0] or [] if unknown
46 46 def client_version
47 47 []
48 48 end
49 49
50 50 # Returns the version string of the scm client
51 51 # Eg: '1.5.0' or 'Unknown version' if unknown
52 52 def client_version_string
53 53 v = client_version || 'Unknown version'
54 54 v.is_a?(Array) ? v.join('.') : v.to_s
55 55 end
56 56
57 57 # Returns true if the current client version is above
58 58 # or equals the given one
59 59 # If option is :unknown is set to true, it will return
60 60 # true if the client version is unknown
61 61 def client_version_above?(v, options={})
62 62 ((client_version <=> v) >= 0) || (client_version.empty? && options[:unknown])
63 63 end
64 64
65 65 def client_available
66 66 true
67 67 end
68 68
69 69 def shell_quote(str)
70 70 if Redmine::Platform.mswin?
71 71 '"' + str.gsub(/"/, '\\"') + '"'
72 72 else
73 73 "'" + str.gsub(/'/, "'\"'\"'") + "'"
74 74 end
75 75 end
76 76 end
77 77
78 78 def initialize(url, root_url=nil, login=nil, password=nil,
79 79 path_encoding=nil)
80 80 @url = url
81 81 @login = login if login && !login.empty?
82 82 @password = (password || "") if @login
83 83 @root_url = root_url.blank? ? retrieve_root_url : root_url
84 84 end
85 85
86 86 def adapter_name
87 87 'Abstract'
88 88 end
89 89
90 90 def supports_cat?
91 91 true
92 92 end
93 93
94 94 def supports_annotate?
95 95 respond_to?('annotate')
96 96 end
97 97
98 98 def root_url
99 99 @root_url
100 100 end
101 101
102 102 def url
103 103 @url
104 104 end
105 105
106 106 def path_encoding
107 107 nil
108 108 end
109 109
110 110 # get info about the svn repository
111 111 def info
112 112 return nil
113 113 end
114 114
115 115 # Returns the entry identified by path and revision identifier
116 116 # or nil if entry doesn't exist in the repository
117 117 def entry(path=nil, identifier=nil)
118 118 parts = path.to_s.split(%r{[\/\\]}).select {|n| !n.blank?}
119 119 search_path = parts[0..-2].join('/')
120 120 search_name = parts[-1]
121 121 if search_path.blank? && search_name.blank?
122 122 # Root entry
123 123 Entry.new(:path => '', :kind => 'dir')
124 124 else
125 125 # Search for the entry in the parent directory
126 126 es = entries(search_path, identifier)
127 127 es ? es.detect {|e| e.name == search_name} : nil
128 128 end
129 129 end
130 130
131 131 # Returns an Entries collection
132 132 # or nil if the given path doesn't exist in the repository
133 133 def entries(path=nil, identifier=nil, options={})
134 134 return nil
135 135 end
136 136
137 137 def branches
138 138 return nil
139 139 end
140 140
141 141 def tags
142 142 return nil
143 143 end
144 144
145 145 def default_branch
146 146 return nil
147 147 end
148 148
149 149 def properties(path, identifier=nil)
150 150 return nil
151 151 end
152 152
153 153 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
154 154 return nil
155 155 end
156 156
157 157 def diff(path, identifier_from, identifier_to=nil)
158 158 return nil
159 159 end
160 160
161 161 def cat(path, identifier=nil)
162 162 return nil
163 163 end
164 164
165 165 def with_leading_slash(path)
166 166 path ||= ''
167 167 (path[0,1]!="/") ? "/#{path}" : path
168 168 end
169 169
170 170 def with_trailling_slash(path)
171 171 path ||= ''
172 172 (path[-1,1] == "/") ? path : "#{path}/"
173 173 end
174 174
175 175 def without_leading_slash(path)
176 176 path ||= ''
177 177 path.gsub(%r{^/+}, '')
178 178 end
179 179
180 180 def without_trailling_slash(path)
181 181 path ||= ''
182 182 (path[-1,1] == "/") ? path[0..-2] : path
183 183 end
184 184
185 185 def shell_quote(str)
186 186 self.class.shell_quote(str)
187 187 end
188 188
189 189 private
190 190 def retrieve_root_url
191 191 info = self.info
192 192 info ? info.root_url : nil
193 193 end
194 194
195 195 def target(path, sq=true)
196 196 path ||= ''
197 197 base = path.match(/^\//) ? root_url : url
198 198 str = "#{base}/#{path}".gsub(/[?<>\*]/, '')
199 199 if sq
200 200 str = shell_quote(str)
201 201 end
202 202 str
203 203 end
204 204
205 205 def logger
206 206 self.class.logger
207 207 end
208 208
209 209 def shellout(cmd, options = {}, &block)
210 210 self.class.shellout(cmd, options, &block)
211 211 end
212 212
213 213 def self.logger
214 214 Rails.logger
215 215 end
216 216
217 217 def self.shellout(cmd, options = {}, &block)
218 218 if logger && logger.debug?
219 219 logger.debug "Shelling out: #{strip_credential(cmd)}"
220 220 end
221 221 if Rails.env == 'development'
222 222 # Capture stderr when running in dev environment
223 223 cmd = "#{cmd} 2>>#{shell_quote(Rails.root.join('log/scm.stderr.log').to_s)}"
224 224 end
225 225 begin
226 226 mode = "r+"
227 227 IO.popen(cmd, mode) do |io|
228 228 io.set_encoding("ASCII-8BIT") if io.respond_to?(:set_encoding)
229 229 io.close_write unless options[:write_stdin]
230 230 block.call(io) if block_given?
231 231 end
232 232 ## If scm command does not exist,
233 233 ## Linux JRuby 1.6.2 (ruby-1.8.7-p330) raises java.io.IOException
234 234 ## in production environment.
235 235 # rescue Errno::ENOENT => e
236 236 rescue Exception => e
237 237 msg = strip_credential(e.message)
238 238 # The command failed, log it and re-raise
239 239 logmsg = "SCM command failed, "
240 240 logmsg += "make sure that your SCM command (e.g. svn) is "
241 241 logmsg += "in PATH (#{ENV['PATH']})\n"
242 242 logmsg += "You can configure your scm commands in config/configuration.yml.\n"
243 243 logmsg += "#{strip_credential(cmd)}\n"
244 244 logmsg += "with: #{msg}"
245 245 logger.error(logmsg)
246 246 raise CommandFailed.new(msg)
247 247 end
248 248 end
249 249
250 250 # Hides username/password in a given command
251 251 def self.strip_credential(cmd)
252 252 q = (Redmine::Platform.mswin? ? '"' : "'")
253 253 cmd.to_s.gsub(/(\-\-(password|username))\s+(#{q}[^#{q}]+#{q}|[^#{q}]\S+)/, '\\1 xxxx')
254 254 end
255 255
256 256 def strip_credential(cmd)
257 257 self.class.strip_credential(cmd)
258 258 end
259 259
260 260 def scm_iconv(to, from, str)
261 261 return nil if str.nil?
262 262 return str if to == from
263 263 begin
264 264 Iconv.conv(to, from, str)
265 265 rescue Iconv::Failure => err
266 266 logger.error("failed to convert from #{from} to #{to}. #{err}")
267 267 nil
268 268 end
269 269 end
270
271 def parse_xml(xml)
272 if RUBY_PLATFORM == 'java'
273 xml = xml.sub(%r{<\?xml[^>]*\?>}, '')
274 end
275 ActiveSupport::XmlMini.parse(xml)
276 end
270 277 end
271 278
272 279 class Entries < Array
273 280 def sort_by_name
274 281 sort {|x,y|
275 282 if x.kind == y.kind
276 283 x.name.to_s <=> y.name.to_s
277 284 else
278 285 x.kind <=> y.kind
279 286 end
280 287 }
281 288 end
282 289
283 290 def revisions
284 291 revisions ||= Revisions.new(collect{|entry| entry.lastrev}.compact)
285 292 end
286 293 end
287 294
288 295 class Info
289 296 attr_accessor :root_url, :lastrev
290 297 def initialize(attributes={})
291 298 self.root_url = attributes[:root_url] if attributes[:root_url]
292 299 self.lastrev = attributes[:lastrev]
293 300 end
294 301 end
295 302
296 303 class Entry
297 304 attr_accessor :name, :path, :kind, :size, :lastrev
298 305 def initialize(attributes={})
299 306 self.name = attributes[:name] if attributes[:name]
300 307 self.path = attributes[:path] if attributes[:path]
301 308 self.kind = attributes[:kind] if attributes[:kind]
302 309 self.size = attributes[:size].to_i if attributes[:size]
303 310 self.lastrev = attributes[:lastrev]
304 311 end
305 312
306 313 def is_file?
307 314 'file' == self.kind
308 315 end
309 316
310 317 def is_dir?
311 318 'dir' == self.kind
312 319 end
313 320
314 321 def is_text?
315 322 Redmine::MimeType.is_type?('text', name)
316 323 end
317 324 end
318 325
319 326 class Revisions < Array
320 327 def latest
321 328 sort {|x,y|
322 329 unless x.time.nil? or y.time.nil?
323 330 x.time <=> y.time
324 331 else
325 332 0
326 333 end
327 334 }.last
328 335 end
329 336 end
330 337
331 338 class Revision
332 339 attr_accessor :scmid, :name, :author, :time, :message,
333 340 :paths, :revision, :branch, :identifier,
334 341 :parents
335 342
336 343 def initialize(attributes={})
337 344 self.identifier = attributes[:identifier]
338 345 self.scmid = attributes[:scmid]
339 346 self.name = attributes[:name] || self.identifier
340 347 self.author = attributes[:author]
341 348 self.time = attributes[:time]
342 349 self.message = attributes[:message] || ""
343 350 self.paths = attributes[:paths]
344 351 self.revision = attributes[:revision]
345 352 self.branch = attributes[:branch]
346 353 self.parents = attributes[:parents]
347 354 end
348 355
349 356 # Returns the readable identifier.
350 357 def format_identifier
351 358 self.identifier.to_s
352 359 end
353 360 end
354 361
355 362 class Annotate
356 363 attr_reader :lines, :revisions
357 364
358 365 def initialize
359 366 @lines = []
360 367 @revisions = []
361 368 end
362 369
363 370 def add_line(line, revision)
364 371 @lines << line
365 372 @revisions << revision
366 373 end
367 374
368 375 def content
369 376 content = lines.join("\n")
370 377 end
371 378
372 379 def empty?
373 380 lines.empty?
374 381 end
375 382 end
376 383
377 384 class Branch < String
378 385 attr_accessor :revision, :scmid
379 386 end
380 387 end
381 388 end
382 389 end
@@ -1,341 +1,341
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 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 'redmine/scm/adapters/abstract_adapter'
19 19 require 'cgi'
20 20
21 21 module Redmine
22 22 module Scm
23 23 module Adapters
24 24 class MercurialAdapter < AbstractAdapter
25 25
26 26 # Mercurial executable name
27 27 HG_BIN = Redmine::Configuration['scm_mercurial_command'] || "hg"
28 28 HELPERS_DIR = File.dirname(__FILE__) + "/mercurial"
29 29 HG_HELPER_EXT = "#{HELPERS_DIR}/redminehelper.py"
30 30 TEMPLATE_NAME = "hg-template"
31 31 TEMPLATE_EXTENSION = "tmpl"
32 32
33 33 # raised if hg command exited with error, e.g. unknown revision.
34 34 class HgCommandAborted < CommandFailed; end
35 35
36 36 class << self
37 37 def client_command
38 38 @@bin ||= HG_BIN
39 39 end
40 40
41 41 def sq_bin
42 42 @@sq_bin ||= shell_quote_command
43 43 end
44 44
45 45 def client_version
46 46 @@client_version ||= (hgversion || [])
47 47 end
48 48
49 49 def client_available
50 50 client_version_above?([1, 2])
51 51 end
52 52
53 53 def hgversion
54 54 # The hg version is expressed either as a
55 55 # release number (eg 0.9.5 or 1.0) or as a revision
56 56 # id composed of 12 hexa characters.
57 57 theversion = hgversion_from_command_line.dup
58 58 if theversion.respond_to?(:force_encoding)
59 59 theversion.force_encoding('ASCII-8BIT')
60 60 end
61 61 if m = theversion.match(%r{\A(.*?)((\d+\.)+\d+)})
62 62 m[2].scan(%r{\d+}).collect(&:to_i)
63 63 end
64 64 end
65 65
66 66 def hgversion_from_command_line
67 67 shellout("#{sq_bin} --version") { |io| io.read }.to_s
68 68 end
69 69
70 70 def template_path
71 71 @@template_path ||= template_path_for(client_version)
72 72 end
73 73
74 74 def template_path_for(version)
75 75 "#{HELPERS_DIR}/#{TEMPLATE_NAME}-1.0.#{TEMPLATE_EXTENSION}"
76 76 end
77 77 end
78 78
79 79 def initialize(url, root_url=nil, login=nil, password=nil, path_encoding=nil)
80 80 super
81 81 @path_encoding = path_encoding.blank? ? 'UTF-8' : path_encoding
82 82 end
83 83
84 84 def path_encoding
85 85 @path_encoding
86 86 end
87 87
88 88 def info
89 89 tip = summary['repository']['tip']
90 90 Info.new(:root_url => CGI.unescape(summary['repository']['root']),
91 91 :lastrev => Revision.new(:revision => tip['revision'],
92 92 :scmid => tip['node']))
93 93 # rescue HgCommandAborted
94 94 rescue Exception => e
95 95 logger.error "hg: error during getting info: #{e.message}"
96 96 nil
97 97 end
98 98
99 99 def tags
100 100 as_ary(summary['repository']['tag']).map { |e| e['name'] }
101 101 end
102 102
103 103 # Returns map of {'tag' => 'nodeid', ...}
104 104 def tagmap
105 105 alist = as_ary(summary['repository']['tag']).map do |e|
106 106 e.values_at('name', 'node')
107 107 end
108 108 Hash[*alist.flatten]
109 109 end
110 110
111 111 def branches
112 112 brs = []
113 113 as_ary(summary['repository']['branch']).each do |e|
114 114 br = Branch.new(e['name'])
115 115 br.revision = e['revision']
116 116 br.scmid = e['node']
117 117 brs << br
118 118 end
119 119 brs
120 120 end
121 121
122 122 # Returns map of {'branch' => 'nodeid', ...}
123 123 def branchmap
124 124 alist = as_ary(summary['repository']['branch']).map do |e|
125 125 e.values_at('name', 'node')
126 126 end
127 127 Hash[*alist.flatten]
128 128 end
129 129
130 130 def summary
131 131 return @summary if @summary
132 132 hg 'rhsummary' do |io|
133 133 output = io.read
134 134 if output.respond_to?(:force_encoding)
135 135 output.force_encoding('UTF-8')
136 136 end
137 137 begin
138 @summary = ActiveSupport::XmlMini.parse(output)['rhsummary']
138 @summary = parse_xml(output)['rhsummary']
139 139 rescue
140 140 end
141 141 end
142 142 end
143 143 private :summary
144 144
145 145 def entries(path=nil, identifier=nil, options={})
146 146 p1 = scm_iconv(@path_encoding, 'UTF-8', path)
147 147 manifest = hg('rhmanifest', '-r', CGI.escape(hgrev(identifier)),
148 148 CGI.escape(without_leading_slash(p1.to_s))) do |io|
149 149 output = io.read
150 150 if output.respond_to?(:force_encoding)
151 151 output.force_encoding('UTF-8')
152 152 end
153 153 begin
154 ActiveSupport::XmlMini.parse(output)['rhmanifest']['repository']['manifest']
154 parse_xml(output)['rhmanifest']['repository']['manifest']
155 155 rescue
156 156 end
157 157 end
158 158 path_prefix = path.blank? ? '' : with_trailling_slash(path)
159 159
160 160 entries = Entries.new
161 161 as_ary(manifest['dir']).each do |e|
162 162 n = scm_iconv('UTF-8', @path_encoding, CGI.unescape(e['name']))
163 163 p = "#{path_prefix}#{n}"
164 164 entries << Entry.new(:name => n, :path => p, :kind => 'dir')
165 165 end
166 166
167 167 as_ary(manifest['file']).each do |e|
168 168 n = scm_iconv('UTF-8', @path_encoding, CGI.unescape(e['name']))
169 169 p = "#{path_prefix}#{n}"
170 170 lr = Revision.new(:revision => e['revision'], :scmid => e['node'],
171 171 :identifier => e['node'],
172 172 :time => Time.at(e['time'].to_i))
173 173 entries << Entry.new(:name => n, :path => p, :kind => 'file',
174 174 :size => e['size'].to_i, :lastrev => lr)
175 175 end
176 176
177 177 entries
178 178 rescue HgCommandAborted
179 179 nil # means not found
180 180 end
181 181
182 182 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
183 183 revs = Revisions.new
184 184 each_revision(path, identifier_from, identifier_to, options) { |e| revs << e }
185 185 revs
186 186 end
187 187
188 188 # Iterates the revisions by using a template file that
189 189 # makes Mercurial produce a xml output.
190 190 def each_revision(path=nil, identifier_from=nil, identifier_to=nil, options={})
191 191 hg_args = ['log', '--debug', '-C', '--style', self.class.template_path]
192 192 hg_args << '-r' << "#{hgrev(identifier_from)}:#{hgrev(identifier_to)}"
193 193 hg_args << '--limit' << options[:limit] if options[:limit]
194 194 hg_args << hgtarget(path) unless path.blank?
195 195 log = hg(*hg_args) do |io|
196 196 output = io.read
197 197 if output.respond_to?(:force_encoding)
198 198 output.force_encoding('UTF-8')
199 199 end
200 200 begin
201 201 # Mercurial < 1.5 does not support footer template for '</log>'
202 ActiveSupport::XmlMini.parse("#{output}</log>")['log']
202 parse_xml("#{output}</log>")['log']
203 203 rescue
204 204 end
205 205 end
206 206 as_ary(log['logentry']).each do |le|
207 207 cpalist = as_ary(le['paths']['path-copied']).map do |e|
208 208 [e['__content__'], e['copyfrom-path']].map do |s|
209 209 scm_iconv('UTF-8', @path_encoding, CGI.unescape(s))
210 210 end
211 211 end
212 212 cpmap = Hash[*cpalist.flatten]
213 213 paths = as_ary(le['paths']['path']).map do |e|
214 214 p = scm_iconv('UTF-8', @path_encoding, CGI.unescape(e['__content__']) )
215 215 {:action => e['action'],
216 216 :path => with_leading_slash(p),
217 217 :from_path => (cpmap.member?(p) ? with_leading_slash(cpmap[p]) : nil),
218 218 :from_revision => (cpmap.member?(p) ? le['node'] : nil)}
219 219 end.sort { |a, b| a[:path] <=> b[:path] }
220 220 parents_ary = []
221 221 as_ary(le['parents']['parent']).map do |par|
222 222 parents_ary << par['__content__'] if par['__content__'] != "000000000000"
223 223 end
224 224 yield Revision.new(:revision => le['revision'],
225 225 :scmid => le['node'],
226 226 :author => (le['author']['__content__'] rescue ''),
227 227 :time => Time.parse(le['date']['__content__']),
228 228 :message => le['msg']['__content__'],
229 229 :paths => paths,
230 230 :parents => parents_ary)
231 231 end
232 232 self
233 233 end
234 234
235 235 # Returns list of nodes in the specified branch
236 236 def nodes_in_branch(branch, options={})
237 237 hg_args = ['rhlog', '--template', '{node|short}\n', '--rhbranch', CGI.escape(branch)]
238 238 hg_args << '--from' << CGI.escape(branch)
239 239 hg_args << '--to' << '0'
240 240 hg_args << '--limit' << options[:limit] if options[:limit]
241 241 hg(*hg_args) { |io| io.readlines.map { |e| e.chomp } }
242 242 end
243 243
244 244 def diff(path, identifier_from, identifier_to=nil)
245 245 hg_args = %w|rhdiff|
246 246 if identifier_to
247 247 hg_args << '-r' << hgrev(identifier_to) << '-r' << hgrev(identifier_from)
248 248 else
249 249 hg_args << '-c' << hgrev(identifier_from)
250 250 end
251 251 unless path.blank?
252 252 p = scm_iconv(@path_encoding, 'UTF-8', path)
253 253 hg_args << CGI.escape(hgtarget(p))
254 254 end
255 255 diff = []
256 256 hg *hg_args do |io|
257 257 io.each_line do |line|
258 258 diff << line
259 259 end
260 260 end
261 261 diff
262 262 rescue HgCommandAborted
263 263 nil # means not found
264 264 end
265 265
266 266 def cat(path, identifier=nil)
267 267 p = CGI.escape(scm_iconv(@path_encoding, 'UTF-8', path))
268 268 hg 'rhcat', '-r', CGI.escape(hgrev(identifier)), hgtarget(p) do |io|
269 269 io.binmode
270 270 io.read
271 271 end
272 272 rescue HgCommandAborted
273 273 nil # means not found
274 274 end
275 275
276 276 def annotate(path, identifier=nil)
277 277 p = CGI.escape(scm_iconv(@path_encoding, 'UTF-8', path))
278 278 blame = Annotate.new
279 279 hg 'rhannotate', '-ncu', '-r', CGI.escape(hgrev(identifier)), hgtarget(p) do |io|
280 280 io.each_line do |line|
281 281 line.force_encoding('ASCII-8BIT') if line.respond_to?(:force_encoding)
282 282 next unless line =~ %r{^([^:]+)\s(\d+)\s([0-9a-f]+):\s(.*)$}
283 283 r = Revision.new(:author => $1.strip, :revision => $2, :scmid => $3,
284 284 :identifier => $3)
285 285 blame.add_line($4.rstrip, r)
286 286 end
287 287 end
288 288 blame
289 289 rescue HgCommandAborted
290 290 # means not found or cannot be annotated
291 291 Annotate.new
292 292 end
293 293
294 294 class Revision < Redmine::Scm::Adapters::Revision
295 295 # Returns the readable identifier
296 296 def format_identifier
297 297 "#{revision}:#{scmid}"
298 298 end
299 299 end
300 300
301 301 # Runs 'hg' command with the given args
302 302 def hg(*args, &block)
303 303 repo_path = root_url || url
304 304 full_args = ['-R', repo_path, '--encoding', 'utf-8']
305 305 full_args << '--config' << "extensions.redminehelper=#{HG_HELPER_EXT}"
306 306 full_args << '--config' << 'diff.git=false'
307 307 full_args += args
308 308 ret = shellout(
309 309 self.class.sq_bin + ' ' + full_args.map { |e| shell_quote e.to_s }.join(' '),
310 310 &block
311 311 )
312 312 if $? && $?.exitstatus != 0
313 313 raise HgCommandAborted, "hg exited with non-zero status: #{$?.exitstatus}"
314 314 end
315 315 ret
316 316 end
317 317 private :hg
318 318
319 319 # Returns correct revision identifier
320 320 def hgrev(identifier, sq=false)
321 321 rev = identifier.blank? ? 'tip' : identifier.to_s
322 322 rev = shell_quote(rev) if sq
323 323 rev
324 324 end
325 325 private :hgrev
326 326
327 327 def hgtarget(path)
328 328 path ||= ''
329 329 root_url + '/' + without_leading_slash(path)
330 330 end
331 331 private :hgtarget
332 332
333 333 def as_ary(o)
334 334 return [] unless o
335 335 o.is_a?(Array) ? o : Array[o]
336 336 end
337 337 private :as_ary
338 338 end
339 339 end
340 340 end
341 341 end
@@ -1,291 +1,291
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 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 'redmine/scm/adapters/abstract_adapter'
19 19 require 'uri'
20 20
21 21 module Redmine
22 22 module Scm
23 23 module Adapters
24 24 class SubversionAdapter < AbstractAdapter
25 25
26 26 # SVN executable name
27 27 SVN_BIN = Redmine::Configuration['scm_subversion_command'] || "svn"
28 28
29 29 class << self
30 30 def client_command
31 31 @@bin ||= SVN_BIN
32 32 end
33 33
34 34 def sq_bin
35 35 @@sq_bin ||= shell_quote_command
36 36 end
37 37
38 38 def client_version
39 39 @@client_version ||= (svn_binary_version || [])
40 40 end
41 41
42 42 def client_available
43 43 # --xml options are introduced in 1.3.
44 44 # http://subversion.apache.org/docs/release-notes/1.3.html
45 45 client_version_above?([1, 3])
46 46 end
47 47
48 48 def svn_binary_version
49 49 scm_version = scm_version_from_command_line.dup
50 50 if scm_version.respond_to?(:force_encoding)
51 51 scm_version.force_encoding('ASCII-8BIT')
52 52 end
53 53 if m = scm_version.match(%r{\A(.*?)((\d+\.)+\d+)})
54 54 m[2].scan(%r{\d+}).collect(&:to_i)
55 55 end
56 56 end
57 57
58 58 def scm_version_from_command_line
59 59 shellout("#{sq_bin} --version") { |io| io.read }.to_s
60 60 end
61 61 end
62 62
63 63 # Get info about the svn repository
64 64 def info
65 65 cmd = "#{self.class.sq_bin} info --xml #{target}"
66 66 cmd << credentials_string
67 67 info = nil
68 68 shellout(cmd) do |io|
69 69 output = io.read
70 70 if output.respond_to?(:force_encoding)
71 71 output.force_encoding('UTF-8')
72 72 end
73 73 begin
74 doc = ActiveSupport::XmlMini.parse(output)
74 doc = parse_xml(output)
75 75 # root_url = doc.elements["info/entry/repository/root"].text
76 76 info = Info.new({:root_url => doc['info']['entry']['repository']['root']['__content__'],
77 77 :lastrev => Revision.new({
78 78 :identifier => doc['info']['entry']['commit']['revision'],
79 79 :time => Time.parse(doc['info']['entry']['commit']['date']['__content__']).localtime,
80 80 :author => (doc['info']['entry']['commit']['author'] ? doc['info']['entry']['commit']['author']['__content__'] : "")
81 81 })
82 82 })
83 83 rescue
84 84 end
85 85 end
86 86 return nil if $? && $?.exitstatus != 0
87 87 info
88 88 rescue CommandFailed
89 89 return nil
90 90 end
91 91
92 92 # Returns an Entries collection
93 93 # or nil if the given path doesn't exist in the repository
94 94 def entries(path=nil, identifier=nil, options={})
95 95 path ||= ''
96 96 identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
97 97 entries = Entries.new
98 98 cmd = "#{self.class.sq_bin} list --xml #{target(path)}@#{identifier}"
99 99 cmd << credentials_string
100 100 shellout(cmd) do |io|
101 101 output = io.read
102 102 if output.respond_to?(:force_encoding)
103 103 output.force_encoding('UTF-8')
104 104 end
105 105 begin
106 doc = ActiveSupport::XmlMini.parse(output)
106 doc = parse_xml(output)
107 107 each_xml_element(doc['lists']['list'], 'entry') do |entry|
108 108 commit = entry['commit']
109 109 commit_date = commit['date']
110 110 # Skip directory if there is no commit date (usually that
111 111 # means that we don't have read access to it)
112 112 next if entry['kind'] == 'dir' && commit_date.nil?
113 113 name = entry['name']['__content__']
114 114 entries << Entry.new({:name => URI.unescape(name),
115 115 :path => ((path.empty? ? "" : "#{path}/") + name),
116 116 :kind => entry['kind'],
117 117 :size => ((s = entry['size']) ? s['__content__'].to_i : nil),
118 118 :lastrev => Revision.new({
119 119 :identifier => commit['revision'],
120 120 :time => Time.parse(commit_date['__content__'].to_s).localtime,
121 121 :author => ((a = commit['author']) ? a['__content__'] : nil)
122 122 })
123 123 })
124 124 end
125 125 rescue Exception => e
126 126 logger.error("Error parsing svn output: #{e.message}")
127 127 logger.error("Output was:\n #{output}")
128 128 end
129 129 end
130 130 return nil if $? && $?.exitstatus != 0
131 131 logger.debug("Found #{entries.size} entries in the repository for #{target(path)}") if logger && logger.debug?
132 132 entries.sort_by_name
133 133 end
134 134
135 135 def properties(path, identifier=nil)
136 136 # proplist xml output supported in svn 1.5.0 and higher
137 137 return nil unless self.class.client_version_above?([1, 5, 0])
138 138
139 139 identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
140 140 cmd = "#{self.class.sq_bin} proplist --verbose --xml #{target(path)}@#{identifier}"
141 141 cmd << credentials_string
142 142 properties = {}
143 143 shellout(cmd) do |io|
144 144 output = io.read
145 145 if output.respond_to?(:force_encoding)
146 146 output.force_encoding('UTF-8')
147 147 end
148 148 begin
149 doc = ActiveSupport::XmlMini.parse(output)
149 doc = parse_xml(output)
150 150 each_xml_element(doc['properties']['target'], 'property') do |property|
151 151 properties[ property['name'] ] = property['__content__'].to_s
152 152 end
153 153 rescue
154 154 end
155 155 end
156 156 return nil if $? && $?.exitstatus != 0
157 157 properties
158 158 end
159 159
160 160 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
161 161 path ||= ''
162 162 identifier_from = (identifier_from && identifier_from.to_i > 0) ? identifier_from.to_i : "HEAD"
163 163 identifier_to = (identifier_to && identifier_to.to_i > 0) ? identifier_to.to_i : 1
164 164 revisions = Revisions.new
165 165 cmd = "#{self.class.sq_bin} log --xml -r #{identifier_from}:#{identifier_to}"
166 166 cmd << credentials_string
167 167 cmd << " --verbose " if options[:with_paths]
168 168 cmd << " --limit #{options[:limit].to_i}" if options[:limit]
169 169 cmd << ' ' + target(path)
170 170 shellout(cmd) do |io|
171 171 output = io.read
172 172 if output.respond_to?(:force_encoding)
173 173 output.force_encoding('UTF-8')
174 174 end
175 175 begin
176 doc = ActiveSupport::XmlMini.parse(output)
176 doc = parse_xml(output)
177 177 each_xml_element(doc['log'], 'logentry') do |logentry|
178 178 paths = []
179 179 each_xml_element(logentry['paths'], 'path') do |path|
180 180 paths << {:action => path['action'],
181 181 :path => path['__content__'],
182 182 :from_path => path['copyfrom-path'],
183 183 :from_revision => path['copyfrom-rev']
184 184 }
185 185 end if logentry['paths'] && logentry['paths']['path']
186 186 paths.sort! { |x,y| x[:path] <=> y[:path] }
187 187
188 188 revisions << Revision.new({:identifier => logentry['revision'],
189 189 :author => (logentry['author'] ? logentry['author']['__content__'] : ""),
190 190 :time => Time.parse(logentry['date']['__content__'].to_s).localtime,
191 191 :message => logentry['msg']['__content__'],
192 192 :paths => paths
193 193 })
194 194 end
195 195 rescue
196 196 end
197 197 end
198 198 return nil if $? && $?.exitstatus != 0
199 199 revisions
200 200 end
201 201
202 202 def diff(path, identifier_from, identifier_to=nil)
203 203 path ||= ''
204 204 identifier_from = (identifier_from and identifier_from.to_i > 0) ? identifier_from.to_i : ''
205 205
206 206 identifier_to = (identifier_to and identifier_to.to_i > 0) ? identifier_to.to_i : (identifier_from.to_i - 1)
207 207
208 208 cmd = "#{self.class.sq_bin} diff -r "
209 209 cmd << "#{identifier_to}:"
210 210 cmd << "#{identifier_from}"
211 211 cmd << " #{target(path)}@#{identifier_from}"
212 212 cmd << credentials_string
213 213 diff = []
214 214 shellout(cmd) do |io|
215 215 io.each_line do |line|
216 216 diff << line
217 217 end
218 218 end
219 219 return nil if $? && $?.exitstatus != 0
220 220 diff
221 221 end
222 222
223 223 def cat(path, identifier=nil)
224 224 identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
225 225 cmd = "#{self.class.sq_bin} cat #{target(path)}@#{identifier}"
226 226 cmd << credentials_string
227 227 cat = nil
228 228 shellout(cmd) do |io|
229 229 io.binmode
230 230 cat = io.read
231 231 end
232 232 return nil if $? && $?.exitstatus != 0
233 233 cat
234 234 end
235 235
236 236 def annotate(path, identifier=nil)
237 237 identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
238 238 cmd = "#{self.class.sq_bin} blame #{target(path)}@#{identifier}"
239 239 cmd << credentials_string
240 240 blame = Annotate.new
241 241 shellout(cmd) do |io|
242 242 io.each_line do |line|
243 243 next unless line =~ %r{^\s*(\d+)\s*(\S+)\s(.*)$}
244 244 rev = $1
245 245 blame.add_line($3.rstrip,
246 246 Revision.new(
247 247 :identifier => rev,
248 248 :revision => rev,
249 249 :author => $2.strip
250 250 ))
251 251 end
252 252 end
253 253 return nil if $? && $?.exitstatus != 0
254 254 blame
255 255 end
256 256
257 257 private
258 258
259 259 def credentials_string
260 260 str = ''
261 261 str << " --username #{shell_quote(@login)}" unless @login.blank?
262 262 str << " --password #{shell_quote(@password)}" unless @login.blank? || @password.blank?
263 263 str << " --no-auth-cache --non-interactive"
264 264 str
265 265 end
266 266
267 267 # Helper that iterates over the child elements of a xml node
268 268 # MiniXml returns a hash when a single child is found
269 269 # or an array of hashes for multiple children
270 270 def each_xml_element(node, name)
271 271 if node && node[name]
272 272 if node[name].is_a?(Hash)
273 273 yield node[name]
274 274 else
275 275 node[name].each do |element|
276 276 yield element
277 277 end
278 278 end
279 279 end
280 280 end
281 281
282 282 def target(path = '')
283 283 base = path.match(/^\//) ? root_url : url
284 284 uri = "#{base}/#{path}"
285 285 uri = URI.escape(URI.escape(uri), '[]')
286 286 shell_quote(uri.gsub(/[?<>\*]/, ''))
287 287 end
288 288 end
289 289 end
290 290 end
291 291 end
General Comments 0
You need to be logged in to leave comments. Login now