##// END OF EJS Templates
Fixed that some arguments where not properly escaped in scm adapters....
Jean-Philippe Lang -
r4425:7d7c67dabad1
parent child
Show More
@@ -1,186 +1,189
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
20 20 module Redmine
21 21 module Scm
22 22 module Adapters
23 23 class BazaarAdapter < AbstractAdapter
24 24
25 25 # Bazaar executable name
26 26 BZR_BIN = "bzr"
27 27
28 28 # Get info about the repository
29 29 def info
30 30 cmd = "#{BZR_BIN} revno #{target('')}"
31 31 info = nil
32 32 shellout(cmd) do |io|
33 33 if io.read =~ %r{^(\d+)\r?$}
34 34 info = Info.new({:root_url => url,
35 35 :lastrev => Revision.new({
36 36 :identifier => $1
37 37 })
38 38 })
39 39 end
40 40 end
41 41 return nil if $? && $?.exitstatus != 0
42 42 info
43 43 rescue CommandFailed
44 44 return nil
45 45 end
46 46
47 47 # Returns an Entries collection
48 48 # or nil if the given path doesn't exist in the repository
49 49 def entries(path=nil, identifier=nil)
50 50 path ||= ''
51 51 entries = Entries.new
52 52 cmd = "#{BZR_BIN} ls -v --show-ids"
53 53 identifier = -1 unless identifier && identifier.to_i > 0
54 54 cmd << " -r#{identifier.to_i}"
55 55 cmd << " #{target(path)}"
56 56 shellout(cmd) do |io|
57 57 prefix = "#{url}/#{path}".gsub('\\', '/')
58 58 logger.debug "PREFIX: #{prefix}"
59 59 re = %r{^V\s+(#{Regexp.escape(prefix)})?(\/?)([^\/]+)(\/?)\s+(\S+)\r?$}
60 60 io.each_line do |line|
61 61 next unless line =~ re
62 62 entries << Entry.new({:name => $3.strip,
63 63 :path => ((path.empty? ? "" : "#{path}/") + $3.strip),
64 64 :kind => ($4.blank? ? 'file' : 'dir'),
65 65 :size => nil,
66 66 :lastrev => Revision.new(:revision => $5.strip)
67 67 })
68 68 end
69 69 end
70 70 return nil if $? && $?.exitstatus != 0
71 71 logger.debug("Found #{entries.size} entries in the repository for #{target(path)}") if logger && logger.debug?
72 72 entries.sort_by_name
73 73 end
74 74
75 75 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
76 76 path ||= ''
77 identifier_from = 'last:1' unless identifier_from and identifier_from.to_i > 0
78 identifier_to = 1 unless identifier_to and identifier_to.to_i > 0
77 identifier_from = (identifier_from and identifier_from.to_i > 0) ? identifier_from.to_i : 'last:1'
78 identifier_to = (identifier_to and identifier_to.to_i > 0) ? identifier_to.to_i : 1
79 79 revisions = Revisions.new
80 cmd = "#{BZR_BIN} log -v --show-ids -r#{identifier_to.to_i}..#{identifier_from} #{target(path)}"
80 cmd = "#{BZR_BIN} log -v --show-ids -r#{identifier_to}..#{identifier_from} #{target(path)}"
81 81 shellout(cmd) do |io|
82 82 revision = nil
83 83 parsing = nil
84 84 io.each_line do |line|
85 85 if line =~ /^----/
86 86 revisions << revision if revision
87 87 revision = Revision.new(:paths => [], :message => '')
88 88 parsing = nil
89 89 else
90 90 next unless revision
91 91
92 92 if line =~ /^revno: (\d+)($|\s\[merge\]$)/
93 93 revision.identifier = $1.to_i
94 94 elsif line =~ /^committer: (.+)$/
95 95 revision.author = $1.strip
96 96 elsif line =~ /^revision-id:(.+)$/
97 97 revision.scmid = $1.strip
98 98 elsif line =~ /^timestamp: (.+)$/
99 99 revision.time = Time.parse($1).localtime
100 100 elsif line =~ /^ -----/
101 101 # partial revisions
102 102 parsing = nil unless parsing == 'message'
103 103 elsif line =~ /^(message|added|modified|removed|renamed):/
104 104 parsing = $1
105 105 elsif line =~ /^ (.*)$/
106 106 if parsing == 'message'
107 107 revision.message << "#{$1}\n"
108 108 else
109 109 if $1 =~ /^(.*)\s+(\S+)$/
110 110 path = $1.strip
111 111 revid = $2
112 112 case parsing
113 113 when 'added'
114 114 revision.paths << {:action => 'A', :path => "/#{path}", :revision => revid}
115 115 when 'modified'
116 116 revision.paths << {:action => 'M', :path => "/#{path}", :revision => revid}
117 117 when 'removed'
118 118 revision.paths << {:action => 'D', :path => "/#{path}", :revision => revid}
119 119 when 'renamed'
120 120 new_path = path.split('=>').last
121 121 revision.paths << {:action => 'M', :path => "/#{new_path.strip}", :revision => revid} if new_path
122 122 end
123 123 end
124 124 end
125 125 else
126 126 parsing = nil
127 127 end
128 128 end
129 129 end
130 130 revisions << revision if revision
131 131 end
132 132 return nil if $? && $?.exitstatus != 0
133 133 revisions
134 134 end
135 135
136 136 def diff(path, identifier_from, identifier_to=nil)
137 137 path ||= ''
138 138 if identifier_to
139 139 identifier_to = identifier_to.to_i
140 140 else
141 141 identifier_to = identifier_from.to_i - 1
142 142 end
143 if identifier_from
144 identifier_from = identifier_from.to_i
145 end
143 146 cmd = "#{BZR_BIN} diff -r#{identifier_to}..#{identifier_from} #{target(path)}"
144 147 diff = []
145 148 shellout(cmd) do |io|
146 149 io.each_line do |line|
147 150 diff << line
148 151 end
149 152 end
150 153 #return nil if $? && $?.exitstatus != 0
151 154 diff
152 155 end
153 156
154 157 def cat(path, identifier=nil)
155 158 cmd = "#{BZR_BIN} cat"
156 159 cmd << " -r#{identifier.to_i}" if identifier && identifier.to_i > 0
157 160 cmd << " #{target(path)}"
158 161 cat = nil
159 162 shellout(cmd) do |io|
160 163 io.binmode
161 164 cat = io.read
162 165 end
163 166 return nil if $? && $?.exitstatus != 0
164 167 cat
165 168 end
166 169
167 170 def annotate(path, identifier=nil)
168 171 cmd = "#{BZR_BIN} annotate --all"
169 172 cmd << " -r#{identifier.to_i}" if identifier && identifier.to_i > 0
170 173 cmd << " #{target(path)}"
171 174 blame = Annotate.new
172 175 shellout(cmd) do |io|
173 176 author = nil
174 177 identifier = nil
175 178 io.each_line do |line|
176 179 next unless line =~ %r{^(\d+) ([^|]+)\| (.*)$}
177 180 blame.add_line($3.rstrip, Revision.new(:identifier => $1.to_i, :author => $2.strip))
178 181 end
179 182 end
180 183 return nil if $? && $?.exitstatus != 0
181 184 blame
182 185 end
183 186 end
184 187 end
185 188 end
186 189 end
@@ -1,362 +1,362
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
20 20 module Redmine
21 21 module Scm
22 22 module Adapters
23 23 class CvsAdapter < AbstractAdapter
24 24
25 25 # CVS executable name
26 26 CVS_BIN = "cvs"
27 27
28 28 # Guidelines for the input:
29 29 # url -> the project-path, relative to the cvsroot (eg. module name)
30 30 # root_url -> the good old, sometimes damned, CVSROOT
31 31 # login -> unnecessary
32 32 # password -> unnecessary too
33 33 def initialize(url, root_url=nil, login=nil, password=nil)
34 34 @url = url
35 35 @login = login if login && !login.empty?
36 36 @password = (password || "") if @login
37 37 #TODO: better Exception here (IllegalArgumentException)
38 38 raise CommandFailed if root_url.blank?
39 39 @root_url = root_url
40 40 end
41 41
42 42 def root_url
43 43 @root_url
44 44 end
45 45
46 46 def url
47 47 @url
48 48 end
49 49
50 50 def info
51 51 logger.debug "<cvs> info"
52 52 Info.new({:root_url => @root_url, :lastrev => nil})
53 53 end
54 54
55 55 def get_previous_revision(revision)
56 56 CvsRevisionHelper.new(revision).prevRev
57 57 end
58 58
59 59 # Returns an Entries collection
60 60 # or nil if the given path doesn't exist in the repository
61 61 # this method is used by the repository-browser (aka LIST)
62 62 def entries(path=nil, identifier=nil)
63 63 logger.debug "<cvs> entries '#{path}' with identifier '#{identifier}'"
64 64 path_with_project="#{url}#{with_leading_slash(path)}"
65 65 entries = Entries.new
66 cmd = "#{CVS_BIN} -d #{root_url} rls -e"
66 cmd = "#{CVS_BIN} -d #{shell_quote root_url} rls -e"
67 67 cmd << " -D \"#{time_to_cvstime(identifier)}\"" if identifier
68 68 cmd << " #{shell_quote path_with_project}"
69 69 shellout(cmd) do |io|
70 70 io.each_line(){|line|
71 71 fields=line.chop.split('/',-1)
72 72 logger.debug(">>InspectLine #{fields.inspect}")
73 73
74 74 if fields[0]!="D"
75 75 entries << Entry.new({:name => fields[-5],
76 76 #:path => fields[-4].include?(path)?fields[-4]:(path + "/"+ fields[-4]),
77 77 :path => "#{path}/#{fields[-5]}",
78 78 :kind => 'file',
79 79 :size => nil,
80 80 :lastrev => Revision.new({
81 81 :revision => fields[-4],
82 82 :name => fields[-4],
83 83 :time => Time.parse(fields[-3]),
84 84 :author => ''
85 85 })
86 86 })
87 87 else
88 88 entries << Entry.new({:name => fields[1],
89 89 :path => "#{path}/#{fields[1]}",
90 90 :kind => 'dir',
91 91 :size => nil,
92 92 :lastrev => nil
93 93 })
94 94 end
95 95 }
96 96 end
97 97 return nil if $? && $?.exitstatus != 0
98 98 entries.sort_by_name
99 99 end
100 100
101 101 STARTLOG="----------------------------"
102 102 ENDLOG ="============================================================================="
103 103
104 104 # Returns all revisions found between identifier_from and identifier_to
105 105 # in the repository. both identifier have to be dates or nil.
106 106 # these method returns nothing but yield every result in block
107 107 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}, &block)
108 108 logger.debug "<cvs> revisions path:'#{path}',identifier_from #{identifier_from}, identifier_to #{identifier_to}"
109 109
110 110 path_with_project="#{url}#{with_leading_slash(path)}"
111 cmd = "#{CVS_BIN} -d #{root_url} rlog"
111 cmd = "#{CVS_BIN} -d #{shell_quote root_url} rlog"
112 112 cmd << " -d\">#{time_to_cvstime(identifier_from)}\"" if identifier_from
113 113 cmd << " #{shell_quote path_with_project}"
114 114 shellout(cmd) do |io|
115 115 state="entry_start"
116 116
117 117 commit_log=String.new
118 118 revision=nil
119 119 date=nil
120 120 author=nil
121 121 entry_path=nil
122 122 entry_name=nil
123 123 file_state=nil
124 124 branch_map=nil
125 125
126 126 io.each_line() do |line|
127 127
128 128 if state!="revision" && /^#{ENDLOG}/ =~ line
129 129 commit_log=String.new
130 130 revision=nil
131 131 state="entry_start"
132 132 end
133 133
134 134 if state=="entry_start"
135 135 branch_map=Hash.new
136 136 if /^RCS file: #{Regexp.escape(root_url_path)}\/#{Regexp.escape(path_with_project)}(.+),v$/ =~ line
137 137 entry_path = normalize_cvs_path($1)
138 138 entry_name = normalize_path(File.basename($1))
139 139 logger.debug("Path #{entry_path} <=> Name #{entry_name}")
140 140 elsif /^head: (.+)$/ =~ line
141 141 entry_headRev = $1 #unless entry.nil?
142 142 elsif /^symbolic names:/ =~ line
143 143 state="symbolic" #unless entry.nil?
144 144 elsif /^#{STARTLOG}/ =~ line
145 145 commit_log=String.new
146 146 state="revision"
147 147 end
148 148 next
149 149 elsif state=="symbolic"
150 150 if /^(.*):\s(.*)/ =~ (line.strip)
151 151 branch_map[$1]=$2
152 152 else
153 153 state="tags"
154 154 next
155 155 end
156 156 elsif state=="tags"
157 157 if /^#{STARTLOG}/ =~ line
158 158 commit_log = ""
159 159 state="revision"
160 160 elsif /^#{ENDLOG}/ =~ line
161 161 state="head"
162 162 end
163 163 next
164 164 elsif state=="revision"
165 165 if /^#{ENDLOG}/ =~ line || /^#{STARTLOG}/ =~ line
166 166 if revision
167 167
168 168 revHelper=CvsRevisionHelper.new(revision)
169 169 revBranch="HEAD"
170 170
171 171 branch_map.each() do |branch_name,branch_point|
172 172 if revHelper.is_in_branch_with_symbol(branch_point)
173 173 revBranch=branch_name
174 174 end
175 175 end
176 176
177 177 logger.debug("********** YIELD Revision #{revision}::#{revBranch}")
178 178
179 179 yield Revision.new({
180 180 :time => date,
181 181 :author => author,
182 182 :message=>commit_log.chomp,
183 183 :paths => [{
184 184 :revision => revision,
185 185 :branch=> revBranch,
186 186 :path=>entry_path,
187 187 :name=>entry_name,
188 188 :kind=>'file',
189 189 :action=>file_state
190 190 }]
191 191 })
192 192 end
193 193
194 194 commit_log=String.new
195 195 revision=nil
196 196
197 197 if /^#{ENDLOG}/ =~ line
198 198 state="entry_start"
199 199 end
200 200 next
201 201 end
202 202
203 203 if /^branches: (.+)$/ =~ line
204 204 #TODO: version.branch = $1
205 205 elsif /^revision (\d+(?:\.\d+)+).*$/ =~ line
206 206 revision = $1
207 207 elsif /^date:\s+(\d+.\d+.\d+\s+\d+:\d+:\d+)/ =~ line
208 208 date = Time.parse($1)
209 209 author = /author: ([^;]+)/.match(line)[1]
210 210 file_state = /state: ([^;]+)/.match(line)[1]
211 211 #TODO: linechanges only available in CVS.... maybe a feature our SVN implementation. i'm sure, they are
212 212 # useful for stats or something else
213 213 # linechanges =/lines: \+(\d+) -(\d+)/.match(line)
214 214 # unless linechanges.nil?
215 215 # version.line_plus = linechanges[1]
216 216 # version.line_minus = linechanges[2]
217 217 # else
218 218 # version.line_plus = 0
219 219 # version.line_minus = 0
220 220 # end
221 221 else
222 222 commit_log << line unless line =~ /^\*\*\* empty log message \*\*\*/
223 223 end
224 224 end
225 225 end
226 226 end
227 227 end
228 228
229 229 def diff(path, identifier_from, identifier_to=nil)
230 230 logger.debug "<cvs> diff path:'#{path}',identifier_from #{identifier_from}, identifier_to #{identifier_to}"
231 231 path_with_project="#{url}#{with_leading_slash(path)}"
232 cmd = "#{CVS_BIN} -d #{root_url} rdiff -u -r#{identifier_to} -r#{identifier_from} #{shell_quote path_with_project}"
232 cmd = "#{CVS_BIN} -d #{shell_quote root_url} rdiff -u -r#{identifier_to.to_i} -r#{identifier_from.to_i} #{shell_quote path_with_project}"
233 233 diff = []
234 234 shellout(cmd) do |io|
235 235 io.each_line do |line|
236 236 diff << line
237 237 end
238 238 end
239 239 return nil if $? && $?.exitstatus != 0
240 240 diff
241 241 end
242 242
243 243 def cat(path, identifier=nil)
244 244 identifier = (identifier) ? identifier : "HEAD"
245 245 logger.debug "<cvs> cat path:'#{path}',identifier #{identifier}"
246 246 path_with_project="#{url}#{with_leading_slash(path)}"
247 cmd = "#{CVS_BIN} -d #{root_url} co"
247 cmd = "#{CVS_BIN} -d #{shell_quote root_url} co"
248 248 cmd << " -D \"#{time_to_cvstime(identifier)}\"" if identifier
249 249 cmd << " -p #{shell_quote path_with_project}"
250 250 cat = nil
251 251 shellout(cmd) do |io|
252 252 cat = io.read
253 253 end
254 254 return nil if $? && $?.exitstatus != 0
255 255 cat
256 256 end
257 257
258 258 def annotate(path, identifier=nil)
259 identifier = (identifier) ? identifier : "HEAD"
259 identifier = (identifier) ? identifier.to_i : "HEAD"
260 260 logger.debug "<cvs> annotate path:'#{path}',identifier #{identifier}"
261 261 path_with_project="#{url}#{with_leading_slash(path)}"
262 cmd = "#{CVS_BIN} -d #{root_url} rannotate -r#{identifier} #{shell_quote path_with_project}"
262 cmd = "#{CVS_BIN} -d #{shell_quote root_url} rannotate -r#{identifier} #{shell_quote path_with_project}"
263 263 blame = Annotate.new
264 264 shellout(cmd) do |io|
265 265 io.each_line do |line|
266 266 next unless line =~ %r{^([\d\.]+)\s+\(([^\)]+)\s+[^\)]+\):\s(.*)$}
267 267 blame.add_line($3.rstrip, Revision.new(:revision => $1, :author => $2.strip))
268 268 end
269 269 end
270 270 return nil if $? && $?.exitstatus != 0
271 271 blame
272 272 end
273 273
274 274 private
275 275
276 276 # Returns the root url without the connexion string
277 277 # :pserver:anonymous@foo.bar:/path => /path
278 278 # :ext:cvsservername:/path => /path
279 279 def root_url_path
280 280 root_url.to_s.gsub(/^:.+:\d*/, '')
281 281 end
282 282
283 283 # convert a date/time into the CVS-format
284 284 def time_to_cvstime(time)
285 285 return nil if time.nil?
286 286 unless time.kind_of? Time
287 287 time = Time.parse(time)
288 288 end
289 289 return time.strftime("%Y-%m-%d %H:%M:%S")
290 290 end
291 291
292 292 def normalize_cvs_path(path)
293 293 normalize_path(path.gsub(/Attic\//,''))
294 294 end
295 295
296 296 def normalize_path(path)
297 297 path.sub(/^(\/)*(.*)/,'\2').sub(/(.*)(,v)+/,'\1')
298 298 end
299 299 end
300 300
301 301 class CvsRevisionHelper
302 302 attr_accessor :complete_rev, :revision, :base, :branchid
303 303
304 304 def initialize(complete_rev)
305 305 @complete_rev = complete_rev
306 306 parseRevision()
307 307 end
308 308
309 309 def branchPoint
310 310 return @base
311 311 end
312 312
313 313 def branchVersion
314 314 if isBranchRevision
315 315 return @base+"."+@branchid
316 316 end
317 317 return @base
318 318 end
319 319
320 320 def isBranchRevision
321 321 !@branchid.nil?
322 322 end
323 323
324 324 def prevRev
325 325 unless @revision==0
326 326 return buildRevision(@revision-1)
327 327 end
328 328 return buildRevision(@revision)
329 329 end
330 330
331 331 def is_in_branch_with_symbol(branch_symbol)
332 332 bpieces=branch_symbol.split(".")
333 333 branch_start="#{bpieces[0..-3].join(".")}.#{bpieces[-1]}"
334 334 return (branchVersion==branch_start)
335 335 end
336 336
337 337 private
338 338 def buildRevision(rev)
339 339 if rev== 0
340 340 @base
341 341 elsif @branchid.nil?
342 342 @base+"."+rev.to_s
343 343 else
344 344 @base+"."+@branchid+"."+rev.to_s
345 345 end
346 346 end
347 347
348 348 # Interpretiert die cvs revisionsnummern wie z.b. 1.14 oder 1.3.0.15
349 349 def parseRevision()
350 350 pieces=@complete_rev.split(".")
351 351 @revision=pieces.last.to_i
352 352 baseSize=1
353 353 baseSize+=(pieces.size/2)
354 354 @base=pieces[0..-baseSize].join(".")
355 355 if baseSize > 2
356 356 @branchid=pieces[-2]
357 357 end
358 358 end
359 359 end
360 360 end
361 361 end
362 362 end
@@ -1,196 +1,196
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 DarcsAdapter < AbstractAdapter
25 25 # Darcs executable name
26 26 DARCS_BIN = "darcs"
27 27
28 28 class << self
29 29 def client_version
30 30 @@client_version ||= (darcs_binary_version || [])
31 31 end
32 32
33 33 def darcs_binary_version
34 34 cmd = "#{DARCS_BIN} --version"
35 35 version = nil
36 36 shellout(cmd) do |io|
37 37 # Read darcs version in first returned line
38 38 if m = io.gets.match(%r{((\d+\.)+\d+)})
39 39 version = m[0].scan(%r{\d+}).collect(&:to_i)
40 40 end
41 41 end
42 42 return nil if $? && $?.exitstatus != 0
43 43 version
44 44 end
45 45 end
46 46
47 47 def initialize(url, root_url=nil, login=nil, password=nil)
48 48 @url = url
49 49 @root_url = url
50 50 end
51 51
52 52 def supports_cat?
53 53 # cat supported in darcs 2.0.0 and higher
54 54 self.class.client_version_above?([2, 0, 0])
55 55 end
56 56
57 57 # Get info about the darcs repository
58 58 def info
59 59 rev = revisions(nil,nil,nil,{:limit => 1})
60 60 rev ? Info.new({:root_url => @url, :lastrev => rev.last}) : nil
61 61 end
62 62
63 63 # Returns an Entries collection
64 64 # or nil if the given path doesn't exist in the repository
65 65 def entries(path=nil, identifier=nil)
66 66 path_prefix = (path.blank? ? '' : "#{path}/")
67 67 path = '.' if path.blank?
68 68 entries = Entries.new
69 cmd = "#{DARCS_BIN} annotate --repodir #{@url} --xml-output"
69 cmd = "#{DARCS_BIN} annotate --repodir #{shell_quote @url} --xml-output"
70 70 cmd << " --match #{shell_quote("hash #{identifier}")}" if identifier
71 71 cmd << " #{shell_quote path}"
72 72 shellout(cmd) do |io|
73 73 begin
74 74 doc = REXML::Document.new(io)
75 75 if doc.root.name == 'directory'
76 76 doc.elements.each('directory/*') do |element|
77 77 next unless ['file', 'directory'].include? element.name
78 78 entries << entry_from_xml(element, path_prefix)
79 79 end
80 80 elsif doc.root.name == 'file'
81 81 entries << entry_from_xml(doc.root, path_prefix)
82 82 end
83 83 rescue
84 84 end
85 85 end
86 86 return nil if $? && $?.exitstatus != 0
87 87 entries.compact.sort_by_name
88 88 end
89 89
90 90 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
91 91 path = '.' if path.blank?
92 92 revisions = Revisions.new
93 cmd = "#{DARCS_BIN} changes --repodir #{@url} --xml-output"
93 cmd = "#{DARCS_BIN} changes --repodir #{shell_quote @url} --xml-output"
94 94 cmd << " --from-match #{shell_quote("hash #{identifier_from}")}" if identifier_from
95 95 cmd << " --last #{options[:limit].to_i}" if options[:limit]
96 96 shellout(cmd) do |io|
97 97 begin
98 98 doc = REXML::Document.new(io)
99 99 doc.elements.each("changelog/patch") do |patch|
100 100 message = patch.elements['name'].text
101 101 message << "\n" + patch.elements['comment'].text.gsub(/\*\*\*END OF DESCRIPTION\*\*\*.*\z/m, '') if patch.elements['comment']
102 102 revisions << Revision.new({:identifier => nil,
103 103 :author => patch.attributes['author'],
104 104 :scmid => patch.attributes['hash'],
105 105 :time => Time.parse(patch.attributes['local_date']),
106 106 :message => message,
107 107 :paths => (options[:with_path] ? get_paths_for_patch(patch.attributes['hash']) : nil)
108 108 })
109 109 end
110 110 rescue
111 111 end
112 112 end
113 113 return nil if $? && $?.exitstatus != 0
114 114 revisions
115 115 end
116 116
117 117 def diff(path, identifier_from, identifier_to=nil)
118 118 path = '*' if path.blank?
119 cmd = "#{DARCS_BIN} diff --repodir #{@url}"
119 cmd = "#{DARCS_BIN} diff --repodir #{shell_quote @url}"
120 120 if identifier_to.nil?
121 121 cmd << " --match #{shell_quote("hash #{identifier_from}")}"
122 122 else
123 123 cmd << " --to-match #{shell_quote("hash #{identifier_from}")}"
124 124 cmd << " --from-match #{shell_quote("hash #{identifier_to}")}"
125 125 end
126 126 cmd << " -u #{shell_quote path}"
127 127 diff = []
128 128 shellout(cmd) do |io|
129 129 io.each_line do |line|
130 130 diff << line
131 131 end
132 132 end
133 133 return nil if $? && $?.exitstatus != 0
134 134 diff
135 135 end
136 136
137 137 def cat(path, identifier=nil)
138 cmd = "#{DARCS_BIN} show content --repodir #{@url}"
138 cmd = "#{DARCS_BIN} show content --repodir #{shell_quote @url}"
139 139 cmd << " --match #{shell_quote("hash #{identifier}")}" if identifier
140 140 cmd << " #{shell_quote path}"
141 141 cat = nil
142 142 shellout(cmd) do |io|
143 143 io.binmode
144 144 cat = io.read
145 145 end
146 146 return nil if $? && $?.exitstatus != 0
147 147 cat
148 148 end
149 149
150 150 private
151 151
152 152 # Returns an Entry from the given XML element
153 153 # or nil if the entry was deleted
154 154 def entry_from_xml(element, path_prefix)
155 155 modified_element = element.elements['modified']
156 156 if modified_element.elements['modified_how'].text.match(/removed/)
157 157 return nil
158 158 end
159 159
160 160 Entry.new({:name => element.attributes['name'],
161 161 :path => path_prefix + element.attributes['name'],
162 162 :kind => element.name == 'file' ? 'file' : 'dir',
163 163 :size => nil,
164 164 :lastrev => Revision.new({
165 165 :identifier => nil,
166 166 :scmid => modified_element.elements['patch'].attributes['hash']
167 167 })
168 168 })
169 169 end
170 170
171 171 # Retrieve changed paths for a single patch
172 172 def get_paths_for_patch(hash)
173 cmd = "#{DARCS_BIN} annotate --repodir #{@url} --summary --xml-output"
173 cmd = "#{DARCS_BIN} annotate --repodir #{shell_quote @url} --summary --xml-output"
174 174 cmd << " --match #{shell_quote("hash #{hash}")} "
175 175 paths = []
176 176 shellout(cmd) do |io|
177 177 begin
178 178 # Darcs xml output has multiple root elements in this case (tested with darcs 1.0.7)
179 179 # A root element is added so that REXML doesn't raise an error
180 180 doc = REXML::Document.new("<fake_root>" + io.read + "</fake_root>")
181 181 doc.elements.each('fake_root/summary/*') do |modif|
182 182 paths << {:action => modif.name[0,1].upcase,
183 183 :path => "/" + modif.text.chomp.gsub(/^\s*/, '')
184 184 }
185 185 end
186 186 rescue
187 187 end
188 188 end
189 189 paths
190 190 rescue CommandFailed
191 191 paths
192 192 end
193 193 end
194 194 end
195 195 end
196 196 end
@@ -1,270 +1,270
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
20 20 module Redmine
21 21 module Scm
22 22 module Adapters
23 23 class GitAdapter < AbstractAdapter
24 24 # Git executable name
25 25 GIT_BIN = "git"
26 26
27 27 def info
28 28 begin
29 29 Info.new(:root_url => url, :lastrev => lastrev('',nil))
30 30 rescue
31 31 nil
32 32 end
33 33 end
34 34
35 35 def branches
36 36 return @branches if @branches
37 37 @branches = []
38 38 cmd = "#{GIT_BIN} --git-dir #{target('')} branch --no-color"
39 39 shellout(cmd) do |io|
40 40 io.each_line do |line|
41 41 @branches << line.match('\s*\*?\s*(.*)$')[1]
42 42 end
43 43 end
44 44 @branches.sort!
45 45 end
46 46
47 47 def tags
48 48 return @tags if @tags
49 49 cmd = "#{GIT_BIN} --git-dir #{target('')} tag"
50 50 shellout(cmd) do |io|
51 51 @tags = io.readlines.sort!.map{|t| t.strip}
52 52 end
53 53 end
54 54
55 55 def default_branch
56 56 branches.include?('master') ? 'master' : branches.first
57 57 end
58 58
59 59 def entries(path=nil, identifier=nil)
60 60 path ||= ''
61 61 entries = Entries.new
62 62 cmd = "#{GIT_BIN} --git-dir #{target('')} ls-tree -l "
63 63 cmd << shell_quote("HEAD:" + path) if identifier.nil?
64 64 cmd << shell_quote(identifier + ":" + path) if identifier
65 65 shellout(cmd) do |io|
66 66 io.each_line do |line|
67 67 e = line.chomp.to_s
68 68 if e =~ /^\d+\s+(\w+)\s+([0-9a-f]{40})\s+([0-9-]+)\t(.+)$/
69 69 type = $1
70 70 sha = $2
71 71 size = $3
72 72 name = $4
73 73 full_path = path.empty? ? name : "#{path}/#{name}"
74 74 entries << Entry.new({:name => name,
75 75 :path => full_path,
76 76 :kind => (type == "tree") ? 'dir' : 'file',
77 77 :size => (type == "tree") ? nil : size,
78 78 :lastrev => lastrev(full_path,identifier)
79 79 }) unless entries.detect{|entry| entry.name == name}
80 80 end
81 81 end
82 82 end
83 83 return nil if $? && $?.exitstatus != 0
84 84 entries.sort_by_name
85 85 end
86 86
87 87 def lastrev(path,rev)
88 88 return nil if path.nil?
89 89 cmd = "#{GIT_BIN} --git-dir #{target('')} log --no-color --date=iso --pretty=fuller --no-merges -n 1 "
90 90 cmd << " #{shell_quote rev} " if rev
91 91 cmd << "-- #{shell_quote path} " unless path.empty?
92 92 shellout(cmd) do |io|
93 93 begin
94 94 id = io.gets.split[1]
95 95 author = io.gets.match('Author:\s+(.*)$')[1]
96 96 2.times { io.gets }
97 97 time = Time.parse(io.gets.match('CommitDate:\s+(.*)$')[1]).localtime
98 98
99 99 Revision.new({
100 100 :identifier => id,
101 101 :scmid => id,
102 102 :author => author,
103 103 :time => time,
104 104 :message => nil,
105 105 :paths => nil
106 106 })
107 107 rescue NoMethodError => e
108 108 logger.error("The revision '#{path}' has a wrong format")
109 109 return nil
110 110 end
111 111 end
112 112 end
113 113
114 114 def revisions(path, identifier_from, identifier_to, options={})
115 115 revisions = Revisions.new
116 116
117 117 cmd = "#{GIT_BIN} --git-dir #{target('')} log --no-color --raw --date=iso --pretty=fuller "
118 118 cmd << " --reverse " if options[:reverse]
119 119 cmd << " --all " if options[:all]
120 cmd << " -n #{options[:limit]} " if options[:limit]
120 cmd << " -n #{options[:limit].to_i} " if options[:limit]
121 121 cmd << "#{shell_quote(identifier_from + '..')}" if identifier_from
122 122 cmd << "#{shell_quote identifier_to}" if identifier_to
123 123 cmd << " --since=#{shell_quote(options[:since].strftime("%Y-%m-%d %H:%M:%S"))}" if options[:since]
124 124 cmd << " -- #{shell_quote path}" if path && !path.empty?
125 125
126 126 shellout(cmd) do |io|
127 127 files=[]
128 128 changeset = {}
129 129 parsing_descr = 0 #0: not parsing desc or files, 1: parsing desc, 2: parsing files
130 130 revno = 1
131 131
132 132 io.each_line do |line|
133 133 if line =~ /^commit ([0-9a-f]{40})$/
134 134 key = "commit"
135 135 value = $1
136 136 if (parsing_descr == 1 || parsing_descr == 2)
137 137 parsing_descr = 0
138 138 revision = Revision.new({
139 139 :identifier => changeset[:commit],
140 140 :scmid => changeset[:commit],
141 141 :author => changeset[:author],
142 142 :time => Time.parse(changeset[:date]),
143 143 :message => changeset[:description],
144 144 :paths => files
145 145 })
146 146 if block_given?
147 147 yield revision
148 148 else
149 149 revisions << revision
150 150 end
151 151 changeset = {}
152 152 files = []
153 153 revno = revno + 1
154 154 end
155 155 changeset[:commit] = $1
156 156 elsif (parsing_descr == 0) && line =~ /^(\w+):\s*(.*)$/
157 157 key = $1
158 158 value = $2
159 159 if key == "Author"
160 160 changeset[:author] = value
161 161 elsif key == "CommitDate"
162 162 changeset[:date] = value
163 163 end
164 164 elsif (parsing_descr == 0) && line.chomp.to_s == ""
165 165 parsing_descr = 1
166 166 changeset[:description] = ""
167 167 elsif (parsing_descr == 1 || parsing_descr == 2) \
168 168 && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\t(.+)$/
169 169 parsing_descr = 2
170 170 fileaction = $1
171 171 filepath = $2
172 172 files << {:action => fileaction, :path => filepath}
173 173 elsif (parsing_descr == 1 || parsing_descr == 2) \
174 174 && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\d+\s+(\S+)\t(.+)$/
175 175 parsing_descr = 2
176 176 fileaction = $1
177 177 filepath = $3
178 178 files << {:action => fileaction, :path => filepath}
179 179 elsif (parsing_descr == 1) && line.chomp.to_s == ""
180 180 parsing_descr = 2
181 181 elsif (parsing_descr == 1)
182 182 changeset[:description] << line[4..-1]
183 183 end
184 184 end
185 185
186 186 if changeset[:commit]
187 187 revision = Revision.new({
188 188 :identifier => changeset[:commit],
189 189 :scmid => changeset[:commit],
190 190 :author => changeset[:author],
191 191 :time => Time.parse(changeset[:date]),
192 192 :message => changeset[:description],
193 193 :paths => files
194 194 })
195 195
196 196 if block_given?
197 197 yield revision
198 198 else
199 199 revisions << revision
200 200 end
201 201 end
202 202 end
203 203
204 204 return nil if $? && $?.exitstatus != 0
205 205 revisions
206 206 end
207 207
208 208 def diff(path, identifier_from, identifier_to=nil)
209 209 path ||= ''
210 210
211 211 if identifier_to
212 212 cmd = "#{GIT_BIN} --git-dir #{target('')} diff --no-color #{shell_quote identifier_to} #{shell_quote identifier_from}"
213 213 else
214 214 cmd = "#{GIT_BIN} --git-dir #{target('')} show --no-color #{shell_quote identifier_from}"
215 215 end
216 216
217 217 cmd << " -- #{shell_quote path}" unless path.empty?
218 218 diff = []
219 219 shellout(cmd) do |io|
220 220 io.each_line do |line|
221 221 diff << line
222 222 end
223 223 end
224 224 return nil if $? && $?.exitstatus != 0
225 225 diff
226 226 end
227 227
228 228 def annotate(path, identifier=nil)
229 229 identifier = 'HEAD' if identifier.blank?
230 230 cmd = "#{GIT_BIN} --git-dir #{target('')} blame -p #{shell_quote identifier} -- #{shell_quote path}"
231 231 blame = Annotate.new
232 232 content = nil
233 233 shellout(cmd) { |io| io.binmode; content = io.read }
234 234 return nil if $? && $?.exitstatus != 0
235 235 # git annotates binary files
236 236 return nil if content.is_binary_data?
237 237 identifier = ''
238 238 # git shows commit author on the first occurrence only
239 239 authors_by_commit = {}
240 240 content.split("\n").each do |line|
241 241 if line =~ /^([0-9a-f]{39,40})\s.*/
242 242 identifier = $1
243 243 elsif line =~ /^author (.+)/
244 244 authors_by_commit[identifier] = $1.strip
245 245 elsif line =~ /^\t(.*)/
246 246 blame.add_line($1, Revision.new(:identifier => identifier, :author => authors_by_commit[identifier]))
247 247 identifier = ''
248 248 author = ''
249 249 end
250 250 end
251 251 blame
252 252 end
253 253
254 254 def cat(path, identifier=nil)
255 255 if identifier.nil?
256 256 identifier = 'HEAD'
257 257 end
258 258 cmd = "#{GIT_BIN} --git-dir #{target('')} show --no-color #{shell_quote(identifier + ':' + path)}"
259 259 cat = nil
260 260 shellout(cmd) do |io|
261 261 io.binmode
262 262 cat = io.read
263 263 end
264 264 return nil if $? && $?.exitstatus != 0
265 265 cat
266 266 end
267 267 end
268 268 end
269 269 end
270 270 end
@@ -1,205 +1,208
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
20 20 module Redmine
21 21 module Scm
22 22 module Adapters
23 23 class MercurialAdapter < AbstractAdapter
24 24
25 25 # Mercurial executable name
26 26 HG_BIN = "hg"
27 27 TEMPLATES_DIR = File.dirname(__FILE__) + "/mercurial"
28 28 TEMPLATE_NAME = "hg-template"
29 29 TEMPLATE_EXTENSION = "tmpl"
30 30
31 31 class << self
32 32 def client_version
33 33 @@client_version ||= (hgversion || [])
34 34 end
35 35
36 36 def hgversion
37 37 # The hg version is expressed either as a
38 38 # release number (eg 0.9.5 or 1.0) or as a revision
39 39 # id composed of 12 hexa characters.
40 40 theversion = hgversion_from_command_line
41 41 if m = theversion.match(%r{\A(.*?)((\d+\.)+\d+)})
42 42 m[2].scan(%r{\d+}).collect(&:to_i)
43 43 end
44 44 end
45 45
46 46 def hgversion_from_command_line
47 47 shellout("#{HG_BIN} --version") { |io| io.read }.to_s
48 48 end
49 49
50 50 def template_path
51 51 @@template_path ||= template_path_for(client_version)
52 52 end
53 53
54 54 def template_path_for(version)
55 55 if ((version <=> [0,9,5]) > 0) || version.empty?
56 56 ver = "1.0"
57 57 else
58 58 ver = "0.9.5"
59 59 end
60 60 "#{TEMPLATES_DIR}/#{TEMPLATE_NAME}-#{ver}.#{TEMPLATE_EXTENSION}"
61 61 end
62 62 end
63 63
64 64 def info
65 65 cmd = "#{HG_BIN} -R #{target('')} root"
66 66 root_url = nil
67 67 shellout(cmd) do |io|
68 68 root_url = io.read
69 69 end
70 70 return nil if $? && $?.exitstatus != 0
71 71 info = Info.new({:root_url => root_url.chomp,
72 72 :lastrev => revisions(nil,nil,nil,{:limit => 1}).last
73 73 })
74 74 info
75 75 rescue CommandFailed
76 76 return nil
77 77 end
78 78
79 79 def entries(path=nil, identifier=nil)
80 80 path ||= ''
81 81 entries = Entries.new
82 82 cmd = "#{HG_BIN} -R #{target('')} --cwd #{target('')} locate"
83 cmd << " -r " + (identifier ? identifier.to_s : "tip")
83 cmd << " -r " + shell_quote(identifier ? identifier.to_s : "tip")
84 84 cmd << " " + shell_quote("path:#{path}") unless path.empty?
85 85 shellout(cmd) do |io|
86 86 io.each_line do |line|
87 87 # HG uses antislashs as separator on Windows
88 88 line = line.gsub(/\\/, "/")
89 89 if path.empty? or e = line.gsub!(%r{^#{with_trailling_slash(path)}},'')
90 90 e ||= line
91 91 e = e.chomp.split(%r{[\/\\]})
92 92 entries << Entry.new({:name => e.first,
93 93 :path => (path.nil? or path.empty? ? e.first : "#{with_trailling_slash(path)}#{e.first}"),
94 94 :kind => (e.size > 1 ? 'dir' : 'file'),
95 95 :lastrev => Revision.new
96 96 }) unless e.empty? || entries.detect{|entry| entry.name == e.first}
97 97 end
98 98 end
99 99 end
100 100 return nil if $? && $?.exitstatus != 0
101 101 entries.sort_by_name
102 102 end
103 103
104 104 # Fetch the revisions by using a template file that
105 105 # makes Mercurial produce a xml output.
106 106 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
107 107 revisions = Revisions.new
108 108 cmd = "#{HG_BIN} --debug --encoding utf8 -R #{target('')} log -C --style #{shell_quote self.class.template_path}"
109 109 if identifier_from && identifier_to
110 110 cmd << " -r #{identifier_from.to_i}:#{identifier_to.to_i}"
111 111 elsif identifier_from
112 112 cmd << " -r #{identifier_from.to_i}:"
113 113 end
114 114 cmd << " --limit #{options[:limit].to_i}" if options[:limit]
115 cmd << " #{path}" if path
115 cmd << " #{shell_quote path}" if path
116 116 shellout(cmd) do |io|
117 117 begin
118 118 # HG doesn't close the XML Document...
119 119 doc = REXML::Document.new(io.read << "</log>")
120 120 doc.elements.each("log/logentry") do |logentry|
121 121 paths = []
122 122 copies = logentry.get_elements('paths/path-copied')
123 123 logentry.elements.each("paths/path") do |path|
124 124 # Detect if the added file is a copy
125 125 if path.attributes['action'] == 'A' and c = copies.find{ |e| e.text == path.text }
126 126 from_path = c.attributes['copyfrom-path']
127 127 from_rev = logentry.attributes['revision']
128 128 end
129 129 paths << {:action => path.attributes['action'],
130 130 :path => "/#{path.text}",
131 131 :from_path => from_path ? "/#{from_path}" : nil,
132 132 :from_revision => from_rev ? from_rev : nil
133 133 }
134 134 end
135 135 paths.sort! { |x,y| x[:path] <=> y[:path] }
136 136
137 137 revisions << Revision.new({:identifier => logentry.attributes['revision'],
138 138 :scmid => logentry.attributes['node'],
139 139 :author => (logentry.elements['author'] ? logentry.elements['author'].text : ""),
140 140 :time => Time.parse(logentry.elements['date'].text).localtime,
141 141 :message => logentry.elements['msg'].text,
142 142 :paths => paths
143 143 })
144 144 end
145 145 rescue
146 146 logger.debug($!)
147 147 end
148 148 end
149 149 return nil if $? && $?.exitstatus != 0
150 150 revisions
151 151 end
152 152
153 153 def diff(path, identifier_from, identifier_to=nil)
154 154 path ||= ''
155 155 if identifier_to
156 156 identifier_to = identifier_to.to_i
157 157 else
158 158 identifier_to = identifier_from.to_i - 1
159 159 end
160 if identifier_from
161 identifier_from = identifier_from.to_i
162 end
160 163 cmd = "#{HG_BIN} -R #{target('')} diff -r #{identifier_to} -r #{identifier_from} --nodates"
161 164 cmd << " -I #{target(path)}" unless path.empty?
162 165 diff = []
163 166 shellout(cmd) do |io|
164 167 io.each_line do |line|
165 168 diff << line
166 169 end
167 170 end
168 171 return nil if $? && $?.exitstatus != 0
169 172 diff
170 173 end
171 174
172 175 def cat(path, identifier=nil)
173 176 cmd = "#{HG_BIN} -R #{target('')} cat"
174 cmd << " -r " + (identifier ? identifier.to_s : "tip")
177 cmd << " -r " + shell_quote(identifier ? identifier.to_s : "tip")
175 178 cmd << " #{target(path)}"
176 179 cat = nil
177 180 shellout(cmd) do |io|
178 181 io.binmode
179 182 cat = io.read
180 183 end
181 184 return nil if $? && $?.exitstatus != 0
182 185 cat
183 186 end
184 187
185 188 def annotate(path, identifier=nil)
186 189 path ||= ''
187 190 cmd = "#{HG_BIN} -R #{target('')}"
188 191 cmd << " annotate -n -u"
189 cmd << " -r " + (identifier ? identifier.to_s : "tip")
192 cmd << " -r " + shell_quote(identifier ? identifier.to_s : "tip")
190 193 cmd << " -r #{identifier.to_i}" if identifier
191 194 cmd << " #{target(path)}"
192 195 blame = Annotate.new
193 196 shellout(cmd) do |io|
194 197 io.each_line do |line|
195 198 next unless line =~ %r{^([^:]+)\s(\d+):(.*)$}
196 199 blame.add_line($3.rstrip, Revision.new(:identifier => $2.to_i, :author => $1.strip))
197 200 end
198 201 end
199 202 return nil if $? && $?.exitstatus != 0
200 203 blame
201 204 end
202 205 end
203 206 end
204 207 end
205 208 end
@@ -1,256 +1,256
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2010 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 = "svn"
28 28
29 29 class << self
30 30 def client_version
31 31 @@client_version ||= (svn_binary_version || [])
32 32 end
33 33
34 34 def svn_binary_version
35 35 cmd = "#{SVN_BIN} --version"
36 36 version = nil
37 37 shellout(cmd) do |io|
38 38 # Read svn version in first returned line
39 39 if m = io.read.to_s.match(%r{\A(.*?)((\d+\.)+\d+)})
40 40 version = m[2].scan(%r{\d+}).collect(&:to_i)
41 41 end
42 42 end
43 43 return nil if $? && $?.exitstatus != 0
44 44 version
45 45 end
46 46 end
47 47
48 48 # Get info about the svn repository
49 49 def info
50 50 cmd = "#{SVN_BIN} info --xml #{target}"
51 51 cmd << credentials_string
52 52 info = nil
53 53 shellout(cmd) do |io|
54 54 output = io.read
55 55 begin
56 56 doc = ActiveSupport::XmlMini.parse(output)
57 57 #root_url = doc.elements["info/entry/repository/root"].text
58 58 info = Info.new({:root_url => doc['info']['entry']['repository']['root']['__content__'],
59 59 :lastrev => Revision.new({
60 60 :identifier => doc['info']['entry']['commit']['revision'],
61 61 :time => Time.parse(doc['info']['entry']['commit']['date']['__content__']).localtime,
62 62 :author => (doc['info']['entry']['commit']['author'] ? doc['info']['entry']['commit']['author']['__content__'] : "")
63 63 })
64 64 })
65 65 rescue
66 66 end
67 67 end
68 68 return nil if $? && $?.exitstatus != 0
69 69 info
70 70 rescue CommandFailed
71 71 return nil
72 72 end
73 73
74 74 # Returns an Entries collection
75 75 # or nil if the given path doesn't exist in the repository
76 76 def entries(path=nil, identifier=nil)
77 77 path ||= ''
78 78 identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
79 79 entries = Entries.new
80 80 cmd = "#{SVN_BIN} list --xml #{target(path)}@#{identifier}"
81 81 cmd << credentials_string
82 82 shellout(cmd) do |io|
83 83 output = io.read
84 84 begin
85 85 doc = ActiveSupport::XmlMini.parse(output)
86 86 each_xml_element(doc['lists']['list'], 'entry') do |entry|
87 87 commit = entry['commit']
88 88 commit_date = commit['date']
89 89 # Skip directory if there is no commit date (usually that
90 90 # means that we don't have read access to it)
91 91 next if entry['kind'] == 'dir' && commit_date.nil?
92 92 name = entry['name']['__content__']
93 93 entries << Entry.new({:name => URI.unescape(name),
94 94 :path => ((path.empty? ? "" : "#{path}/") + name),
95 95 :kind => entry['kind'],
96 96 :size => ((s = entry['size']) ? s['__content__'].to_i : nil),
97 97 :lastrev => Revision.new({
98 98 :identifier => commit['revision'],
99 99 :time => Time.parse(commit_date['__content__'].to_s).localtime,
100 100 :author => ((a = commit['author']) ? a['__content__'] : nil)
101 101 })
102 102 })
103 103 end
104 104 rescue Exception => e
105 105 logger.error("Error parsing svn output: #{e.message}")
106 106 logger.error("Output was:\n #{output}")
107 107 end
108 108 end
109 109 return nil if $? && $?.exitstatus != 0
110 110 logger.debug("Found #{entries.size} entries in the repository for #{target(path)}") if logger && logger.debug?
111 111 entries.sort_by_name
112 112 end
113 113
114 114 def properties(path, identifier=nil)
115 115 # proplist xml output supported in svn 1.5.0 and higher
116 116 return nil unless self.class.client_version_above?([1, 5, 0])
117 117
118 118 identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
119 119 cmd = "#{SVN_BIN} proplist --verbose --xml #{target(path)}@#{identifier}"
120 120 cmd << credentials_string
121 121 properties = {}
122 122 shellout(cmd) do |io|
123 123 output = io.read
124 124 begin
125 125 doc = ActiveSupport::XmlMini.parse(output)
126 126 each_xml_element(doc['properties']['target'], 'property') do |property|
127 127 properties[ property['name'] ] = property['__content__'].to_s
128 128 end
129 129 rescue
130 130 end
131 131 end
132 132 return nil if $? && $?.exitstatus != 0
133 133 properties
134 134 end
135 135
136 136 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
137 137 path ||= ''
138 identifier_from = (identifier_from and identifier_from.to_i > 0) ? identifier_from.to_i : "HEAD"
139 identifier_to = (identifier_to and identifier_to.to_i > 0) ? identifier_to.to_i : 1
138 identifier_from = (identifier_from && identifier_from.to_i > 0) ? identifier_from.to_i : "HEAD"
139 identifier_to = (identifier_to && identifier_to.to_i > 0) ? identifier_to.to_i : 1
140 140 revisions = Revisions.new
141 141 cmd = "#{SVN_BIN} log --xml -r #{identifier_from}:#{identifier_to}"
142 142 cmd << credentials_string
143 143 cmd << " --verbose " if options[:with_paths]
144 144 cmd << " --limit #{options[:limit].to_i}" if options[:limit]
145 145 cmd << ' ' + target(path)
146 146 shellout(cmd) do |io|
147 147 output = io.read
148 148 begin
149 149 doc = ActiveSupport::XmlMini.parse(output)
150 150 each_xml_element(doc['log'], 'logentry') do |logentry|
151 151 paths = []
152 152 each_xml_element(logentry['paths'], 'path') do |path|
153 153 paths << {:action => path['action'],
154 154 :path => path['__content__'],
155 155 :from_path => path['copyfrom-path'],
156 156 :from_revision => path['copyfrom-rev']
157 157 }
158 158 end if logentry['paths'] && logentry['paths']['path']
159 159 paths.sort! { |x,y| x[:path] <=> y[:path] }
160 160
161 161 revisions << Revision.new({:identifier => logentry['revision'],
162 162 :author => (logentry['author'] ? logentry['author']['__content__'] : ""),
163 163 :time => Time.parse(logentry['date']['__content__'].to_s).localtime,
164 164 :message => logentry['msg']['__content__'],
165 165 :paths => paths
166 166 })
167 167 end
168 168 rescue
169 169 end
170 170 end
171 171 return nil if $? && $?.exitstatus != 0
172 172 revisions
173 173 end
174 174
175 175 def diff(path, identifier_from, identifier_to=nil, type="inline")
176 176 path ||= ''
177 177 identifier_from = (identifier_from and identifier_from.to_i > 0) ? identifier_from.to_i : ''
178 178 identifier_to = (identifier_to and identifier_to.to_i > 0) ? identifier_to.to_i : (identifier_from.to_i - 1)
179 179
180 180 cmd = "#{SVN_BIN} diff -r "
181 181 cmd << "#{identifier_to}:"
182 182 cmd << "#{identifier_from}"
183 183 cmd << " #{target(path)}@#{identifier_from}"
184 184 cmd << credentials_string
185 185 diff = []
186 186 shellout(cmd) do |io|
187 187 io.each_line do |line|
188 188 diff << line
189 189 end
190 190 end
191 191 return nil if $? && $?.exitstatus != 0
192 192 diff
193 193 end
194 194
195 195 def cat(path, identifier=nil)
196 196 identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
197 197 cmd = "#{SVN_BIN} cat #{target(path)}@#{identifier}"
198 198 cmd << credentials_string
199 199 cat = nil
200 200 shellout(cmd) do |io|
201 201 io.binmode
202 202 cat = io.read
203 203 end
204 204 return nil if $? && $?.exitstatus != 0
205 205 cat
206 206 end
207 207
208 208 def annotate(path, identifier=nil)
209 209 identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
210 210 cmd = "#{SVN_BIN} blame #{target(path)}@#{identifier}"
211 211 cmd << credentials_string
212 212 blame = Annotate.new
213 213 shellout(cmd) do |io|
214 214 io.each_line do |line|
215 215 next unless line =~ %r{^\s*(\d+)\s*(\S+)\s(.*)$}
216 216 blame.add_line($3.rstrip, Revision.new(:identifier => $1.to_i, :author => $2.strip))
217 217 end
218 218 end
219 219 return nil if $? && $?.exitstatus != 0
220 220 blame
221 221 end
222 222
223 223 private
224 224
225 225 def credentials_string
226 226 str = ''
227 227 str << " --username #{shell_quote(@login)}" unless @login.blank?
228 228 str << " --password #{shell_quote(@password)}" unless @login.blank? || @password.blank?
229 229 str << " --no-auth-cache --non-interactive"
230 230 str
231 231 end
232 232
233 233 # Helper that iterates over the child elements of a xml node
234 234 # MiniXml returns a hash when a single child is found or an array of hashes for multiple children
235 235 def each_xml_element(node, name)
236 236 if node && node[name]
237 237 if node[name].is_a?(Hash)
238 238 yield node[name]
239 239 else
240 240 node[name].each do |element|
241 241 yield element
242 242 end
243 243 end
244 244 end
245 245 end
246 246
247 247 def target(path = '')
248 248 base = path.match(/^\//) ? root_url : url
249 249 uri = "#{base}/#{path}"
250 250 uri = URI.escape(URI.escape(uri), '[]')
251 251 shell_quote(uri.gsub(/[?<>\*]/, ''))
252 252 end
253 253 end
254 254 end
255 255 end
256 256 end
General Comments 0
You need to be logged in to leave comments. Login now