markryall-basketcase 1.1.7
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.
- data/.gitignore +5 -0
- data/History.txt +8 -0
- data/Manifest.txt +7 -0
- data/README.txt +109 -0
- data/Rakefile +26 -0
- data/VERSION +1 -0
- data/bin/basketcase +13 -0
- data/bin/bc-mirror +127 -0
- data/lib/basketcase.rb +1046 -0
- data/spec/auto_sync_spec.rb +61 -0
- data/spec/basketcase_spec.rb +43 -0
- data/spec/cleartool +28 -0
- data/spec/spec_helper.rb +5 -0
- metadata +70 -0
data/lib/basketcase.rb
ADDED
|
@@ -0,0 +1,1046 @@
|
|
|
1
|
+
require 'pathname'
|
|
2
|
+
require 'forwardable'
|
|
3
|
+
|
|
4
|
+
class Basketcase
|
|
5
|
+
|
|
6
|
+
VERSION = '1.1.0'
|
|
7
|
+
|
|
8
|
+
@usage = <<EOF
|
|
9
|
+
usage: basketcase <command> [<options>]
|
|
10
|
+
|
|
11
|
+
GLOBAL OPTIONS
|
|
12
|
+
|
|
13
|
+
-t/--test test/dry-run/simulate mode
|
|
14
|
+
(ie. don\'t actually do anything)
|
|
15
|
+
|
|
16
|
+
-d/--debug debug cleartool interaction
|
|
17
|
+
|
|
18
|
+
COMMANDS (type 'basketcase help <command>' for details)
|
|
19
|
+
|
|
20
|
+
EOF
|
|
21
|
+
|
|
22
|
+
def log_debug(msg)
|
|
23
|
+
return unless @debug_mode
|
|
24
|
+
$stderr.puts(msg)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def just_testing?
|
|
28
|
+
@test_mode
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
module Utils
|
|
32
|
+
|
|
33
|
+
def mkpath(path)
|
|
34
|
+
path = path.to_str
|
|
35
|
+
path = path.tr('\\', '/')
|
|
36
|
+
path = path.sub(%r{^\./},'')
|
|
37
|
+
path = path.sub(%r{^([A-Za-z]):\/}, '/cygdrive/\1/')
|
|
38
|
+
Pathname.new(path)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
include Utils
|
|
44
|
+
|
|
45
|
+
def ignored?(path)
|
|
46
|
+
path = Pathname(path).expand_path
|
|
47
|
+
require_ignore_patterns_for(path.parent)
|
|
48
|
+
@ignore_patterns.detect do |pattern|
|
|
49
|
+
File.fnmatch(pattern, path, File::FNM_PATHNAME | File::FNM_DOTMATCH)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def add_ignore_pattern(pattern)
|
|
56
|
+
@ignore_patterns ||= []
|
|
57
|
+
path = File.expand_path(pattern)
|
|
58
|
+
log_debug "ignore #{path}"
|
|
59
|
+
@ignore_patterns << path
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def ignore(pattern)
|
|
63
|
+
pattern = pattern.to_str
|
|
64
|
+
if pattern[-1,1] == '/' # a directory
|
|
65
|
+
add_ignore_pattern pattern.chop # ignore the directory itself
|
|
66
|
+
add_ignore_pattern pattern + '**/*' # and any files within it
|
|
67
|
+
else
|
|
68
|
+
add_ignore_pattern pattern
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def define_standard_ignore_patterns
|
|
73
|
+
# Standard ignore patterns
|
|
74
|
+
ignore "**/*.hijacked"
|
|
75
|
+
ignore "**/*.keep"
|
|
76
|
+
ignore "**/*.keep.[0-9]"
|
|
77
|
+
ignore "**/#*#"
|
|
78
|
+
ignore "**/*~"
|
|
79
|
+
ignore "**/basketcase-*.tmp"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def require_ignore_patterns_for(dir)
|
|
83
|
+
@ignore_patterns_loaded ||= {}
|
|
84
|
+
dir = Pathname(dir).expand_path
|
|
85
|
+
return(nil) if @ignore_patterns_loaded[dir]
|
|
86
|
+
require_ignore_patterns_for(dir.parent) unless dir.root?
|
|
87
|
+
bcignore_file = dir + ".bcignore"
|
|
88
|
+
if bcignore_file.exist?
|
|
89
|
+
log_debug "loading #{bcignore_file}"
|
|
90
|
+
bcignore_file.each_line do |line|
|
|
91
|
+
next if line =~ %r{^#}
|
|
92
|
+
ignore(dir + line.strip)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
@ignore_patterns_loaded[dir] = true
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
public
|
|
99
|
+
|
|
100
|
+
# Represents the status of an element
|
|
101
|
+
class ElementStatus
|
|
102
|
+
|
|
103
|
+
def initialize(path, status, base_version = nil)
|
|
104
|
+
@path = path
|
|
105
|
+
@status = status
|
|
106
|
+
@base_version = base_version
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
attr_reader :path, :status, :base_version
|
|
110
|
+
|
|
111
|
+
def to_s
|
|
112
|
+
s = "#{path} (#{status})"
|
|
113
|
+
s += " [#{base_version}]" if base_version
|
|
114
|
+
return s
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Object responsible for nice fomatting of output
|
|
120
|
+
DefaultListener = lambda do |element|
|
|
121
|
+
printf("%-7s %-15s %s\n", element.status,
|
|
122
|
+
element.base_version, element.path)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
class TargetList
|
|
126
|
+
|
|
127
|
+
include Enumerable
|
|
128
|
+
include Basketcase::Utils
|
|
129
|
+
|
|
130
|
+
def initialize(targets)
|
|
131
|
+
@target_paths = targets.map { |t| mkpath(t) }
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def each
|
|
135
|
+
@target_paths.each do |t|
|
|
136
|
+
yield(t)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def to_s
|
|
141
|
+
@target_paths.map { |f| "\"#{f}\"" }.join(" ")
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def empty?
|
|
145
|
+
@target_paths.empty?
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def size
|
|
149
|
+
@target_paths.size
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def parents
|
|
153
|
+
TargetList.new(@target_paths.map { |t| t.parent }.uniq)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
class UsageException < Exception
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Base ClearCase command
|
|
162
|
+
class Command
|
|
163
|
+
|
|
164
|
+
include Basketcase::Utils
|
|
165
|
+
|
|
166
|
+
extend Forwardable
|
|
167
|
+
def_delegators :@basketcase, :log_debug, :just_testing?, :ignored?, :make_command, :run
|
|
168
|
+
|
|
169
|
+
def synopsis
|
|
170
|
+
""
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def help
|
|
174
|
+
"Sorry, no help provided ..."
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def initialize(basketcase)
|
|
178
|
+
@basketcase = basketcase
|
|
179
|
+
@listener = DefaultListener
|
|
180
|
+
@recursive = false
|
|
181
|
+
@graphical = false
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
attr_writer :listener
|
|
185
|
+
attr_writer :targets
|
|
186
|
+
|
|
187
|
+
def report(status, path, version = nil)
|
|
188
|
+
@listener.call(ElementStatus.new(path, status, version))
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def option_recurse
|
|
192
|
+
@recursive = true
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
alias :option_r :option_recurse
|
|
196
|
+
|
|
197
|
+
def option_graphical
|
|
198
|
+
@graphical = true
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
alias :option_g :option_graphical
|
|
202
|
+
|
|
203
|
+
def option_comment(comment)
|
|
204
|
+
@comment = comment
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
alias :option_m :option_comment
|
|
208
|
+
|
|
209
|
+
attr_accessor :comment
|
|
210
|
+
|
|
211
|
+
# Handle command-line arguments:
|
|
212
|
+
# - For option arguments of the form "-X", call the corresponding
|
|
213
|
+
# option_X() method.
|
|
214
|
+
# - Remaining arguments are stored in @targets
|
|
215
|
+
def accept_args(args)
|
|
216
|
+
while /^-+(.+)/ === args[0]
|
|
217
|
+
option = args.shift
|
|
218
|
+
option_method_name = "option_#{$1}"
|
|
219
|
+
unless respond_to?(option_method_name)
|
|
220
|
+
raise UsageException, "Unrecognised option: #{option}"
|
|
221
|
+
end
|
|
222
|
+
option_method = method(option_method_name)
|
|
223
|
+
parameters = []
|
|
224
|
+
option_method.arity.times { parameters << args.shift }
|
|
225
|
+
option_method.call(*parameters)
|
|
226
|
+
end
|
|
227
|
+
@targets = args
|
|
228
|
+
self
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def effective_targets
|
|
232
|
+
TargetList.new(@targets.empty? ? ['.'] : @targets)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def specified_targets
|
|
236
|
+
raise UsageException, "No target specified" if @targets.empty?
|
|
237
|
+
TargetList.new(@targets)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
private
|
|
241
|
+
|
|
242
|
+
def cleartool(command)
|
|
243
|
+
log_debug "RUNNING: cleartool #{command}"
|
|
244
|
+
IO.popen("cleartool " + command).each_line do |line|
|
|
245
|
+
line.sub!("\r", '')
|
|
246
|
+
log_debug "<<< " + line
|
|
247
|
+
yield(line) if block_given?
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def cleartool_unsafe(command, &block)
|
|
252
|
+
if just_testing?
|
|
253
|
+
puts "WOULD RUN: cleartool #{command}"
|
|
254
|
+
return
|
|
255
|
+
end
|
|
256
|
+
cleartool(command, &block)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def view_root
|
|
260
|
+
@root ||= catch(:root) do
|
|
261
|
+
cleartool("pwv -root") do |line|
|
|
262
|
+
throw :root, mkpath(line.chomp)
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
log_debug "view_root = #{@root}"
|
|
266
|
+
@root
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def cannot_deal_with(line)
|
|
270
|
+
$stderr.puts "unrecognised output: " + line
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def edit(file)
|
|
274
|
+
editor = ENV["EDITOR"] || "notepad"
|
|
275
|
+
system("#{editor} #{file}")
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
class HelpCommand < Command
|
|
281
|
+
|
|
282
|
+
def synopsis
|
|
283
|
+
"[<command>]"
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def help
|
|
287
|
+
"Display usage instructions."
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def execute
|
|
291
|
+
if @targets.empty?
|
|
292
|
+
puts @basketcase.usage
|
|
293
|
+
exit
|
|
294
|
+
end
|
|
295
|
+
@targets.each do |command_name|
|
|
296
|
+
command = make_command(command_name)
|
|
297
|
+
puts
|
|
298
|
+
puts "% basketcase #{command_name} #{command.synopsis}"
|
|
299
|
+
puts
|
|
300
|
+
puts command.help.gsub(/^/, " ")
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
class LsCommand < Command
|
|
307
|
+
|
|
308
|
+
def synopsis
|
|
309
|
+
"[<element> ...]"
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def help
|
|
313
|
+
<<EOF
|
|
314
|
+
List element status.
|
|
315
|
+
|
|
316
|
+
-a(ll) Show all files.
|
|
317
|
+
(by default, up-to-date files are not reported)
|
|
318
|
+
|
|
319
|
+
-r(ecurse) Recursively list sub-directories.
|
|
320
|
+
(by default, just lists current directory)
|
|
321
|
+
EOF
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def option_all
|
|
325
|
+
@include_all = true
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
alias :option_a :option_all
|
|
329
|
+
|
|
330
|
+
def option_directory
|
|
331
|
+
@directory_only = true
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
alias :option_d :option_directory
|
|
335
|
+
|
|
336
|
+
def execute
|
|
337
|
+
args = ''
|
|
338
|
+
args += ' -recurse' if @recursive
|
|
339
|
+
args += ' -directory' if @directory_only
|
|
340
|
+
cleartool("ls #{args} #{effective_targets}") do |line|
|
|
341
|
+
case line
|
|
342
|
+
when /^(.+)@@(\S+) \[hijacked/
|
|
343
|
+
report(:HIJACK, mkpath($1), $2)
|
|
344
|
+
when /^(.+)@@(\S+) \[loaded but missing\]/
|
|
345
|
+
report(:MISSING, mkpath($1), $2)
|
|
346
|
+
when /^(.+)@@\S+\\CHECKEDOUT(?: from (\S+))?/
|
|
347
|
+
element_path = mkpath($1)
|
|
348
|
+
status = element_path.exist? ? :CO : :MISSING
|
|
349
|
+
report(status, element_path, $2 || 'new')
|
|
350
|
+
when /^(.+)@@(\S+) +Rule: /
|
|
351
|
+
next unless @include_all
|
|
352
|
+
report(:OK, mkpath($1), $2)
|
|
353
|
+
when /^(.+)/
|
|
354
|
+
path = mkpath($1)
|
|
355
|
+
if ignored?(path)
|
|
356
|
+
log_debug "ignoring #{path}"
|
|
357
|
+
next
|
|
358
|
+
end
|
|
359
|
+
report(:LOCAL, path)
|
|
360
|
+
else
|
|
361
|
+
cannot_deal_with line
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
class LsCoCommand < Command
|
|
369
|
+
|
|
370
|
+
def synopsis
|
|
371
|
+
"[-r] [-d] [<element> ...]"
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def help
|
|
375
|
+
"List checkouts by ALL users"
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
def option_directory
|
|
379
|
+
@directory_only = true
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
alias :option_d :option_directory
|
|
383
|
+
|
|
384
|
+
def execute
|
|
385
|
+
args = ''
|
|
386
|
+
args += ' -recurse' if @recursive
|
|
387
|
+
args += ' -directory' if @directory_only
|
|
388
|
+
cleartool("lsco #{args} #{effective_targets}") do |line|
|
|
389
|
+
case line
|
|
390
|
+
when /^.*\s(\S+)\s+checkout.*version "(\S+)" from (\S+)/
|
|
391
|
+
report($1, mkpath($2), $3)
|
|
392
|
+
when /^Added /
|
|
393
|
+
# ignore
|
|
394
|
+
when /^ /
|
|
395
|
+
# ignore
|
|
396
|
+
else
|
|
397
|
+
cannot_deal_with line
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
class UpdateCommand < Command
|
|
405
|
+
|
|
406
|
+
def synopsis
|
|
407
|
+
"[-nomerge] [<element> ...]"
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
def help
|
|
411
|
+
<<EOF
|
|
412
|
+
Update your (snapshot) view.
|
|
413
|
+
|
|
414
|
+
-nomerge Don\'t attempt to merge in changes to checked-out files.
|
|
415
|
+
EOF
|
|
416
|
+
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
def option_nomerge
|
|
420
|
+
@nomerge = true
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
def relative_path(s)
|
|
424
|
+
full_path = view_root + mkpath(s)
|
|
425
|
+
full_path.relative_path_from(Pathname.pwd)
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
def execute_update
|
|
429
|
+
args = '-log nul -force'
|
|
430
|
+
args += ' -print' if just_testing?
|
|
431
|
+
cleartool("update #{args} #{effective_targets}") do |line|
|
|
432
|
+
case line
|
|
433
|
+
when /^Processing dir "(.*)"/
|
|
434
|
+
# ignore
|
|
435
|
+
when /^\.*$/
|
|
436
|
+
# ignore
|
|
437
|
+
when /^Making dir "(.*)"/
|
|
438
|
+
report(:NEW, relative_path($1))
|
|
439
|
+
when /^Loading "(.*)"/
|
|
440
|
+
report(:UPDATED, relative_path($1))
|
|
441
|
+
when /^Unloaded "(.*)"/
|
|
442
|
+
report(:REMOVED, relative_path($1))
|
|
443
|
+
when /^Keeping hijacked object "(.*)" - base "(.*)"/
|
|
444
|
+
report(:HIJACK, relative_path($1), $2)
|
|
445
|
+
when /^Keeping "(.*)"/
|
|
446
|
+
# ignore
|
|
447
|
+
when /^End dir/
|
|
448
|
+
# ignore
|
|
449
|
+
when /^Done loading/
|
|
450
|
+
# ignore
|
|
451
|
+
else
|
|
452
|
+
cannot_deal_with line
|
|
453
|
+
end
|
|
454
|
+
end
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
def execute_merge
|
|
458
|
+
args = '-log nul -flatest '
|
|
459
|
+
if just_testing?
|
|
460
|
+
args += "-print"
|
|
461
|
+
elsif @graphical
|
|
462
|
+
args += "-gmerge"
|
|
463
|
+
else
|
|
464
|
+
args += "-merge -gmerge"
|
|
465
|
+
end
|
|
466
|
+
cleartool("findmerge #{effective_targets} #{args}") do |line|
|
|
467
|
+
case line
|
|
468
|
+
when /^Needs Merge "(.+)" \[to \S+ from (\S+) base (\S+)\]/
|
|
469
|
+
report(:MERGE, mkpath($1), $2)
|
|
470
|
+
end
|
|
471
|
+
end
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
def execute
|
|
475
|
+
execute_update
|
|
476
|
+
execute_merge unless @nomerge
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
class CheckinCommand < Command
|
|
482
|
+
|
|
483
|
+
def synopsis
|
|
484
|
+
"<element> ..."
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
def help
|
|
488
|
+
"Check-in elements, prompting for a check-in message."
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
def execute
|
|
492
|
+
prompt_for_comment
|
|
493
|
+
comment_file = Pathname.new("basketcase-checkin-comment.tmp")
|
|
494
|
+
comment_file.open("w") do |out|
|
|
495
|
+
out.puts(@comment)
|
|
496
|
+
end
|
|
497
|
+
cleartool_unsafe("checkin -cfile #{comment_file} #{specified_targets}") do |line|
|
|
498
|
+
case line
|
|
499
|
+
when /^Loading /
|
|
500
|
+
# ignore
|
|
501
|
+
when /^Making dir /
|
|
502
|
+
# ignore
|
|
503
|
+
when /^Checked in "(.+)" version "(\S+)"\./
|
|
504
|
+
report(:COMMIT, mkpath($1), $2)
|
|
505
|
+
else
|
|
506
|
+
cannot_deal_with line
|
|
507
|
+
end
|
|
508
|
+
end
|
|
509
|
+
comment_file.unlink
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
def prompt_for_comment
|
|
513
|
+
return if @comment
|
|
514
|
+
comment_file = Pathname.new("basketcase-comment.tmp")
|
|
515
|
+
begin
|
|
516
|
+
comment_file.open('w') do |out|
|
|
517
|
+
out.puts <<EOF
|
|
518
|
+
# Please enter the commit message for your changes.
|
|
519
|
+
# (Comment lines starting with '#' will not be included)
|
|
520
|
+
#
|
|
521
|
+
# Changes to be committed:
|
|
522
|
+
EOF
|
|
523
|
+
specified_targets.each do |target|
|
|
524
|
+
out.puts "#\t#{target}"
|
|
525
|
+
end
|
|
526
|
+
end
|
|
527
|
+
edit(comment_file)
|
|
528
|
+
@comment = comment_file.read.gsub(/^#.*\n/, '')
|
|
529
|
+
ensure
|
|
530
|
+
comment_file.unlink
|
|
531
|
+
end
|
|
532
|
+
raise UsageException, "No check-in comment provided" if @comment.empty?
|
|
533
|
+
@comment
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
class CheckoutCommand < Command
|
|
539
|
+
|
|
540
|
+
def synopsis
|
|
541
|
+
"<element> ..."
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
def help
|
|
545
|
+
""
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
def help
|
|
549
|
+
<<EOF
|
|
550
|
+
Check-out elements (unreserved).
|
|
551
|
+
By default, any hijacked version is discarded.
|
|
552
|
+
|
|
553
|
+
-h(ijack) Retain the hijacked version.
|
|
554
|
+
EOF
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
def initialize(*args)
|
|
558
|
+
super(*args)
|
|
559
|
+
@keep_or_revert = '-nquery'
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
def option_hijack
|
|
563
|
+
@keep_or_revert = '-usehijack'
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
alias :option_h :option_hijack
|
|
567
|
+
|
|
568
|
+
def execute
|
|
569
|
+
cleartool_unsafe("checkout -unreserved -ncomment #{@keep_or_revert} #{specified_targets}") do |line|
|
|
570
|
+
case line
|
|
571
|
+
when /^Checked out "(.+)" from version "(\S+)"\./
|
|
572
|
+
report(:CO, mkpath($1), $2)
|
|
573
|
+
end
|
|
574
|
+
end
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
class UncheckoutCommand < Command
|
|
580
|
+
|
|
581
|
+
def synopsis
|
|
582
|
+
"[-r] <element> ..."
|
|
583
|
+
end
|
|
584
|
+
|
|
585
|
+
def help
|
|
586
|
+
<<EOF
|
|
587
|
+
Undo a checkout, reverting to the checked-in version.
|
|
588
|
+
|
|
589
|
+
-r(emove) Don\'t retain the existing version in a '.keep' file.
|
|
590
|
+
EOF
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
def initialize(*args)
|
|
594
|
+
super(*args)
|
|
595
|
+
@action = '-keep'
|
|
596
|
+
end
|
|
597
|
+
|
|
598
|
+
def option_remove
|
|
599
|
+
@action = '-rm'
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
alias :option_r :option_remove
|
|
603
|
+
|
|
604
|
+
attr_accessor :action
|
|
605
|
+
|
|
606
|
+
def execute
|
|
607
|
+
cleartool_unsafe("uncheckout #{@action} #{specified_targets}") do |line|
|
|
608
|
+
case line
|
|
609
|
+
when /^Loading /
|
|
610
|
+
# ignore
|
|
611
|
+
when /^Making dir /
|
|
612
|
+
# ignore
|
|
613
|
+
when /^Checkout cancelled for "(.+)"\./
|
|
614
|
+
report(:UNCO, mkpath($1))
|
|
615
|
+
when /^Private version .* saved in "(.+)"\./
|
|
616
|
+
report(:KEPT, mkpath($1))
|
|
617
|
+
else
|
|
618
|
+
cannot_deal_with line
|
|
619
|
+
end
|
|
620
|
+
end
|
|
621
|
+
end
|
|
622
|
+
|
|
623
|
+
end
|
|
624
|
+
|
|
625
|
+
class DirectoryModificationCommand < Command
|
|
626
|
+
|
|
627
|
+
def find_locked_elements(paths)
|
|
628
|
+
locked_elements = []
|
|
629
|
+
run(LsCommand, '-a', '-d', *paths) do |e|
|
|
630
|
+
locked_elements << e.path if e.status == :OK
|
|
631
|
+
end
|
|
632
|
+
locked_elements
|
|
633
|
+
end
|
|
634
|
+
|
|
635
|
+
def checkout(target_list)
|
|
636
|
+
return if target_list.empty?
|
|
637
|
+
run(CheckoutCommand, *target_list)
|
|
638
|
+
end
|
|
639
|
+
|
|
640
|
+
def unlock_parent_directories(target_list)
|
|
641
|
+
checkout find_locked_elements(target_list.parents)
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
end
|
|
645
|
+
|
|
646
|
+
class RemoveCommand < DirectoryModificationCommand
|
|
647
|
+
|
|
648
|
+
def synopsis
|
|
649
|
+
"<element> ..."
|
|
650
|
+
end
|
|
651
|
+
|
|
652
|
+
def help
|
|
653
|
+
<<EOF
|
|
654
|
+
Mark an element as deleted.
|
|
655
|
+
(Parent directories are checked-out automatically)
|
|
656
|
+
EOF
|
|
657
|
+
end
|
|
658
|
+
|
|
659
|
+
def execute
|
|
660
|
+
unlock_parent_directories(specified_targets)
|
|
661
|
+
cleartool_unsafe("rmname -ncomment #{specified_targets}") do |line|
|
|
662
|
+
case line
|
|
663
|
+
when /^Unloaded /
|
|
664
|
+
# ignore
|
|
665
|
+
when /^Removed "(.+)"\./
|
|
666
|
+
report(:REMOVED, mkpath($1))
|
|
667
|
+
else
|
|
668
|
+
cannot_deal_with line
|
|
669
|
+
end
|
|
670
|
+
end
|
|
671
|
+
end
|
|
672
|
+
|
|
673
|
+
end
|
|
674
|
+
|
|
675
|
+
class AddCommand < DirectoryModificationCommand
|
|
676
|
+
|
|
677
|
+
def synopsis
|
|
678
|
+
"<element> ..."
|
|
679
|
+
end
|
|
680
|
+
|
|
681
|
+
def help
|
|
682
|
+
<<EOF
|
|
683
|
+
Add elements to the repository.
|
|
684
|
+
(Parent directories are checked-out automatically)
|
|
685
|
+
EOF
|
|
686
|
+
end
|
|
687
|
+
|
|
688
|
+
def execute
|
|
689
|
+
unlock_parent_directories(specified_targets)
|
|
690
|
+
cleartool_unsafe("mkelem -ncomment #{specified_targets}") do |line|
|
|
691
|
+
case line
|
|
692
|
+
when /^Created element /
|
|
693
|
+
# ignore
|
|
694
|
+
when /^Checked out "(.+)" from version "(\S+)"\./
|
|
695
|
+
report(:ADDED, mkpath($1), $2)
|
|
696
|
+
else
|
|
697
|
+
cannot_deal_with line
|
|
698
|
+
end
|
|
699
|
+
end
|
|
700
|
+
end
|
|
701
|
+
|
|
702
|
+
end
|
|
703
|
+
|
|
704
|
+
class MoveCommand < DirectoryModificationCommand
|
|
705
|
+
|
|
706
|
+
def synopsis
|
|
707
|
+
"<from> <to>"
|
|
708
|
+
end
|
|
709
|
+
|
|
710
|
+
def help
|
|
711
|
+
<<EOF
|
|
712
|
+
Move/rename an element.
|
|
713
|
+
(Parent directories are checked-out automatically)
|
|
714
|
+
EOF
|
|
715
|
+
end
|
|
716
|
+
|
|
717
|
+
def execute
|
|
718
|
+
raise UsageException, "expected two arguments" unless (specified_targets.size == 2)
|
|
719
|
+
unlock_parent_directories(specified_targets)
|
|
720
|
+
cleartool_unsafe("move -ncomment #{specified_targets}") do |line|
|
|
721
|
+
case line
|
|
722
|
+
when /^Moved "(.+)" to "(.+)"\./
|
|
723
|
+
report(:REMOVED, mkpath($1))
|
|
724
|
+
report(:ADDED, mkpath($2))
|
|
725
|
+
else
|
|
726
|
+
cannot_deal_with line
|
|
727
|
+
end
|
|
728
|
+
end
|
|
729
|
+
end
|
|
730
|
+
|
|
731
|
+
end
|
|
732
|
+
|
|
733
|
+
class DiffCommand < Command
|
|
734
|
+
|
|
735
|
+
def synopsis
|
|
736
|
+
"[-g] <element>"
|
|
737
|
+
end
|
|
738
|
+
|
|
739
|
+
def help
|
|
740
|
+
<<EOF
|
|
741
|
+
Compare a file to the latest checked-in version.
|
|
742
|
+
|
|
743
|
+
-g Graphical display.
|
|
744
|
+
EOF
|
|
745
|
+
end
|
|
746
|
+
|
|
747
|
+
def execute
|
|
748
|
+
args = ''
|
|
749
|
+
args += ' -graphical' if @graphical
|
|
750
|
+
specified_targets.each do |target|
|
|
751
|
+
cleartool("diff #{args} -predecessor #{target}") do |line|
|
|
752
|
+
puts line
|
|
753
|
+
end
|
|
754
|
+
end
|
|
755
|
+
end
|
|
756
|
+
|
|
757
|
+
end
|
|
758
|
+
|
|
759
|
+
class LogCommand < Command
|
|
760
|
+
|
|
761
|
+
def synopsis
|
|
762
|
+
"[<element> ...]"
|
|
763
|
+
end
|
|
764
|
+
|
|
765
|
+
def help
|
|
766
|
+
<<EOF
|
|
767
|
+
List the history of specified elements.
|
|
768
|
+
EOF
|
|
769
|
+
end
|
|
770
|
+
|
|
771
|
+
def option_directory
|
|
772
|
+
@directory_only = true
|
|
773
|
+
end
|
|
774
|
+
|
|
775
|
+
alias :option_d :option_directory
|
|
776
|
+
|
|
777
|
+
def execute
|
|
778
|
+
args = ''
|
|
779
|
+
args += ' -recurse' if @recursive
|
|
780
|
+
args += ' -directory' if @directory_only
|
|
781
|
+
args += ' -graphical' if @graphical
|
|
782
|
+
cleartool("lshistory #{args} #{effective_targets}") do |line|
|
|
783
|
+
puts line
|
|
784
|
+
end
|
|
785
|
+
end
|
|
786
|
+
|
|
787
|
+
end
|
|
788
|
+
|
|
789
|
+
class VersionTreeCommand < Command
|
|
790
|
+
|
|
791
|
+
def synopsis
|
|
792
|
+
"<element>"
|
|
793
|
+
end
|
|
794
|
+
|
|
795
|
+
def help
|
|
796
|
+
<<EOF
|
|
797
|
+
Display a version-tree of specified elements.
|
|
798
|
+
|
|
799
|
+
-g Graphical display.
|
|
800
|
+
EOF
|
|
801
|
+
end
|
|
802
|
+
|
|
803
|
+
def execute
|
|
804
|
+
args = ''
|
|
805
|
+
args += ' -graphical' if @graphical
|
|
806
|
+
cleartool("lsvtree #{args} #{effective_targets}") do |line|
|
|
807
|
+
puts line
|
|
808
|
+
end
|
|
809
|
+
end
|
|
810
|
+
|
|
811
|
+
end
|
|
812
|
+
|
|
813
|
+
class AutoCommand < Command
|
|
814
|
+
|
|
815
|
+
def each_element(&block)
|
|
816
|
+
run(LsCommand, '-r', *effective_targets, &block)
|
|
817
|
+
end
|
|
818
|
+
|
|
819
|
+
def find_checkouts
|
|
820
|
+
checkouts = []
|
|
821
|
+
each_element do |e|
|
|
822
|
+
checkouts << e.path if e.status == :CO
|
|
823
|
+
end
|
|
824
|
+
checkouts
|
|
825
|
+
end
|
|
826
|
+
|
|
827
|
+
end
|
|
828
|
+
|
|
829
|
+
class AutoCheckinCommand < AutoCommand
|
|
830
|
+
|
|
831
|
+
def synopsis
|
|
832
|
+
"[<element> ...]"
|
|
833
|
+
end
|
|
834
|
+
|
|
835
|
+
def help
|
|
836
|
+
<<EOF
|
|
837
|
+
Bulk commit: check-in all checked-out elements.
|
|
838
|
+
EOF
|
|
839
|
+
end
|
|
840
|
+
|
|
841
|
+
def execute
|
|
842
|
+
checked_out_elements = find_checkouts
|
|
843
|
+
if checked_out_elements.empty?
|
|
844
|
+
puts "Nothing to check-in"
|
|
845
|
+
return
|
|
846
|
+
end
|
|
847
|
+
run(CheckinCommand, '-m', comment, *checked_out_elements)
|
|
848
|
+
end
|
|
849
|
+
|
|
850
|
+
end
|
|
851
|
+
|
|
852
|
+
class AutoUncheckoutCommand < AutoCommand
|
|
853
|
+
|
|
854
|
+
def synopsis
|
|
855
|
+
"[<element> ...]"
|
|
856
|
+
end
|
|
857
|
+
|
|
858
|
+
def help
|
|
859
|
+
<<EOF
|
|
860
|
+
Bulk revert: revert all checked-out elements.
|
|
861
|
+
EOF
|
|
862
|
+
end
|
|
863
|
+
|
|
864
|
+
def execute
|
|
865
|
+
checked_out_elements = find_checkouts
|
|
866
|
+
if checked_out_elements.empty?
|
|
867
|
+
puts "Nothing to revert"
|
|
868
|
+
return
|
|
869
|
+
end
|
|
870
|
+
run(UncheckoutCommand, '-r', *checked_out_elements)
|
|
871
|
+
end
|
|
872
|
+
|
|
873
|
+
end
|
|
874
|
+
|
|
875
|
+
class AutoSyncCommand < AutoCommand
|
|
876
|
+
|
|
877
|
+
def initialize(*args)
|
|
878
|
+
super(*args)
|
|
879
|
+
@control_file = Pathname.new("basketcase-autosync.tmp")
|
|
880
|
+
@actions = []
|
|
881
|
+
end
|
|
882
|
+
|
|
883
|
+
def synopsis
|
|
884
|
+
"[<element> ...]"
|
|
885
|
+
end
|
|
886
|
+
|
|
887
|
+
def help
|
|
888
|
+
<<EOF
|
|
889
|
+
Bulk add/remove: offer to add new elements, and remove missing ones.
|
|
890
|
+
|
|
891
|
+
-n Don\'t prompt to confirm actions.
|
|
892
|
+
EOF
|
|
893
|
+
end
|
|
894
|
+
|
|
895
|
+
def option_noprompt
|
|
896
|
+
@noprompt = true
|
|
897
|
+
end
|
|
898
|
+
|
|
899
|
+
alias :option_n :option_noprompt
|
|
900
|
+
|
|
901
|
+
def collect_actions
|
|
902
|
+
each_element do |e|
|
|
903
|
+
case e.status
|
|
904
|
+
when :LOCAL
|
|
905
|
+
@actions << ['add', e.path]
|
|
906
|
+
when :MISSING
|
|
907
|
+
@actions << ['rm', e.path]
|
|
908
|
+
when :HIJACK
|
|
909
|
+
@actions << ['co -h', e.path]
|
|
910
|
+
end
|
|
911
|
+
end
|
|
912
|
+
end
|
|
913
|
+
|
|
914
|
+
def prompt_for_confirmation
|
|
915
|
+
@control_file.open('w') do |control|
|
|
916
|
+
control.puts <<EOF
|
|
917
|
+
# basketcase proposes the actions listed below.
|
|
918
|
+
# Delete any that you don't wish to occur, then save this file.
|
|
919
|
+
#
|
|
920
|
+
EOF
|
|
921
|
+
@actions.each do |a|
|
|
922
|
+
control.puts a.join("\t")
|
|
923
|
+
end
|
|
924
|
+
end
|
|
925
|
+
edit(@control_file)
|
|
926
|
+
@actions = []
|
|
927
|
+
@control_file.open('r') do |control|
|
|
928
|
+
control.each_line do |line|
|
|
929
|
+
if line =~ /^(add|rm|co -h)\s+(.*)/
|
|
930
|
+
@actions << [$1, $2]
|
|
931
|
+
end
|
|
932
|
+
end
|
|
933
|
+
end
|
|
934
|
+
end
|
|
935
|
+
|
|
936
|
+
NUM_ELEMENTS_PER_COMMAND=10
|
|
937
|
+
|
|
938
|
+
def apply_actions
|
|
939
|
+
['add', 'rm', 'co -h'].each do |command|
|
|
940
|
+
elements = @actions.map { |a| a[1] if a[0] == command }.compact
|
|
941
|
+
unless elements.empty?
|
|
942
|
+
elements.each_slice(NUM_ELEMENTS_PER_COMMAND) {|subelements| run(*(command.split(' ') + subelements)) }
|
|
943
|
+
end
|
|
944
|
+
end
|
|
945
|
+
end
|
|
946
|
+
|
|
947
|
+
def execute
|
|
948
|
+
collect_actions
|
|
949
|
+
if @actions.empty?
|
|
950
|
+
puts "No changes required"
|
|
951
|
+
return
|
|
952
|
+
end
|
|
953
|
+
prompt_for_confirmation unless @noprompt
|
|
954
|
+
apply_actions
|
|
955
|
+
end
|
|
956
|
+
|
|
957
|
+
end
|
|
958
|
+
|
|
959
|
+
@registry = {}
|
|
960
|
+
|
|
961
|
+
class << self
|
|
962
|
+
|
|
963
|
+
def command(command_class, names)
|
|
964
|
+
names.each { |name| @registry[name] = command_class }
|
|
965
|
+
@usage << " % #{names.join(', ')}\n"
|
|
966
|
+
end
|
|
967
|
+
|
|
968
|
+
def command_class(name)
|
|
969
|
+
return name if Class === name
|
|
970
|
+
@registry[name] || raise(UsageException, "Unknown command: #{name}")
|
|
971
|
+
end
|
|
972
|
+
|
|
973
|
+
attr_reader :usage
|
|
974
|
+
|
|
975
|
+
end
|
|
976
|
+
|
|
977
|
+
command LsCommand, %w(list ls status stat)
|
|
978
|
+
command LsCoCommand, %w(lsco)
|
|
979
|
+
command DiffCommand, %w(diff)
|
|
980
|
+
command LogCommand, %w(log history)
|
|
981
|
+
command VersionTreeCommand, %w(tree vtree)
|
|
982
|
+
|
|
983
|
+
command UpdateCommand, %w(update up)
|
|
984
|
+
command CheckinCommand, %w(checkin ci commit)
|
|
985
|
+
command CheckoutCommand, %w(checkout co edit)
|
|
986
|
+
command UncheckoutCommand, %w(uncheckout unco revert)
|
|
987
|
+
command AddCommand, %w(add)
|
|
988
|
+
command RemoveCommand, %w(remove rm delete del)
|
|
989
|
+
command MoveCommand, %w(move mv rename)
|
|
990
|
+
command AutoCheckinCommand, %w(auto-checkin auto-ci auto-commit)
|
|
991
|
+
command AutoUncheckoutCommand, %w(auto-uncheckout auto-unco auto-revert)
|
|
992
|
+
command AutoSyncCommand, %w(auto-sync auto-addrm)
|
|
993
|
+
|
|
994
|
+
command HelpCommand, %w(help)
|
|
995
|
+
|
|
996
|
+
def usage
|
|
997
|
+
Basketcase.usage
|
|
998
|
+
end
|
|
999
|
+
|
|
1000
|
+
def make_command(name)
|
|
1001
|
+
Basketcase.command_class(name).new(self)
|
|
1002
|
+
end
|
|
1003
|
+
|
|
1004
|
+
def run(name, *args, &block)
|
|
1005
|
+
command = make_command(name)
|
|
1006
|
+
command.accept_args(args) if args
|
|
1007
|
+
command.listener = block if block_given?
|
|
1008
|
+
command.execute
|
|
1009
|
+
end
|
|
1010
|
+
|
|
1011
|
+
def sync_io
|
|
1012
|
+
$stdout.sync = true
|
|
1013
|
+
$stderr.sync = true
|
|
1014
|
+
end
|
|
1015
|
+
|
|
1016
|
+
def handle_global_options
|
|
1017
|
+
while /^-/ === @args[0]
|
|
1018
|
+
option = @args.shift
|
|
1019
|
+
case option
|
|
1020
|
+
when '--test', '-t'
|
|
1021
|
+
@test_mode = true
|
|
1022
|
+
when '--debug', '-d'
|
|
1023
|
+
@debug_mode = true
|
|
1024
|
+
else
|
|
1025
|
+
raise UsageException, "Unrecognised global argument: #{option}"
|
|
1026
|
+
end
|
|
1027
|
+
end
|
|
1028
|
+
end
|
|
1029
|
+
|
|
1030
|
+
def do(*args)
|
|
1031
|
+
@args = args
|
|
1032
|
+
begin
|
|
1033
|
+
sync_io
|
|
1034
|
+
handle_global_options
|
|
1035
|
+
raise UsageException, "no command specified" if @args.empty?
|
|
1036
|
+
define_standard_ignore_patterns
|
|
1037
|
+
run(*@args)
|
|
1038
|
+
rescue UsageException => usage
|
|
1039
|
+
$stderr.puts "ERROR: #{usage.message}"
|
|
1040
|
+
$stderr.puts
|
|
1041
|
+
$stderr.puts "try 'basketcase help' for usage info"
|
|
1042
|
+
exit(1)
|
|
1043
|
+
end
|
|
1044
|
+
end
|
|
1045
|
+
|
|
1046
|
+
end
|