rscm 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. data/README +198 -0
  2. data/Rakefile +118 -0
  3. data/ext/rscm.jar +0 -0
  4. data/lib/rscm.rb +10 -0
  5. data/lib/rscm/abstract_log_parser.rb +49 -0
  6. data/lib/rscm/abstract_scm.rb +229 -0
  7. data/lib/rscm/changes.rb +271 -0
  8. data/lib/rscm/cvs/cvs.rb +363 -0
  9. data/lib/rscm/cvs/cvs_log_parser.rb +161 -0
  10. data/lib/rscm/darcs/darcs.rb +69 -0
  11. data/lib/rscm/line_editor.rb +46 -0
  12. data/lib/rscm/logging.rb +5 -0
  13. data/lib/rscm/monotone/monotone.rb +107 -0
  14. data/lib/rscm/mooky/mooky.rb +13 -0
  15. data/lib/rscm/parser.rb +39 -0
  16. data/lib/rscm/path_converter.rb +92 -0
  17. data/lib/rscm/perforce/perforce.rb +415 -0
  18. data/lib/rscm/starteam/starteam.rb +99 -0
  19. data/lib/rscm/svn/svn.rb +337 -0
  20. data/lib/rscm/svn/svn_log_parser.rb +134 -0
  21. data/lib/rscm/time_ext.rb +125 -0
  22. data/test/rscm/apply_label_scm_tests.rb +26 -0
  23. data/test/rscm/changes_fixture.rb +20 -0
  24. data/test/rscm/changes_test.rb +129 -0
  25. data/test/rscm/cvs/cvs_log_parser_test.rb +575 -0
  26. data/test/rscm/cvs/cvs_test.rb +22 -0
  27. data/test/rscm/darcs/darcs_test.rb +14 -0
  28. data/test/rscm/difftool_test.rb +40 -0
  29. data/test/rscm/file_ext.rb +12 -0
  30. data/test/rscm/generic_scm_tests.rb +282 -0
  31. data/test/rscm/line_editor_test.rb +76 -0
  32. data/test/rscm/mockit.rb +130 -0
  33. data/test/rscm/mockit_test.rb +117 -0
  34. data/test/rscm/monotone/monotone_test.rb +19 -0
  35. data/test/rscm/mooky/mooky_test.rb +14 -0
  36. data/test/rscm/parser_test.rb +47 -0
  37. data/test/rscm/path_converter_test.rb +52 -0
  38. data/test/rscm/perforce/perforce_test.rb +14 -0
  39. data/test/rscm/starteam/starteam_test.rb +36 -0
  40. data/test/rscm/svn/svn_log_parser_test.rb +111 -0
  41. data/test/rscm/svn/svn_test.rb +28 -0
  42. data/test/rscm/tempdir.rb +12 -0
  43. metadata +81 -0
