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