basketcase 1.0.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.
@@ -0,0 +1,3 @@
1
+ === 1.0.0 / 2008-09-16
2
+
3
+ * Converted into a gem, using sow.
@@ -0,0 +1,6 @@
1
+ History.txt
2
+ Manifest.txt
3
+ README.txt
4
+ Rakefile
5
+ bin/basketcase
6
+ lib/basketcase.rb
@@ -0,0 +1,108 @@
1
+ = basketcase
2
+
3
+ BasketCase is a (Ruby) script that encapsulates the Rational ClearCase
4
+ command-line interface, <code>cleartool</code>, making it (slightly) more
5
+ comfortable for developers more used to non-locking version-control systems
6
+ such as CVS or Subversion.
7
+
8
+ == Features
9
+
10
+ BasketCase can help you:
11
+
12
+ * <strong>List</strong> modified elements.
13
+ * <strong>Update</strong> a snapshot view, including <strong>automatic merge</strong> of modified elements.
14
+ * <strong>Check-out</strong> (unreserved) and <strong>check-in</strong> elements.
15
+ * <strong>Undo a check-out</strong>, reverting to the base version.
16
+ * Perform a <strong>bulk check-in</strong> (or revert).
17
+ * <strong>Add</strong>, <strong>remove</strong> and <strong>rename</strong> elements.
18
+ * Display <strong>change-logs</strong> and <strong>version-trees</strong>.
19
+ * Display <strong>differences</strong> for modified elements.
20
+
21
+ == Synopsis
22
+
23
+ usage: basketcase <command> [<options>]
24
+
25
+ GLOBAL OPTIONS
26
+
27
+ -t/--test test/dry-run/simulate mode
28
+ (ie. don't actually do anything)
29
+
30
+ -d/--debug debug cleartool interaction
31
+
32
+ COMMANDS (type 'basketcase help <command>' for details)
33
+
34
+ % list, ls, status, stat
35
+ % lsco
36
+ % diff
37
+ % log, history
38
+ % tree, vtree
39
+ % update, up
40
+ % checkin, ci, commit
41
+ % checkout, co, edit
42
+ % uncheckout, unco, revert
43
+ % add
44
+ % remove, rm, delete, del
45
+ % move, mv, rename
46
+ % auto-checkin, auto-ci, auto-commit
47
+ % auto-uncheckout, auto-unco, auto-revert
48
+ % auto-sync, auto-addrm
49
+ % help
50
+
51
+ == Installation
52
+
53
+ Is as easy as:
54
+
55
+ sudo gem install basketcase
56
+
57
+ == Background
58
+
59
+ In mid-2006, Mike Williams worked on a client project which, despite the
60
+ team's wishes, was burdened with ClearCase as it's source-code control
61
+ system.
62
+
63
+ The team was attempting to use Agile practices such as collective code
64
+ ownership, refactoring and continuous-integration, and ClearCase was in the
65
+ way:
66
+
67
+ * ClearCase enables and in many ways favours "reserved" check-outs of
68
+ elements, preventing collective code ownership.
69
+ * When add, removing or moving elements, ClearCase will sometimes apply the
70
+ change to the repository immediately, without waiting for a "commit".
71
+ * When updating, ClearCase will not attempt to merge other developers'
72
+ changes to elements you have checked-out ... leaving your view in an
73
+ inconsistent state.
74
+ * Performing an automatic merge from the command-line requires an unwieldy,
75
+ obscure command.
76
+ * There is no easy way to do a bulk-commit from the command-line.
77
+
78
+ Mike wrote BasketCase in frustration.
79
+
80
+ == See also
81
+
82
+ * http://rubyforge.org/projects/basketcase/
83
+ * http://dogbiscuit.org/mdub/
84
+
85
+ == License
86
+
87
+ (The MIT License)
88
+
89
+ Copyright (c) 2008 Mike Williams
90
+
91
+ Permission is hereby granted, free of charge, to any person obtaining
92
+ a copy of this software and associated documentation files (the
93
+ 'Software'), to deal in the Software without restriction, including
94
+ without limitation the rights to use, copy, modify, merge, publish,
95
+ distribute, sublicense, and/or sell copies of the Software, and to
96
+ permit persons to whom the Software is furnished to do so, subject to
97
+ the following conditions:
98
+
99
+ The above copyright notice and this permission notice shall be
100
+ included in all copies or substantial portions of the Software.
101
+
102
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
103
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
104
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
105
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
106
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
107
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
108
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,8 @@
1
+ require 'rubygems'
2
+ require 'hoe'
3
+ require './lib/basketcase.rb'
4
+
5
+ Hoe.new('basketcase', Basketcase::VERSION) do |p|
6
+ p.developer('mdub', 'mdub@dogbiscuit.org')
7
+ p.remote_rdoc_dir = ''
8
+ end
@@ -0,0 +1,13 @@
1
+ #! /usr/bin/env ruby
2
+
3
+ # This is an attempt to wrap up the ClearCase command-line interface
4
+ # (cleartool) to enable more CVS-like (or Subversion-like) usage of
5
+ # ClearCase.
6
+ #
7
+ # @author Mike Williams
8
+
9
+ $LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__) + "/../lib"))
10
+
11
+ require 'basketcase'
12
+
13
+ Basketcase.new.do(*ARGV)
@@ -0,0 +1,1042 @@
1
+ require 'pathname'
2
+ require 'forwardable'
3
+
4
+ class Basketcase
5
+
6
+ VERSION = '1.0.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
+ option_method.call(*args.shift(option_method.arity))
224
+ end
225
+ @targets = args
226
+ self
227
+ end
228
+
229
+ def effective_targets
230
+ TargetList.new(@targets.empty? ? ['.'] : @targets)
231
+ end
232
+
233
+ def specified_targets
234
+ raise UsageException, "No target specified" if @targets.empty?
235
+ TargetList.new(@targets)
236
+ end
237
+
238
+ private
239
+
240
+ def cleartool(command)
241
+ log_debug "RUNNING: cleartool #{command}"
242
+ IO.popen("cleartool " + command).each_line do |line|
243
+ line.sub!("\r", '')
244
+ log_debug "<<< " + line
245
+ yield(line) if block_given?
246
+ end
247
+ end
248
+
249
+ def cleartool_unsafe(command, &block)
250
+ if just_testing?
251
+ puts "WOULD RUN: cleartool #{command}"
252
+ return
253
+ end
254
+ cleartool(command, &block)
255
+ end
256
+
257
+ def view_root
258
+ @root ||= catch(:root) do
259
+ cleartool("pwv -root") do |line|
260
+ throw :root, mkpath(line.chomp)
261
+ end
262
+ end
263
+ log_debug "view_root = #{@root}"
264
+ @root
265
+ end
266
+
267
+ def cannot_deal_with(line)
268
+ $stderr.puts "unrecognised output: " + line
269
+ end
270
+
271
+ def edit(file)
272
+ editor = ENV["EDITOR"] || "notepad"
273
+ system("#{editor} #{file}")
274
+ end
275
+
276
+ end
277
+
278
+ class HelpCommand < Command
279
+
280
+ def synopsis
281
+ "[<command>]"
282
+ end
283
+
284
+ def help
285
+ "Display usage instructions."
286
+ end
287
+
288
+ def execute
289
+ if @targets.empty?
290
+ puts @basketcase.usage
291
+ exit
292
+ end
293
+ @targets.each do |command_name|
294
+ command = make_command(command_name)
295
+ puts
296
+ puts "% basketcase #{command_name} #{command.synopsis}"
297
+ puts
298
+ puts command.help.gsub(/^/, " ")
299
+ end
300
+ end
301
+
302
+ end
303
+
304
+ class LsCommand < Command
305
+
306
+ def synopsis
307
+ "[<element> ...]"
308
+ end
309
+
310
+ def help
311
+ <<EOF
312
+ List element status.
313
+
314
+ -a(ll) Show all files.
315
+ (by default, up-to-date files are not reported)
316
+
317
+ -r(ecurse) Recursively list sub-directories.
318
+ (by default, just lists current directory)
319
+ EOF
320
+ end
321
+
322
+ def option_all
323
+ @include_all = true
324
+ end
325
+
326
+ alias :option_a :option_all
327
+
328
+ def option_directory
329
+ @directory_only = true
330
+ end
331
+
332
+ alias :option_d :option_directory
333
+
334
+ def execute
335
+ args = ''
336
+ args += ' -recurse' if @recursive
337
+ args += ' -directory' if @directory_only
338
+ cleartool("ls #{args} #{effective_targets}") do |line|
339
+ case line
340
+ when /^(.+)@@(\S+) \[hijacked/
341
+ report(:HIJACK, mkpath($1), $2)
342
+ when /^(.+)@@(\S+) \[loaded but missing\]/
343
+ report(:MISSING, mkpath($1), $2)
344
+ when /^(.+)@@\S+\\CHECKEDOUT(?: from (\S+))?/
345
+ element_path = mkpath($1)
346
+ status = element_path.exist? ? :CO : :MISSING
347
+ report(status, element_path, $2 || 'new')
348
+ when /^(.+)@@(\S+) +Rule: /
349
+ next unless @include_all
350
+ report(:OK, mkpath($1), $2)
351
+ when /^(.+)/
352
+ path = mkpath($1)
353
+ if ignored?(path)
354
+ log_debug "ignoring #{path}"
355
+ next
356
+ end
357
+ report(:LOCAL, path)
358
+ else
359
+ cannot_deal_with line
360
+ end
361
+ end
362
+ end
363
+
364
+ end
365
+
366
+ class LsCoCommand < Command
367
+
368
+ def synopsis
369
+ "[-r] [-d] [<element> ...]"
370
+ end
371
+
372
+ def help
373
+ "List checkouts by ALL users"
374
+ end
375
+
376
+ def option_directory
377
+ @directory_only = true
378
+ end
379
+
380
+ alias :option_d :option_directory
381
+
382
+ def execute
383
+ args = ''
384
+ args += ' -recurse' if @recursive
385
+ args += ' -directory' if @directory_only
386
+ cleartool("lsco #{args} #{effective_targets}") do |line|
387
+ case line
388
+ when /^.*\s(\S+)\s+checkout.*version "(\S+)" from (\S+)/
389
+ report($1, mkpath($2), $3)
390
+ when /^Added /
391
+ # ignore
392
+ when /^ /
393
+ # ignore
394
+ else
395
+ cannot_deal_with line
396
+ end
397
+ end
398
+ end
399
+
400
+ end
401
+
402
+ class UpdateCommand < Command
403
+
404
+ def synopsis
405
+ "[-nomerge] [<element> ...]"
406
+ end
407
+
408
+ def help
409
+ <<EOF
410
+ Update your (snapshot) view.
411
+
412
+ -nomerge Don\'t attempt to merge in changes to checked-out files.
413
+ EOF
414
+
415
+ end
416
+
417
+ def option_nomerge
418
+ @nomerge = true
419
+ end
420
+
421
+ def relative_path(s)
422
+ full_path = view_root + mkpath(s)
423
+ full_path.relative_path_from(Pathname.pwd)
424
+ end
425
+
426
+ def execute_update
427
+ args = '-log nul -force'
428
+ args += ' -print' if just_testing?
429
+ cleartool("update #{args} #{effective_targets}") do |line|
430
+ case line
431
+ when /^Processing dir "(.*)"/
432
+ # ignore
433
+ when /^\.*$/
434
+ # ignore
435
+ when /^Making dir "(.*)"/
436
+ report(:NEW, relative_path($1))
437
+ when /^Loading "(.*)"/
438
+ report(:UPDATED, relative_path($1))
439
+ when /^Unloaded "(.*)"/
440
+ report(:REMOVED, relative_path($1))
441
+ when /^Keeping hijacked object "(.*)" - base "(.*)"/
442
+ report(:HIJACK, relative_path($1), $2)
443
+ when /^Keeping "(.*)"/
444
+ # ignore
445
+ when /^End dir/
446
+ # ignore
447
+ when /^Done loading/
448
+ # ignore
449
+ else
450
+ cannot_deal_with line
451
+ end
452
+ end
453
+ end
454
+
455
+ def execute_merge
456
+ args = '-log nul -flatest '
457
+ if just_testing?
458
+ args += "-print"
459
+ elsif @graphical
460
+ args += "-gmerge"
461
+ else
462
+ args += "-merge -gmerge"
463
+ end
464
+ cleartool("findmerge #{effective_targets} #{args}") do |line|
465
+ case line
466
+ when /^Needs Merge "(.+)" \[to \S+ from (\S+) base (\S+)\]/
467
+ report(:MERGE, mkpath($1), $2)
468
+ end
469
+ end
470
+ end
471
+
472
+ def execute
473
+ execute_update
474
+ execute_merge unless @nomerge
475
+ end
476
+
477
+ end
478
+
479
+ class CheckinCommand < Command
480
+
481
+ def synopsis
482
+ "<element> ..."
483
+ end
484
+
485
+ def help
486
+ "Check-in elements, prompting for a check-in message."
487
+ end
488
+
489
+ def execute
490
+ prompt_for_comment
491
+ comment_file = Pathname.new("basketcase-checkin-comment.tmp")
492
+ comment_file.open("w") do |out|
493
+ out.puts(@comment)
494
+ end
495
+ cleartool_unsafe("checkin -cfile #{comment_file} #{specified_targets}") do |line|
496
+ case line
497
+ when /^Loading /
498
+ # ignore
499
+ when /^Making dir /
500
+ # ignore
501
+ when /^Checked in "(.+)" version "(\S+)"\./
502
+ report(:COMMIT, mkpath($1), $2)
503
+ else
504
+ cannot_deal_with line
505
+ end
506
+ end
507
+ comment_file.unlink
508
+ end
509
+
510
+ def prompt_for_comment
511
+ return if @comment
512
+ comment_file = Pathname.new("basketcase-comment.tmp")
513
+ begin
514
+ comment_file.open('w') do |out|
515
+ out.puts <<EOF
516
+ # Please enter the commit message for your changes.
517
+ # (Comment lines starting with '#' will not be included)
518
+ #
519
+ # Changes to be committed:
520
+ EOF
521
+ specified_targets.each do |target|
522
+ out.puts "#\t#{target}"
523
+ end
524
+ end
525
+ edit(comment_file)
526
+ @comment = comment_file.read.gsub(/^#.*\n/, '')
527
+ ensure
528
+ comment_file.unlink
529
+ end
530
+ raise UsageException, "No check-in comment provided" if @comment.empty?
531
+ @comment
532
+ end
533
+
534
+ end
535
+
536
+ class CheckoutCommand < Command
537
+
538
+ def synopsis
539
+ "<element> ..."
540
+ end
541
+
542
+ def help
543
+ ""
544
+ end
545
+
546
+ def help
547
+ <<EOF
548
+ Check-out elements (unreserved).
549
+ By default, any hijacked version is discarded.
550
+
551
+ -h(ijack) Retain the hijacked version.
552
+ EOF
553
+ end
554
+
555
+ def initialize(*args)
556
+ super(*args)
557
+ @keep_or_revert = '-nquery'
558
+ end
559
+
560
+ def option_hijack
561
+ @keep_or_revert = '-usehijack'
562
+ end
563
+
564
+ alias :option_h :option_hijack
565
+
566
+ def execute
567
+ cleartool_unsafe("checkout -unreserved -ncomment #{@keep_or_revert} #{specified_targets}") do |line|
568
+ case line
569
+ when /^Checked out "(.+)" from version "(\S+)"\./
570
+ report(:CO, mkpath($1), $2)
571
+ end
572
+ end
573
+ end
574
+
575
+ end
576
+
577
+ class UncheckoutCommand < Command
578
+
579
+ def synopsis
580
+ "[-r] <element> ..."
581
+ end
582
+
583
+ def help
584
+ <<EOF
585
+ Undo a checkout, reverting to the checked-in version.
586
+
587
+ -r(emove) Don\'t retain the existing version in a '.keep' file.
588
+ EOF
589
+ end
590
+
591
+ def initialize(*args)
592
+ super(*args)
593
+ @action = '-keep'
594
+ end
595
+
596
+ def option_remove
597
+ @action = '-rm'
598
+ end
599
+
600
+ alias :option_r :option_remove
601
+
602
+ attr_accessor :action
603
+
604
+ def execute
605
+ cleartool_unsafe("uncheckout #{@action} #{specified_targets}") do |line|
606
+ case line
607
+ when /^Loading /
608
+ # ignore
609
+ when /^Making dir /
610
+ # ignore
611
+ when /^Checkout cancelled for "(.+)"\./
612
+ report(:UNCO, mkpath($1))
613
+ when /^Private version .* saved in "(.+)"\./
614
+ report(:KEPT, mkpath($1))
615
+ else
616
+ cannot_deal_with line
617
+ end
618
+ end
619
+ end
620
+
621
+ end
622
+
623
+ class DirectoryModificationCommand < Command
624
+
625
+ def find_locked_elements(paths)
626
+ locked_elements = []
627
+ run(LsCommand, '-a', '-d', *paths) do |e|
628
+ locked_elements << e.path if e.status == :OK
629
+ end
630
+ locked_elements
631
+ end
632
+
633
+ def checkout(target_list)
634
+ return if target_list.empty?
635
+ run(CheckoutCommand, *target_list)
636
+ end
637
+
638
+ def unlock_parent_directories(target_list)
639
+ checkout find_locked_elements(target_list.parents)
640
+ end
641
+
642
+ end
643
+
644
+ class RemoveCommand < DirectoryModificationCommand
645
+
646
+ def synopsis
647
+ "<element> ..."
648
+ end
649
+
650
+ def help
651
+ <<EOF
652
+ Mark an element as deleted.
653
+ (Parent directories are checked-out automatically)
654
+ EOF
655
+ end
656
+
657
+ def execute
658
+ unlock_parent_directories(specified_targets)
659
+ cleartool_unsafe("rmname -ncomment #{specified_targets}") do |line|
660
+ case line
661
+ when /^Unloaded /
662
+ # ignore
663
+ when /^Removed "(.+)"\./
664
+ report(:REMOVED, mkpath($1))
665
+ else
666
+ cannot_deal_with line
667
+ end
668
+ end
669
+ end
670
+
671
+ end
672
+
673
+ class AddCommand < DirectoryModificationCommand
674
+
675
+ def synopsis
676
+ "<element> ..."
677
+ end
678
+
679
+ def help
680
+ <<EOF
681
+ Add elements to the repository.
682
+ (Parent directories are checked-out automatically)
683
+ EOF
684
+ end
685
+
686
+ def execute
687
+ unlock_parent_directories(specified_targets)
688
+ cleartool_unsafe("mkelem -ncomment #{specified_targets}") do |line|
689
+ case line
690
+ when /^Created element /
691
+ # ignore
692
+ when /^Checked out "(.+)" from version "(\S+)"\./
693
+ report(:ADDED, mkpath($1), $2)
694
+ else
695
+ cannot_deal_with line
696
+ end
697
+ end
698
+ end
699
+
700
+ end
701
+
702
+ class MoveCommand < DirectoryModificationCommand
703
+
704
+ def synopsis
705
+ "<from> <to>"
706
+ end
707
+
708
+ def help
709
+ <<EOF
710
+ Move/rename an element.
711
+ (Parent directories are checked-out automatically)
712
+ EOF
713
+ end
714
+
715
+ def execute
716
+ raise UsageException, "expected two arguments" unless (specified_targets.size == 2)
717
+ unlock_parent_directories(specified_targets)
718
+ cleartool_unsafe("move -ncomment #{specified_targets}") do |line|
719
+ case line
720
+ when /^Moved "(.+)" to "(.+)"\./
721
+ report(:REMOVED, mkpath($1))
722
+ report(:ADDED, mkpath($2))
723
+ else
724
+ cannot_deal_with line
725
+ end
726
+ end
727
+ end
728
+
729
+ end
730
+
731
+ class DiffCommand < Command
732
+
733
+ def synopsis
734
+ "[-g] <element>"
735
+ end
736
+
737
+ def help
738
+ <<EOF
739
+ Compare a file to the latest checked-in version.
740
+
741
+ -g Graphical display.
742
+ EOF
743
+ end
744
+
745
+ def execute
746
+ args = ''
747
+ args += ' -graphical' if @graphical
748
+ specified_targets.each do |target|
749
+ cleartool("diff #{args} -predecessor #{target}") do |line|
750
+ puts line
751
+ end
752
+ end
753
+ end
754
+
755
+ end
756
+
757
+ class LogCommand < Command
758
+
759
+ def synopsis
760
+ "[<element> ...]"
761
+ end
762
+
763
+ def help
764
+ <<EOF
765
+ List the history of specified elements.
766
+ EOF
767
+ end
768
+
769
+ def option_directory
770
+ @directory_only = true
771
+ end
772
+
773
+ alias :option_d :option_directory
774
+
775
+ def execute
776
+ args = ''
777
+ args += ' -recurse' if @recursive
778
+ args += ' -directory' if @directory_only
779
+ args += ' -graphical' if @graphical
780
+ cleartool("lshistory #{args} #{effective_targets}") do |line|
781
+ puts line
782
+ end
783
+ end
784
+
785
+ end
786
+
787
+ class VersionTreeCommand < Command
788
+
789
+ def synopsis
790
+ "<element>"
791
+ end
792
+
793
+ def help
794
+ <<EOF
795
+ Display a version-tree of specified elements.
796
+
797
+ -g Graphical display.
798
+ EOF
799
+ end
800
+
801
+ def execute
802
+ args = ''
803
+ args += ' -graphical' if @graphical
804
+ cleartool("lsvtree #{args} #{effective_targets}") do |line|
805
+ puts line
806
+ end
807
+ end
808
+
809
+ end
810
+
811
+ class AutoCommand < Command
812
+
813
+ def each_element(&block)
814
+ run(LsCommand, '-r', *effective_targets, &block)
815
+ end
816
+
817
+ def find_checkouts
818
+ checkouts = []
819
+ each_element do |e|
820
+ checkouts << e.path if e.status == :CO
821
+ end
822
+ checkouts
823
+ end
824
+
825
+ end
826
+
827
+ class AutoCheckinCommand < AutoCommand
828
+
829
+ def synopsis
830
+ "[<element> ...]"
831
+ end
832
+
833
+ def help
834
+ <<EOF
835
+ Bulk commit: check-in all checked-out elements.
836
+ EOF
837
+ end
838
+
839
+ def execute
840
+ checked_out_elements = find_checkouts
841
+ if checked_out_elements.empty?
842
+ puts "Nothing to check-in"
843
+ return
844
+ end
845
+ run(CheckinCommand, '-m', comment, *checked_out_elements)
846
+ end
847
+
848
+ end
849
+
850
+ class AutoUncheckoutCommand < AutoCommand
851
+
852
+ def synopsis
853
+ "[<element> ...]"
854
+ end
855
+
856
+ def help
857
+ <<EOF
858
+ Bulk revert: revert all checked-out elements.
859
+ EOF
860
+ end
861
+
862
+ def execute
863
+ checked_out_elements = find_checkouts
864
+ if checked_out_elements.empty?
865
+ puts "Nothing to revert"
866
+ return
867
+ end
868
+ run(UncheckoutCommand, '-r', *checked_out_elements)
869
+ end
870
+
871
+ end
872
+
873
+ class AutoSyncCommand < AutoCommand
874
+
875
+ def initialize(*args)
876
+ super(*args)
877
+ @control_file = Pathname.new("basketcase-autosync.tmp")
878
+ @actions = []
879
+ end
880
+
881
+ def synopsis
882
+ "[<element> ...]"
883
+ end
884
+
885
+ def help
886
+ <<EOF
887
+ Bulk add/remove: offer to add new elements, and remove missing ones.
888
+
889
+ -n Don\'t prompt to confirm actions.
890
+ EOF
891
+ end
892
+
893
+ def option_noprompt
894
+ @noprompt = true
895
+ end
896
+
897
+ alias :option_n :option_noprompt
898
+
899
+ def collect_actions
900
+ each_element do |e|
901
+ case e.status
902
+ when :LOCAL
903
+ @actions << ['add', e.path]
904
+ when :MISSING
905
+ @actions << ['rm', e.path]
906
+ when :HIJACK
907
+ @actions << ['co -h', e.path]
908
+ end
909
+ end
910
+ end
911
+
912
+ def prompt_for_confirmation
913
+ @control_file.open('w') do |control|
914
+ control.puts <<EOF
915
+ # basketcase proposes the actions listed below.
916
+ # Delete any that you don't wish to occur, then save this file.
917
+ #
918
+ EOF
919
+ @actions.each do |a|
920
+ control.puts a.join("\t")
921
+ end
922
+ end
923
+ edit(@control_file)
924
+ @actions = []
925
+ @control_file.open('r') do |control|
926
+ control.each_line do |line|
927
+ if line =~ /^(add|rm|co -h)\s+(.*)/
928
+ @actions << [$1, $2]
929
+ end
930
+ end
931
+ end
932
+ end
933
+
934
+ def apply_actions
935
+ ['add', 'rm', 'co -h'].each do |command|
936
+ elements = @actions.map { |a| a[1] if a[0] == command }.compact
937
+ unless elements.empty?
938
+ run(*(command.split(' ') + elements))
939
+ end
940
+ end
941
+ end
942
+
943
+ def execute
944
+ collect_actions
945
+ if @actions.empty?
946
+ puts "No changes required"
947
+ return
948
+ end
949
+ prompt_for_confirmation unless @noprompt
950
+ apply_actions
951
+ end
952
+
953
+ end
954
+
955
+ @registry = {}
956
+
957
+ class << self
958
+
959
+ def command(command_class, names)
960
+ names.each { |name| @registry[name] = command_class }
961
+ @usage << " % #{names.join(', ')}\n"
962
+ end
963
+
964
+ def command_class(name)
965
+ return name if Class === name
966
+ @registry[name] || raise(UsageException, "Unknown command: #{name}")
967
+ end
968
+
969
+ attr_reader :usage
970
+
971
+ end
972
+
973
+ command LsCommand, %w(list ls status stat)
974
+ command LsCoCommand, %w(lsco)
975
+ command DiffCommand, %w(diff)
976
+ command LogCommand, %w(log history)
977
+ command VersionTreeCommand, %w(tree vtree)
978
+
979
+ command UpdateCommand, %w(update up)
980
+ command CheckinCommand, %w(checkin ci commit)
981
+ command CheckoutCommand, %w(checkout co edit)
982
+ command UncheckoutCommand, %w(uncheckout unco revert)
983
+ command AddCommand, %w(add)
984
+ command RemoveCommand, %w(remove rm delete del)
985
+ command MoveCommand, %w(move mv rename)
986
+ command AutoCheckinCommand, %w(auto-checkin auto-ci auto-commit)
987
+ command AutoUncheckoutCommand, %w(auto-uncheckout auto-unco auto-revert)
988
+ command AutoSyncCommand, %w(auto-sync auto-addrm)
989
+
990
+ command HelpCommand, %w(help)
991
+
992
+ def usage
993
+ Basketcase.usage
994
+ end
995
+
996
+ def make_command(name)
997
+ Basketcase.command_class(name).new(self)
998
+ end
999
+
1000
+ def run(name, *args, &block)
1001
+ command = make_command(name)
1002
+ command.accept_args(args) if args
1003
+ command.listener = block if block_given?
1004
+ command.execute
1005
+ end
1006
+
1007
+ def sync_io
1008
+ $stdout.sync = true
1009
+ $stderr.sync = true
1010
+ end
1011
+
1012
+ def handle_global_options
1013
+ while /^-/ === @args[0]
1014
+ option = @args.shift
1015
+ case option
1016
+ when '--test', '-t'
1017
+ @test_mode = true
1018
+ when '--debug', '-d'
1019
+ @debug_mode = true
1020
+ else
1021
+ raise UsageException, "Unrecognised global argument: #{option}"
1022
+ end
1023
+ end
1024
+ end
1025
+
1026
+ def do(*args)
1027
+ @args = args
1028
+ begin
1029
+ sync_io
1030
+ handle_global_options
1031
+ raise UsageException, "no command specified" if @args.empty?
1032
+ define_standard_ignore_patterns
1033
+ run(*@args)
1034
+ rescue UsageException => usage
1035
+ $stderr.puts "ERROR: #{usage.message}"
1036
+ $stderr.puts
1037
+ $stderr.puts "try 'basketcase help' for usage info"
1038
+ exit(1)
1039
+ end
1040
+ end
1041
+
1042
+ end
metadata ADDED
@@ -0,0 +1,71 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: basketcase
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - mdub
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-10-15 00:00:00 +11:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: hoe
17
+ type: :development
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 1.7.0
24
+ version:
25
+ description: ""
26
+ email:
27
+ - mdub@dogbiscuit.org
28
+ executables:
29
+ - basketcase
30
+ extensions: []
31
+
32
+ extra_rdoc_files:
33
+ - History.txt
34
+ - Manifest.txt
35
+ - README.txt
36
+ files:
37
+ - History.txt
38
+ - Manifest.txt
39
+ - README.txt
40
+ - Rakefile
41
+ - bin/basketcase
42
+ - lib/basketcase.rb
43
+ has_rdoc: true
44
+ homepage: BasketCase is a (Ruby) script that encapsulates the Rational ClearCase
45
+ post_install_message:
46
+ rdoc_options:
47
+ - --main
48
+ - README.txt
49
+ require_paths:
50
+ - lib
51
+ required_ruby_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: "0"
56
+ version:
57
+ required_rubygems_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: "0"
62
+ version:
63
+ requirements: []
64
+
65
+ rubyforge_project: basketcase
66
+ rubygems_version: 1.2.0
67
+ signing_key:
68
+ specification_version: 2
69
+ summary: ""
70
+ test_files: []
71
+