##// END OF EJS Templates
SCM AbstractAdapter use shell_quote to more properly escape path (closes #838 by John Goerzen)....
Jean-Philippe Lang -
r1224:2c1e63720ec9
parent child
Show More
@@ -1,386 +1,386
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 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 def initialize(url, root_url=nil, login=nil, password=nil)
28 28 @url = url
29 29 @login = login if login && !login.empty?
30 30 @password = (password || "") if @login
31 31 @root_url = root_url.blank? ? retrieve_root_url : root_url
32 32 end
33 33
34 34 def adapter_name
35 35 'Abstract'
36 36 end
37 37
38 38 def supports_cat?
39 39 true
40 40 end
41 41
42 42 def supports_annotate?
43 43 respond_to?('annotate')
44 44 end
45 45
46 46 def root_url
47 47 @root_url
48 48 end
49 49
50 50 def url
51 51 @url
52 52 end
53 53
54 54 # get info about the svn repository
55 55 def info
56 56 return nil
57 57 end
58 58
59 59 # Returns the entry identified by path and revision identifier
60 60 # or nil if entry doesn't exist in the repository
61 61 def entry(path=nil, identifier=nil)
62 62 e = entries(path, identifier)
63 63 e ? e.first : nil
64 64 end
65 65
66 66 # Returns an Entries collection
67 67 # or nil if the given path doesn't exist in the repository
68 68 def entries(path=nil, identifier=nil)
69 69 return nil
70 70 end
71 71
72 72 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
73 73 return nil
74 74 end
75 75
76 76 def diff(path, identifier_from, identifier_to=nil, type="inline")
77 77 return nil
78 78 end
79 79
80 80 def cat(path, identifier=nil)
81 81 return nil
82 82 end
83 83
84 84 def with_leading_slash(path)
85 85 path ||= ''
86 86 (path[0,1]!="/") ? "/#{path}" : path
87 87 end
88 88
89 89 def shell_quote(str)
90 90 if RUBY_PLATFORM =~ /mswin/
91 91 '"' + str.gsub(/"/, '\\"') + '"'
92 92 else
93 93 "'" + str.gsub(/'/, "'\"'\"'") + "'"
94 94 end
95 95 end
96 96
97 97 private
98 98 def retrieve_root_url
99 99 info = self.info
100 100 info ? info.root_url : nil
101 101 end
102 102
103 103 def target(path)
104 path ||= ""
105 base = path.match(/^\//) ? root_url : url
106 " \"" << "#{base}/#{path}".gsub(/["?<>\*]/, '') << "\""
104 path ||= ''
105 base = path.match(/^\//) ? root_url : url
106 shell_quote("#{base}/#{path}".gsub(/[?<>\*]/, ''))
107 107 end
108 108
109 109 def logger
110 110 RAILS_DEFAULT_LOGGER
111 111 end
112 112
113 113 def shellout(cmd, &block)
114 114 logger.debug "Shelling out: #{cmd}" if logger && logger.debug?
115 115 begin
116 116 IO.popen(cmd, "r+") do |io|
117 117 io.close_write
118 118 block.call(io) if block_given?
119 119 end
120 120 rescue Errno::ENOENT => e
121 121 # The command failed, log it and re-raise
122 122 logger.error("SCM command failed: #{cmd}\n with: #{e.message}")
123 123 raise CommandFailed.new(e.message)
124 124 end
125 125 end
126 126 end
127 127
128 128 class Entries < Array
129 129 def sort_by_name
130 130 sort {|x,y|
131 131 if x.kind == y.kind
132 132 x.name <=> y.name
133 133 else
134 134 x.kind <=> y.kind
135 135 end
136 136 }
137 137 end
138 138
139 139 def revisions
140 140 revisions ||= Revisions.new(collect{|entry| entry.lastrev}.compact)
141 141 end
142 142 end
143 143
144 144 class Info
145 145 attr_accessor :root_url, :lastrev
146 146 def initialize(attributes={})
147 147 self.root_url = attributes[:root_url] if attributes[:root_url]
148 148 self.lastrev = attributes[:lastrev]
149 149 end
150 150 end
151 151
152 152 class Entry
153 153 attr_accessor :name, :path, :kind, :size, :lastrev
154 154 def initialize(attributes={})
155 155 self.name = attributes[:name] if attributes[:name]
156 156 self.path = attributes[:path] if attributes[:path]
157 157 self.kind = attributes[:kind] if attributes[:kind]
158 158 self.size = attributes[:size].to_i if attributes[:size]
159 159 self.lastrev = attributes[:lastrev]
160 160 end
161 161
162 162 def is_file?
163 163 'file' == self.kind
164 164 end
165 165
166 166 def is_dir?
167 167 'dir' == self.kind
168 168 end
169 169
170 170 def is_text?
171 171 Redmine::MimeType.is_type?('text', name)
172 172 end
173 173 end
174 174
175 175 class Revisions < Array
176 176 def latest
177 177 sort {|x,y|
178 178 unless x.time.nil? or y.time.nil?
179 179 x.time <=> y.time
180 180 else
181 181 0
182 182 end
183 183 }.last
184 184 end
185 185 end
186 186
187 187 class Revision
188 188 attr_accessor :identifier, :scmid, :name, :author, :time, :message, :paths, :revision, :branch
189 189 def initialize(attributes={})
190 190 self.identifier = attributes[:identifier]
191 191 self.scmid = attributes[:scmid]
192 192 self.name = attributes[:name] || self.identifier
193 193 self.author = attributes[:author]
194 194 self.time = attributes[:time]
195 195 self.message = attributes[:message] || ""
196 196 self.paths = attributes[:paths]
197 197 self.revision = attributes[:revision]
198 198 self.branch = attributes[:branch]
199 199 end
200 200
201 201 end
202 202
203 203 # A line of Diff
204 204 class Diff
205 205 attr_accessor :nb_line_left
206 206 attr_accessor :line_left
207 207 attr_accessor :nb_line_right
208 208 attr_accessor :line_right
209 209 attr_accessor :type_diff_right
210 210 attr_accessor :type_diff_left
211 211
212 212 def initialize ()
213 213 self.nb_line_left = ''
214 214 self.nb_line_right = ''
215 215 self.line_left = ''
216 216 self.line_right = ''
217 217 self.type_diff_right = ''
218 218 self.type_diff_left = ''
219 219 end
220 220
221 221 def inspect
222 222 puts '### Start Line Diff ###'
223 223 puts self.nb_line_left
224 224 puts self.line_left
225 225 puts self.nb_line_right
226 226 puts self.line_right
227 227 end
228 228 end
229 229
230 230 class DiffTableList < Array
231 231 def initialize (diff, type="inline")
232 232 diff_table = DiffTable.new type
233 233 diff.each do |line|
234 234 if line =~ /^(---|\+\+\+) (.*)$/
235 235 self << diff_table if diff_table.length > 1
236 236 diff_table = DiffTable.new type
237 237 end
238 238 a = diff_table.add_line line
239 239 end
240 240 self << diff_table unless diff_table.empty?
241 241 self
242 242 end
243 243 end
244 244
245 245 # Class for create a Diff
246 246 class DiffTable < Hash
247 247 attr_reader :file_name, :line_num_l, :line_num_r
248 248
249 249 # Initialize with a Diff file and the type of Diff View
250 250 # The type view must be inline or sbs (side_by_side)
251 251 def initialize(type="inline")
252 252 @parsing = false
253 253 @nb_line = 1
254 254 @start = false
255 255 @before = 'same'
256 256 @second = true
257 257 @type = type
258 258 end
259 259
260 260 # Function for add a line of this Diff
261 261 def add_line(line)
262 262 unless @parsing
263 263 if line =~ /^(---|\+\+\+) (.*)$/
264 264 @file_name = $2
265 265 return false
266 266 elsif line =~ /^@@ (\+|\-)(\d+)(,\d+)? (\+|\-)(\d+)(,\d+)? @@/
267 267 @line_num_l = $5.to_i
268 268 @line_num_r = $2.to_i
269 269 @parsing = true
270 270 end
271 271 else
272 272 if line =~ /^[^\+\-\s@\\]/
273 273 self.delete(self.keys.sort.last)
274 274 @parsing = false
275 275 return false
276 276 elsif line =~ /^@@ (\+|\-)(\d+)(,\d+)? (\+|\-)(\d+)(,\d+)? @@/
277 277 @line_num_l = $5.to_i
278 278 @line_num_r = $2.to_i
279 279 else
280 280 @nb_line += 1 if parse_line(line, @type)
281 281 end
282 282 end
283 283 return true
284 284 end
285 285
286 286 def inspect
287 287 puts '### DIFF TABLE ###'
288 288 puts "file : #{file_name}"
289 289 self.each do |d|
290 290 d.inspect
291 291 end
292 292 end
293 293
294 294 private
295 295 # Test if is a Side By Side type
296 296 def sbs?(type, func)
297 297 if @start and type == "sbs"
298 298 if @before == func and @second
299 299 tmp_nb_line = @nb_line
300 300 self[tmp_nb_line] = Diff.new
301 301 else
302 302 @second = false
303 303 tmp_nb_line = @start
304 304 @start += 1
305 305 @nb_line -= 1
306 306 end
307 307 else
308 308 tmp_nb_line = @nb_line
309 309 @start = @nb_line
310 310 self[tmp_nb_line] = Diff.new
311 311 @second = true
312 312 end
313 313 unless self[tmp_nb_line]
314 314 @nb_line += 1
315 315 self[tmp_nb_line] = Diff.new
316 316 else
317 317 self[tmp_nb_line]
318 318 end
319 319 end
320 320
321 321 # Escape the HTML for the diff
322 322 def escapeHTML(line)
323 323 CGI.escapeHTML(line)
324 324 end
325 325
326 326 def parse_line(line, type="inline")
327 327 if line[0, 1] == "+"
328 328 diff = sbs? type, 'add'
329 329 @before = 'add'
330 330 diff.line_left = escapeHTML line[1..-1]
331 331 diff.nb_line_left = @line_num_l
332 332 diff.type_diff_left = 'diff_in'
333 333 @line_num_l += 1
334 334 true
335 335 elsif line[0, 1] == "-"
336 336 diff = sbs? type, 'remove'
337 337 @before = 'remove'
338 338 diff.line_right = escapeHTML line[1..-1]
339 339 diff.nb_line_right = @line_num_r
340 340 diff.type_diff_right = 'diff_out'
341 341 @line_num_r += 1
342 342 true
343 343 elsif line[0, 1] =~ /\s/
344 344 @before = 'same'
345 345 @start = false
346 346 diff = Diff.new
347 347 diff.line_right = escapeHTML line[1..-1]
348 348 diff.nb_line_right = @line_num_r
349 349 diff.line_left = escapeHTML line[1..-1]
350 350 diff.nb_line_left = @line_num_l
351 351 self[@nb_line] = diff
352 352 @line_num_l += 1
353 353 @line_num_r += 1
354 354 true
355 355 elsif line[0, 1] = "\\"
356 356 true
357 357 else
358 358 false
359 359 end
360 360 end
361 361 end
362 362
363 363 class Annotate
364 364 attr_reader :lines, :revisions
365 365
366 366 def initialize
367 367 @lines = []
368 368 @revisions = []
369 369 end
370 370
371 371 def add_line(line, revision)
372 372 @lines << line
373 373 @revisions << revision
374 374 end
375 375
376 376 def content
377 377 content = lines.join("\n")
378 378 end
379 379
380 380 def empty?
381 381 lines.empty?
382 382 end
383 383 end
384 384 end
385 385 end
386 386 end
@@ -1,193 +1,193
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 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 'rexml/document'
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 = "svn"
28 28
29 29 # Get info about the svn repository
30 30 def info
31 31 cmd = "#{SVN_BIN} info --xml #{target('')}"
32 32 cmd << credentials_string
33 33 info = nil
34 34 shellout(cmd) do |io|
35 35 begin
36 36 doc = REXML::Document.new(io)
37 37 #root_url = doc.elements["info/entry/repository/root"].text
38 38 info = Info.new({:root_url => doc.elements["info/entry/repository/root"].text,
39 39 :lastrev => Revision.new({
40 40 :identifier => doc.elements["info/entry/commit"].attributes['revision'],
41 41 :time => Time.parse(doc.elements["info/entry/commit/date"].text).localtime,
42 42 :author => (doc.elements["info/entry/commit/author"] ? doc.elements["info/entry/commit/author"].text : "")
43 43 })
44 44 })
45 45 rescue
46 46 end
47 47 end
48 48 return nil if $? && $?.exitstatus != 0
49 49 info
50 50 rescue CommandFailed
51 51 return nil
52 52 end
53 53
54 54 # Returns the entry identified by path and revision identifier
55 55 # or nil if entry doesn't exist in the repository
56 56 def entry(path=nil, identifier=nil)
57 57 e = entries(path, identifier)
58 58 e ? e.first : nil
59 59 end
60 60
61 61 # Returns an Entries collection
62 62 # or nil if the given path doesn't exist in the repository
63 63 def entries(path=nil, identifier=nil)
64 64 path ||= ''
65 65 identifier = 'HEAD' unless identifier and identifier > 0
66 66 entries = Entries.new
67 67 cmd = "#{SVN_BIN} list --xml #{target(path)}@#{identifier}"
68 68 cmd << credentials_string
69 69 shellout(cmd) do |io|
70 70 output = io.read
71 71 begin
72 72 doc = REXML::Document.new(output)
73 73 doc.elements.each("lists/list/entry") do |entry|
74 74 entries << Entry.new({:name => entry.elements['name'].text,
75 75 :path => ((path.empty? ? "" : "#{path}/") + entry.elements['name'].text),
76 76 :kind => entry.attributes['kind'],
77 77 :size => (entry.elements['size'] and entry.elements['size'].text).to_i,
78 78 :lastrev => Revision.new({
79 79 :identifier => entry.elements['commit'].attributes['revision'],
80 80 :time => Time.parse(entry.elements['commit'].elements['date'].text).localtime,
81 81 :author => (entry.elements['commit'].elements['author'] ? entry.elements['commit'].elements['author'].text : "")
82 82 })
83 83 })
84 84 end
85 85 rescue Exception => e
86 86 logger.error("Error parsing svn output: #{e.message}")
87 87 logger.error("Output was:\n #{output}")
88 88 end
89 89 end
90 90 return nil if $? && $?.exitstatus != 0
91 91 logger.debug("Found #{entries.size} entries in the repository for #{target(path)}") if logger && logger.debug?
92 92 entries.sort_by_name
93 93 end
94 94
95 95 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
96 96 path ||= ''
97 97 identifier_from = 'HEAD' unless identifier_from and identifier_from.to_i > 0
98 98 identifier_to = 1 unless identifier_to and identifier_to.to_i > 0
99 99 revisions = Revisions.new
100 100 cmd = "#{SVN_BIN} log --xml -r #{identifier_from}:#{identifier_to}"
101 101 cmd << credentials_string
102 102 cmd << " --verbose " if options[:with_paths]
103 cmd << target(path)
103 cmd << ' ' + target(path)
104 104 shellout(cmd) do |io|
105 105 begin
106 106 doc = REXML::Document.new(io)
107 107 doc.elements.each("log/logentry") do |logentry|
108 108 paths = []
109 109 logentry.elements.each("paths/path") do |path|
110 110 paths << {:action => path.attributes['action'],
111 111 :path => path.text,
112 112 :from_path => path.attributes['copyfrom-path'],
113 113 :from_revision => path.attributes['copyfrom-rev']
114 114 }
115 115 end
116 116 paths.sort! { |x,y| x[:path] <=> y[:path] }
117 117
118 118 revisions << Revision.new({:identifier => logentry.attributes['revision'],
119 119 :author => (logentry.elements['author'] ? logentry.elements['author'].text : ""),
120 120 :time => Time.parse(logentry.elements['date'].text).localtime,
121 121 :message => logentry.elements['msg'].text,
122 122 :paths => paths
123 123 })
124 124 end
125 125 rescue
126 126 end
127 127 end
128 128 return nil if $? && $?.exitstatus != 0
129 129 revisions
130 130 end
131 131
132 132 def diff(path, identifier_from, identifier_to=nil, type="inline")
133 133 path ||= ''
134 134 if identifier_to and identifier_to.to_i > 0
135 135 identifier_to = identifier_to.to_i
136 136 else
137 137 identifier_to = identifier_from.to_i - 1
138 138 end
139 139 cmd = "#{SVN_BIN} diff -r "
140 140 cmd << "#{identifier_to}:"
141 141 cmd << "#{identifier_from}"
142 cmd << "#{target(path)}@#{identifier_from}"
142 cmd << " #{target(path)}@#{identifier_from}"
143 143 cmd << credentials_string
144 144 diff = []
145 145 shellout(cmd) do |io|
146 146 io.each_line do |line|
147 147 diff << line
148 148 end
149 149 end
150 150 return nil if $? && $?.exitstatus != 0
151 151 DiffTableList.new diff, type
152 152 end
153 153
154 154 def cat(path, identifier=nil)
155 155 identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
156 156 cmd = "#{SVN_BIN} cat #{target(path)}@#{identifier}"
157 157 cmd << credentials_string
158 158 cat = nil
159 159 shellout(cmd) do |io|
160 160 io.binmode
161 161 cat = io.read
162 162 end
163 163 return nil if $? && $?.exitstatus != 0
164 164 cat
165 165 end
166 166
167 167 def annotate(path, identifier=nil)
168 168 identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
169 169 cmd = "#{SVN_BIN} blame #{target(path)}@#{identifier}"
170 170 cmd << credentials_string
171 171 blame = Annotate.new
172 172 shellout(cmd) do |io|
173 173 io.each_line do |line|
174 174 next unless line =~ %r{^\s*(\d+)\s*(\S+)\s(.*)$}
175 175 blame.add_line($3.rstrip, Revision.new(:identifier => $1.to_i, :author => $2.strip))
176 176 end
177 177 end
178 178 return nil if $? && $?.exitstatus != 0
179 179 blame
180 180 end
181 181
182 182 private
183 183
184 184 def credentials_string
185 185 str = ''
186 186 str << " --username #{shell_quote(@login)}" unless @login.blank?
187 187 str << " --password #{shell_quote(@password)}" unless @login.blank? || @password.blank?
188 188 str
189 189 end
190 190 end
191 191 end
192 192 end
193 193 end
General Comments 0
You need to be logged in to leave comments. Login now