basketcase 1.0.0

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