##// END OF EJS Templates
scm: cvs: fix age column on repository view is off by timezone delta (#7827)....
Toshi MARUYAMA -
r4968:732ee35b2fdb
parent child
Show More
@@ -1,429 +1,439
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_rlog(identifier) if identifier
104 104 cmd_args << path_with_project
105 105 scm_cmd(*cmd_args) do |io|
106 106 io.each_line() do |line|
107 107 fields = line.chop.split('/',-1)
108 108 logger.debug(">>InspectLine #{fields.inspect}")
109 109 if fields[0]!="D"
110 time = nil
111 # Thu Dec 13 16:27:22 2007
112 time_l = fields[-3].split(' ')
113 if time_l.size == 5 && time_l[4].length == 4
114 begin
115 time = Time.parse(
116 "#{time_l[1]} #{time_l[2]} #{time_l[3]} GMT #{time_l[4]}")
117 rescue
118 end
119 end
110 120 entries << Entry.new(
111 121 {
112 122 :name => fields[-5],
113 123 #:path => fields[-4].include?(path)?fields[-4]:(path + "/"+ fields[-4]),
114 124 :path => "#{path}/#{fields[-5]}",
115 125 :kind => 'file',
116 126 :size => nil,
117 127 :lastrev => Revision.new(
118 128 {
119 129 :revision => fields[-4],
120 130 :name => fields[-4],
121 :time => Time.parse(fields[-3]),
131 :time => time,
122 132 :author => ''
123 133 })
124 134 })
125 135 else
126 136 entries << Entry.new(
127 137 {
128 138 :name => fields[1],
129 139 :path => "#{path}/#{fields[1]}",
130 140 :kind => 'dir',
131 141 :size => nil,
132 142 :lastrev => nil
133 143 })
134 144 end
135 145 end
136 146 end
137 147 entries.sort_by_name
138 148 rescue ScmCommandAborted
139 149 nil
140 150 end
141 151
142 152 STARTLOG="----------------------------"
143 153 ENDLOG ="============================================================================="
144 154
145 155 # Returns all revisions found between identifier_from and identifier_to
146 156 # in the repository. both identifier have to be dates or nil.
147 157 # these method returns nothing but yield every result in block
148 158 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}, &block)
149 159 logger.debug "<cvs> revisions path:'#{path}',identifier_from #{identifier_from}, identifier_to #{identifier_to}"
150 160
151 161 path_with_project="#{url}#{with_leading_slash(path)}"
152 162 cmd_args = %w|rlog|
153 163 cmd_args << "-d" << ">#{time_to_cvstime_rlog(identifier_from)}" if identifier_from
154 164 cmd_args << path_with_project
155 165 scm_cmd(*cmd_args) do |io|
156 166 state="entry_start"
157 167
158 168 commit_log=String.new
159 169 revision=nil
160 170 date=nil
161 171 author=nil
162 172 entry_path=nil
163 173 entry_name=nil
164 174 file_state=nil
165 175 branch_map=nil
166 176
167 177 io.each_line() do |line|
168 178
169 179 if state!="revision" && /^#{ENDLOG}/ =~ line
170 180 commit_log=String.new
171 181 revision=nil
172 182 state="entry_start"
173 183 end
174 184
175 185 if state=="entry_start"
176 186 branch_map=Hash.new
177 187 if /^RCS file: #{Regexp.escape(root_url_path)}\/#{Regexp.escape(path_with_project)}(.+),v$/ =~ line
178 188 entry_path = normalize_cvs_path($1)
179 189 entry_name = normalize_path(File.basename($1))
180 190 logger.debug("Path #{entry_path} <=> Name #{entry_name}")
181 191 elsif /^head: (.+)$/ =~ line
182 192 entry_headRev = $1 #unless entry.nil?
183 193 elsif /^symbolic names:/ =~ line
184 194 state="symbolic" #unless entry.nil?
185 195 elsif /^#{STARTLOG}/ =~ line
186 196 commit_log=String.new
187 197 state="revision"
188 198 end
189 199 next
190 200 elsif state=="symbolic"
191 201 if /^(.*):\s(.*)/ =~ (line.strip)
192 202 branch_map[$1]=$2
193 203 else
194 204 state="tags"
195 205 next
196 206 end
197 207 elsif state=="tags"
198 208 if /^#{STARTLOG}/ =~ line
199 209 commit_log = ""
200 210 state="revision"
201 211 elsif /^#{ENDLOG}/ =~ line
202 212 state="head"
203 213 end
204 214 next
205 215 elsif state=="revision"
206 216 if /^#{ENDLOG}/ =~ line || /^#{STARTLOG}/ =~ line
207 217 if revision
208 218
209 219 revHelper=CvsRevisionHelper.new(revision)
210 220 revBranch="HEAD"
211 221
212 222 branch_map.each() do |branch_name,branch_point|
213 223 if revHelper.is_in_branch_with_symbol(branch_point)
214 224 revBranch=branch_name
215 225 end
216 226 end
217 227
218 228 logger.debug("********** YIELD Revision #{revision}::#{revBranch}")
219 229
220 230 yield Revision.new({
221 231 :time => date,
222 232 :author => author,
223 233 :message=>commit_log.chomp,
224 234 :paths => [{
225 235 :revision => revision,
226 236 :branch=> revBranch,
227 237 :path=>entry_path,
228 238 :name=>entry_name,
229 239 :kind=>'file',
230 240 :action=>file_state
231 241 }]
232 242 })
233 243 end
234 244
235 245 commit_log=String.new
236 246 revision=nil
237 247
238 248 if /^#{ENDLOG}/ =~ line
239 249 state="entry_start"
240 250 end
241 251 next
242 252 end
243 253
244 254 if /^branches: (.+)$/ =~ line
245 255 #TODO: version.branch = $1
246 256 elsif /^revision (\d+(?:\.\d+)+).*$/ =~ line
247 257 revision = $1
248 258 elsif /^date:\s+(\d+.\d+.\d+\s+\d+:\d+:\d+)/ =~ line
249 259 date = Time.parse($1)
250 260 author = /author: ([^;]+)/.match(line)[1]
251 261 file_state = /state: ([^;]+)/.match(line)[1]
252 262 #TODO: linechanges only available in CVS.... maybe a feature our SVN implementation. i'm sure, they are
253 263 # useful for stats or something else
254 264 # linechanges =/lines: \+(\d+) -(\d+)/.match(line)
255 265 # unless linechanges.nil?
256 266 # version.line_plus = linechanges[1]
257 267 # version.line_minus = linechanges[2]
258 268 # else
259 269 # version.line_plus = 0
260 270 # version.line_minus = 0
261 271 # end
262 272 else
263 273 commit_log << line unless line =~ /^\*\*\* empty log message \*\*\*/
264 274 end
265 275 end
266 276 end
267 277 end
268 278 rescue ScmCommandAborted
269 279 Revisions.new
270 280 end
271 281
272 282 def diff(path, identifier_from, identifier_to=nil)
273 283 logger.debug "<cvs> diff path:'#{path}',identifier_from #{identifier_from}, identifier_to #{identifier_to}"
274 284 path_with_project="#{url}#{with_leading_slash(path)}"
275 285 cmd = "#{self.class.sq_bin} -d #{shell_quote root_url} rdiff -u -r#{identifier_to} -r#{identifier_from} #{shell_quote path_with_project}"
276 286 diff = []
277 287 shellout(cmd) do |io|
278 288 io.each_line do |line|
279 289 diff << line
280 290 end
281 291 end
282 292 return nil if $? && $?.exitstatus != 0
283 293 diff
284 294 end
285 295
286 296 def cat(path, identifier=nil)
287 297 identifier = (identifier) ? identifier : "HEAD"
288 298 logger.debug "<cvs> cat path:'#{path}',identifier #{identifier}"
289 299 path_with_project="#{url}#{with_leading_slash(path)}"
290 300 cmd = "#{self.class.sq_bin} -d #{shell_quote root_url} co"
291 301 cmd << " -D \"#{time_to_cvstime(identifier)}\"" if identifier
292 302 cmd << " -p #{shell_quote path_with_project}"
293 303 cat = nil
294 304 shellout(cmd) do |io|
295 305 io.binmode
296 306 cat = io.read
297 307 end
298 308 return nil if $? && $?.exitstatus != 0
299 309 cat
300 310 end
301 311
302 312 def annotate(path, identifier=nil)
303 313 identifier = (identifier) ? identifier.to_i : "HEAD"
304 314 logger.debug "<cvs> annotate path:'#{path}',identifier #{identifier}"
305 315 path_with_project="#{url}#{with_leading_slash(path)}"
306 316 cmd = "#{self.class.sq_bin} -d #{shell_quote root_url} rannotate -r#{identifier} #{shell_quote path_with_project}"
307 317 blame = Annotate.new
308 318 shellout(cmd) do |io|
309 319 io.each_line do |line|
310 320 next unless line =~ %r{^([\d\.]+)\s+\(([^\)]+)\s+[^\)]+\):\s(.*)$}
311 321 blame.add_line($3.rstrip, Revision.new(:revision => $1, :author => $2.strip))
312 322 end
313 323 end
314 324 return nil if $? && $?.exitstatus != 0
315 325 blame
316 326 end
317 327
318 328 private
319 329
320 330 # Returns the root url without the connexion string
321 331 # :pserver:anonymous@foo.bar:/path => /path
322 332 # :ext:cvsservername:/path => /path
323 333 def root_url_path
324 334 root_url.to_s.gsub(/^:.+:\d*/, '')
325 335 end
326 336
327 337 # convert a date/time into the CVS-format
328 338 def time_to_cvstime(time)
329 339 return nil if time.nil?
330 340 return Time.now if time == 'HEAD'
331 341
332 342 unless time.kind_of? Time
333 343 time = Time.parse(time)
334 344 end
335 345 return time.strftime("%Y-%m-%d %H:%M:%S")
336 346 end
337 347
338 348 def time_to_cvstime_rlog(time)
339 349 return nil if time.nil?
340 350 t1 = time.clone.localtime
341 351 return t1.strftime("%Y-%m-%d %H:%M:%S")
342 352 end
343 353
344 354 def normalize_cvs_path(path)
345 355 normalize_path(path.gsub(/Attic\//,''))
346 356 end
347 357
348 358 def normalize_path(path)
349 359 path.sub(/^(\/)*(.*)/,'\2').sub(/(.*)(,v)+/,'\1')
350 360 end
351 361
352 362 def scm_cmd(*args, &block)
353 363 full_args = [CVS_BIN, '-d', root_url]
354 364 full_args += args
355 365 ret = shellout(full_args.map { |e| shell_quote e.to_s }.join(' '), &block)
356 366 if $? && $?.exitstatus != 0
357 367 raise ScmCommandAborted, "cvs exited with non-zero status: #{$?.exitstatus}"
358 368 end
359 369 ret
360 370 end
361 371 private :scm_cmd
362 372 end
363 373
364 374 class CvsRevisionHelper
365 375 attr_accessor :complete_rev, :revision, :base, :branchid
366 376
367 377 def initialize(complete_rev)
368 378 @complete_rev = complete_rev
369 379 parseRevision()
370 380 end
371 381
372 382 def branchPoint
373 383 return @base
374 384 end
375 385
376 386 def branchVersion
377 387 if isBranchRevision
378 388 return @base+"."+@branchid
379 389 end
380 390 return @base
381 391 end
382 392
383 393 def isBranchRevision
384 394 !@branchid.nil?
385 395 end
386 396
387 397 def prevRev
388 398 unless @revision==0
389 399 return buildRevision(@revision-1)
390 400 end
391 401 return buildRevision(@revision)
392 402 end
393 403
394 404 def is_in_branch_with_symbol(branch_symbol)
395 405 bpieces=branch_symbol.split(".")
396 406 branch_start="#{bpieces[0..-3].join(".")}.#{bpieces[-1]}"
397 407 return (branchVersion==branch_start)
398 408 end
399 409
400 410 private
401 411 def buildRevision(rev)
402 412 if rev== 0
403 413 if @branchid.nil?
404 414 @base+".0"
405 415 else
406 416 @base
407 417 end
408 418 elsif @branchid.nil?
409 419 @base+"."+rev.to_s
410 420 else
411 421 @base+"."+@branchid+"."+rev.to_s
412 422 end
413 423 end
414 424
415 425 # Interpretiert die cvs revisionsnummern wie z.b. 1.14 oder 1.3.0.15
416 426 def parseRevision()
417 427 pieces=@complete_rev.split(".")
418 428 @revision=pieces.last.to_i
419 429 baseSize=1
420 430 baseSize+=(pieces.size/2)
421 431 @base=pieces[0..-baseSize].join(".")
422 432 if baseSize > 2
423 433 @branchid=pieces[-2]
424 434 end
425 435 end
426 436 end
427 437 end
428 438 end
429 439 end
@@ -1,59 +1,67
1 1 require File.expand_path('../../../../../../test_helper', __FILE__)
2 2 begin
3 3 require 'mocha'
4 4
5 5 class CvsAdapterTest < ActiveSupport::TestCase
6 6
7 7 REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/cvs_repository'
8 8 REPOSITORY_PATH.gsub!(/\//, "\\") if Redmine::Platform.mswin?
9 9 MODULE_NAME = 'test'
10 10
11 11 if File.directory?(REPOSITORY_PATH)
12 12 def setup
13 13 @adapter = Redmine::Scm::Adapters::CvsAdapter.new(MODULE_NAME, REPOSITORY_PATH)
14 14 end
15 15
16 16 def test_scm_version
17 17 to_test = { "\nConcurrent Versions System (CVS) 1.12.13 (client/server)\n" => [1,12,13],
18 18 "\r\n1.12.12\r\n1.12.11" => [1,12,12],
19 19 "1.12.11\r\n1.12.10\r\n" => [1,12,11]}
20 20 to_test.each do |s, v|
21 21 test_scm_version_for(s, v)
22 22 end
23 23 end
24 24
25 25 def test_revisions_all
26 26 cnt = 0
27 27 @adapter.revisions('', nil, nil, :with_paths => true) do |revision|
28 28 cnt += 1
29 29 end
30 30 assert_equal 14, cnt
31 31 end
32 32
33 33 def test_revisions_from_rev3
34 34 rev3_committed_on = Time.gm(2007, 12, 13, 16, 27, 22)
35 35 cnt = 0
36 36 @adapter.revisions('', rev3_committed_on, nil, :with_paths => true) do |revision|
37 37 cnt += 1
38 38 end
39 39 assert_equal 2, cnt
40 40 end
41 41
42 def test_entries_rev3
43 rev3_committed_on = Time.gm(2007, 12, 13, 16, 27, 22)
44 entries = @adapter.entries('sources', rev3_committed_on)
45 assert_equal 2, entries.size
46 assert_equal entries[0].name, "watchers_controller.rb"
47 assert_equal entries[0].lastrev.time, Time.gm(2007, 12, 13, 16, 27, 22)
48 end
49
42 50 private
43 51
44 52 def test_scm_version_for(scm_command_version, version)
45 53 @adapter.class.expects(:scm_version_from_command_line).returns(scm_command_version)
46 54 assert_equal version, @adapter.class.scm_command_version
47 55 end
48 56 else
49 57 puts "Cvs test repository NOT FOUND. Skipping unit tests !!!"
50 58 def test_fake; assert true end
51 59 end
52 60 end
53 61
54 62 rescue LoadError
55 63 class CvsMochaFake < ActiveSupport::TestCase
56 64 def test_fake; assert(false, "Requires mocha to run those tests") end
57 65 end
58 66 end
59 67
General Comments 0
You need to be logged in to leave comments. Login now