##// END OF EJS Templates
scm: cvs: fix CVS diffs do not handle new files properly (#7615)....
Toshi MARUYAMA -
r4819:a2e47f9fbaf5
parent child
Show More
@@ -1,403 +1,407
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 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
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 @url = url
67 67 @login = login if login && !login.empty?
68 68 @password = (password || "") if @login
69 69 #TODO: better Exception here (IllegalArgumentException)
70 70 raise CommandFailed if root_url.blank?
71 71 @root_url = root_url
72 72 end
73 73
74 74 def root_url
75 75 @root_url
76 76 end
77 77
78 78 def url
79 79 @url
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)
95 95 logger.debug "<cvs> entries '#{path}' with identifier '#{identifier}'"
96 96 path_with_project="#{url}#{with_leading_slash(path)}"
97 97 entries = Entries.new
98 98 cmd = "#{self.class.sq_bin} -d #{shell_quote root_url} rls -e"
99 99 cmd << " -D \"#{time_to_cvstime(identifier)}\"" if identifier
100 100 cmd << " #{shell_quote path_with_project}"
101 101 shellout(cmd) do |io|
102 102 io.each_line(){|line|
103 103 fields=line.chop.split('/',-1)
104 104 logger.debug(">>InspectLine #{fields.inspect}")
105 105
106 106 if fields[0]!="D"
107 107 entries << Entry.new({:name => fields[-5],
108 108 #:path => fields[-4].include?(path)?fields[-4]:(path + "/"+ fields[-4]),
109 109 :path => "#{path}/#{fields[-5]}",
110 110 :kind => 'file',
111 111 :size => nil,
112 112 :lastrev => Revision.new({
113 113 :revision => fields[-4],
114 114 :name => fields[-4],
115 115 :time => Time.parse(fields[-3]),
116 116 :author => ''
117 117 })
118 118 })
119 119 else
120 120 entries << Entry.new({:name => fields[1],
121 121 :path => "#{path}/#{fields[1]}",
122 122 :kind => 'dir',
123 123 :size => nil,
124 124 :lastrev => nil
125 125 })
126 126 end
127 127 }
128 128 end
129 129 return nil if $? && $?.exitstatus != 0
130 130 entries.sort_by_name
131 131 end
132 132
133 133 STARTLOG="----------------------------"
134 134 ENDLOG ="============================================================================="
135 135
136 136 # Returns all revisions found between identifier_from and identifier_to
137 137 # in the repository. both identifier have to be dates or nil.
138 138 # these method returns nothing but yield every result in block
139 139 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}, &block)
140 140 logger.debug "<cvs> revisions path:'#{path}',identifier_from #{identifier_from}, identifier_to #{identifier_to}"
141 141
142 142 path_with_project="#{url}#{with_leading_slash(path)}"
143 143 cmd = "#{self.class.sq_bin} -d #{shell_quote root_url} rlog"
144 144 cmd << " -d\">#{time_to_cvstime_rlog(identifier_from)}\"" if identifier_from
145 145 cmd << " #{shell_quote path_with_project}"
146 146 shellout(cmd) do |io|
147 147 state="entry_start"
148 148
149 149 commit_log=String.new
150 150 revision=nil
151 151 date=nil
152 152 author=nil
153 153 entry_path=nil
154 154 entry_name=nil
155 155 file_state=nil
156 156 branch_map=nil
157 157
158 158 io.each_line() do |line|
159 159
160 160 if state!="revision" && /^#{ENDLOG}/ =~ line
161 161 commit_log=String.new
162 162 revision=nil
163 163 state="entry_start"
164 164 end
165 165
166 166 if state=="entry_start"
167 167 branch_map=Hash.new
168 168 if /^RCS file: #{Regexp.escape(root_url_path)}\/#{Regexp.escape(path_with_project)}(.+),v$/ =~ line
169 169 entry_path = normalize_cvs_path($1)
170 170 entry_name = normalize_path(File.basename($1))
171 171 logger.debug("Path #{entry_path} <=> Name #{entry_name}")
172 172 elsif /^head: (.+)$/ =~ line
173 173 entry_headRev = $1 #unless entry.nil?
174 174 elsif /^symbolic names:/ =~ line
175 175 state="symbolic" #unless entry.nil?
176 176 elsif /^#{STARTLOG}/ =~ line
177 177 commit_log=String.new
178 178 state="revision"
179 179 end
180 180 next
181 181 elsif state=="symbolic"
182 182 if /^(.*):\s(.*)/ =~ (line.strip)
183 183 branch_map[$1]=$2
184 184 else
185 185 state="tags"
186 186 next
187 187 end
188 188 elsif state=="tags"
189 189 if /^#{STARTLOG}/ =~ line
190 190 commit_log = ""
191 191 state="revision"
192 192 elsif /^#{ENDLOG}/ =~ line
193 193 state="head"
194 194 end
195 195 next
196 196 elsif state=="revision"
197 197 if /^#{ENDLOG}/ =~ line || /^#{STARTLOG}/ =~ line
198 198 if revision
199 199
200 200 revHelper=CvsRevisionHelper.new(revision)
201 201 revBranch="HEAD"
202 202
203 203 branch_map.each() do |branch_name,branch_point|
204 204 if revHelper.is_in_branch_with_symbol(branch_point)
205 205 revBranch=branch_name
206 206 end
207 207 end
208 208
209 209 logger.debug("********** YIELD Revision #{revision}::#{revBranch}")
210 210
211 211 yield Revision.new({
212 212 :time => date,
213 213 :author => author,
214 214 :message=>commit_log.chomp,
215 215 :paths => [{
216 216 :revision => revision,
217 217 :branch=> revBranch,
218 218 :path=>entry_path,
219 219 :name=>entry_name,
220 220 :kind=>'file',
221 221 :action=>file_state
222 222 }]
223 223 })
224 224 end
225 225
226 226 commit_log=String.new
227 227 revision=nil
228 228
229 229 if /^#{ENDLOG}/ =~ line
230 230 state="entry_start"
231 231 end
232 232 next
233 233 end
234 234
235 235 if /^branches: (.+)$/ =~ line
236 236 #TODO: version.branch = $1
237 237 elsif /^revision (\d+(?:\.\d+)+).*$/ =~ line
238 238 revision = $1
239 239 elsif /^date:\s+(\d+.\d+.\d+\s+\d+:\d+:\d+)/ =~ line
240 240 date = Time.parse($1)
241 241 author = /author: ([^;]+)/.match(line)[1]
242 242 file_state = /state: ([^;]+)/.match(line)[1]
243 243 #TODO: linechanges only available in CVS.... maybe a feature our SVN implementation. i'm sure, they are
244 244 # useful for stats or something else
245 245 # linechanges =/lines: \+(\d+) -(\d+)/.match(line)
246 246 # unless linechanges.nil?
247 247 # version.line_plus = linechanges[1]
248 248 # version.line_minus = linechanges[2]
249 249 # else
250 250 # version.line_plus = 0
251 251 # version.line_minus = 0
252 252 # end
253 253 else
254 254 commit_log << line unless line =~ /^\*\*\* empty log message \*\*\*/
255 255 end
256 256 end
257 257 end
258 258 end
259 259 end
260 260
261 261 def diff(path, identifier_from, identifier_to=nil)
262 262 logger.debug "<cvs> diff path:'#{path}',identifier_from #{identifier_from}, identifier_to #{identifier_to}"
263 263 path_with_project="#{url}#{with_leading_slash(path)}"
264 264 cmd = "#{self.class.sq_bin} -d #{shell_quote root_url} rdiff -u -r#{identifier_to} -r#{identifier_from} #{shell_quote path_with_project}"
265 265 diff = []
266 266 shellout(cmd) do |io|
267 267 io.each_line do |line|
268 268 diff << line
269 269 end
270 270 end
271 271 return nil if $? && $?.exitstatus != 0
272 272 diff
273 273 end
274 274
275 275 def cat(path, identifier=nil)
276 276 identifier = (identifier) ? identifier : "HEAD"
277 277 logger.debug "<cvs> cat path:'#{path}',identifier #{identifier}"
278 278 path_with_project="#{url}#{with_leading_slash(path)}"
279 279 cmd = "#{self.class.sq_bin} -d #{shell_quote root_url} co"
280 280 cmd << " -D \"#{time_to_cvstime(identifier)}\"" if identifier
281 281 cmd << " -p #{shell_quote path_with_project}"
282 282 cat = nil
283 283 shellout(cmd) do |io|
284 284 io.binmode
285 285 cat = io.read
286 286 end
287 287 return nil if $? && $?.exitstatus != 0
288 288 cat
289 289 end
290 290
291 291 def annotate(path, identifier=nil)
292 292 identifier = (identifier) ? identifier.to_i : "HEAD"
293 293 logger.debug "<cvs> annotate path:'#{path}',identifier #{identifier}"
294 294 path_with_project="#{url}#{with_leading_slash(path)}"
295 295 cmd = "#{self.class.sq_bin} -d #{shell_quote root_url} rannotate -r#{identifier} #{shell_quote path_with_project}"
296 296 blame = Annotate.new
297 297 shellout(cmd) do |io|
298 298 io.each_line do |line|
299 299 next unless line =~ %r{^([\d\.]+)\s+\(([^\)]+)\s+[^\)]+\):\s(.*)$}
300 300 blame.add_line($3.rstrip, Revision.new(:revision => $1, :author => $2.strip))
301 301 end
302 302 end
303 303 return nil if $? && $?.exitstatus != 0
304 304 blame
305 305 end
306 306
307 307 private
308 308
309 309 # Returns the root url without the connexion string
310 310 # :pserver:anonymous@foo.bar:/path => /path
311 311 # :ext:cvsservername:/path => /path
312 312 def root_url_path
313 313 root_url.to_s.gsub(/^:.+:\d*/, '')
314 314 end
315 315
316 316 # convert a date/time into the CVS-format
317 317 def time_to_cvstime(time)
318 318 return nil if time.nil?
319 319 return Time.now if time == 'HEAD'
320 320
321 321 unless time.kind_of? Time
322 322 time = Time.parse(time)
323 323 end
324 324 return time.strftime("%Y-%m-%d %H:%M:%S")
325 325 end
326 326
327 327 def time_to_cvstime_rlog(time)
328 328 return nil if time.nil?
329 329 t1 = time.clone.localtime
330 330 return t1.strftime("%Y-%m-%d %H:%M:%S")
331 331 end
332 332
333 333 def normalize_cvs_path(path)
334 334 normalize_path(path.gsub(/Attic\//,''))
335 335 end
336 336
337 337 def normalize_path(path)
338 338 path.sub(/^(\/)*(.*)/,'\2').sub(/(.*)(,v)+/,'\1')
339 339 end
340 340 end
341 341
342 342 class CvsRevisionHelper
343 343 attr_accessor :complete_rev, :revision, :base, :branchid
344 344
345 345 def initialize(complete_rev)
346 346 @complete_rev = complete_rev
347 347 parseRevision()
348 348 end
349 349
350 350 def branchPoint
351 351 return @base
352 352 end
353 353
354 354 def branchVersion
355 355 if isBranchRevision
356 356 return @base+"."+@branchid
357 357 end
358 358 return @base
359 359 end
360 360
361 361 def isBranchRevision
362 362 !@branchid.nil?
363 363 end
364 364
365 365 def prevRev
366 366 unless @revision==0
367 367 return buildRevision(@revision-1)
368 368 end
369 369 return buildRevision(@revision)
370 370 end
371 371
372 372 def is_in_branch_with_symbol(branch_symbol)
373 373 bpieces=branch_symbol.split(".")
374 374 branch_start="#{bpieces[0..-3].join(".")}.#{bpieces[-1]}"
375 375 return (branchVersion==branch_start)
376 376 end
377 377
378 378 private
379 379 def buildRevision(rev)
380 380 if rev== 0
381 if @branchid.nil?
382 @base+".0"
383 else
381 384 @base
385 end
382 386 elsif @branchid.nil?
383 387 @base+"."+rev.to_s
384 388 else
385 389 @base+"."+@branchid+"."+rev.to_s
386 390 end
387 391 end
388 392
389 393 # Interpretiert die cvs revisionsnummern wie z.b. 1.14 oder 1.3.0.15
390 394 def parseRevision()
391 395 pieces=@complete_rev.split(".")
392 396 @revision=pieces.last.to_i
393 397 baseSize=1
394 398 baseSize+=(pieces.size/2)
395 399 @base=pieces[0..-baseSize].join(".")
396 400 if baseSize > 2
397 401 @branchid=pieces[-2]
398 402 end
399 403 end
400 404 end
401 405 end
402 406 end
403 407 end
@@ -1,187 +1,205
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2008 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 require 'repositories_controller'
20 20
21 21 # Re-raise errors caught by the controller.
22 22 class RepositoriesController; def rescue_action(e) raise e end; end
23 23
24 24 class RepositoriesCvsControllerTest < ActionController::TestCase
25 25 fixtures :projects, :users, :roles, :members, :member_roles, :repositories, :enabled_modules
26 26
27 27 # No '..' in the repository path
28 28 REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/cvs_repository'
29 29 REPOSITORY_PATH.gsub!(/\//, "\\") if Redmine::Platform.mswin?
30 30 # CVS module
31 31 MODULE_NAME = 'test'
32 32 PRJ_ID = 3
33 33
34 34 def setup
35 35 @controller = RepositoriesController.new
36 36 @request = ActionController::TestRequest.new
37 37 @response = ActionController::TestResponse.new
38 38 Setting.default_language = 'en'
39 39 User.current = nil
40 40
41 41 @project = Project.find(PRJ_ID)
42 42 @repository = Repository::Cvs.create(:project => Project.find(PRJ_ID),
43 43 :root_url => REPOSITORY_PATH,
44 44 :url => MODULE_NAME)
45 45 assert @repository
46 46 end
47 47
48 48 if File.directory?(REPOSITORY_PATH)
49 49 def test_show
50 50 @repository.fetch_changesets
51 51 @repository.reload
52 52 get :show, :id => PRJ_ID
53 53 assert_response :success
54 54 assert_template 'show'
55 55 assert_not_nil assigns(:entries)
56 56 assert_not_nil assigns(:changesets)
57 57 end
58 58
59 59 def test_browse_root
60 60 @repository.fetch_changesets
61 61 @repository.reload
62 62 get :show, :id => PRJ_ID
63 63 assert_response :success
64 64 assert_template 'show'
65 65 assert_not_nil assigns(:entries)
66 66 assert_equal 3, assigns(:entries).size
67 67
68 68 entry = assigns(:entries).detect {|e| e.name == 'images'}
69 69 assert_equal 'dir', entry.kind
70 70
71 71 entry = assigns(:entries).detect {|e| e.name == 'README'}
72 72 assert_equal 'file', entry.kind
73 73 end
74 74
75 75 def test_browse_directory
76 76 @repository.fetch_changesets
77 77 @repository.reload
78 78 get :show, :id => PRJ_ID, :path => ['images']
79 79 assert_response :success
80 80 assert_template 'show'
81 81 assert_not_nil assigns(:entries)
82 82 assert_equal ['add.png', 'delete.png', 'edit.png'], assigns(:entries).collect(&:name)
83 83 entry = assigns(:entries).detect {|e| e.name == 'edit.png'}
84 84 assert_not_nil entry
85 85 assert_equal 'file', entry.kind
86 86 assert_equal 'images/edit.png', entry.path
87 87 end
88 88
89 89 def test_browse_at_given_revision
90 90 @repository.fetch_changesets
91 91 @repository.reload
92 92 get :show, :id => PRJ_ID, :path => ['images'], :rev => 1
93 93 assert_response :success
94 94 assert_template 'show'
95 95 assert_not_nil assigns(:entries)
96 96 assert_equal ['delete.png', 'edit.png'], assigns(:entries).collect(&:name)
97 97 end
98 98
99 99 def test_entry
100 100 @repository.fetch_changesets
101 101 @repository.reload
102 102 get :entry, :id => PRJ_ID, :path => ['sources', 'watchers_controller.rb']
103 103 assert_response :success
104 104 assert_template 'entry'
105 105 assert_no_tag :tag => 'td', :attributes => { :class => /line-code/},
106 106 :content => /before_filter/
107 107 end
108 108
109 109 def test_entry_at_given_revision
110 110 # changesets must be loaded
111 111 @repository.fetch_changesets
112 112 @repository.reload
113 113 get :entry, :id => PRJ_ID, :path => ['sources', 'watchers_controller.rb'], :rev => 2
114 114 assert_response :success
115 115 assert_template 'entry'
116 116 # this line was removed in r3
117 117 assert_tag :tag => 'td', :attributes => { :class => /line-code/},
118 118 :content => /before_filter/
119 119 end
120 120
121 121 def test_entry_not_found
122 122 @repository.fetch_changesets
123 123 @repository.reload
124 124 get :entry, :id => PRJ_ID, :path => ['sources', 'zzz.c']
125 125 assert_tag :tag => 'p', :attributes => { :id => /errorExplanation/ },
126 126 :content => /The entry or revision was not found in the repository/
127 127 end
128 128
129 129 def test_entry_download
130 130 @repository.fetch_changesets
131 131 @repository.reload
132 132 get :entry, :id => PRJ_ID, :path => ['sources', 'watchers_controller.rb'], :format => 'raw'
133 133 assert_response :success
134 134 end
135 135
136 136 def test_directory_entry
137 137 @repository.fetch_changesets
138 138 @repository.reload
139 139 get :entry, :id => PRJ_ID, :path => ['sources']
140 140 assert_response :success
141 141 assert_template 'show'
142 142 assert_not_nil assigns(:entry)
143 143 assert_equal 'sources', assigns(:entry).name
144 144 end
145 145
146 146 def test_diff
147 147 @repository.fetch_changesets
148 148 @repository.reload
149 149 get :diff, :id => PRJ_ID, :rev => 3, :type => 'inline'
150 150 assert_response :success
151 151 assert_template 'diff'
152 152 assert_tag :tag => 'td', :attributes => { :class => 'line-code diff_out' },
153 153 :content => /watched.remove_watcher/
154 154 assert_tag :tag => 'td', :attributes => { :class => 'line-code diff_in' },
155 155 :content => /watched.remove_all_watcher/
156 156 end
157 157
158 def test_diff_new_files
159 @repository.fetch_changesets
160 @repository.reload
161 get :diff, :id => PRJ_ID, :rev => 1, :type => 'inline'
162 assert_response :success
163 assert_template 'diff'
164 assert_tag :tag => 'td', :attributes => { :class => 'line-code diff_in' },
165 :content => /watched.remove_watcher/
166 assert_tag :tag => 'th', :attributes => { :class => 'filename' },
167 :content => /test\/README/
168 assert_tag :tag => 'th', :attributes => { :class => 'filename' },
169 :content => /test\/images\/delete.png /
170 assert_tag :tag => 'th', :attributes => { :class => 'filename' },
171 :content => /test\/images\/edit.png/
172 assert_tag :tag => 'th', :attributes => { :class => 'filename' },
173 :content => /test\/sources\/watchers_controller.rb/
174 end
175
158 176 def test_annotate
159 177 @repository.fetch_changesets
160 178 @repository.reload
161 179 get :annotate, :id => PRJ_ID, :path => ['sources', 'watchers_controller.rb']
162 180 assert_response :success
163 181 assert_template 'annotate'
164 182 # 1.1 line
165 183 assert_tag :tag => 'th', :attributes => { :class => 'line-num' },
166 184 :content => '18',
167 185 :sibling => { :tag => 'td', :attributes => { :class => 'revision' },
168 186 :content => /1.1/,
169 187 :sibling => { :tag => 'td', :attributes => { :class => 'author' },
170 188 :content => /LANG/
171 189 }
172 190 }
173 191 # 1.2 line
174 192 assert_tag :tag => 'th', :attributes => { :class => 'line-num' },
175 193 :content => '32',
176 194 :sibling => { :tag => 'td', :attributes => { :class => 'revision' },
177 195 :content => /1.2/,
178 196 :sibling => { :tag => 'td', :attributes => { :class => 'author' },
179 197 :content => /LANG/
180 198 }
181 199 }
182 200 end
183 201 else
184 202 puts "CVS test repository NOT FOUND. Skipping functional tests !!!"
185 203 def test_fake; assert true end
186 204 end
187 205 end
General Comments 0
You need to be logged in to leave comments. Login now