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