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/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