@@ -0,0 +1,271 @@
1
+ require 'xmlrpc/utils'
2
+ require 'rscm/time_ext'
3
+
4
+ module RSCM
5
+
6
+ # TODO: add a hook to get committers from a separate class - to support registered pairs
7
+ # We'll be able to do lots of cool analysis with visitors later -> graphs. mmmmm.
8
+
9
+ # A collection of changesets.
10
+ class ChangeSets
11
+ include Enumerable
12
+ include XMLRPC::Marshallable
13
+
14
+ attr_reader :changesets
15
+
16
+ def initialize(changesets=[])
17
+ @changesets = changesets
18
+ end
19
+
20
+ # Accepts a visitor that will receive callbacks while
21
+ # iterating over this instance's internal structure.
22
+ # The visitor should respond to the following methods:
23
+ #
24
+ # * visit_changesets(changesets)
25
+ # * visit_changeset(changeset)
26
+ # * visit_change(change)
27
+ #
28
+ def accept(visitor)
29
+ visitor.visit_changesets(self)
30
+ self.each{|changeset| changeset.accept(visitor)}
31
+ end
32
+
33
+ def [](change)
34
+ @changesets[change]
35
+ end
36
+
37
+ def each(&block)
38
+ @changesets.each(&block)
39
+ end
40
+
41
+ def reverse
42
+ ChangeSets.new(@changesets.dup.reverse)
43
+ end
44
+
45
+ def length
46
+ @changesets.length
47
+ end
48
+
49
+ def ==(other)
50
+ return false if !other.is_a?(self.class)
51
+ @changesets == other.changesets
52
+ end
53
+
54
+ def empty?
55
+ @changesets.empty?
56
+ end
57
+
58
+ # The set of developers that contributed to all of the contained ChangeSet s.
59
+ def developers
60
+ result = []
61
+ each do |changeset|
62
+ result << changeset.developer unless result.index(changeset.developer)
63
+ end
64
+ result
65
+ end
66
+
67
+ # The latest ChangeSet (with the latest time)
68
+ # or nil if there are none.
69
+ def latest
70
+ result = nil
71
+ each do |changeset|
72
+ result = changeset if result.nil? || result.time < changeset.time
73
+ end
74
+ result
75
+ end
76
+
77
+ # Adds a Change or a ChangeSet.
78
+ # If the argument is a Change and no corresponding ChangeSet exists,
79
+ # a new ChangeSet is created, added, and the Change is added to that ChangeSet -
80
+ # and then finally the newly created ChangeSet is returned.
81
+ # Otherwise nil is returned.
82
+ def add(change_or_changeset)
83
+ if(change_or_changeset.is_a?(ChangeSet))
84
+ @changesets << change_or_changeset
85
+ return change_or_changeset
86
+ else
87
+ changeset = @changesets.find { |a_changeset| a_changeset.can_contain?(change_or_changeset) }
88
+ if(changeset.nil?)
89
+ changeset = ChangeSet.new
90
+ @changesets << changeset
91
+ changeset << change_or_changeset
92
+ return changeset
93
+ end
94
+ changeset << change_or_changeset
95
+ return nil
96
+ end
97
+ end
98
+
99
+ def push(*change_or_changesets)
100
+ change_or_changesets.each { |change_or_changeset| self << (change_or_changeset) }
101
+ self
102
+ end
103
+
104
+ # The most recent time of all the ChangeSet s.
105
+ def time
106
+ time = nil
107
+ changesets.each do |changeset|
108
+ time = changeset.time if @time.nil? || @time < changeset.time
109
+ end
110
+ time
111
+ end
112
+
113
+ # Sorts the changesets according to time
114
+ def sort!
115
+ @changesets.sort!
116
+ self
117
+ end
118
+
119
+ end
120
+
121
+ # Represents a collection of Change that were committed at the same time.
122
+ # Non-transactional SCMs (such as CVS and StarTeam) emulate ChangeSet
123
+ # by grouping Change s that were committed by the same developer, with the
124
+ # same commit message, and within a "reasonably" small timespan.
125
+ class ChangeSet
126
+ include Enumerable
127
+ include XMLRPC::Marshallable
128
+
129
+ attr_reader :changes
130
+ attr_accessor :revision
131
+ attr_accessor :developer
132
+ attr_accessor :message
133
+ attr_accessor :time
134
+
135
+ def initialize(changes=[])
136
+ @changes = changes
137
+ end
138
+
139
+ def accept(visitor)
140
+ visitor.visit_changeset(self)
141
+ @changes.each{|change| change.accept(visitor)}
142
+ end
143
+
144
+ def << (change)
145
+ @changes << change
146
+ self.time = change.time if self.time.nil? || self.time < change.time unless change.time.nil?
147
+ self.developer = change.developer if change.developer
148
+ self.message = change.message if change.message
149
+ end
150
+
151
+ def [] (change)
152
+ @changes[change]
153
+ end
154
+
155
+ def each(&block)
156
+ @changes.each(&block)
157
+ end
158
+
159
+ def length
160
+ @changes.length
161
+ end
162
+
163
+ def time=(t)
164
+ raise "time must be a Time object - it was a #{t.class.name} with the string value #{t}" unless t.is_a?(Time)
165
+ raise "can't set time to an inferiour value than the previous value" if @time && (t < @time)
166
+ @time = t
167
+ end
168
+
169
+ def ==(other)
170
+ return false if !other.is_a?(self.class)
171
+ @changes == other.changes
172
+ end
173
+
174
+ def <=>(other)
175
+ @time <=> other.time
176
+ end
177
+
178
+ # Whether this instance can contain a Change. Used
179
+ # by non-transactional SCMs.
180
+ def can_contain?(change)
181
+ self.developer == change.developer &&
182
+ self.message == change.message &&
183
+ (self.time - change.time).abs < 60
184
+ end
185
+
186
+ # String representation that can be used for debugging.
187
+ def to_s
188
+ result = "#{revision} | #{developer} | #{time} | #{message}\n"
189
+ self.each do |change|
190
+ result << " " << change.to_s << "\n"
191
+ end
192
+ result
193
+ end
194
+
195
+ # Returns the identifier of the changeset. This is the revision
196
+ # (if defined) or an UTC time if revision is undefined.
197
+ def identifier
198
+ @revision || @time
199
+ end
200
+
201
+ end
202
+
203
+ # Represents a change to an individual file.
204
+ class Change
205
+ include XMLRPC::Marshallable
206
+
207
+ MODIFIED = "MODIFIED"
208
+ DELETED = "DELETED"
209
+ ADDED = "ADDED"
210
+ MOVED = "MOVED"
211
+
212
+ attr_accessor :status
213
+ attr_accessor :path
214
+ attr_accessor :previous_revision
215
+ attr_accessor :revision
216
+
217
+ # TODO: Remove redundant attributes that are in ChangeSet
218
+ attr_accessor :developer
219
+ attr_accessor :message
220
+ # This is a UTC ruby time
221
+ attr_accessor :time
222
+
223
+ def initialize(path=nil, developer=nil, message=nil, revision=nil, time=nil, status=DELETED)
224
+ @path, @developer, @message, @revision, @time, @status = path, developer, message, revision, time, status
225
+ end
226
+
227
+ def accept(visitor)
228
+ visitor.visit_change(self)
229
+ end
230
+
231
+ def to_s
232
+ "#{path} | #{revision}"
233
+ end
234
+
235
+ def developer=(developer)
236
+ raise "can't be null" if developer.nil?
237
+ @developer = developer
238
+ end
239
+
240
+ def message=(message)
241
+ raise "can't be null" if message.nil?
242
+ @message = message
243
+ end
244
+
245
+ def path=(path)
246
+ raise "can't be null" if path.nil?
247
+ @path = path
248
+ end
249
+
250
+ def revision=(revision)
251
+ raise "can't be null" if revision.nil?
252
+ @revision = revision
253
+ end
254
+
255
+ def time=(time)
256
+ raise "time must be a Time object" unless time.is_a?(Time)
257
+ @time = time
258
+ end
259
+
260
+ def ==(other)
261
+ return false if !other.is_a?(self.class)
262
+ self.path == other.path &&
263
+ self.developer == other.developer &&
264
+ self.message == other.message &&
265
+ self.revision == other.revision &&
266
+ self.time == other.time
267
+ end
268
+
269
+ end
270
+
271
+ end
@@ -0,0 +1,363 @@
1
+ require 'rscm/abstract_scm'
2
+ require 'rscm/path_converter'
3
+ require 'rscm/line_editor'
4
+ require 'rscm/cvs/cvs_log_parser'
5
+
6
+ module RSCM
7
+
8
+ # RSCM implementation for CVS.
9
+ #
10
+ # You need the cvs executable on the PATH in order for it to work.
11
+ #
12
+ # NOTE: On Cygwin this has to be the win32 build of cvs and not the Cygwin one.
13
+ class CVS < AbstractSCM
14
+
15
+ public
16
+ attr_accessor :root
17
+ attr_accessor :mod
18
+ attr_accessor :branch
19
+ attr_accessor :password
20
+
21
+ def initialize(root=nil, mod=nil, branch=nil, password=nil)
22
+ @root, @mod, @branch, @password = root, mod, branch, password
23
+ end
24
+
25
+ def name
26
+ "CVS"
27
+ end
28
+
29
+ def import(dir, message)
30
+ modname = File.basename(dir)
31
+ cvs(dir, "import -m \"#{message}\" #{modname} VENDOR START")
32
+ end
33
+
34
+ def add(checkout_dir, relative_filename)
35
+ cvs(checkout_dir, "add #{relative_filename}")
36
+ end
37
+
38
+ # The extra simulate parameter is not in accordance with the AbstractSCM API,
39
+ # but it's optional and is only being used from within this class (uptodate? method).
40
+ def checkout(checkout_dir, to_identifier=nil, simulate=false)
41
+ checked_out_files = []
42
+ if(checked_out?(checkout_dir))
43
+ path_regex = /^[U|P] (.*)/
44
+ cvs(checkout_dir, update_command(to_identifier), simulate) do |line|
45
+ if(line =~ path_regex)
46
+ path = $1.chomp
47
+ yield path if block_given?
48
+ checked_out_files << path
49
+ end
50
+ end
51
+ else
52
+ prefix = File.basename(checkout_dir)
53
+ path_regex = /^[U|P] #{prefix}\/(.*)/
54
+ # This is a workaround for the fact that -d . doesn't work - must be an existing sub folder.
55
+ mkdir_p(checkout_dir) unless File.exist?(checkout_dir)
56
+ target_dir = File.basename(checkout_dir)
57
+ run_checkout_command_dir = File.dirname(checkout_dir)
58
+ # -D is sticky, but subsequent updates will reset stickiness with -A
59
+ cvs(run_checkout_command_dir, checkout_command(target_dir, to_identifier), simulate) do |line|
60
+ if(line =~ path_regex)
61
+ path = $1.chomp
62
+ yield path if block_given?
63
+ checked_out_files << path
64
+ end
65
+ end
66
+ end
67
+ checked_out_files
68
+ end
69
+
70
+ def checkout_commandline(to_identifier=Time.infinity)
71
+ "cvs checkout #{branch_option} #{revision_option(to_identifier)} #{mod}"
72
+ end
73
+
74
+ def update_commandline
75
+ "cvs update #{branch_option} -d -P -A"
76
+ end
77
+
78
+ def commit(checkout_dir, message, &proc)
79
+ cvs(checkout_dir, commit_command(message), &proc)
80
+ end
81
+
82
+ def uptodate?(checkout_dir, since)
83
+ if(!checked_out?(checkout_dir))
84
+ return false
85
+ end
86
+
87
+ # simulate a checkout
88
+ files = checkout(checkout_dir, nil, true)
89
+ files.empty?
90
+ end
91
+
92
+ def changesets(checkout_dir, from_identifier, to_identifier=Time.infinity, files=nil)
93
+ checkout(checkout_dir) unless uptodate?(checkout_dir, nil) # must checkout to get changesets
94
+ begin
95
+ parse_log(checkout_dir, new_changes_command(from_identifier, to_identifier, files))
96
+ rescue => e
97
+ parse_log(checkout_dir, old_changes_command(from_identifier, to_identifier, files))
98
+ end
99
+ end
100
+
101
+ def diff(checkout_dir, change)
102
+ with_working_dir(checkout_dir) do
103
+ opts = case change.status
104
+ when /#{Change::MODIFIED}/; "#{revision_option(change.previous_revision)} #{revision_option(change.revision)}"
105
+ when /#{Change::DELETED}/; "#{revision_option(change.previous_revision)}"
106
+ when /#{Change::ADDED}/; "#{revision_option(Time.epoch)} #{revision_option(change.revision)}"
107
+ end
108
+ # IPORTANT! CVS NT has a bug in the -N diff option
109
+ # http://www.cvsnt.org/pipermail/cvsnt-bugs/2004-November/000786.html
110
+ cmd = command_line("diff -Nu #{opts} #{change.path}")
111
+ safer_popen(cmd, "r", 1) do |io|
112
+ return(yield(io))
113
+ end
114
+ end
115
+ end
116
+
117
+ def apply_label(checkout_dir, label)
118
+ cvs(checkout_dir, "tag -c #{label}")
119
+ end
120
+
121
+ def install_trigger(trigger_command, trigger_files_checkout_dir)
122
+ raise "mod can't be null or empty" if (mod.nil? || mod == "")
123
+
124
+ root_cvs = create_root_cvs
125
+ root_cvs.checkout(trigger_files_checkout_dir)
126
+ with_working_dir(trigger_files_checkout_dir) do
127
+ trigger_line = "#{mod} #{trigger_command}\n"
128
+ File.open("loginfo", File::WRONLY | File::APPEND) do |file|
129
+ file.puts(trigger_line)
130
+ end
131
+ begin
132
+ commit(trigger_files_checkout_dir, "Installed trigger for CVS module '#{mod}'")
133
+ rescue
134
+ raise "Couldn't commit the trigger back to CVS. Try to manually check out CVSROOT/loginfo, " +
135
+ "add the following line and commit it back:\n\n#{trigger_line}"
136
+ end
137
+ end
138
+ end
139
+
140
+ def trigger_installed?(trigger_command, trigger_files_checkout_dir)
141
+ loginfo_line = "#{mod} #{trigger_command}"
142
+ regex = Regexp.new(Regexp.escape(loginfo_line))
143
+
144
+ root_cvs = create_root_cvs
145
+ begin
146
+ root_cvs.checkout(trigger_files_checkout_dir)
147
+ loginfo = File.join(trigger_files_checkout_dir, "loginfo")
148
+ return false if !File.exist?(loginfo)
149
+
150
+ # returns true if commented out. doesn't modify the file.
151
+ in_local_copy = LineEditor.comment_out(File.new(loginfo), regex, "# ", "")
152
+ # Also verify that loginfo has been committed back to the repo
153
+ entries = File.join(trigger_files_checkout_dir, "CVS", "Entries")
154
+ committed = File.mtime(entries) >= File.mtime(loginfo)
155
+
156
+ in_local_copy && committed
157
+ rescue Exception => e
158
+ $stderr.puts(e.message)
159
+ $stderr.puts(e.backtrace.join("\n"))
160
+ false
161
+ end
162
+ end
163
+
164
+ def uninstall_trigger(trigger_command, trigger_files_checkout_dir)
165
+ loginfo_line = "#{mod} #{trigger_command}"
166
+ regex = Regexp.new(Regexp.escape(loginfo_line))
167
+
168
+ root_cvs = create_root_cvs
169
+ root_cvs.checkout(trigger_files_checkout_dir)
170
+ loginfo_path = File.join(trigger_files_checkout_dir, "loginfo")
171
+ File.comment_out(loginfo_path, regex, "# ")
172
+ with_working_dir(trigger_files_checkout_dir) do
173
+ commit(trigger_files_checkout_dir, "Uninstalled trigger for CVS mod '#{mod}'")
174
+ end
175
+ raise "Couldn't uninstall/commit trigger to loginfo" if trigger_installed?(trigger_command, trigger_files_checkout_dir)
176
+ end
177
+
178
+ def create
179
+ raise "Can't create CVS repository for #{root}" unless can_create?
180
+ File.mkpath(path)
181
+ cvs(path, "init")
182
+ end
183
+
184
+ def can_create?
185
+ begin
186
+ local?
187
+ rescue
188
+ false
189
+ end
190
+ end
191
+
192
+ def supports_trigger?
193
+ true
194
+ end
195
+
196
+ def exists?
197
+ if(local?)
198
+ File.exists?("#{path}/CVSROOT/loginfo")
199
+ else
200
+ # don't know. assume yes.
201
+ true
202
+ end
203
+ end
204
+
205
+ def checked_out?(checkout_dir)
206
+ rootcvs = File.expand_path("#{checkout_dir}/CVS/Root")
207
+ File.exists?(rootcvs)
208
+ end
209
+
210
+ private
211
+
212
+ def cvs(dir, cmd, simulate=false)
213
+ dir = PathConverter.nativepath_to_filepath(dir)
214
+ dir = File.expand_path(dir)
215
+ execed_command_line = command_line(cmd, password, simulate)
216
+ with_working_dir(dir) do
217
+ safer_popen(execed_command_line) do |stdout|
218
+ stdout.each_line do |progress|
219
+ yield progress if block_given?
220
+ end
221
+ end
222
+ end
223
+ end
224
+
225
+ def parse_log(checkout_dir, cmd, &proc)
226
+ logged_command_line = command_line(cmd, hidden_password)
227
+ yield logged_command_line if block_given?
228
+
229
+ execed_command_line = command_line(cmd, password)
230
+ changesets = nil
231
+ with_working_dir(checkout_dir) do
232
+ safer_popen(execed_command_line) do |stdout|
233
+ parser = CVSLogParser.new(stdout)
234
+ parser.cvspath = path
235
+ parser.cvsmodule = mod
236
+ changesets = parser.parse_changesets
237
+ end
238
+ end
239
+ changesets
240
+ end
241
+
242
+ def new_changes_command(from_identifier, to_identifier, files)
243
+ # https://www.cvshome.org/docs/manual/cvs-1.11.17/cvs_16.html#SEC144
244
+ # -N => Suppress the header if no revisions are selected.
245
+ # -S => Do not print the list of tags for this file.
246
+ "log #{branch_option} -N -S #{period_option(from_identifier, to_identifier)}"
247
+ end
248
+
249
+ def branch_specified?
250
+ branch && branch.strip != ""
251
+ end
252
+
253
+ def branch_option
254
+ branch_specified? ? "-r#{branch}" : ""
255
+ end
256
+
257
+ def update_command(to_identifier)
258
+ "update #{branch_option} -d -P -A #{revision_option(to_identifier)}"
259
+ end
260
+
261
+ def checkout_command(target_dir, to_identifier)
262
+ "checkout #{branch_option} -d #{target_dir} #{mod} #{revision_option(to_identifier)}"
263
+ end
264
+
265
+ def old_changes_command(from_identifier, to_identifier, files)
266
+ # Many servers don't support the new -S option
267
+ "log #{branch_option} -N #{period_option(from_identifier, to_identifier)}"
268
+ end
269
+
270
+ def hidden_password
271
+ if(password && password != "")
272
+ "********"
273
+ else
274
+ ""
275
+ end
276
+ end
277
+
278
+ def period_option(from_identifier, to_identifier)
279
+ if(from_identifier.nil? && to_identifier.nil?)
280
+ ""
281
+ else
282
+ "-d\"#{cvsdate(from_identifier)}<=#{cvsdate(to_identifier)}\" "
283
+ end
284
+ end
285
+
286
+ def cvsdate(time)
287
+ return "" unless time
288
+ # CVS wants all dates as UTC.
289
+ time.utc.strftime("%Y-%m-%d %H:%M:%S UTC")
290
+ end
291
+
292
+ def root_with_password(password)
293
+ result = nil
294
+ if local?
295
+ result = root
296
+ elsif password && password != ""
297
+ protocol, user, host, path = parse_cvs_root
298
+ result = ":#{protocol}:#{user}:#{password}@#{host}:#{path}"
299
+ else
300
+ result = root
301
+ end
302
+ end
303
+
304
+ def command_line(cmd, password=nil, simulate=false)
305
+ cvs_options = simulate ? "-n" : ""
306
+ "cvs \"-d#{root_with_password(password)}\" #{cvs_options} -q #{cmd}"
307
+ end
308
+
309
+ def create_root_cvs
310
+ CVS.new(self.root, "CVSROOT", nil, self.password)
311
+ end
312
+
313
+ def revision_option(identifier)
314
+ option = nil
315
+ if(identifier.is_a?(Time))
316
+ option = "-D\"#{cvsdate(identifier)}\""
317
+ elsif(identifier.is_a?(String))
318
+ option = "-r#{identifier}"
319
+ else
320
+ ""
321
+ end
322
+ end
323
+
324
+ def commit_command(message)
325
+ "commit -m \"#{message}\""
326
+ end
327
+
328
+ def local?
329
+ protocol == "local"
330
+ end
331
+
332
+ def path
333
+ parse_cvs_root[3]
334
+ end
335
+
336
+ def protocol
337
+ parse_cvs_root[0]
338
+ end
339
+
340
+ # parses the root into tokens
341
+ # [protocol, user, host, path]
342
+ #
343
+ def parse_cvs_root
344
+ md = case
345
+ when root =~ /^:local:/ then /^:(local):(.*)/.match(root)
346
+ when root =~ /^:ext:/ then /^:(ext):(.*)@(.*):(.*)/.match(root)
347
+ when root =~ /^:pserver:/ then /^:(pserver):(.*)@(.*):(.*)/.match(root)
348
+ end
349
+ result = case
350
+ when root =~ /^:local:/ then [md[1], nil, nil, md[2]]
351
+ when root =~ /^:ext:/ then md[1..4]
352
+ when root =~ /^:pserver:/ then md[1..4]
353
+ else ["local", nil, nil, root]
354
+ end
355
+ end
356
+ end
357
+
358
+ # Convenience factory method used in testing
359
+ def CVS.local(cvsroot_dir, mod)
360
+ cvsroot_dir = PathConverter.filepath_to_nativepath(cvsroot_dir, true)
361
+ CVS.new(":local:#{cvsroot_dir}", mod)
362
+ end
363
+ end