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