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