rscm 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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