##// END OF EJS Templates
Make sure that #scm_iconv returns a string with the target encoding (#14534)....
Jean-Philippe Lang -
r13520:9915962356ce
parent child
Show More
@@ -1,435 +1,435
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 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 'cgi'
19 19 require 'redmine/scm/adapters'
20 20
21 21 module Redmine
22 22 module Scm
23 23 module Adapters
24 24 class AbstractAdapter #:nodoc:
25 25
26 26 # raised if scm command exited with error, e.g. unknown revision.
27 27 class ScmCommandAborted < ::Redmine::Scm::Adapters::CommandFailed; end
28 28
29 29 class << self
30 30 def client_command
31 31 ""
32 32 end
33 33
34 34 def shell_quote_command
35 35 if Redmine::Platform.mswin? && RUBY_PLATFORM == 'java'
36 36 client_command
37 37 else
38 38 shell_quote(client_command)
39 39 end
40 40 end
41 41
42 42 # Returns the version of the scm client
43 43 # Eg: [1, 5, 0] or [] if unknown
44 44 def client_version
45 45 []
46 46 end
47 47
48 48 # Returns the version string of the scm client
49 49 # Eg: '1.5.0' or 'Unknown version' if unknown
50 50 def client_version_string
51 51 v = client_version || 'Unknown version'
52 52 v.is_a?(Array) ? v.join('.') : v.to_s
53 53 end
54 54
55 55 # Returns true if the current client version is above
56 56 # or equals the given one
57 57 # If option is :unknown is set to true, it will return
58 58 # true if the client version is unknown
59 59 def client_version_above?(v, options={})
60 60 ((client_version <=> v) >= 0) || (client_version.empty? && options[:unknown])
61 61 end
62 62
63 63 def client_available
64 64 true
65 65 end
66 66
67 67 def shell_quote(str)
68 68 if Redmine::Platform.mswin?
69 69 '"' + str.gsub(/"/, '\\"') + '"'
70 70 else
71 71 "'" + str.gsub(/'/, "'\"'\"'") + "'"
72 72 end
73 73 end
74 74 end
75 75
76 76 def initialize(url, root_url=nil, login=nil, password=nil,
77 77 path_encoding=nil)
78 78 @url = url
79 79 @login = login if login && !login.empty?
80 80 @password = (password || "") if @login
81 81 @root_url = root_url.blank? ? retrieve_root_url : root_url
82 82 end
83 83
84 84 def adapter_name
85 85 'Abstract'
86 86 end
87 87
88 88 def supports_cat?
89 89 true
90 90 end
91 91
92 92 def supports_annotate?
93 93 respond_to?('annotate')
94 94 end
95 95
96 96 def root_url
97 97 @root_url
98 98 end
99 99
100 100 def url
101 101 @url
102 102 end
103 103
104 104 def path_encoding
105 105 nil
106 106 end
107 107
108 108 # get info about the svn repository
109 109 def info
110 110 return nil
111 111 end
112 112
113 113 # Returns the entry identified by path and revision identifier
114 114 # or nil if entry doesn't exist in the repository
115 115 def entry(path=nil, identifier=nil)
116 116 parts = path.to_s.split(%r{[\/\\]}).select {|n| !n.blank?}
117 117 search_path = parts[0..-2].join('/')
118 118 search_name = parts[-1]
119 119 if search_path.blank? && search_name.blank?
120 120 # Root entry
121 121 Entry.new(:path => '', :kind => 'dir')
122 122 else
123 123 # Search for the entry in the parent directory
124 124 es = entries(search_path, identifier)
125 125 es ? es.detect {|e| e.name == search_name} : nil
126 126 end
127 127 end
128 128
129 129 # Returns an Entries collection
130 130 # or nil if the given path doesn't exist in the repository
131 131 def entries(path=nil, identifier=nil, options={})
132 132 return nil
133 133 end
134 134
135 135 def branches
136 136 return nil
137 137 end
138 138
139 139 def tags
140 140 return nil
141 141 end
142 142
143 143 def default_branch
144 144 return nil
145 145 end
146 146
147 147 def properties(path, identifier=nil)
148 148 return nil
149 149 end
150 150
151 151 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
152 152 return nil
153 153 end
154 154
155 155 def diff(path, identifier_from, identifier_to=nil)
156 156 return nil
157 157 end
158 158
159 159 def cat(path, identifier=nil)
160 160 return nil
161 161 end
162 162
163 163 def with_leading_slash(path)
164 164 path ||= ''
165 165 (path[0,1]!="/") ? "/#{path}" : path
166 166 end
167 167
168 168 def with_trailling_slash(path)
169 169 path ||= ''
170 170 (path[-1,1] == "/") ? path : "#{path}/"
171 171 end
172 172
173 173 def without_leading_slash(path)
174 174 path ||= ''
175 175 path.gsub(%r{^/+}, '')
176 176 end
177 177
178 178 def without_trailling_slash(path)
179 179 path ||= ''
180 180 (path[-1,1] == "/") ? path[0..-2] : path
181 181 end
182 182
183 183 def shell_quote(str)
184 184 self.class.shell_quote(str)
185 185 end
186 186
187 187 private
188 188 def retrieve_root_url
189 189 info = self.info
190 190 info ? info.root_url : nil
191 191 end
192 192
193 193 def target(path, sq=true)
194 194 path ||= ''
195 195 base = path.match(/^\//) ? root_url : url
196 196 str = "#{base}/#{path}".gsub(/[?<>\*]/, '')
197 197 if sq
198 198 str = shell_quote(str)
199 199 end
200 200 str
201 201 end
202 202
203 203 def logger
204 204 self.class.logger
205 205 end
206 206
207 207 def shellout(cmd, options = {}, &block)
208 208 self.class.shellout(cmd, options, &block)
209 209 end
210 210
211 211 def self.logger
212 212 Rails.logger
213 213 end
214 214
215 215 # Path to the file where scm stderr output is logged
216 216 # Returns nil if the log file is not writable
217 217 def self.stderr_log_file
218 218 if @stderr_log_file.nil?
219 219 writable = false
220 220 path = Redmine::Configuration['scm_stderr_log_file'].presence
221 221 path ||= Rails.root.join("log/#{Rails.env}.scm.stderr.log").to_s
222 222 if File.exists?(path)
223 223 if File.file?(path) && File.writable?(path)
224 224 writable = true
225 225 else
226 226 logger.warn("SCM log file (#{path}) is not writable")
227 227 end
228 228 else
229 229 begin
230 230 File.open(path, "w") {}
231 231 writable = true
232 232 rescue => e
233 233 logger.warn("SCM log file (#{path}) cannot be created: #{e.message}")
234 234 end
235 235 end
236 236 @stderr_log_file = writable ? path : false
237 237 end
238 238 @stderr_log_file || nil
239 239 end
240 240
241 241 def self.shellout(cmd, options = {}, &block)
242 242 if logger && logger.debug?
243 243 logger.debug "Shelling out: #{strip_credential(cmd)}"
244 244 # Capture stderr in a log file
245 245 if stderr_log_file
246 246 cmd = "#{cmd} 2>>#{shell_quote(stderr_log_file)}"
247 247 end
248 248 end
249 249 begin
250 250 mode = "r+"
251 251 IO.popen(cmd, mode) do |io|
252 252 io.set_encoding("ASCII-8BIT") if io.respond_to?(:set_encoding)
253 253 io.close_write unless options[:write_stdin]
254 254 block.call(io) if block_given?
255 255 end
256 256 ## If scm command does not exist,
257 257 ## Linux JRuby 1.6.2 (ruby-1.8.7-p330) raises java.io.IOException
258 258 ## in production environment.
259 259 # rescue Errno::ENOENT => e
260 260 rescue Exception => e
261 261 msg = strip_credential(e.message)
262 262 # The command failed, log it and re-raise
263 263 logmsg = "SCM command failed, "
264 264 logmsg += "make sure that your SCM command (e.g. svn) is "
265 265 logmsg += "in PATH (#{ENV['PATH']})\n"
266 266 logmsg += "You can configure your scm commands in config/configuration.yml.\n"
267 267 logmsg += "#{strip_credential(cmd)}\n"
268 268 logmsg += "with: #{msg}"
269 269 logger.error(logmsg)
270 270 raise CommandFailed.new(msg)
271 271 end
272 272 end
273 273
274 274 # Hides username/password in a given command
275 275 def self.strip_credential(cmd)
276 276 q = (Redmine::Platform.mswin? ? '"' : "'")
277 277 cmd.to_s.gsub(/(\-\-(password|username))\s+(#{q}[^#{q}]+#{q}|[^#{q}]\S+)/, '\\1 xxxx')
278 278 end
279 279
280 280 def strip_credential(cmd)
281 281 self.class.strip_credential(cmd)
282 282 end
283 283
284 284 def scm_iconv(to, from, str)
285 285 return nil if str.nil?
286 return str if to == from
286 return str if to == from && str.encoding.to_s == from
287 287 str.force_encoding(from)
288 288 begin
289 289 str.encode(to)
290 290 rescue Exception => err
291 291 logger.error("failed to convert from #{from} to #{to}. #{err}")
292 292 nil
293 293 end
294 294 end
295 295
296 296 def parse_xml(xml)
297 297 if RUBY_PLATFORM == 'java'
298 298 xml = xml.sub(%r{<\?xml[^>]*\?>}, '')
299 299 end
300 300 ActiveSupport::XmlMini.parse(xml)
301 301 end
302 302 end
303 303
304 304 class Entries < Array
305 305 def sort_by_name
306 306 dup.sort! {|x,y|
307 307 if x.kind == y.kind
308 308 x.name.to_s <=> y.name.to_s
309 309 else
310 310 x.kind <=> y.kind
311 311 end
312 312 }
313 313 end
314 314
315 315 def revisions
316 316 revisions ||= Revisions.new(collect{|entry| entry.lastrev}.compact)
317 317 end
318 318 end
319 319
320 320 class Info
321 321 attr_accessor :root_url, :lastrev
322 322 def initialize(attributes={})
323 323 self.root_url = attributes[:root_url] if attributes[:root_url]
324 324 self.lastrev = attributes[:lastrev]
325 325 end
326 326 end
327 327
328 328 class Entry
329 329 attr_accessor :name, :path, :kind, :size, :lastrev, :changeset
330 330
331 331 def initialize(attributes={})
332 332 self.name = attributes[:name] if attributes[:name]
333 333 self.path = attributes[:path] if attributes[:path]
334 334 self.kind = attributes[:kind] if attributes[:kind]
335 335 self.size = attributes[:size].to_i if attributes[:size]
336 336 self.lastrev = attributes[:lastrev]
337 337 end
338 338
339 339 def is_file?
340 340 'file' == self.kind
341 341 end
342 342
343 343 def is_dir?
344 344 'dir' == self.kind
345 345 end
346 346
347 347 def is_text?
348 348 Redmine::MimeType.is_type?('text', name)
349 349 end
350 350
351 351 def author
352 352 if changeset
353 353 changeset.author.to_s
354 354 elsif lastrev
355 355 Redmine::CodesetUtil.replace_invalid_utf8(lastrev.author.to_s.split('<').first)
356 356 end
357 357 end
358 358 end
359 359
360 360 class Revisions < Array
361 361 def latest
362 362 sort {|x,y|
363 363 unless x.time.nil? or y.time.nil?
364 364 x.time <=> y.time
365 365 else
366 366 0
367 367 end
368 368 }.last
369 369 end
370 370 end
371 371
372 372 class Revision
373 373 attr_accessor :scmid, :name, :author, :time, :message,
374 374 :paths, :revision, :branch, :identifier,
375 375 :parents
376 376
377 377 def initialize(attributes={})
378 378 self.identifier = attributes[:identifier]
379 379 self.scmid = attributes[:scmid]
380 380 self.name = attributes[:name] || self.identifier
381 381 self.author = attributes[:author]
382 382 self.time = attributes[:time]
383 383 self.message = attributes[:message] || ""
384 384 self.paths = attributes[:paths]
385 385 self.revision = attributes[:revision]
386 386 self.branch = attributes[:branch]
387 387 self.parents = attributes[:parents]
388 388 end
389 389
390 390 # Returns the readable identifier.
391 391 def format_identifier
392 392 self.identifier.to_s
393 393 end
394 394
395 395 def ==(other)
396 396 if other.nil?
397 397 false
398 398 elsif scmid.present?
399 399 scmid == other.scmid
400 400 elsif identifier.present?
401 401 identifier == other.identifier
402 402 elsif revision.present?
403 403 revision == other.revision
404 404 end
405 405 end
406 406 end
407 407
408 408 class Annotate
409 409 attr_reader :lines, :revisions
410 410
411 411 def initialize
412 412 @lines = []
413 413 @revisions = []
414 414 end
415 415
416 416 def add_line(line, revision)
417 417 @lines << line
418 418 @revisions << revision
419 419 end
420 420
421 421 def content
422 422 content = lines.join("\n")
423 423 end
424 424
425 425 def empty?
426 426 lines.empty?
427 427 end
428 428 end
429 429
430 430 class Branch < String
431 431 attr_accessor :revision, :scmid
432 432 end
433 433 end
434 434 end
435 435 end
@@ -1,259 +1,260
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 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 'pp'
20 20 class RepositoryCvsTest < ActiveSupport::TestCase
21 21 fixtures :projects
22 22
23 23 include Redmine::I18n
24 24
25 25 REPOSITORY_PATH = repository_path('cvs')
26 26 REPOSITORY_PATH.gsub!(/\//, "\\") if Redmine::Platform.mswin?
27 27 # CVS module
28 28 MODULE_NAME = 'test'
29 29 CHANGESETS_NUM = 7
30 30
31 31 def setup
32 32 @project = Project.find(3)
33 33 @repository = Repository::Cvs.create(:project => @project,
34 34 :root_url => REPOSITORY_PATH,
35 35 :url => MODULE_NAME,
36 36 :log_encoding => 'UTF-8')
37 37 assert @repository
38 38 end
39 39
40 40 def test_blank_module_error_message
41 41 set_language_if_valid 'en'
42 42 repo = Repository::Cvs.new(
43 43 :project => @project,
44 44 :identifier => 'test',
45 45 :log_encoding => 'UTF-8',
46 46 :root_url => REPOSITORY_PATH
47 47 )
48 48 assert !repo.save
49 49 assert_include "Module cannot be blank",
50 50 repo.errors.full_messages
51 51 end
52 52
53 53 def test_blank_module_error_message_fr
54 54 set_language_if_valid 'fr'
55 55 str = "Module doit \xc3\xaatre renseign\xc3\xa9(e)".force_encoding('UTF-8')
56 56 repo = Repository::Cvs.new(
57 57 :project => @project,
58 58 :identifier => 'test',
59 59 :log_encoding => 'UTF-8',
60 60 :path_encoding => '',
61 61 :url => '',
62 62 :root_url => REPOSITORY_PATH
63 63 )
64 64 assert !repo.save
65 65 assert_include str, repo.errors.full_messages
66 66 end
67 67
68 68 def test_blank_cvsroot_error_message
69 69 set_language_if_valid 'en'
70 70 repo = Repository::Cvs.new(
71 71 :project => @project,
72 72 :identifier => 'test',
73 73 :log_encoding => 'UTF-8',
74 74 :url => MODULE_NAME
75 75 )
76 76 assert !repo.save
77 77 assert_include "CVSROOT cannot be blank",
78 78 repo.errors.full_messages
79 79 end
80 80
81 81 def test_blank_cvsroot_error_message_fr
82 82 set_language_if_valid 'fr'
83 83 str = "CVSROOT doit \xc3\xaatre renseign\xc3\xa9(e)".force_encoding('UTF-8')
84 84 repo = Repository::Cvs.new(
85 85 :project => @project,
86 86 :identifier => 'test',
87 87 :log_encoding => 'UTF-8',
88 88 :path_encoding => '',
89 89 :url => MODULE_NAME,
90 90 :root_url => ''
91 91 )
92 92 assert !repo.save
93 93 assert_include str, repo.errors.full_messages
94 94 end
95 95
96 96 def test_root_url_should_be_validated_against_regexp_set_in_configuration
97 97 Redmine::Configuration.with 'scm_cvs_path_regexp' => '/cvspath/[a-z]+' do
98 98 repo = Repository::Cvs.new(
99 99 :project => @project,
100 100 :identifier => 'test',
101 101 :log_encoding => 'UTF-8',
102 102 :path_encoding => '',
103 103 :url => MODULE_NAME
104 104 )
105 105 repo.root_url = '/wrong_path'
106 106 assert !repo.valid?
107 107 assert repo.errors[:root_url].present?
108 108
109 109 repo.root_url = '/cvspath/foo'
110 110 assert repo.valid?
111 111 end
112 112 end
113 113
114 114 if File.directory?(REPOSITORY_PATH)
115 115 def test_fetch_changesets_from_scratch
116 116 assert_equal 0, @repository.changesets.count
117 117 @repository.fetch_changesets
118 118 @project.reload
119 119
120 120 assert_equal CHANGESETS_NUM, @repository.changesets.count
121 121 assert_equal 16, @repository.filechanges.count
122 122 assert_not_nil @repository.changesets.find_by_comments('Two files changed')
123 123
124 124 r2 = @repository.changesets.find_by_revision('2')
125 125 assert_equal 'v1-20071213-162510', r2.scmid
126 126 end
127 127
128 128 def test_fetch_changesets_incremental
129 129 assert_equal 0, @repository.changesets.count
130 130 @repository.fetch_changesets
131 131 @project.reload
132 132 assert_equal CHANGESETS_NUM, @repository.changesets.count
133 133
134 134 # Remove changesets with revision > 3
135 135 @repository.changesets.each {|c| c.destroy if c.revision.to_i > 3}
136 136 @project.reload
137 137 @repository.reload
138 138 assert_equal 3, @repository.changesets.count
139 139 assert_equal %w|3 2 1|, @repository.changesets.collect(&:revision)
140 140
141 141 rev3_commit = @repository.changesets.reorder('committed_on DESC').first
142 142 assert_equal '3', rev3_commit.revision
143 143 # 2007-12-14 01:27:22 +0900
144 144 rev3_committed_on = Time.gm(2007, 12, 13, 16, 27, 22)
145 145 assert_equal 'HEAD-20071213-162722', rev3_commit.scmid
146 146 assert_equal rev3_committed_on, rev3_commit.committed_on
147 147 latest_rev = @repository.latest_changeset
148 148 assert_equal rev3_committed_on, latest_rev.committed_on
149 149
150 150 @repository.fetch_changesets
151 151 @project.reload
152 152 @repository.reload
153 153 assert_equal CHANGESETS_NUM, @repository.changesets.count
154 154 assert_equal %w|7 6 5 4 3 2 1|, @repository.changesets.collect(&:revision)
155 155 rev5_commit = @repository.changesets.find_by_revision('5')
156 156 assert_equal 'HEAD-20071213-163001', rev5_commit.scmid
157 157 # 2007-12-14 01:30:01 +0900
158 158 rev5_committed_on = Time.gm(2007, 12, 13, 16, 30, 1)
159 159 assert_equal rev5_committed_on, rev5_commit.committed_on
160 160 end
161 161
162 162 def test_deleted_files_should_not_be_listed
163 163 assert_equal 0, @repository.changesets.count
164 164 @repository.fetch_changesets
165 165 @project.reload
166 166 assert_equal CHANGESETS_NUM, @repository.changesets.count
167 167
168 168 entries = @repository.entries('sources')
169 169 assert entries.detect {|e| e.name == 'watchers_controller.rb'}
170 170 assert_nil entries.detect {|e| e.name == 'welcome_controller.rb'}
171 171 end
172 172
173 173 def test_entries_rev3
174 174 assert_equal 0, @repository.changesets.count
175 175 @repository.fetch_changesets
176 176 @project.reload
177 177 assert_equal CHANGESETS_NUM, @repository.changesets.count
178 178 entries = @repository.entries('', '3')
179 179 assert_kind_of Redmine::Scm::Adapters::Entries, entries
180 180 assert_equal 3, entries.size
181 181 assert_equal entries[2].name, "README"
182 assert_equal 'UTF-8', entries[2].path.encoding.to_s
182 183 assert_equal entries[2].lastrev.time, Time.gm(2007, 12, 13, 16, 27, 22)
183 184 assert_equal entries[2].lastrev.identifier, '3'
184 185 assert_equal entries[2].lastrev.revision, '3'
185 186 assert_equal entries[2].lastrev.author, 'LANG'
186 187 end
187 188
188 189 def test_entries_invalid_path
189 190 assert_equal 0, @repository.changesets.count
190 191 @repository.fetch_changesets
191 192 @project.reload
192 193 assert_equal CHANGESETS_NUM, @repository.changesets.count
193 194 assert_nil @repository.entries('missing')
194 195 assert_nil @repository.entries('missing', '3')
195 196 end
196 197
197 198 def test_entries_invalid_revision
198 199 assert_equal 0, @repository.changesets.count
199 200 @repository.fetch_changesets
200 201 @project.reload
201 202 assert_equal CHANGESETS_NUM, @repository.changesets.count
202 203 assert_nil @repository.entries('', '123')
203 204 end
204 205
205 206 def test_cat
206 207 assert_equal 0, @repository.changesets.count
207 208 @repository.fetch_changesets
208 209 @project.reload
209 210 assert_equal CHANGESETS_NUM, @repository.changesets.count
210 211 buf = @repository.cat('README')
211 212 assert buf
212 213 lines = buf.split("\n")
213 214 assert_equal 3, lines.length
214 215 buf = lines[1].gsub(/\r$/, "")
215 216 assert_equal 'with one change', buf
216 217 buf = @repository.cat('README', '1')
217 218 assert buf
218 219 lines = buf.split("\n")
219 220 assert_equal 1, lines.length
220 221 buf = lines[0].gsub(/\r$/, "")
221 222 assert_equal 'CVS test repository', buf
222 223 assert_nil @repository.cat('missing.rb')
223 224
224 225 # sources/welcome_controller.rb is removed at revision 5.
225 226 assert @repository.cat('sources/welcome_controller.rb', '4')
226 227 assert @repository.cat('sources/welcome_controller.rb', '5').blank?
227 228
228 229 # invalid revision
229 230 assert @repository.cat('README', '123').blank?
230 231 end
231 232
232 233 def test_annotate
233 234 assert_equal 0, @repository.changesets.count
234 235 @repository.fetch_changesets
235 236 @project.reload
236 237 assert_equal CHANGESETS_NUM, @repository.changesets.count
237 238 ann = @repository.annotate('README')
238 239 assert ann
239 240 assert_equal 3, ann.revisions.length
240 241 assert_equal '1.2', ann.revisions[1].revision
241 242 assert_equal 'LANG', ann.revisions[1].author
242 243 assert_equal 'with one change', ann.lines[1]
243 244
244 245 ann = @repository.annotate('README', '1')
245 246 assert ann
246 247 assert_equal 1, ann.revisions.length
247 248 assert_equal '1.1', ann.revisions[0].revision
248 249 assert_equal 'LANG', ann.revisions[0].author
249 250 assert_equal 'CVS test repository', ann.lines[0]
250 251
251 252 # invalid revision
252 253 assert_nil @repository.annotate('README', '123')
253 254 end
254 255
255 256 else
256 257 puts "CVS test repository NOT FOUND. Skipping unit tests !!!"
257 258 def test_fake; assert true end
258 259 end
259 260 end
General Comments 0
You need to be logged in to leave comments. Login now