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