##// END OF EJS Templates
scm: cvs: use scm_cmd() in adapter revisions()....
Toshi MARUYAMA -
r4965:eea06ab7702f
parent child
Show More
@@ -1,423 +1,425
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 # raised if scm command exited with error, e.g. unknown revision.
29 29 class ScmCommandAborted < CommandFailed; end
30 30
31 31 class << self
32 32 def client_command
33 33 @@bin ||= CVS_BIN
34 34 end
35 35
36 36 def sq_bin
37 37 @@sq_bin ||= shell_quote(CVS_BIN)
38 38 end
39 39
40 40 def client_version
41 41 @@client_version ||= (scm_command_version || [])
42 42 end
43 43
44 44 def client_available
45 45 client_version_above?([1, 12])
46 46 end
47 47
48 48 def scm_command_version
49 49 scm_version = scm_version_from_command_line.dup
50 50 if scm_version.respond_to?(:force_encoding)
51 51 scm_version.force_encoding('ASCII-8BIT')
52 52 end
53 53 if m = scm_version.match(%r{\A(.*?)((\d+\.)+\d+)}m)
54 54 m[2].scan(%r{\d+}).collect(&:to_i)
55 55 end
56 56 end
57 57
58 58 def scm_version_from_command_line
59 59 shellout("#{sq_bin} --version") { |io| io.read }.to_s
60 60 end
61 61 end
62 62
63 63 # Guidelines for the input:
64 64 # url -> the project-path, relative to the cvsroot (eg. module name)
65 65 # root_url -> the good old, sometimes damned, CVSROOT
66 66 # login -> unnecessary
67 67 # password -> unnecessary too
68 68 def initialize(url, root_url=nil, login=nil, password=nil,
69 69 path_encoding=nil)
70 70 @url = url
71 71 @login = login if login && !login.empty?
72 72 @password = (password || "") if @login
73 73 #TODO: better Exception here (IllegalArgumentException)
74 74 raise CommandFailed if root_url.blank?
75 75 @root_url = root_url
76 76 end
77 77
78 78 def root_url
79 79 @root_url
80 80 end
81 81
82 82 def url
83 83 @url
84 84 end
85 85
86 86 def info
87 87 logger.debug "<cvs> info"
88 88 Info.new({:root_url => @root_url, :lastrev => nil})
89 89 end
90 90
91 91 def get_previous_revision(revision)
92 92 CvsRevisionHelper.new(revision).prevRev
93 93 end
94 94
95 95 # Returns an Entries collection
96 96 # or nil if the given path doesn't exist in the repository
97 97 # this method is used by the repository-browser (aka LIST)
98 98 def entries(path=nil, identifier=nil)
99 99 logger.debug "<cvs> entries '#{path}' with identifier '#{identifier}'"
100 100 path_with_project="#{url}#{with_leading_slash(path)}"
101 101 entries = Entries.new
102 102 cmd_args = %w|rls -e|
103 103 cmd_args << "-D" << time_to_cvstime(identifier) if identifier
104 104 cmd_args << path_with_project
105 105 scm_cmd(*cmd_args) do |io|
106 106 io.each_line(){|line|
107 107 fields=line.chop.split('/',-1)
108 108 logger.debug(">>InspectLine #{fields.inspect}")
109 109
110 110 if fields[0]!="D"
111 111 entries << Entry.new({:name => fields[-5],
112 112 #:path => fields[-4].include?(path)?fields[-4]:(path + "/"+ fields[-4]),
113 113 :path => "#{path}/#{fields[-5]}",
114 114 :kind => 'file',
115 115 :size => nil,
116 116 :lastrev => Revision.new({
117 117 :revision => fields[-4],
118 118 :name => fields[-4],
119 119 :time => Time.parse(fields[-3]),
120 120 :author => ''
121 121 })
122 122 })
123 123 else
124 124 entries << Entry.new({:name => fields[1],
125 125 :path => "#{path}/#{fields[1]}",
126 126 :kind => 'dir',
127 127 :size => nil,
128 128 :lastrev => nil
129 129 })
130 130 end
131 131 }
132 132 end
133 133 entries.sort_by_name
134 134 rescue ScmCommandAborted
135 135 nil
136 136 end
137 137
138 138 STARTLOG="----------------------------"
139 139 ENDLOG ="============================================================================="
140 140
141 141 # Returns all revisions found between identifier_from and identifier_to
142 142 # in the repository. both identifier have to be dates or nil.
143 143 # these method returns nothing but yield every result in block
144 144 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}, &block)
145 145 logger.debug "<cvs> revisions path:'#{path}',identifier_from #{identifier_from}, identifier_to #{identifier_to}"
146 146
147 147 path_with_project="#{url}#{with_leading_slash(path)}"
148 cmd = "#{self.class.sq_bin} -d #{shell_quote root_url} rlog"
149 cmd << " -d\">#{time_to_cvstime_rlog(identifier_from)}\"" if identifier_from
150 cmd << " #{shell_quote path_with_project}"
151 shellout(cmd) do |io|
148 cmd_args = %w|rlog|
149 cmd_args << "-d" << ">#{time_to_cvstime_rlog(identifier_from)}" if identifier_from
150 cmd_args << path_with_project
151 scm_cmd(*cmd_args) do |io|
152 152 state="entry_start"
153 153
154 154 commit_log=String.new
155 155 revision=nil
156 156 date=nil
157 157 author=nil
158 158 entry_path=nil
159 159 entry_name=nil
160 160 file_state=nil
161 161 branch_map=nil
162 162
163 163 io.each_line() do |line|
164 164
165 165 if state!="revision" && /^#{ENDLOG}/ =~ line
166 166 commit_log=String.new
167 167 revision=nil
168 168 state="entry_start"
169 169 end
170 170
171 171 if state=="entry_start"
172 172 branch_map=Hash.new
173 173 if /^RCS file: #{Regexp.escape(root_url_path)}\/#{Regexp.escape(path_with_project)}(.+),v$/ =~ line
174 174 entry_path = normalize_cvs_path($1)
175 175 entry_name = normalize_path(File.basename($1))
176 176 logger.debug("Path #{entry_path} <=> Name #{entry_name}")
177 177 elsif /^head: (.+)$/ =~ line
178 178 entry_headRev = $1 #unless entry.nil?
179 179 elsif /^symbolic names:/ =~ line
180 180 state="symbolic" #unless entry.nil?
181 181 elsif /^#{STARTLOG}/ =~ line
182 182 commit_log=String.new
183 183 state="revision"
184 184 end
185 185 next
186 186 elsif state=="symbolic"
187 187 if /^(.*):\s(.*)/ =~ (line.strip)
188 188 branch_map[$1]=$2
189 189 else
190 190 state="tags"
191 191 next
192 192 end
193 193 elsif state=="tags"
194 194 if /^#{STARTLOG}/ =~ line
195 195 commit_log = ""
196 196 state="revision"
197 197 elsif /^#{ENDLOG}/ =~ line
198 198 state="head"
199 199 end
200 200 next
201 201 elsif state=="revision"
202 202 if /^#{ENDLOG}/ =~ line || /^#{STARTLOG}/ =~ line
203 203 if revision
204 204
205 205 revHelper=CvsRevisionHelper.new(revision)
206 206 revBranch="HEAD"
207 207
208 208 branch_map.each() do |branch_name,branch_point|
209 209 if revHelper.is_in_branch_with_symbol(branch_point)
210 210 revBranch=branch_name
211 211 end
212 212 end
213 213
214 214 logger.debug("********** YIELD Revision #{revision}::#{revBranch}")
215 215
216 216 yield Revision.new({
217 217 :time => date,
218 218 :author => author,
219 219 :message=>commit_log.chomp,
220 220 :paths => [{
221 221 :revision => revision,
222 222 :branch=> revBranch,
223 223 :path=>entry_path,
224 224 :name=>entry_name,
225 225 :kind=>'file',
226 226 :action=>file_state
227 227 }]
228 228 })
229 229 end
230 230
231 231 commit_log=String.new
232 232 revision=nil
233 233
234 234 if /^#{ENDLOG}/ =~ line
235 235 state="entry_start"
236 236 end
237 237 next
238 238 end
239 239
240 240 if /^branches: (.+)$/ =~ line
241 241 #TODO: version.branch = $1
242 242 elsif /^revision (\d+(?:\.\d+)+).*$/ =~ line
243 243 revision = $1
244 244 elsif /^date:\s+(\d+.\d+.\d+\s+\d+:\d+:\d+)/ =~ line
245 245 date = Time.parse($1)
246 246 author = /author: ([^;]+)/.match(line)[1]
247 247 file_state = /state: ([^;]+)/.match(line)[1]
248 248 #TODO: linechanges only available in CVS.... maybe a feature our SVN implementation. i'm sure, they are
249 249 # useful for stats or something else
250 250 # linechanges =/lines: \+(\d+) -(\d+)/.match(line)
251 251 # unless linechanges.nil?
252 252 # version.line_plus = linechanges[1]
253 253 # version.line_minus = linechanges[2]
254 254 # else
255 255 # version.line_plus = 0
256 256 # version.line_minus = 0
257 257 # end
258 258 else
259 259 commit_log << line unless line =~ /^\*\*\* empty log message \*\*\*/
260 260 end
261 261 end
262 262 end
263 263 end
264 rescue ScmCommandAborted
265 Revisions.new
264 266 end
265 267
266 268 def diff(path, identifier_from, identifier_to=nil)
267 269 logger.debug "<cvs> diff path:'#{path}',identifier_from #{identifier_from}, identifier_to #{identifier_to}"
268 270 path_with_project="#{url}#{with_leading_slash(path)}"
269 271 cmd = "#{self.class.sq_bin} -d #{shell_quote root_url} rdiff -u -r#{identifier_to} -r#{identifier_from} #{shell_quote path_with_project}"
270 272 diff = []
271 273 shellout(cmd) do |io|
272 274 io.each_line do |line|
273 275 diff << line
274 276 end
275 277 end
276 278 return nil if $? && $?.exitstatus != 0
277 279 diff
278 280 end
279 281
280 282 def cat(path, identifier=nil)
281 283 identifier = (identifier) ? identifier : "HEAD"
282 284 logger.debug "<cvs> cat path:'#{path}',identifier #{identifier}"
283 285 path_with_project="#{url}#{with_leading_slash(path)}"
284 286 cmd = "#{self.class.sq_bin} -d #{shell_quote root_url} co"
285 287 cmd << " -D \"#{time_to_cvstime(identifier)}\"" if identifier
286 288 cmd << " -p #{shell_quote path_with_project}"
287 289 cat = nil
288 290 shellout(cmd) do |io|
289 291 io.binmode
290 292 cat = io.read
291 293 end
292 294 return nil if $? && $?.exitstatus != 0
293 295 cat
294 296 end
295 297
296 298 def annotate(path, identifier=nil)
297 299 identifier = (identifier) ? identifier.to_i : "HEAD"
298 300 logger.debug "<cvs> annotate path:'#{path}',identifier #{identifier}"
299 301 path_with_project="#{url}#{with_leading_slash(path)}"
300 302 cmd = "#{self.class.sq_bin} -d #{shell_quote root_url} rannotate -r#{identifier} #{shell_quote path_with_project}"
301 303 blame = Annotate.new
302 304 shellout(cmd) do |io|
303 305 io.each_line do |line|
304 306 next unless line =~ %r{^([\d\.]+)\s+\(([^\)]+)\s+[^\)]+\):\s(.*)$}
305 307 blame.add_line($3.rstrip, Revision.new(:revision => $1, :author => $2.strip))
306 308 end
307 309 end
308 310 return nil if $? && $?.exitstatus != 0
309 311 blame
310 312 end
311 313
312 314 private
313 315
314 316 # Returns the root url without the connexion string
315 317 # :pserver:anonymous@foo.bar:/path => /path
316 318 # :ext:cvsservername:/path => /path
317 319 def root_url_path
318 320 root_url.to_s.gsub(/^:.+:\d*/, '')
319 321 end
320 322
321 323 # convert a date/time into the CVS-format
322 324 def time_to_cvstime(time)
323 325 return nil if time.nil?
324 326 return Time.now if time == 'HEAD'
325 327
326 328 unless time.kind_of? Time
327 329 time = Time.parse(time)
328 330 end
329 331 return time.strftime("%Y-%m-%d %H:%M:%S")
330 332 end
331 333
332 334 def time_to_cvstime_rlog(time)
333 335 return nil if time.nil?
334 336 t1 = time.clone.localtime
335 337 return t1.strftime("%Y-%m-%d %H:%M:%S")
336 338 end
337 339
338 340 def normalize_cvs_path(path)
339 341 normalize_path(path.gsub(/Attic\//,''))
340 342 end
341 343
342 344 def normalize_path(path)
343 345 path.sub(/^(\/)*(.*)/,'\2').sub(/(.*)(,v)+/,'\1')
344 346 end
345 347
346 348 def scm_cmd(*args, &block)
347 349 full_args = [CVS_BIN, '-d', root_url]
348 350 full_args += args
349 351 ret = shellout(full_args.map { |e| shell_quote e.to_s }.join(' '), &block)
350 352 if $? && $?.exitstatus != 0
351 353 raise ScmCommandAborted, "cvs exited with non-zero status: #{$?.exitstatus}"
352 354 end
353 355 ret
354 356 end
355 357 private :scm_cmd
356 358 end
357 359
358 360 class CvsRevisionHelper
359 361 attr_accessor :complete_rev, :revision, :base, :branchid
360 362
361 363 def initialize(complete_rev)
362 364 @complete_rev = complete_rev
363 365 parseRevision()
364 366 end
365 367
366 368 def branchPoint
367 369 return @base
368 370 end
369 371
370 372 def branchVersion
371 373 if isBranchRevision
372 374 return @base+"."+@branchid
373 375 end
374 376 return @base
375 377 end
376 378
377 379 def isBranchRevision
378 380 !@branchid.nil?
379 381 end
380 382
381 383 def prevRev
382 384 unless @revision==0
383 385 return buildRevision(@revision-1)
384 386 end
385 387 return buildRevision(@revision)
386 388 end
387 389
388 390 def is_in_branch_with_symbol(branch_symbol)
389 391 bpieces=branch_symbol.split(".")
390 392 branch_start="#{bpieces[0..-3].join(".")}.#{bpieces[-1]}"
391 393 return (branchVersion==branch_start)
392 394 end
393 395
394 396 private
395 397 def buildRevision(rev)
396 398 if rev== 0
397 399 if @branchid.nil?
398 400 @base+".0"
399 401 else
400 402 @base
401 403 end
402 404 elsif @branchid.nil?
403 405 @base+"."+rev.to_s
404 406 else
405 407 @base+"."+@branchid+"."+rev.to_s
406 408 end
407 409 end
408 410
409 411 # Interpretiert die cvs revisionsnummern wie z.b. 1.14 oder 1.3.0.15
410 412 def parseRevision()
411 413 pieces=@complete_rev.split(".")
412 414 @revision=pieces.last.to_i
413 415 baseSize=1
414 416 baseSize+=(pieces.size/2)
415 417 @base=pieces[0..-baseSize].join(".")
416 418 if baseSize > 2
417 419 @branchid=pieces[-2]
418 420 end
419 421 end
420 422 end
421 423 end
422 424 end
423 425 end
General Comments 0
You need to be logged in to leave comments. Login now