treebis 0.0.1
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/README +98 -0
- data/Rakefile +49 -0
- data/VERSION +1 -0
- data/doc/additional-goodies/persistent-dotfile.md +38 -0
- data/doc/svg/go-green.svg +19 -0
- data/doc/usage-examples.md +41 -0
- data/lib/treebis.rb +1516 -0
- data/test/test-for-doc.rb +136 -0
- metadata +94 -0
data/lib/treebis.rb
ADDED
@@ -0,0 +1,1516 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'json'
|
3
|
+
require 'open3'
|
4
|
+
require 'pathname'
|
5
|
+
require 'shellwords'
|
6
|
+
require 'tempfile'
|
7
|
+
|
8
|
+
module Treebis
|
9
|
+
|
10
|
+
@task_set = nil
|
11
|
+
class << self
|
12
|
+
def dir_task path
|
13
|
+
taskfile = path + '/treebis-task.rb'
|
14
|
+
require taskfile
|
15
|
+
name = File.basename(path).to_sym
|
16
|
+
task = @task_set[name]
|
17
|
+
task.from path
|
18
|
+
task
|
19
|
+
end
|
20
|
+
def tasks
|
21
|
+
@task_set ||= TaskSet.new
|
22
|
+
end
|
23
|
+
def unindent content
|
24
|
+
/\A(\s*)/ =~ content
|
25
|
+
content.gsub(/^#{Regexp.escape($1)}/,'')
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
module Antecedent
|
30
|
+
def init_path_antecedent
|
31
|
+
@antecedent = {}
|
32
|
+
end
|
33
|
+
def path_antecedent domain, string
|
34
|
+
if @antecedent[domain] && head =common_head(@antecedent[domain], string)
|
35
|
+
@antecedent[domain] = [@antecedent[domain], string].max_by(&:length)
|
36
|
+
path_antecedent_truncate head, string
|
37
|
+
else
|
38
|
+
@antecedent[domain] = string
|
39
|
+
string
|
40
|
+
end
|
41
|
+
end
|
42
|
+
private
|
43
|
+
def path_antecedent_truncate common_head, string
|
44
|
+
tail = string[common_head.length..-1]
|
45
|
+
common_head =~ %r<(.{0,3}/?[^/]+/?)\Z>
|
46
|
+
head = $1
|
47
|
+
"...#{head}#{tail}"
|
48
|
+
end
|
49
|
+
def common_head str1, str2
|
50
|
+
for i in 0..( [str1.length, str2.length].min - 1 )
|
51
|
+
break unless str1[i] == str2[i]
|
52
|
+
end
|
53
|
+
ret = str1[0..i-1]
|
54
|
+
ret
|
55
|
+
end
|
56
|
+
end
|
57
|
+
# like Open3 but stay in the ruby runtime
|
58
|
+
module Capture3
|
59
|
+
# @return [out_string, err_string, result]
|
60
|
+
def capture3 &block
|
61
|
+
prev_out, prev_err, result = $stdout, $stderr, nil
|
62
|
+
out_str, err_str = nil, nil
|
63
|
+
$stderr = StringIO.new
|
64
|
+
$stdout = StdoutStringIoHack.new
|
65
|
+
begin
|
66
|
+
result = block.call
|
67
|
+
ensure
|
68
|
+
$stderr.rewind
|
69
|
+
out_str = $stdout.to_str
|
70
|
+
err_str = $stderr.read
|
71
|
+
$stdout = prev_out
|
72
|
+
$stderr = prev_err
|
73
|
+
end
|
74
|
+
[out_str, err_str, result]
|
75
|
+
end
|
76
|
+
class StdoutStringIoHack < File
|
77
|
+
private(*ancestors[1..2].map(&:public_instance_methods).flatten)
|
78
|
+
these = %w(puts write <<)
|
79
|
+
public(*these)
|
80
|
+
def initialize
|
81
|
+
super('/dev/null','w+')
|
82
|
+
@buffer = StringIO.new
|
83
|
+
end
|
84
|
+
these.each do |this|
|
85
|
+
alias_method "old_#{this}", this # avoid silly warnings
|
86
|
+
define_method(this){|*a| @buffer.send(this, *a) }
|
87
|
+
end
|
88
|
+
def to_str
|
89
|
+
@buffer.rewind
|
90
|
+
@buffer.read
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
module Colorize
|
95
|
+
Codes = {:bright=>'1', :red=>'31', :green=>'32', :yellow=>'33',
|
96
|
+
:blue=>'34',:magenta=>'35',:bold=>'1',:blink=>'5'}
|
97
|
+
def colorize str, *codenames
|
98
|
+
return str if codenames == [nil] || codenames.empty?
|
99
|
+
codes = nil
|
100
|
+
if codenames.first == :background
|
101
|
+
fail("not yet") unless codenames.size == 2
|
102
|
+
codes = ["4#{Codes[codenames.last][1..1]}"]
|
103
|
+
# this isn't really excusable in any way
|
104
|
+
else
|
105
|
+
codes = codenames.map{|x| Codes[x]}
|
106
|
+
end
|
107
|
+
"\e["+codes.join(';')+"m#{str}\e[0m"
|
108
|
+
end
|
109
|
+
module_function :colorize
|
110
|
+
end
|
111
|
+
module Config
|
112
|
+
extend self
|
113
|
+
@color = true
|
114
|
+
def color?; @color end
|
115
|
+
def no_color!; @color = false end
|
116
|
+
def color!; @color = true end
|
117
|
+
|
118
|
+
def default_out_stream
|
119
|
+
proc{ $stderr } # for capture_3 to work this has
|
120
|
+
# to act like a referece to this global variable
|
121
|
+
end
|
122
|
+
|
123
|
+
@default_prefix = ' '*6 # sorta like nanoc
|
124
|
+
attr_accessor :default_prefix
|
125
|
+
|
126
|
+
def new_default_file_utils_proxy
|
127
|
+
FileUtilsProxy.new do |fu|
|
128
|
+
fu.prefix = default_prefix
|
129
|
+
fu.ui = default_out_stream
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
module DirAsHash
|
135
|
+
#
|
136
|
+
# Return an entire filsystem node as a hash whose files are represented
|
137
|
+
# as strings holding the contents of the file and whose folders
|
138
|
+
# are other such hashes. The hash element keys are the entry strings.
|
139
|
+
# Careful! the only real use for this so far is in testing.
|
140
|
+
#
|
141
|
+
def dir_as_hash path, opts={}
|
142
|
+
blacklist = Blacklist.get(opts[:skip])
|
143
|
+
list1 = Dir.new(path).each.map.reject{ |x|
|
144
|
+
0==x.index('.') || blacklist.include?(x)
|
145
|
+
}
|
146
|
+
list2 = list1.map{ |entry|
|
147
|
+
p = path + '/' + entry
|
148
|
+
[ entry, File.directory?(p) ?
|
149
|
+
dir_as_hash(p, :skip=>blacklist.submatcher(entry)) : File.read(p) ]
|
150
|
+
}
|
151
|
+
Hash[ list2 ]
|
152
|
+
end
|
153
|
+
module_function :dir_as_hash
|
154
|
+
|
155
|
+
def hash_to_dir hash, path, file_utils=FileUtils
|
156
|
+
fail("must not exist: #{path}") if File.exist? path
|
157
|
+
file_utils.mkdir_p(path,:verbose=>true)
|
158
|
+
hash.each do |k,v|
|
159
|
+
path2 = File.join(path, k)
|
160
|
+
case v
|
161
|
+
when String
|
162
|
+
File.open(path2, 'w'){|fh| fh.write(v)}
|
163
|
+
when Hash
|
164
|
+
hash_to_dir(v, path2, file_utils)
|
165
|
+
else
|
166
|
+
fail("bad type for dir hash: #{v.inspect}")
|
167
|
+
end
|
168
|
+
end
|
169
|
+
true
|
170
|
+
end
|
171
|
+
module_function :hash_to_dir
|
172
|
+
|
173
|
+
class Blacklist
|
174
|
+
#
|
175
|
+
# ruby implementation of fileglob-like patterns for data structures
|
176
|
+
# this would be faster and uglier with string matchers as hash keys
|
177
|
+
# this might move to Tardo lib
|
178
|
+
#
|
179
|
+
module MatcherFactory
|
180
|
+
def create_matcher mixed
|
181
|
+
case mixed
|
182
|
+
when Matcher; mixed # keep this first for primitive subclasses
|
183
|
+
when NilClass; EmptyMatcher
|
184
|
+
when String
|
185
|
+
%r{\A([^/]+)(?:/(.+)|)\Z} =~ mixed or
|
186
|
+
fail("path: #{mixed.inspect}")
|
187
|
+
head, tail = $1, $2
|
188
|
+
if head.index('*')
|
189
|
+
GlobNodeMatcher.get(head, tail)
|
190
|
+
else
|
191
|
+
StringMatcher.new(head, tail)
|
192
|
+
end
|
193
|
+
when Array; Blacklist.new(mixed)
|
194
|
+
else
|
195
|
+
fail("can't build matcher from #{mixed.inspect}")
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
module Matcher; end
|
200
|
+
include Matcher, MatcherFactory
|
201
|
+
class << self
|
202
|
+
include MatcherFactory
|
203
|
+
alias_method :get, :create_matcher
|
204
|
+
end
|
205
|
+
private
|
206
|
+
def initialize lizt
|
207
|
+
@matchers = lizt.map do |glob|
|
208
|
+
create_matcher(glob)
|
209
|
+
end
|
210
|
+
end
|
211
|
+
public
|
212
|
+
def include? str
|
213
|
+
@matchers.detect{ |x| x.include?(str) }
|
214
|
+
end
|
215
|
+
def submatcher str
|
216
|
+
these = []
|
217
|
+
@matchers.each do |m|
|
218
|
+
this = m.submatcher(str)
|
219
|
+
these.push(this) if this
|
220
|
+
end
|
221
|
+
ret =
|
222
|
+
if these.empty?
|
223
|
+
nil
|
224
|
+
else
|
225
|
+
Blacklist.new(these)
|
226
|
+
end
|
227
|
+
ret
|
228
|
+
end
|
229
|
+
private
|
230
|
+
class EmptyMatcherClass
|
231
|
+
include Matcher
|
232
|
+
def initialize; end
|
233
|
+
def submatcher m
|
234
|
+
self
|
235
|
+
end
|
236
|
+
def include? *a
|
237
|
+
false
|
238
|
+
end
|
239
|
+
end
|
240
|
+
EmptyMatcher = EmptyMatcherClass.new
|
241
|
+
class MatcherWithTail
|
242
|
+
include MatcherFactory, Matcher
|
243
|
+
def include? str
|
244
|
+
if @tail
|
245
|
+
false
|
246
|
+
else
|
247
|
+
head_include?(str)
|
248
|
+
end
|
249
|
+
end
|
250
|
+
def tail= str
|
251
|
+
if str.nil?
|
252
|
+
@tail = nil
|
253
|
+
else
|
254
|
+
@tail = create_matcher(str)
|
255
|
+
end
|
256
|
+
end
|
257
|
+
def submatcher(sub)
|
258
|
+
if head_include?(sub)
|
259
|
+
@tail
|
260
|
+
else
|
261
|
+
nil
|
262
|
+
end
|
263
|
+
end
|
264
|
+
end
|
265
|
+
class GlobNodeMatcher < MatcherWithTail
|
266
|
+
# **/ --> (.*?\/)+ ; * --> .*? ; ? --> . ;
|
267
|
+
# "**/*.rb" --> /^(.*?\/)+.*?\.rb$/ -thx heftig
|
268
|
+
class << self
|
269
|
+
include MatcherFactory
|
270
|
+
def get globtoken, tail
|
271
|
+
if /(?:\*\*|\/)/ =~ globtoken
|
272
|
+
GlobNodeMatcherDeep.new(globtoken, tail)
|
273
|
+
else
|
274
|
+
new globtoken, tail
|
275
|
+
end
|
276
|
+
end
|
277
|
+
end
|
278
|
+
def initialize globtoken, tail
|
279
|
+
/(?:\*\*|\/)/ =~ globtoken and fail("use factory")
|
280
|
+
/\*/ =~ globtoken or fail("no")
|
281
|
+
regexp_str = globtoken.split('*').map do |x|
|
282
|
+
Regexp.escape(x)
|
283
|
+
end.join('.*?')
|
284
|
+
@re = Regexp.new("\\A#{regexp_str}\\Z")
|
285
|
+
self.tail = tail
|
286
|
+
end
|
287
|
+
def head_include? str
|
288
|
+
@re =~ str
|
289
|
+
end
|
290
|
+
end
|
291
|
+
class GlobNodeMatcherDeep < GlobNodeMatcher
|
292
|
+
def initialize globtoken, tail
|
293
|
+
/\*\*/ =~ globtoken or fail("not deep: #{globtoken.inspect}")
|
294
|
+
tail or fail("for now deep globs must look like \"**/*.bar\"")
|
295
|
+
self.tail = tail
|
296
|
+
end
|
297
|
+
undef_method :head_include? # just to be safe, we don't want this
|
298
|
+
def include? str
|
299
|
+
@tail.include? str
|
300
|
+
end
|
301
|
+
def submatcher str
|
302
|
+
self # you just keep travelling down the tree when you're '**'
|
303
|
+
end
|
304
|
+
end
|
305
|
+
class StringMatcher < MatcherWithTail
|
306
|
+
def initialize str, tail=nil
|
307
|
+
@head = str
|
308
|
+
self.tail = tail
|
309
|
+
end
|
310
|
+
def head_include? str
|
311
|
+
@head == str
|
312
|
+
end
|
313
|
+
end
|
314
|
+
end
|
315
|
+
end
|
316
|
+
class Fail < RuntimeError; end
|
317
|
+
class FileUtilsAsClass
|
318
|
+
# make it so we can effectively subclass FileUtils and override methods
|
319
|
+
# and call up to the originals
|
320
|
+
include FileUtils
|
321
|
+
public :cp, :mkdir_p, :mv, :rm, :remove_entry_secure
|
322
|
+
end
|
323
|
+
module Stylize; end
|
324
|
+
class FileUtilsProxy < FileUtilsAsClass
|
325
|
+
# We like the idea of doing FileUtils.foo(:verbose=>true) and outputting
|
326
|
+
# whatever the response it to screen, but sometimes we want to format its
|
327
|
+
# response a little more when possible.
|
328
|
+
#
|
329
|
+
include Antecedent, Capture3, Colorize, Stylize
|
330
|
+
def initialize &block
|
331
|
+
@color = nil
|
332
|
+
@prefix = ''
|
333
|
+
@pretty = false
|
334
|
+
init_path_antecedent
|
335
|
+
@ui = Config.default_out_stream
|
336
|
+
@ui_stack = nil
|
337
|
+
yield(self) if block_given?
|
338
|
+
end
|
339
|
+
def color? &block
|
340
|
+
if block_given?
|
341
|
+
@color = block
|
342
|
+
else
|
343
|
+
@color.call
|
344
|
+
end
|
345
|
+
end
|
346
|
+
def cp *args
|
347
|
+
opts = args.last.kind_of?(Hash) ? args.last : {}
|
348
|
+
ret, out, err = nil, "", nil
|
349
|
+
keep = {:pretty_name_target => opts.delete(:pretty_name_target) }
|
350
|
+
if ! @pretty
|
351
|
+
ret = super(*args)
|
352
|
+
else
|
353
|
+
out, err, ret = capture3{ super(*args) }
|
354
|
+
opts.merge!(keep)
|
355
|
+
pretty_puts_cp out, err, ret, *args
|
356
|
+
end
|
357
|
+
ret
|
358
|
+
end
|
359
|
+
attr_reader :prefix
|
360
|
+
def prefix= str
|
361
|
+
@pretty = true
|
362
|
+
@prefix = str
|
363
|
+
end
|
364
|
+
def pretty!
|
365
|
+
@pretty = true
|
366
|
+
end
|
367
|
+
def remove_entry_secure path, force=false, opts={}
|
368
|
+
opts = {:verbose=>true}.merge(opts)
|
369
|
+
# parent call doesn't write to stdout or stderr, returns nil
|
370
|
+
did = nil
|
371
|
+
if File.exist?(path)
|
372
|
+
super(path, force)
|
373
|
+
did = true
|
374
|
+
err = "rm -rf #{path}"
|
375
|
+
colors = [:bright,:green]
|
376
|
+
else
|
377
|
+
did = false
|
378
|
+
err = "rm -rf (didn't exist:) #{path}"
|
379
|
+
colors = [:bright, :red]
|
380
|
+
end
|
381
|
+
if opts[:verbose]
|
382
|
+
if @pretty
|
383
|
+
err.sub!(/\Arm -rf/){colorize('rm -rf',*colors)}
|
384
|
+
err = "#{prefix}#{err}"
|
385
|
+
end
|
386
|
+
ui.puts err
|
387
|
+
end
|
388
|
+
did
|
389
|
+
end
|
390
|
+
def rm *a
|
391
|
+
if @pretty
|
392
|
+
b = capture3{ super(*a) }
|
393
|
+
pretty_puts_rm(*(b+a))
|
394
|
+
else
|
395
|
+
ret = super(*a)
|
396
|
+
end
|
397
|
+
ret
|
398
|
+
end
|
399
|
+
def not_pretty!
|
400
|
+
@pretty = false
|
401
|
+
end
|
402
|
+
def mkdir_p *args
|
403
|
+
ret, out, err = nil, "", nil
|
404
|
+
if File.exist?(args.first)
|
405
|
+
err = "treebis: mkdir_p: exists: #{args.first}"
|
406
|
+
elsif @pretty
|
407
|
+
out, err, ret = capture3{ super(*args) }
|
408
|
+
else
|
409
|
+
ret = super
|
410
|
+
end
|
411
|
+
pretty_puts_mkdir_p out, err, ret, *args
|
412
|
+
ret
|
413
|
+
end
|
414
|
+
# keep this public because runner context refers to it
|
415
|
+
attr_writer :ui
|
416
|
+
def ui
|
417
|
+
@ui.kind_of?(Proc) ? @ui.call : @ui # see Config
|
418
|
+
end
|
419
|
+
# not sure about this, it breaks the otherwise faithful 'api'
|
420
|
+
# we don't like it because of that, we do like it better than
|
421
|
+
# capture3 because we don't need ugly hacks or tossing around globals
|
422
|
+
def ui_push
|
423
|
+
@ui_stack ||= []
|
424
|
+
@ui_stack.push @ui
|
425
|
+
@ui = StringIO.new
|
426
|
+
end
|
427
|
+
def ui_pop
|
428
|
+
it = @ui
|
429
|
+
@ui = @ui_stack.pop
|
430
|
+
resp = it.kind_of?(StringIO) ? (it.rewind && it.read) : it
|
431
|
+
resp
|
432
|
+
end
|
433
|
+
private
|
434
|
+
def pretty_puts_cp out, err, ret, *args
|
435
|
+
fail("unexpected out: #{out.inspect} or ret: #{ret.inspect}") unless
|
436
|
+
ret.nil? && out == ''
|
437
|
+
opts = args.last.kind_of?(Hash) ? args.last : {}
|
438
|
+
p1, p2 = pretty_puts_cp_paths(args[0], args[1], opts)
|
439
|
+
# Regexp.escape(args[0..1].map{|x|Shellwords.escape(x)}.join(' '))
|
440
|
+
matched =
|
441
|
+
/\Acp #{Regexp.escape(args[0])} #{Regexp.escape(args[1])}(.*)\Z/ =~ err
|
442
|
+
if err.empty?
|
443
|
+
err = "no response? cp #{p1} #{p2}"
|
444
|
+
elsif matched
|
445
|
+
err = "cp #{p1} #{p2}#{$1}"
|
446
|
+
end
|
447
|
+
err.sub!(/\A(?:no response\? )?cp\b/){colorize('cp',:bright,:green)}
|
448
|
+
ui.puts("%s%s" % [prefix, err])
|
449
|
+
end
|
450
|
+
# use a variety of advanced cutting edge technology to determine
|
451
|
+
# a shortest way to express unambiguesque-ly paths
|
452
|
+
def pretty_puts_cp_paths path1, path2, opts
|
453
|
+
path2 = if opts[:pretty_name_target]
|
454
|
+
opts[:pretty_name_target]
|
455
|
+
else
|
456
|
+
s1 = Pathname.new(path2).relative_path_from(Pathname.new(path1)).to_s
|
457
|
+
s2 = path_antecedent(:cp_tgt, path2)
|
458
|
+
s3 = path2
|
459
|
+
shortest, idx = [s1, s2, s3].each_with_index.min_by{|v, i|v.length}
|
460
|
+
shortest
|
461
|
+
end
|
462
|
+
path1 = path_antecedent(:cp_src, path1)
|
463
|
+
[path1, path2]
|
464
|
+
end
|
465
|
+
def pretty_puts_mkdir_p out, err, ret, *args
|
466
|
+
fail("expecing mkdir_p never to write to stdout") unless out == ""
|
467
|
+
return unless args.last.kind_of?(Hash) && args.last[:verbose]
|
468
|
+
if @pretty
|
469
|
+
err2 = err.empty? ? "mkdir -p #{ret}" : err # sometimes this. weird
|
470
|
+
err3 = err2.sub(/\A(mkdir -p)/){ colorize($1, :bright, :green)}
|
471
|
+
ui.puts sprintf("#{prefix}#{err3}")
|
472
|
+
else
|
473
|
+
ui.puts(err) unless err == "" # emulate it
|
474
|
+
end
|
475
|
+
end
|
476
|
+
def pretty_puts_rm out, err, ret, *a
|
477
|
+
if out=='' && err==''
|
478
|
+
err = "rm #{ret.join(' ')}"
|
479
|
+
color = :green
|
480
|
+
# @todo what's happening here? (change color to red)
|
481
|
+
else
|
482
|
+
color = :green
|
483
|
+
end
|
484
|
+
err = err.sub(/\Arm\b/){colorize('rm', :bright, color)}
|
485
|
+
ui.puts sprintf("#{prefix}#{err}")
|
486
|
+
end
|
487
|
+
end
|
488
|
+
module PathString
|
489
|
+
def no_leading_dot_slash str
|
490
|
+
str.sub(/\A\.\//,'')
|
491
|
+
end
|
492
|
+
end
|
493
|
+
module Shellopts
|
494
|
+
def shellopts hash
|
495
|
+
hash.map.flatten.reject{|x| x.empty? || x.nil? }
|
496
|
+
end
|
497
|
+
end
|
498
|
+
module Sopen2
|
499
|
+
@ui = nil
|
500
|
+
module_function
|
501
|
+
def sopen2 *a
|
502
|
+
Open3.popen3(*a) do |ins,out,err|
|
503
|
+
return [out.read, err.read]
|
504
|
+
end
|
505
|
+
end
|
506
|
+
def sopen2assert *a
|
507
|
+
out, err = sopen2(*a)
|
508
|
+
if err.length > 0
|
509
|
+
stderr.puts err
|
510
|
+
fail(err.split("\n").first+"...")
|
511
|
+
else
|
512
|
+
out
|
513
|
+
end
|
514
|
+
end
|
515
|
+
def stderr
|
516
|
+
@stderr ||= ((@ui && @ui.respond_to?(:err)) ? @ui.err : $stderr)
|
517
|
+
end
|
518
|
+
end
|
519
|
+
module Stylize
|
520
|
+
# includer must implement color? and prefix() and ui()
|
521
|
+
|
522
|
+
include Colorize
|
523
|
+
|
524
|
+
ReasonStyles = { :identical => :skip, :"won't overwrite" => :skip,
|
525
|
+
:changed => :did, :wrote => :did, :exists=>:skip,
|
526
|
+
:patched => :changed2, :notice => :changed2,
|
527
|
+
:exec => :changed2}
|
528
|
+
StyleCodes = { :skip => [:bold, :red], :did => [:bold, :green] ,
|
529
|
+
:changed2 => [:bold, :yellow] }
|
530
|
+
|
531
|
+
#
|
532
|
+
# @return nil if not found. feel free to override.
|
533
|
+
#
|
534
|
+
def get_style foo
|
535
|
+
StyleCodes[ReasonStyles[foo]]
|
536
|
+
end
|
537
|
+
|
538
|
+
#
|
539
|
+
# write to ui.puts() whatever you want with the notice style. if color?,
|
540
|
+
# colorize head and separate it with a space and print tail without color.
|
541
|
+
# if not color?, the above but in "b&w"
|
542
|
+
#
|
543
|
+
def notice head, tail
|
544
|
+
if color? # && /\A(\s*\S+)(.*)\Z/ =~ msg
|
545
|
+
msg = colorize(head, * get_style(:notice))+' '+tail
|
546
|
+
else
|
547
|
+
msg = "#{head} #{tail}"
|
548
|
+
end
|
549
|
+
ui.puts "#{prefix}#{msg}"
|
550
|
+
end
|
551
|
+
end
|
552
|
+
class TaskSet
|
553
|
+
def initialize
|
554
|
+
@tasks = {}
|
555
|
+
end
|
556
|
+
def task name, &block
|
557
|
+
fail("can't reopen task: #{name.inspect}") if @tasks.key?(name)
|
558
|
+
@tasks[name] = Task.new(name,&block)
|
559
|
+
end
|
560
|
+
def [](name)
|
561
|
+
@tasks[name]
|
562
|
+
end
|
563
|
+
end
|
564
|
+
class Task
|
565
|
+
def initialize name=nil, &block
|
566
|
+
@block = block
|
567
|
+
@file_utils = nil
|
568
|
+
@from = nil
|
569
|
+
@name = name
|
570
|
+
@ui_stack = []
|
571
|
+
end
|
572
|
+
attr_reader :erb_values
|
573
|
+
def erb_values hash
|
574
|
+
@erb_values = hash
|
575
|
+
self
|
576
|
+
end
|
577
|
+
# @api private
|
578
|
+
def erb_values_binding
|
579
|
+
@erb_values_binding ||= begin
|
580
|
+
obj = Object.new
|
581
|
+
bnd = obj.send(:binding)
|
582
|
+
sing = class << obj; self end
|
583
|
+
sing2 = class << bnd; self end
|
584
|
+
hash = @erb_values or fail("need @erb_values")
|
585
|
+
ks = hash.keys
|
586
|
+
sing2.send(:define_method, :keys){ ks }
|
587
|
+
sing2.send(:define_method, :inspect){'#<TreebisErbBinding>'}
|
588
|
+
ks.each{ |k| sing.send(:define_method, k){ hash[k] } }
|
589
|
+
bnd
|
590
|
+
end
|
591
|
+
end
|
592
|
+
def file_utils
|
593
|
+
@file_utils ||= Config.new_default_file_utils_proxy
|
594
|
+
end
|
595
|
+
def file_utils= mixed
|
596
|
+
@file_utils = mixed
|
597
|
+
end
|
598
|
+
def from path
|
599
|
+
@from = path
|
600
|
+
end
|
601
|
+
def on path
|
602
|
+
RunContext.new(self, @block, @from, path, file_utils)
|
603
|
+
end
|
604
|
+
def ui_capture &block
|
605
|
+
file_utils.ui_push
|
606
|
+
instance_eval(&block)
|
607
|
+
file_utils.ui_pop
|
608
|
+
end
|
609
|
+
class RunContext
|
610
|
+
include Colorize, PathString, Shellopts, Stylize
|
611
|
+
def initialize task, block, from_path, on_path, file_utils
|
612
|
+
@task, @block, @from_path, @on_path = task, block, from_path, on_path
|
613
|
+
@file_utils = file_utils
|
614
|
+
@noop = false # no setters yet
|
615
|
+
@opts = {}
|
616
|
+
@overwrite = false
|
617
|
+
@unindent = true # no setters yet
|
618
|
+
end
|
619
|
+
attr_accessor :file_utils, :opts
|
620
|
+
#
|
621
|
+
# if this is operating on a whole folder (if the patch filename is
|
622
|
+
# called 'diff' as opposed to 'somefile.diff'), depending on how you
|
623
|
+
# build the patch you may want the --posix flag
|
624
|
+
#
|
625
|
+
def apply diff_file, opts={}
|
626
|
+
shell_opts = shellopts(opts)
|
627
|
+
patchfile, _ = normalize_from diff_file
|
628
|
+
/\A(?:()|(.+)\.)diff\Z/ =~ diff_file or fail("patch needs .diff ext.")
|
629
|
+
basename = $1 || $2
|
630
|
+
if ! File.exist?(patchfile) && @opts[:patch_hack]
|
631
|
+
return patch_hack(diff_file)
|
632
|
+
end
|
633
|
+
target, _ = normalize_on(basename)
|
634
|
+
cmd = nil
|
635
|
+
if target == './'
|
636
|
+
shell_opts.unshift('-p0') unless shell_opts.grep(/\A-p\d+\Z/).any?
|
637
|
+
cmd = ['patch '+Shellwords.shelljoin(shell_opts)+' < '+
|
638
|
+
Shellwords.shellescape(patchfile)]
|
639
|
+
else
|
640
|
+
target_arr = (target == './') ? [] : [target]
|
641
|
+
cmd = ['patch', '-u', *shell_opts] + target_arr + [patchfile]
|
642
|
+
end
|
643
|
+
notice "patching:", cmd.join(' ')
|
644
|
+
out, err = Sopen2::sopen2(*cmd)
|
645
|
+
if err.length > 0
|
646
|
+
cmd_str = cmd.shelljoin
|
647
|
+
msg = sprintf(
|
648
|
+
"Failed to run patch command: %s\nstdout was: %sstderr was: %s",
|
649
|
+
cmd_str, out, err)
|
650
|
+
fail msg
|
651
|
+
else
|
652
|
+
pretty_puts_apply out, cmd
|
653
|
+
end
|
654
|
+
end
|
655
|
+
def erb path
|
656
|
+
require 'erb'
|
657
|
+
in_full, in_local = normalize_from path
|
658
|
+
out_full, out_local = normalize_on path
|
659
|
+
tmpl = ERB.new(File.read(in_full))
|
660
|
+
tmpl.filename = in_full # just used when errors
|
661
|
+
bnd = @task.erb_values_binding
|
662
|
+
out = tmpl.result(bnd)
|
663
|
+
write out_local, out
|
664
|
+
end
|
665
|
+
def copy path
|
666
|
+
if path.index('*')
|
667
|
+
copy_glob(path)
|
668
|
+
else
|
669
|
+
full, local = normalize_from path
|
670
|
+
copy_go full, local, path
|
671
|
+
end
|
672
|
+
end
|
673
|
+
def copy_glob path
|
674
|
+
path.index('*') or fail("not glob: #{path.inspect}")
|
675
|
+
full, local = normalize_from path
|
676
|
+
these = Dir[full]
|
677
|
+
if these.empty?
|
678
|
+
notice('copy', "no files matched #{path.inspect}")
|
679
|
+
else
|
680
|
+
opts = these.map do |this|
|
681
|
+
[this, * copy_unglob(this, local, path)]
|
682
|
+
end
|
683
|
+
opts.each {|a| copy_go(*a) }
|
684
|
+
end
|
685
|
+
end
|
686
|
+
def copy_go full, local, path
|
687
|
+
tgt = File.join(@on_path, local)
|
688
|
+
skip = false
|
689
|
+
if File.exist?(tgt) && File.read(full) == File.read(tgt)
|
690
|
+
report_action :identical, path
|
691
|
+
else
|
692
|
+
file_utils.cp(full, tgt, :verbose=>true, :noop=>@noop,
|
693
|
+
:pretty_name_target => no_leading_dot_slash(path)
|
694
|
+
)
|
695
|
+
end
|
696
|
+
end
|
697
|
+
private :copy_go
|
698
|
+
# annoying, ridiculous, fragile: make pretty reporting of glob ops
|
699
|
+
def copy_unglob full, locl, path
|
700
|
+
re = self.class.glob_to_regex(locl)
|
701
|
+
(b = full =~ re && $2 ) or fail("no: #{full.inspect}")
|
702
|
+
(d,f = locl =~re && [$1, $3]) or fail("no: #{locl.inspect}")
|
703
|
+
(h,j = path =~re && [$1, $3]) or fail("no: #{path.inspect}")
|
704
|
+
ret = ["#{d}#{b}#{f}", "#{h}#{b}#{j}"]
|
705
|
+
ret
|
706
|
+
end
|
707
|
+
private :copy_unglob
|
708
|
+
def move from, to
|
709
|
+
fr_full, fr_local = normalize_on from
|
710
|
+
to_full, fr_lcoal = normalize_on to
|
711
|
+
file_utils.mv(fr_full, to_full, :verbose=>true, :noop=>@noop)
|
712
|
+
end
|
713
|
+
def from path
|
714
|
+
@from_path = path
|
715
|
+
end
|
716
|
+
def mkdir_p_unless_exists dir_basename=nil
|
717
|
+
if dir_basename
|
718
|
+
full, short = normalize_on(dir_basename)
|
719
|
+
else
|
720
|
+
full, short = @on_path, @on_path
|
721
|
+
end
|
722
|
+
if File.exist?(full)
|
723
|
+
report_action :exists, short
|
724
|
+
else
|
725
|
+
file_utils.mkdir_p(full, :verbose => true, :noop=>@noop)
|
726
|
+
end
|
727
|
+
end
|
728
|
+
alias_method :mkdir_p, :mkdir_p_unless_exists # temporary!!
|
729
|
+
def remove path
|
730
|
+
full, _ = normalize_on(path)
|
731
|
+
file_utils.rm(full, :verbose => true, :noop => @noop)
|
732
|
+
end
|
733
|
+
def rm_rf_if_exist
|
734
|
+
path = @on_path
|
735
|
+
if File.exist?(path) && rm_rf_sanity_check(path)
|
736
|
+
file_utils.remove_entry_secure(@on_path)
|
737
|
+
end
|
738
|
+
end
|
739
|
+
alias_method :rm_rf, :rm_rf_if_exist # for now
|
740
|
+
def run opts={}
|
741
|
+
@opts = @opts.merge(opts.reject{|k,v| v.nil?}) # not sure about this
|
742
|
+
self.instance_eval(&@block)
|
743
|
+
end
|
744
|
+
def prefix
|
745
|
+
@file_utils.prefix
|
746
|
+
end
|
747
|
+
def ui
|
748
|
+
@file_utils.ui
|
749
|
+
end
|
750
|
+
def write path, content
|
751
|
+
out_path, short = normalize_on path
|
752
|
+
content = Treebis.unindent(content) if @unindent
|
753
|
+
action = if File.exist? out_path
|
754
|
+
File.read(out_path) == content ? :identical :
|
755
|
+
( @overwrite ? :changed : :"won't overwrite" )
|
756
|
+
else
|
757
|
+
:wrote
|
758
|
+
end
|
759
|
+
xtra = nil
|
760
|
+
if [:changed, :wrote].include?(action)
|
761
|
+
b = fh = nil
|
762
|
+
File.open(out_path,'w'){|fh| b = fh.write(content)}
|
763
|
+
xtra = "(#{b} bytes)"
|
764
|
+
end
|
765
|
+
report_action action, short, xtra
|
766
|
+
end
|
767
|
+
private
|
768
|
+
def color?; Config.color? end
|
769
|
+
def fail(*a); raise ::Treebis::Fail.new(*a) end
|
770
|
+
def normalize_from path
|
771
|
+
fail("expecting leading dot: #{path}") unless short = undot(path)
|
772
|
+
full = File.join(@from_path, short)
|
773
|
+
[full, short]
|
774
|
+
end
|
775
|
+
def normalize_on path
|
776
|
+
short = (thing = undot(path)) ? thing : path
|
777
|
+
full = File.join(@on_path, short)
|
778
|
+
[full, short]
|
779
|
+
end
|
780
|
+
def patch_hack diff_file
|
781
|
+
/\A(.+)\.diff\Z/ =~ diff_file
|
782
|
+
use = $1 or fail("diff_file parse fail: #{diff_file}")
|
783
|
+
copy use
|
784
|
+
end
|
785
|
+
def pretty_puts_apply out, cmd
|
786
|
+
report_action :exec, cmd.join(' ')
|
787
|
+
these = out.split("\n")
|
788
|
+
these.each do |this|
|
789
|
+
if /^\Apatching file (.+)\Z/ =~ this
|
790
|
+
report_action :patched, $1
|
791
|
+
else
|
792
|
+
ui.puts("#{prefix}#{this}")
|
793
|
+
end
|
794
|
+
end
|
795
|
+
end
|
796
|
+
# see also notice()
|
797
|
+
def report_action reason, msg=nil, xtra=nil
|
798
|
+
reason_s = stylize(reason.to_s, reason)
|
799
|
+
puts = ["#{prefix}#{reason_s}", msg, xtra].compact.join(' ')
|
800
|
+
ui.puts puts
|
801
|
+
end
|
802
|
+
def rm_rf_sanity_check path
|
803
|
+
# not sure what we want here
|
804
|
+
fail('no') if @on_path.nil? || path != @on_path
|
805
|
+
true
|
806
|
+
end
|
807
|
+
def stylize str, reason
|
808
|
+
return str unless Config.color?
|
809
|
+
colorize(reason.to_s, * get_style(reason))
|
810
|
+
end
|
811
|
+
def undot path
|
812
|
+
$1 if /^(?:\.\/)?(.+)$/ =~ path
|
813
|
+
end
|
814
|
+
class << self
|
815
|
+
# this is not for general use
|
816
|
+
def glob_to_regex glob
|
817
|
+
fail("this is really strict: (only) 1 '*': #{glob.inspect}") unless
|
818
|
+
glob.scan(/\*/).size == 1
|
819
|
+
a, b = glob.split('*')
|
820
|
+
re = Regexp.new(
|
821
|
+
'\\A(.*'+Regexp.escape(a)+')(.*?)('+Regexp.escape(b)+')\\Z'
|
822
|
+
)
|
823
|
+
re
|
824
|
+
end
|
825
|
+
end
|
826
|
+
end
|
827
|
+
end
|
828
|
+
end
|
829
|
+
|
830
|
+
# Experimental extension for tests running tests with a persistent tempdir
|
831
|
+
# now you can set and get general properties, and delegate this behavior.
|
832
|
+
#
|
833
|
+
module Treebis
|
834
|
+
module PersistentDotfile
|
835
|
+
class << self
|
836
|
+
def extend_to(tgt, dotfile_path, opts={})
|
837
|
+
opts = {:file_utils=>FileUtils, :dotfile_path=>dotfile_path}.
|
838
|
+
merge(opts)
|
839
|
+
tgt.extend ClassMethods
|
840
|
+
tgt.persistent_dotfile_init opts
|
841
|
+
end
|
842
|
+
def include_to(mod, *a)
|
843
|
+
extend_to(mod, *a)
|
844
|
+
mod.send(:include, InstanceMethods)
|
845
|
+
end
|
846
|
+
end
|
847
|
+
DelegatedMethods = %w(tmpdir empty_tmpdir persistent_set persistent_get)
|
848
|
+
module ClassMethods
|
849
|
+
attr_accessor :dotfile_path, :file_utils
|
850
|
+
|
851
|
+
def persistent_dotfile_init opts
|
852
|
+
@dotfile_path ||= opts[:dotfile_path]
|
853
|
+
@file_utils ||= opts[:file_utils]
|
854
|
+
@persistent_struct ||= nil
|
855
|
+
@tmpdir ||= nil
|
856
|
+
end
|
857
|
+
|
858
|
+
#
|
859
|
+
# if it exists delete it. create it. file_utils must be defined
|
860
|
+
# @return path to new empty directory
|
861
|
+
#
|
862
|
+
def empty_tmpdir path, futils_opts={}
|
863
|
+
futils_opts = {:verbose=>true}.merge(futils_opts)
|
864
|
+
full_path = File.join(tmpdir, path)
|
865
|
+
if File.exist? full_path
|
866
|
+
file_utils.remove_entry_secure full_path, futils_opts
|
867
|
+
end
|
868
|
+
file_utils.mkdir_p full_path, futils_opts
|
869
|
+
full_path
|
870
|
+
end
|
871
|
+
|
872
|
+
# Get a path to a temporary directory, suitable to be used in tests.
|
873
|
+
# The contents of this directory are undefined, but it is writable
|
874
|
+
# and as the name implies temporary so a given test should feel free
|
875
|
+
# to erase it and create a new empty one at this same path if desired.
|
876
|
+
# (see callee for details.)
|
877
|
+
#
|
878
|
+
def tmpdir
|
879
|
+
if @tmpdir.nil?
|
880
|
+
@tmpdir = get_tmpdir
|
881
|
+
elsif ! File.exist?(@tmpdir) # check every time!
|
882
|
+
@tmpdir = get_tmpdir
|
883
|
+
end
|
884
|
+
@tmpdir
|
885
|
+
end
|
886
|
+
|
887
|
+
def persistent_delegate_to(mod)
|
888
|
+
if instance_variable_defined?('@persistent_delegate') # rcov
|
889
|
+
@persistent_delegate
|
890
|
+
else
|
891
|
+
@persistent_delegate = begin
|
892
|
+
mm = Module.new
|
893
|
+
str = "#{self}::PersistentDotfileDelegate"
|
894
|
+
const_set('PersistentDotfileDelegate', mm)
|
895
|
+
class << mm; self end.send(:define_method,:inspect){str}
|
896
|
+
buck = self
|
897
|
+
DelegatedMethods.each do |meth|
|
898
|
+
mm.send(:define_method, meth){|*a| buck.send(meth,*a) }
|
899
|
+
end
|
900
|
+
mm
|
901
|
+
end
|
902
|
+
end
|
903
|
+
mod.extend(@persistent_delegate)
|
904
|
+
mod.send(:include, @persistent_delegate)
|
905
|
+
end
|
906
|
+
|
907
|
+
def persistent_get path
|
908
|
+
struct = persistent_struct or return nil
|
909
|
+
struct[path]
|
910
|
+
end
|
911
|
+
|
912
|
+
# this might cause bugs if different classes use the same
|
913
|
+
# dotfile name
|
914
|
+
def persistent_struct
|
915
|
+
if @persistent_struct
|
916
|
+
@persistent_struct
|
917
|
+
elsif @persistent_struct == false
|
918
|
+
nil
|
919
|
+
elsif File.exists? dotfile_path
|
920
|
+
@persistent_struct = JSON.parse(File.read(dotfile_path))
|
921
|
+
else
|
922
|
+
@persistent_struct = false
|
923
|
+
end
|
924
|
+
end
|
925
|
+
|
926
|
+
def persistent_set path, value
|
927
|
+
struct = persistent_struct || (@persistent_struct = {})
|
928
|
+
struct[path] = value
|
929
|
+
File.open(dotfile_path, 'w+') do |fh|
|
930
|
+
fh.write JSON.pretty_generate(struct)
|
931
|
+
end
|
932
|
+
nil
|
933
|
+
end
|
934
|
+
|
935
|
+
private
|
936
|
+
|
937
|
+
# Return the same tmpdir used in the previous run, if a "./treebis.json"
|
938
|
+
# file exists in the cwd and it refers to a temp folder that still
|
939
|
+
# exists. Else make a new temp folder and write its location to this
|
940
|
+
# file. Using the same tempfolder on successive runs is one way to
|
941
|
+
# allow us to look at the files it generates between runs.
|
942
|
+
#
|
943
|
+
def get_tmpdir
|
944
|
+
tmpdir = nil
|
945
|
+
if tmpdir_path = persistent_get('tmpdir')
|
946
|
+
if File.exist?(tmpdir_path)
|
947
|
+
tmpdir = tmpdir_path
|
948
|
+
end
|
949
|
+
end
|
950
|
+
unless tmpdir
|
951
|
+
tmpdir = Dir::tmpdir + '/treebis'
|
952
|
+
file_utils.mkdir_p(tmpdir,:verbose=>true)
|
953
|
+
persistent_set 'tmpdir', tmpdir
|
954
|
+
end
|
955
|
+
tmpdir
|
956
|
+
end
|
957
|
+
end
|
958
|
+
|
959
|
+
module InstanceMethods
|
960
|
+
DelegatedMethods.each do |this|
|
961
|
+
define_method(this){ |*a| self.class.send(this, *a) }
|
962
|
+
end
|
963
|
+
end
|
964
|
+
end
|
965
|
+
end
|
966
|
+
|
967
|
+
|
968
|
+
|
969
|
+
if [__FILE__, '/usr/bin/rcov'].include?($PROGRAM_NAME) # ick
|
970
|
+
# fakefs stuff removed in ac877, see notes there for why
|
971
|
+
require 'test/unit'
|
972
|
+
require 'test/unit/ui/console/testrunner'
|
973
|
+
|
974
|
+
module ::Treebis::Test
|
975
|
+
|
976
|
+
module TestAntecedents
|
977
|
+
def setup_antecedents
|
978
|
+
src = empty_tmpdir('sourceis')
|
979
|
+
task.new do
|
980
|
+
mkdir_p "foo/bar/baz"
|
981
|
+
write "foo/bar/baz/alpha.txt", 'x'
|
982
|
+
write "foo/bar/beta.txt", 'x'
|
983
|
+
write "foo/gamma.txt", 'x'
|
984
|
+
end.on(src).run
|
985
|
+
src
|
986
|
+
end
|
987
|
+
def test_antecedents
|
988
|
+
src = setup_antecedents
|
989
|
+
tt = task.new do
|
990
|
+
from src
|
991
|
+
mkdir_p "foo/bar/baz"
|
992
|
+
copy "foo/bar/baz/alpha.txt"
|
993
|
+
copy "foo/bar/beta.txt"
|
994
|
+
copy "foo/gamma.txt"
|
995
|
+
end
|
996
|
+
tgt = empty_tmpdir('targetis')
|
997
|
+
bb, cc, aa = capture3{ tt.on(tgt).run }
|
998
|
+
assert_equal [nil, ''], [aa,bb]
|
999
|
+
penu, last = cc.split("\n")[-2..-1]
|
1000
|
+
assert penu.index("...bar/beta.txt foo/bar/beta.txt")
|
1001
|
+
assert last.index("...eis/foo/gamma.txt foo/gamma.txt")
|
1002
|
+
end
|
1003
|
+
|
1004
|
+
def test_antecedents_raw
|
1005
|
+
fu = file_utils
|
1006
|
+
src = setup_antecedents
|
1007
|
+
tgt = empty_tmpdir('targetis')
|
1008
|
+
these = %w( foo/bar/baz/alpha.txt
|
1009
|
+
foo/bar/beta.txt
|
1010
|
+
foo/gamma.txt )
|
1011
|
+
fu.pretty!
|
1012
|
+
out, err = capture3 do
|
1013
|
+
fu.mkdir_p File.join(tgt,'foo/bar/baz')
|
1014
|
+
these.each do |foo|
|
1015
|
+
from, to = File.join(src,foo), File.join(tgt,foo)
|
1016
|
+
fu.cp from, to
|
1017
|
+
end
|
1018
|
+
end
|
1019
|
+
assert_equal '', out
|
1020
|
+
penu, last = err.split("\n")[-2..-1]
|
1021
|
+
assert penu.index(' ...bar/beta.txt ...bar/beta.txt')
|
1022
|
+
assert last.index('...eis/foo/gamma.txt ...tis/foo/gamma.txt')
|
1023
|
+
end
|
1024
|
+
end
|
1025
|
+
|
1026
|
+
module TestColorAndRmAndMoveAndPatch
|
1027
|
+
def test_rm_rf
|
1028
|
+
t = task.new do
|
1029
|
+
rm_rf
|
1030
|
+
mkdir_p
|
1031
|
+
write('foo.txt', "hi i'm foo")
|
1032
|
+
end
|
1033
|
+
out = tmpdir+'/blearg'
|
1034
|
+
t.on(out).run
|
1035
|
+
t.on(out).run
|
1036
|
+
tree = dir_as_hash(out)
|
1037
|
+
assert_equal({"foo.txt"=>"hi i'm foo"}, tree)
|
1038
|
+
end
|
1039
|
+
def test_no_color
|
1040
|
+
t = task.new do
|
1041
|
+
report_action :fake, "blah"
|
1042
|
+
end
|
1043
|
+
Treebis::Config.no_color!
|
1044
|
+
out = t.ui_capture{ on('x').run }
|
1045
|
+
|
1046
|
+
Treebis::Config.color!
|
1047
|
+
assert_equal(" fake blah\n", out)
|
1048
|
+
end
|
1049
|
+
def test_no_overwrite
|
1050
|
+
dir = tmpdir + '/no-overwrite/'
|
1051
|
+
Fu.remove_entry_secure(dir) if File.exist?(dir)
|
1052
|
+
t = task.new do
|
1053
|
+
mkdir_p_unless_exists
|
1054
|
+
write 'foo.txt','blah content'
|
1055
|
+
end
|
1056
|
+
t2 = task.new do
|
1057
|
+
write 'foo.txt','blah content again'
|
1058
|
+
end
|
1059
|
+
str1 = t.ui_capture{ on(dir).run }
|
1060
|
+
str2 = t2.ui_capture{ on(dir).run }
|
1061
|
+
assert_match( /wrote/, str1 )
|
1062
|
+
assert_match( /won/, str2 )
|
1063
|
+
end
|
1064
|
+
def setup_sourcedir
|
1065
|
+
src_dir = tmpdir+'/banana'
|
1066
|
+
# return if File.exist?(src_dir)
|
1067
|
+
task = Treebis::Task.new do
|
1068
|
+
rm_rf
|
1069
|
+
mkdir_p
|
1070
|
+
mkdir_p 'dir1'
|
1071
|
+
write 'dir1/stooges.txt.diff', <<-FILE
|
1072
|
+
--- a/stooges.txt 2010-04-25 03:23:18.000000000 -0400
|
1073
|
+
+++ b/stooges.txt 2010-04-25 03:23:12.000000000 -0400
|
1074
|
+
@@ -1,2 +1,3 @@
|
1075
|
+
larry
|
1076
|
+
+moe
|
1077
|
+
curly
|
1078
|
+
FILE
|
1079
|
+
|
1080
|
+
write 'stooges.orig.txt', <<-FILE
|
1081
|
+
larry
|
1082
|
+
curly
|
1083
|
+
FILE
|
1084
|
+
|
1085
|
+
write 'treebis-task.rb', <<-FILE
|
1086
|
+
Treebis.tasks.task(:banana) do
|
1087
|
+
copy './stooges.orig.txt'
|
1088
|
+
mkdir_p_unless_exists './dir1'
|
1089
|
+
move './stooges.orig.txt', './dir1/stooges.txt'
|
1090
|
+
apply './dir1/stooges.txt.diff'
|
1091
|
+
end
|
1092
|
+
FILE
|
1093
|
+
end
|
1094
|
+
task.on(src_dir).run
|
1095
|
+
end
|
1096
|
+
def test_move
|
1097
|
+
setup_sourcedir
|
1098
|
+
task = Treebis.dir_task(tmpdir+'/banana')
|
1099
|
+
out_dir = tmpdir+'/test-move-output'
|
1100
|
+
file_utils.remove_entry_secure(out_dir) if File.exist?(out_dir)
|
1101
|
+
file_utils.mkdir_p(out_dir)
|
1102
|
+
task.on(out_dir).run
|
1103
|
+
tree = dir_as_hash(out_dir)
|
1104
|
+
assert_equal({"dir1"=>{"stooges.txt"=>"larry\nmoe\ncurly\n"}}, tree)
|
1105
|
+
end
|
1106
|
+
end
|
1107
|
+
|
1108
|
+
module TestColorize
|
1109
|
+
def test_colorize
|
1110
|
+
obj = Object.new
|
1111
|
+
obj.extend Treebis::Colorize
|
1112
|
+
str = obj.send(:colorize, "foo",:background, :blue)
|
1113
|
+
assert_equal "\e[44mfoo\e[0m", str
|
1114
|
+
end
|
1115
|
+
end
|
1116
|
+
|
1117
|
+
module TestDirAsHash
|
1118
|
+
def test_baby_jug_fail
|
1119
|
+
hh = {'foo'=>nil}
|
1120
|
+
e = assert_raise(RuntimeError) do
|
1121
|
+
hash_to_dir(hh, empty_tmpdir('babyjug')+'/bar', file_utils)
|
1122
|
+
end
|
1123
|
+
assert_match(/bad type for dir hash/, e.message)
|
1124
|
+
end
|
1125
|
+
#
|
1126
|
+
# http://www.youtube.com/watch?v=Ib0Tll3sGB0
|
1127
|
+
#
|
1128
|
+
def test_dir_as_hash
|
1129
|
+
h = {
|
1130
|
+
'not'=>'funny',
|
1131
|
+
'baby-jug'=>{
|
1132
|
+
'what-does'=>'the baby have',
|
1133
|
+
'blood'=>'where?'},
|
1134
|
+
'not-funny'=>{
|
1135
|
+
'youre' => {
|
1136
|
+
'right' => 'its not funny',
|
1137
|
+
'not'=>{
|
1138
|
+
'funny'=>'ha ha',
|
1139
|
+
'not funny'=>'BLOO-DUH'
|
1140
|
+
}
|
1141
|
+
}
|
1142
|
+
}
|
1143
|
+
}
|
1144
|
+
td = empty_tmpdir('blah')+'/not-there'
|
1145
|
+
hash_to_dir(h, td, file_utils)
|
1146
|
+
skips = [ 'baby-jug/blood',
|
1147
|
+
'not-funny/youre/not/funny']
|
1148
|
+
wow = dir_as_hash(td, :skip=>skips)
|
1149
|
+
no_way = {
|
1150
|
+
"baby-jug"=>{"what-does"=>"the baby have"},
|
1151
|
+
"not"=>"funny",
|
1152
|
+
"not-funny"=>{
|
1153
|
+
"youre"=>{
|
1154
|
+
"right"=>"its not funny",
|
1155
|
+
"not"=>{"not funny"=>"BLOO-DUH"}
|
1156
|
+
}
|
1157
|
+
}
|
1158
|
+
}
|
1159
|
+
assert_equal no_way, wow
|
1160
|
+
end
|
1161
|
+
def test_dir_as_hash_with_globs
|
1162
|
+
h = {
|
1163
|
+
'file1.txt' => 'foo',
|
1164
|
+
'file2.rb' => 'baz',
|
1165
|
+
'dir1' => {
|
1166
|
+
'file3.txt' => 'bar',
|
1167
|
+
'file4.rb' => 'bling'
|
1168
|
+
}
|
1169
|
+
}
|
1170
|
+
td = empty_tmpdir('blah')+'/not-there'
|
1171
|
+
hash_to_dir(h, td, file_utils)
|
1172
|
+
skips = [ 'dir1/*.rb' ]
|
1173
|
+
wow = dir_as_hash(td, :skip=>skips)
|
1174
|
+
no_way = {
|
1175
|
+
"file1.txt"=>"foo", "file2.rb"=>"baz",
|
1176
|
+
"dir1"=>{"file3.txt"=>"bar"},
|
1177
|
+
}
|
1178
|
+
assert_equal no_way, wow
|
1179
|
+
end
|
1180
|
+
end
|
1181
|
+
|
1182
|
+
module TestFileUtilsProxy
|
1183
|
+
def test_cp_not_pretty
|
1184
|
+
src_dir = empty_tmpdir('foo')
|
1185
|
+
tgt_dir = empty_tmpdir('bar')
|
1186
|
+
|
1187
|
+
task.new{
|
1188
|
+
mkdir_p
|
1189
|
+
write 'some-file.txt', <<-X
|
1190
|
+
i am some text
|
1191
|
+
X
|
1192
|
+
}.on(src_dir).run
|
1193
|
+
|
1194
|
+
task.new {
|
1195
|
+
from(src_dir)
|
1196
|
+
report_action :notice, "the below is not supposed to be pretty"
|
1197
|
+
file_utils.not_pretty!
|
1198
|
+
copy 'some-file.txt'
|
1199
|
+
mkdir_p 'emptoz'
|
1200
|
+
file_utils.pretty!
|
1201
|
+
report_action :notice, "the above was not pretty"
|
1202
|
+
}.on(tgt_dir).run
|
1203
|
+
|
1204
|
+
act = dir_as_hash tgt_dir
|
1205
|
+
exp = {
|
1206
|
+
"some-file.txt"=>"i am some text\n",
|
1207
|
+
"emptoz" => {}
|
1208
|
+
}
|
1209
|
+
assert_equal(exp, act)
|
1210
|
+
end
|
1211
|
+
def test_patch_fileutils
|
1212
|
+
tmpdir = empty_tmpdir('oilspill')
|
1213
|
+
out, err = capture3 do
|
1214
|
+
file_utils.remove_entry_secure tmpdir+'/not-there'
|
1215
|
+
file_utils.mkdir_p tmpdir, :verbose => true
|
1216
|
+
end
|
1217
|
+
assert_match(/didn't exist.+not-there/, err)
|
1218
|
+
assert_match(/mkdir_p.+exists.+oilspill/, err)
|
1219
|
+
end
|
1220
|
+
def test_pretty_puts_cp_unexpected_output
|
1221
|
+
e = assert_raises(RuntimeError) do
|
1222
|
+
file_utils.send(:pretty_puts_cp, false, '', '')
|
1223
|
+
end
|
1224
|
+
assert_match(/unexpected out/, e.message)
|
1225
|
+
end
|
1226
|
+
def test_custom_fileutils_implementation
|
1227
|
+
task = self.task.new do
|
1228
|
+
from 'not there'
|
1229
|
+
copy 'doesnt even exist'
|
1230
|
+
end
|
1231
|
+
fu = Object.new
|
1232
|
+
class << fu
|
1233
|
+
def prefix; '_____' end
|
1234
|
+
def ui; $stdout end
|
1235
|
+
end
|
1236
|
+
hi_there = false
|
1237
|
+
class << fu; self end.send(:define_method, :cp) do |*blah|
|
1238
|
+
hi_there = blah
|
1239
|
+
end
|
1240
|
+
task.file_utils = fu
|
1241
|
+
task.on('doesnt exist here').run
|
1242
|
+
exp =
|
1243
|
+
["not there/doesnt even exist", "doesnt exist here/doesnt even exist"]
|
1244
|
+
act = hi_there[0..1]
|
1245
|
+
assert_equal exp, act
|
1246
|
+
end
|
1247
|
+
end
|
1248
|
+
|
1249
|
+
module TestGlobMatcher
|
1250
|
+
def test_glob_matcher
|
1251
|
+
obj = Object.new
|
1252
|
+
obj.extend Treebis::DirAsHash::Blacklist::MatcherFactory
|
1253
|
+
err = assert_raises(RuntimeError) do
|
1254
|
+
obj.create_matcher(false)
|
1255
|
+
end
|
1256
|
+
assert_equal "can't build matcher from false", err.message
|
1257
|
+
end
|
1258
|
+
end
|
1259
|
+
|
1260
|
+
module TestPatch
|
1261
|
+
def test_patch_fails
|
1262
|
+
src = empty_tmpdir 'patch/evil'
|
1263
|
+
task.new do
|
1264
|
+
write "badly-formed-patch-file.diff", <<-P
|
1265
|
+
i am not a good patch
|
1266
|
+
P
|
1267
|
+
end.on(src).run
|
1268
|
+
|
1269
|
+
tgt = empty_tmpdir 'patch/innocent'
|
1270
|
+
patch_task = task.new do
|
1271
|
+
from src
|
1272
|
+
apply 'badly-formed-patch-file.diff'
|
1273
|
+
end
|
1274
|
+
e = assert_raises(Treebis::Fail) do
|
1275
|
+
patch_task.on(tgt).run
|
1276
|
+
end
|
1277
|
+
assert_match(/Failed to run patch command/, e.message)
|
1278
|
+
end
|
1279
|
+
def test_patch_hack
|
1280
|
+
task.new {
|
1281
|
+
write "some-file.txt", <<-X
|
1282
|
+
i am the new content
|
1283
|
+
X
|
1284
|
+
}.on(src = empty_tmpdir('src')).run
|
1285
|
+
task.new {
|
1286
|
+
write "some-file.txt", <<-X
|
1287
|
+
never see me
|
1288
|
+
X
|
1289
|
+
}.on(tgt = empty_tmpdir('tgt')).run
|
1290
|
+
task.new {
|
1291
|
+
from src
|
1292
|
+
opts[:patch_hack] = true
|
1293
|
+
apply "some-file.txt.diff"
|
1294
|
+
}.on(tgt).run
|
1295
|
+
assert_equal(
|
1296
|
+
{"some-file.txt"=>"i am the new content\n"},
|
1297
|
+
dir_as_hash(tgt)
|
1298
|
+
)
|
1299
|
+
end
|
1300
|
+
def test_unexpected_string_from_patch
|
1301
|
+
out, err, res = capture3 do
|
1302
|
+
task.new do
|
1303
|
+
pretty_puts_apply("i am another string", [])
|
1304
|
+
nil
|
1305
|
+
end.on(empty_tmpdir("blah")).run
|
1306
|
+
end
|
1307
|
+
assert_equal [nil, ""], [res, out]
|
1308
|
+
assert err.index("i am another string")
|
1309
|
+
end
|
1310
|
+
end
|
1311
|
+
|
1312
|
+
module TestPersistentDotfile
|
1313
|
+
def test_persistent_dotfile_empty_tempdir
|
1314
|
+
blah1 = empty_tmpdir('blah/blah')
|
1315
|
+
blah2 = empty_tmpdir('blah/blah')
|
1316
|
+
assert_equal(blah1, blah2)
|
1317
|
+
assert_equal({}, dir_as_hash(blah2))
|
1318
|
+
end
|
1319
|
+
def test_delegate_to
|
1320
|
+
parent_mod = Module.new
|
1321
|
+
Treebis::PersistentDotfile.extend_to(
|
1322
|
+
parent_mod, TestCase::DotfilePath
|
1323
|
+
)
|
1324
|
+
child_mod = Module.new
|
1325
|
+
child_mod2 = Module.new
|
1326
|
+
parent_mod.persistent_delegate_to(child_mod)
|
1327
|
+
parent_mod.persistent_delegate_to(child_mod2)
|
1328
|
+
foo = parent_mod.empty_tmpdir('bliz')
|
1329
|
+
bar = child_mod.empty_tmpdir('bliz')
|
1330
|
+
baz = child_mod2.empty_tmpdir('bliz')
|
1331
|
+
assert_equal(foo, bar)
|
1332
|
+
assert_equal(foo, baz)
|
1333
|
+
end
|
1334
|
+
end
|
1335
|
+
|
1336
|
+
module TestRemove
|
1337
|
+
def test_remove_pretty
|
1338
|
+
task.new {
|
1339
|
+
write "foo.txt", "bar baz"
|
1340
|
+
}.on(tgt = empty_tmpdir("foo")).run
|
1341
|
+
|
1342
|
+
assert_equal({"foo.txt"=>"bar baz"}, dir_as_hash(tgt))
|
1343
|
+
|
1344
|
+
task.new {
|
1345
|
+
remove "foo.txt"
|
1346
|
+
}.on(tgt).run
|
1347
|
+
|
1348
|
+
assert_equal({}, dir_as_hash(tgt))
|
1349
|
+
end
|
1350
|
+
|
1351
|
+
def test_remove_not_pretty
|
1352
|
+
task.new {
|
1353
|
+
write "foo.txt", "bar baz"
|
1354
|
+
}.on(tgt = empty_tmpdir("foo")).run
|
1355
|
+
|
1356
|
+
assert_equal({"foo.txt"=>"bar baz"}, dir_as_hash(tgt))
|
1357
|
+
|
1358
|
+
bb, cc, aa = capture3{
|
1359
|
+
tt = task.new do
|
1360
|
+
file_utils.not_pretty!
|
1361
|
+
remove "foo.txt"
|
1362
|
+
end
|
1363
|
+
tt.on(tgt).run
|
1364
|
+
}
|
1365
|
+
assert_match(/\Arm /, cc)
|
1366
|
+
assert aa.kind_of?(Array)
|
1367
|
+
assert_equal 1, aa.size
|
1368
|
+
assert_equal "", bb
|
1369
|
+
assert_equal({}, dir_as_hash(tgt))
|
1370
|
+
end
|
1371
|
+
end
|
1372
|
+
|
1373
|
+
|
1374
|
+
module TestTaskMisc
|
1375
|
+
def setup_notice_task
|
1376
|
+
@task = task.new{notice("hello","HI"); 'foo'}
|
1377
|
+
end
|
1378
|
+
def assert_notice_outcome
|
1379
|
+
@out, @err, @res = capture3{ @task.on(nil).run }
|
1380
|
+
assert_equal ['', 'foo'], [@out, @res]
|
1381
|
+
assert_match(/hello.*HI/, @err)
|
1382
|
+
end
|
1383
|
+
def test_notice_color
|
1384
|
+
setup_notice_task
|
1385
|
+
assert_notice_outcome
|
1386
|
+
assert @err.index("\e[")
|
1387
|
+
end
|
1388
|
+
def test_notice_no_color
|
1389
|
+
cfg = Treebis::Config
|
1390
|
+
prev = cfg.color?
|
1391
|
+
cfg.no_color!
|
1392
|
+
begin
|
1393
|
+
setup_notice_task
|
1394
|
+
assert_notice_outcome
|
1395
|
+
assert @err.index('hello HI')
|
1396
|
+
ensure
|
1397
|
+
prev ? cfg.color! : cfg.no_color!
|
1398
|
+
end
|
1399
|
+
end
|
1400
|
+
end
|
1401
|
+
|
1402
|
+
module TestSopen
|
1403
|
+
def test_sopen2_fails
|
1404
|
+
e = assert_raises(RuntimeError) do
|
1405
|
+
Treebis::Sopen2.sopen2assert('patch', '-u', 'foo', 'bar')
|
1406
|
+
end
|
1407
|
+
assert_match(/Can't open patch file bar/, e.message)
|
1408
|
+
end
|
1409
|
+
def test_sopen2_succeeds
|
1410
|
+
out = Treebis::Sopen2.sopen2assert('echo', 'baz')
|
1411
|
+
assert_equal "baz\n", out
|
1412
|
+
end
|
1413
|
+
end
|
1414
|
+
|
1415
|
+
module TestTempdirAndTasksAndCopy
|
1416
|
+
def test_cant_reopen_tasks
|
1417
|
+
tasks = Treebis::TaskSet.new
|
1418
|
+
tasks.task(:same){}
|
1419
|
+
e = assert_raises(RuntimeError) do
|
1420
|
+
tasks.task(:same){}
|
1421
|
+
end
|
1422
|
+
exp = "can't reopen task: :same"
|
1423
|
+
assert_equal(exp, e.message)
|
1424
|
+
end
|
1425
|
+
def test_can_open_same_name_in_different_task_sets
|
1426
|
+
Treebis::TaskSet.new.task(:same){}
|
1427
|
+
Treebis::TaskSet.new.task(:same){}
|
1428
|
+
end
|
1429
|
+
def test_copy_one_file_nothing_exist
|
1430
|
+
out_dir = tmpdir+'/out-dir'
|
1431
|
+
src_file = tmpdir+'/baz.txt'
|
1432
|
+
per_file = self.class.dotfile_path
|
1433
|
+
file_utils.remove_entry_secure(out_dir) if File.exist?(out_dir)
|
1434
|
+
file_utils.remove_entry_secure(src_file) if File.exist?(src_file)
|
1435
|
+
file_utils.remove_entry_secure(per_file) if File.exist?(per_file)
|
1436
|
+
test_copy_one_file
|
1437
|
+
end
|
1438
|
+
def test_copy_one_file_almost_nothing_exist
|
1439
|
+
out_dir = tmpdir+'/out-dir'
|
1440
|
+
src_file = tmpdir+'/baz.txt'
|
1441
|
+
per_file = self.class.dotfile_path
|
1442
|
+
file_utils.remove_entry_secure(out_dir) if File.exist?(out_dir)
|
1443
|
+
file_utils.remove_entry_secure(src_file) if File.exist?(src_file)
|
1444
|
+
if File.exist?(per_file)
|
1445
|
+
struct = JSON.parse(File.read(per_file))
|
1446
|
+
if File.exist?(struct["tmpdir"])
|
1447
|
+
file_utils.remove_entry_secure(struct["tmpdir"])
|
1448
|
+
end
|
1449
|
+
end
|
1450
|
+
test_copy_one_file
|
1451
|
+
end
|
1452
|
+
def test_copy_one_file_a_bunch_of_tmpdir_crap
|
1453
|
+
out_dir = tmpdir+'/out-dir'
|
1454
|
+
src_file = tmpdir+'/baz.txt'
|
1455
|
+
file_utils.remove_entry_secure(out_dir) if File.exist?(out_dir)
|
1456
|
+
file_utils.remove_entry_secure(src_file) if File.exist?(src_file)
|
1457
|
+
self.class.instance_variable_set('@tmpdir',nil)
|
1458
|
+
tmpdir # gets to a hard to reach line
|
1459
|
+
test_copy_one_file
|
1460
|
+
end
|
1461
|
+
def test_copy_one_file
|
1462
|
+
the_tmpdir = tmpdir
|
1463
|
+
# doc start
|
1464
|
+
content = "i am the content of\na file called baz.txt\n"
|
1465
|
+
File.open(the_tmpdir+'/baz.txt','w+') do |fh|
|
1466
|
+
fh.puts(content)
|
1467
|
+
end
|
1468
|
+
tasks = Treebis::TaskSet.new
|
1469
|
+
tasks.task(:default) do
|
1470
|
+
mkdir_p_unless_exists # makes output dir referred to in on() below
|
1471
|
+
from the_tmpdir # the source directory
|
1472
|
+
copy('./baz.txt')
|
1473
|
+
end
|
1474
|
+
tasks[:default].on(the_tmpdir+'/out-dir/').run()
|
1475
|
+
output = File.read(the_tmpdir+'/out-dir/baz.txt')
|
1476
|
+
# doc stop
|
1477
|
+
assert_equal(content, output)
|
1478
|
+
end
|
1479
|
+
end
|
1480
|
+
|
1481
|
+
class TestCase < ::Test::Unit::TestCase
|
1482
|
+
DotfilePath = './treebis.persistent.json'
|
1483
|
+
include Treebis::DirAsHash, Treebis::Capture3
|
1484
|
+
include TestAntecedents
|
1485
|
+
include TestColorAndRmAndMoveAndPatch
|
1486
|
+
include TestColorize
|
1487
|
+
include TestDirAsHash
|
1488
|
+
include TestFileUtilsProxy
|
1489
|
+
include TestGlobMatcher
|
1490
|
+
include TestPatch
|
1491
|
+
include TestPersistentDotfile
|
1492
|
+
include TestRemove
|
1493
|
+
include TestTaskMisc
|
1494
|
+
include TestTempdirAndTasksAndCopy
|
1495
|
+
include TestSopen
|
1496
|
+
|
1497
|
+
futils_prefix = sprintf( "%s%s --> ", Treebis::Config.default_prefix,
|
1498
|
+
Treebis::Colorize.colorize('for test:', :bright, :blue) )
|
1499
|
+
|
1500
|
+
file_utils = Treebis::Config.new_default_file_utils_proxy
|
1501
|
+
file_utils.prefix = futils_prefix
|
1502
|
+
|
1503
|
+
Treebis::PersistentDotfile.include_to( self,
|
1504
|
+
DotfilePath, :file_utils => file_utils )
|
1505
|
+
|
1506
|
+
@file_utils = file_utils
|
1507
|
+
define_method( :file_utils ){ file_utils }
|
1508
|
+
alias_method :fu, :file_utils
|
1509
|
+
class << self
|
1510
|
+
attr_accessor :file_utils
|
1511
|
+
end
|
1512
|
+
def task; Treebis::Task end
|
1513
|
+
end
|
1514
|
+
::Test::Unit::UI::Console::TestRunner.run(TestCase)
|
1515
|
+
end
|
1516
|
+
end
|