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.
- data/History.txt +3 -0
- data/Manifest.txt +6 -0
- data/README.txt +108 -0
- data/Rakefile +8 -0
- data/bin/basketcase +13 -0
- data/lib/basketcase.rb +1042 -0
- metadata +71 -0
data/History.txt
ADDED
data/Manifest.txt
ADDED
data/README.txt
ADDED
@@ -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.
|
data/Rakefile
ADDED
data/bin/basketcase
ADDED
@@ -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)
|
data/lib/basketcase.rb
ADDED
@@ -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
|
+
|