tap 0.12.4 → 0.17.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History +34 -0
- data/README +62 -41
- data/bin/tap +36 -40
- data/cmd/console.rb +14 -6
- data/cmd/manifest.rb +62 -58
- data/cmd/run.rb +49 -31
- data/doc/API +84 -0
- data/doc/Class Reference +83 -115
- data/doc/Examples/Command Line +36 -0
- data/doc/Examples/Workflow +40 -0
- data/lib/tap/app.rb +293 -214
- data/lib/tap/app/node.rb +43 -0
- data/lib/tap/app/queue.rb +77 -0
- data/lib/tap/app/stack.rb +16 -0
- data/lib/tap/app/state.rb +22 -0
- data/lib/tap/constants.rb +2 -2
- data/lib/tap/env.rb +400 -314
- data/lib/tap/env/constant.rb +227 -0
- data/lib/tap/env/gems.rb +63 -0
- data/lib/tap/env/manifest.rb +89 -0
- data/lib/tap/env/minimap.rb +292 -0
- data/lib/tap/{support → env}/string_ext.rb +2 -2
- data/lib/tap/exe.rb +113 -125
- data/lib/tap/join.rb +175 -0
- data/lib/tap/joins.rb +9 -0
- data/lib/tap/joins/switch.rb +44 -0
- data/lib/tap/joins/sync.rb +99 -0
- data/lib/tap/root.rb +100 -491
- data/lib/tap/root/utils.rb +220 -0
- data/lib/tap/{support → root}/versions.rb +31 -29
- data/lib/tap/schema.rb +248 -0
- data/lib/tap/schema/parser.rb +413 -0
- data/lib/tap/schema/utils.rb +82 -0
- data/lib/tap/support/intern.rb +19 -6
- data/lib/tap/support/templater.rb +8 -3
- data/lib/tap/task.rb +175 -171
- data/lib/tap/tasks/dump.rb +58 -0
- data/lib/tap/tasks/load.rb +62 -0
- metadata +30 -73
- data/cmd/destroy.rb +0 -27
- data/cmd/generate.rb +0 -27
- data/doc/Command Reference +0 -105
- data/doc/Syntax Reference +0 -234
- data/doc/Tutorial +0 -348
- data/lib/tap/dump.rb +0 -142
- data/lib/tap/file_task.rb +0 -384
- data/lib/tap/generator/arguments.rb +0 -13
- data/lib/tap/generator/base.rb +0 -176
- data/lib/tap/generator/destroy.rb +0 -60
- data/lib/tap/generator/generate.rb +0 -93
- data/lib/tap/generator/generators/command/command_generator.rb +0 -21
- data/lib/tap/generator/generators/command/templates/command.erb +0 -32
- data/lib/tap/generator/generators/config/config_generator.rb +0 -98
- data/lib/tap/generator/generators/generator/generator_generator.rb +0 -37
- data/lib/tap/generator/generators/generator/templates/task.erb +0 -27
- data/lib/tap/generator/generators/generator/templates/test.erb +0 -26
- data/lib/tap/generator/generators/root/root_generator.rb +0 -84
- data/lib/tap/generator/generators/root/templates/MIT-LICENSE +0 -22
- data/lib/tap/generator/generators/root/templates/README +0 -14
- data/lib/tap/generator/generators/root/templates/Rakefile +0 -84
- data/lib/tap/generator/generators/root/templates/Rapfile +0 -11
- data/lib/tap/generator/generators/root/templates/gemspec +0 -27
- data/lib/tap/generator/generators/root/templates/test/tap_test_helper.rb +0 -3
- data/lib/tap/generator/generators/task/task_generator.rb +0 -25
- data/lib/tap/generator/generators/task/templates/task.erb +0 -14
- data/lib/tap/generator/generators/task/templates/test.erb +0 -19
- data/lib/tap/generator/manifest.rb +0 -20
- data/lib/tap/generator/preview.rb +0 -69
- data/lib/tap/load.rb +0 -64
- data/lib/tap/spec.rb +0 -41
- data/lib/tap/support/aggregator.rb +0 -65
- data/lib/tap/support/audit.rb +0 -333
- data/lib/tap/support/constant.rb +0 -143
- data/lib/tap/support/constant_manifest.rb +0 -126
- data/lib/tap/support/dependencies.rb +0 -54
- data/lib/tap/support/dependency.rb +0 -44
- data/lib/tap/support/executable.rb +0 -198
- data/lib/tap/support/executable_queue.rb +0 -125
- data/lib/tap/support/gems.rb +0 -43
- data/lib/tap/support/join.rb +0 -144
- data/lib/tap/support/joins.rb +0 -12
- data/lib/tap/support/joins/switch.rb +0 -27
- data/lib/tap/support/joins/sync_merge.rb +0 -38
- data/lib/tap/support/manifest.rb +0 -171
- data/lib/tap/support/minimap.rb +0 -90
- data/lib/tap/support/node.rb +0 -176
- data/lib/tap/support/parser.rb +0 -450
- data/lib/tap/support/schema.rb +0 -385
- data/lib/tap/support/shell_utils.rb +0 -67
- data/lib/tap/test.rb +0 -77
- data/lib/tap/test/assertions.rb +0 -38
- data/lib/tap/test/env_vars.rb +0 -29
- data/lib/tap/test/extensions.rb +0 -73
- data/lib/tap/test/file_test.rb +0 -362
- data/lib/tap/test/file_test_class.rb +0 -15
- data/lib/tap/test/regexp_escape.rb +0 -87
- data/lib/tap/test/script_test.rb +0 -46
- data/lib/tap/test/script_tester.rb +0 -115
- data/lib/tap/test/subset_test.rb +0 -260
- data/lib/tap/test/subset_test_class.rb +0 -99
- data/lib/tap/test/tap_test.rb +0 -109
- data/lib/tap/test/utils.rb +0 -231
@@ -0,0 +1,227 @@
|
|
1
|
+
require 'tap/env/string_ext'
|
2
|
+
|
3
|
+
module Tap
|
4
|
+
class Env
|
5
|
+
|
6
|
+
# A Constant serves as a placeholder for an actual constant, sort of like
|
7
|
+
# autoload. Use the constantize method to retrieve the actual constant; if
|
8
|
+
# it doesn't exist, constantize requires require_path and tries again.
|
9
|
+
#
|
10
|
+
# Object.const_defined?(:Net) # => false
|
11
|
+
# $".include?('net/http') # => false
|
12
|
+
#
|
13
|
+
# http = Constant.new('Net::HTTP', 'net/http.rb')
|
14
|
+
# http.constantize # => Net::HTTP
|
15
|
+
# $".include?('net/http.rb') # => true
|
16
|
+
#
|
17
|
+
# === Unloading
|
18
|
+
#
|
19
|
+
# Constant also supports constant unloading. Unloading can be useful in
|
20
|
+
# various development modes, but make cause code to behave unpredictably.
|
21
|
+
# When a Constant unloads, the constant value is detached from the nesting
|
22
|
+
# constant and the require path is removed from $". This allows a require
|
23
|
+
# statement to re-require, and in theory, reload the constant.
|
24
|
+
#
|
25
|
+
# # [simple.rb]
|
26
|
+
# # class Simple
|
27
|
+
# # end
|
28
|
+
#
|
29
|
+
# const = Constant.new('Simple', 'simple')
|
30
|
+
# const.constantize # => Simple
|
31
|
+
# Object.const_defined?(:Simple) # => true
|
32
|
+
#
|
33
|
+
# const.unload # => Simple
|
34
|
+
# Object.const_defined?(:Simple) # => false
|
35
|
+
#
|
36
|
+
# const.constantize # => Simple
|
37
|
+
# Object.const_defined?(:Simple) # => true
|
38
|
+
#
|
39
|
+
# Unloading and reloading works best for scripts that have no side effects;
|
40
|
+
# ie scripts that do not require other files and only define the specified
|
41
|
+
# class or module.
|
42
|
+
class Constant
|
43
|
+
class << self
|
44
|
+
|
45
|
+
# Constantize tries to look up the specified constant under const. A
|
46
|
+
# block may be given to manually look up missing constants; the last
|
47
|
+
# existing const and any non-existant constant names are yielded to the
|
48
|
+
# block, which is expected to return the desired constant. For instance
|
49
|
+
# in the example 'Non::Existant' is essentially mapping to ConstName.
|
50
|
+
#
|
51
|
+
# module ConstName; end
|
52
|
+
#
|
53
|
+
# Constant.constantize('ConstName') # => ConstName
|
54
|
+
# Constant.constantize('Non::Existant') { ConstName } # => ConstName
|
55
|
+
#
|
56
|
+
# Raises a NameError for invalid/missing constants.
|
57
|
+
def constantize(const_name, const=Object) # :yields: const, missing_const_names
|
58
|
+
unless CONST_REGEXP =~ const_name
|
59
|
+
raise NameError, "#{const_name.inspect} is not a valid constant name!"
|
60
|
+
end
|
61
|
+
|
62
|
+
constants = $1.split(/::/)
|
63
|
+
while !constants.empty?
|
64
|
+
unless const_is_defined?(const, constants[0])
|
65
|
+
if block_given?
|
66
|
+
return yield(const, constants)
|
67
|
+
else
|
68
|
+
raise NameError.new("uninitialized constant #{const_name}", constants[0])
|
69
|
+
end
|
70
|
+
end
|
71
|
+
const = const.const_get(constants.shift)
|
72
|
+
end
|
73
|
+
const
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
# helper method. Determines if the named constant is defined in const.
|
79
|
+
# The implementation (annoyingly) has to be different for ruby 1.9 due
|
80
|
+
# to changes in the API.
|
81
|
+
case RUBY_VERSION
|
82
|
+
when /^1.9/
|
83
|
+
def const_is_defined?(const, const_name) # :nodoc:
|
84
|
+
const.const_defined?(const_name, false)
|
85
|
+
end
|
86
|
+
else
|
87
|
+
def const_is_defined?(const, const_name) # :nodoc:
|
88
|
+
const.const_defined?(const_name)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Matches a valid constant
|
94
|
+
CONST_REGEXP = /\A(?:::)?([A-Z]\w*(?:::[A-Z]\w*)*)\z/
|
95
|
+
|
96
|
+
# The full constant name
|
97
|
+
attr_reader :const_name
|
98
|
+
|
99
|
+
# The path to load to initialize a missing constant
|
100
|
+
attr_reader :require_path
|
101
|
+
|
102
|
+
# An optional comment
|
103
|
+
attr_accessor :comment
|
104
|
+
|
105
|
+
# Initializes a new Constant with the specified constant name,
|
106
|
+
# require_path, and comment. The const_name should be a valid
|
107
|
+
# constant name.
|
108
|
+
def initialize(const_name, require_path=nil, comment=nil)
|
109
|
+
@const_name = const_name
|
110
|
+
@require_path = require_path
|
111
|
+
@comment = comment
|
112
|
+
end
|
113
|
+
|
114
|
+
# Returns the underscored const_name.
|
115
|
+
#
|
116
|
+
# Constant.new("Const::Name").path # => 'const/name'
|
117
|
+
#
|
118
|
+
def path
|
119
|
+
@path ||= const_name.underscore
|
120
|
+
end
|
121
|
+
|
122
|
+
# Returns the basename of path.
|
123
|
+
#
|
124
|
+
# Constant.new("Const::Name").basename # => 'name'
|
125
|
+
#
|
126
|
+
def basename
|
127
|
+
@basename ||= File.basename(path)
|
128
|
+
end
|
129
|
+
|
130
|
+
# Returns the path, minus the basename of path.
|
131
|
+
#
|
132
|
+
# Constant.new("Const::Name").dirname # => 'const'
|
133
|
+
#
|
134
|
+
def dirname
|
135
|
+
@dirname ||= (dirname = File.dirname(path)) == "." ? "" : dirname
|
136
|
+
end
|
137
|
+
|
138
|
+
# Returns the name of the constant, minus nesting.
|
139
|
+
#
|
140
|
+
# Constant.new("Const::Name").name # => 'Name'
|
141
|
+
#
|
142
|
+
def name
|
143
|
+
@name ||= (const_name =~ /.*::(.*)\z/ ? $1 : const_name)
|
144
|
+
end
|
145
|
+
|
146
|
+
# Returns the nesting constant of const_name.
|
147
|
+
#
|
148
|
+
# Constant.new("Const::Name").nesting # => 'Const'
|
149
|
+
#
|
150
|
+
def nesting
|
151
|
+
@nesting ||= (const_name =~ /(.*)::.*\z/ ? $1 : '')
|
152
|
+
end
|
153
|
+
|
154
|
+
# Returns the number of constants in nesting.
|
155
|
+
#
|
156
|
+
# Constant.new("Const::Name").nesting_depth # => 1
|
157
|
+
#
|
158
|
+
def nesting_depth
|
159
|
+
@nesting_depth ||= nesting.split(/::/).length
|
160
|
+
end
|
161
|
+
|
162
|
+
# Returns the Lazydoc document for require_path.
|
163
|
+
def document
|
164
|
+
require_path ? Lazydoc[require_path] : nil
|
165
|
+
end
|
166
|
+
|
167
|
+
# True if another is a Constant with the same const_name,
|
168
|
+
# require_path, and comment as self.
|
169
|
+
def ==(another)
|
170
|
+
another.kind_of?(Constant) &&
|
171
|
+
another.const_name == self.const_name &&
|
172
|
+
another.require_path == self.require_path &&
|
173
|
+
another.comment == self.comment
|
174
|
+
end
|
175
|
+
|
176
|
+
# Looks up and returns the constant indicated by const_name. If the
|
177
|
+
# constant cannot be found, constantize requires require_path and
|
178
|
+
# tries again.
|
179
|
+
#
|
180
|
+
# Raises a NameError if the constant cannot be found.
|
181
|
+
def constantize
|
182
|
+
Constant.constantize(const_name) do
|
183
|
+
require require_path if require_path
|
184
|
+
Constant.constantize(const_name)
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
# Undefines the constant indicated by const_name. The nesting constants
|
189
|
+
# are not removed. If specified, require_path will be removed from $".
|
190
|
+
#
|
191
|
+
# When removing require_path, unload will add '.rb' to the require_path if
|
192
|
+
# require_path has no extension (this echos the behavior of require).
|
193
|
+
# Other extension names like '.so', '.dll', etc. are not tried and will
|
194
|
+
# not be removed.
|
195
|
+
#
|
196
|
+
# Does nothing if const_name doesn't exist. Returns the unloaded constant.
|
197
|
+
# Obviously, <em>this method should be used with caution</em>.
|
198
|
+
def unload(unrequire=true)
|
199
|
+
const = nesting.empty? ? Object : Constant.constantize(nesting) { Object }
|
200
|
+
|
201
|
+
if const.const_defined?(name)
|
202
|
+
if unrequire && require_path
|
203
|
+
path = File.extname(require_path).empty? ? "#{require_path}.rb" : require_path
|
204
|
+
$".delete(path)
|
205
|
+
end
|
206
|
+
|
207
|
+
return const.send(:remove_const, name)
|
208
|
+
end
|
209
|
+
|
210
|
+
nil
|
211
|
+
end
|
212
|
+
|
213
|
+
# Returns a string like:
|
214
|
+
#
|
215
|
+
# "#<Tap::Env::Constant:object_id Const::Name (require_path)>"
|
216
|
+
#
|
217
|
+
def inspect
|
218
|
+
"#<#{self.class}:#{object_id} #{const_name}#{@require_path == nil ? "" : " (#{@require_path})"}>"
|
219
|
+
end
|
220
|
+
|
221
|
+
# Returns the minikey for self, ie path. (see Tap::Env::Minimap)
|
222
|
+
def minikey
|
223
|
+
path
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
data/lib/tap/env/gems.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
|
3
|
+
module Tap
|
4
|
+
class Env
|
5
|
+
|
6
|
+
# Methods for working with {RubyGems}[http://www.rubygems.org/].
|
7
|
+
module Gems
|
8
|
+
module_function
|
9
|
+
|
10
|
+
# Returns the gemspec for the specified gem. A gem version
|
11
|
+
# can be specified in the name, like 'gem >= 1.2'. The gem
|
12
|
+
# is not activated by this method.
|
13
|
+
def gemspec(gem_name)
|
14
|
+
return gem_name if gem_name.kind_of?(Gem::Specification)
|
15
|
+
|
16
|
+
dependency = if gem_name.kind_of?(Gem::Dependency)
|
17
|
+
gem_name
|
18
|
+
else
|
19
|
+
# figure the version of the gem, by default >= 0.0.0
|
20
|
+
gem_name.to_s =~ /^([^~<=>]*)(.*)$/
|
21
|
+
name, version = $1.strip, $2
|
22
|
+
return nil if name.empty?
|
23
|
+
version = Gem::Requirement.default if version.empty?
|
24
|
+
|
25
|
+
# note the last gem matching the dependency requirements
|
26
|
+
# is the latest matching gem
|
27
|
+
Gem::Dependency.new(name, version)
|
28
|
+
end
|
29
|
+
|
30
|
+
Gem.source_index.search(dependency).last
|
31
|
+
end
|
32
|
+
|
33
|
+
# Selects gem specs for which the block returns true. If
|
34
|
+
# latest is specified, only the latest version of each
|
35
|
+
# gem will be passed to the block.
|
36
|
+
def select_gems(latest=true)
|
37
|
+
specs = latest ?
|
38
|
+
Gem.source_index.latest_specs :
|
39
|
+
Gem.source_index.gems.collect {|(name, spec)| spec }
|
40
|
+
|
41
|
+
# this song and dance is to ensure that specs are sorted
|
42
|
+
# by name (ascending) then version (descending) so that
|
43
|
+
# the latest version of a spec appears first
|
44
|
+
specs_by_name = {}
|
45
|
+
specs.each do |spec|
|
46
|
+
next unless !block_given? || yield(spec)
|
47
|
+
(specs_by_name[spec.name] ||= []) << spec
|
48
|
+
end
|
49
|
+
|
50
|
+
specs = []
|
51
|
+
specs_by_name.keys.sort.each do |name|
|
52
|
+
specs_by_name[name].sort_by do |spec|
|
53
|
+
spec.version
|
54
|
+
end.reverse_each do |spec|
|
55
|
+
specs << spec
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
specs
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'tap/env/minimap'
|
2
|
+
require 'tap/env/constant'
|
3
|
+
|
4
|
+
module Tap
|
5
|
+
class Env
|
6
|
+
|
7
|
+
class Manifest
|
8
|
+
include Enumerable
|
9
|
+
include Minimap
|
10
|
+
|
11
|
+
# The environment this manifest summarizes
|
12
|
+
attr_reader :env
|
13
|
+
|
14
|
+
attr_reader :type
|
15
|
+
|
16
|
+
# Initializes a new Manifest.
|
17
|
+
def initialize(env, type)
|
18
|
+
@env = env
|
19
|
+
@type = type
|
20
|
+
end
|
21
|
+
|
22
|
+
def entries
|
23
|
+
env.registry[type]
|
24
|
+
end
|
25
|
+
|
26
|
+
# True if entries are empty.
|
27
|
+
def empty?
|
28
|
+
entries.empty?
|
29
|
+
end
|
30
|
+
|
31
|
+
def all_empty?
|
32
|
+
env.all? do |current|
|
33
|
+
current.manifest(type).empty?
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Iterates over each entry in self.
|
38
|
+
def each
|
39
|
+
entries.each {|entry| yield(entry) }
|
40
|
+
end
|
41
|
+
|
42
|
+
# Searches across env.each for the first entry minimatching key. A single
|
43
|
+
# env can be specified by using a compound key like 'env_key:key'.
|
44
|
+
#
|
45
|
+
# Returns nil if no matching entry is found.
|
46
|
+
def seek(key)
|
47
|
+
env.seek(type, key)
|
48
|
+
end
|
49
|
+
|
50
|
+
def [](key)
|
51
|
+
entry = seek(key)
|
52
|
+
entry.kind_of?(Constant) ? entry.constantize : entry
|
53
|
+
end
|
54
|
+
|
55
|
+
# Same as env.inspect but adds manifest to the templater
|
56
|
+
def inspect(template=nil, globals={}, filename=nil)
|
57
|
+
return super() unless template
|
58
|
+
|
59
|
+
env.inspect(template, globals, filename) do |templater, globalz|
|
60
|
+
env = templater.env
|
61
|
+
templater.manifest = env.manifest(type)
|
62
|
+
yield(templater, globalz) if block_given?
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
SUMMARY_TEMPLATE = %Q{<% if !entries.empty? && count > 1 %>
|
67
|
+
<%= env_key %>:
|
68
|
+
<% end %>
|
69
|
+
<% entries.each do |key, entry| %>
|
70
|
+
<%= key.ljust(width) %> # <%= entry.respond_to?(:comment) ? entry.comment : entry %>
|
71
|
+
<% end %>
|
72
|
+
}
|
73
|
+
|
74
|
+
def summarize(template=SUMMARY_TEMPLATE)
|
75
|
+
inspect(template, :width => 11, :count => 0) do |templater, globals|
|
76
|
+
width = globals[:width]
|
77
|
+
templater.entries = templater.manifest.minimap.collect! do |key, entry|
|
78
|
+
width = key.length if width < key.length
|
79
|
+
[key, entry]
|
80
|
+
end
|
81
|
+
|
82
|
+
globals[:width] = width
|
83
|
+
globals[:count] += 1 unless templater.entries.empty?
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,292 @@
|
|
1
|
+
module Tap
|
2
|
+
class Env
|
3
|
+
|
4
|
+
# Minimap adds minimization and search methods to an array of paths.
|
5
|
+
#
|
6
|
+
# paths = %w{
|
7
|
+
# path/to/file-0.1.0.txt
|
8
|
+
# path/to/file-0.2.0.txt
|
9
|
+
# path/to/another_file.txt
|
10
|
+
# }
|
11
|
+
# paths.extend Env::Minimap
|
12
|
+
#
|
13
|
+
# paths.minimatch('file') # => 'path/to/file-0.1.0.txt'
|
14
|
+
# paths.minimatch('file-0.2.0') # => 'path/to/file-0.2.0.txt'
|
15
|
+
# paths.minimatch('another_file') # => 'path/to/another_file.txt'
|
16
|
+
#
|
17
|
+
# More generally, Minimap may extend any object responding to each.
|
18
|
+
# Non-string entries are allowed; if the entry responds to minikey, then
|
19
|
+
# minimap will call that method to determine the 'path' to the entry.
|
20
|
+
# Otherwise, to_s is used. Override the entry_to_minikey method to
|
21
|
+
# change this default behavior.
|
22
|
+
#
|
23
|
+
# class ConstantMap < Array
|
24
|
+
# include Env::Minimap
|
25
|
+
#
|
26
|
+
# def entry_to_minikey(const)
|
27
|
+
# const.underscore
|
28
|
+
# end
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# constants = ConstantMap[Tap::Env::Minimap, Tap::Env]
|
32
|
+
# constants.minimatch('env') # => Tap::Env
|
33
|
+
# constants.minimatch('minimap') # => Tap::Env::Minimap
|
34
|
+
#
|
35
|
+
module Minimap
|
36
|
+
|
37
|
+
# Provides a minimized map of the entries using keys provided minikey.
|
38
|
+
#
|
39
|
+
# paths = %w{
|
40
|
+
# path/to/file-0.1.0.txt
|
41
|
+
# path/to/file-0.2.0.txt
|
42
|
+
# path/to/another_file.txt
|
43
|
+
# }.extend Minimap
|
44
|
+
#
|
45
|
+
# paths.minimap
|
46
|
+
# # => [
|
47
|
+
# # ['file-0.1.0', 'path/to/file-0.1.0.txt'],
|
48
|
+
# # ['file-0.2.0', 'path/to/file-0.2.0.txt'],
|
49
|
+
# # ['another_file','path/to/another_file.txt']]
|
50
|
+
#
|
51
|
+
def minimap
|
52
|
+
hash = {}
|
53
|
+
map = []
|
54
|
+
each {|entry| map << (hash[entry_to_minikey(entry)] = [entry]) }
|
55
|
+
minimize(hash.keys) do |key, mini_key|
|
56
|
+
hash[key].unshift mini_key
|
57
|
+
end
|
58
|
+
|
59
|
+
map
|
60
|
+
end
|
61
|
+
|
62
|
+
# Returns the first entry whose minikey mini-matches the input, or nil if
|
63
|
+
# no such entry exists.
|
64
|
+
#
|
65
|
+
# paths = %w{
|
66
|
+
# path/to/file-0.1.0.txt
|
67
|
+
# path/to/file-0.2.0.txt
|
68
|
+
# path/to/another_file.txt
|
69
|
+
# }.extend Minimap
|
70
|
+
#
|
71
|
+
# paths.minimatch('file-0.2.0') # => 'path/to/file-0.2.0.txt'
|
72
|
+
# paths.minimatch('file-0.3.0') # => nil
|
73
|
+
#
|
74
|
+
def minimatch(key)
|
75
|
+
key = key.to_s
|
76
|
+
each do |entry|
|
77
|
+
return entry if minimal_match?(entry_to_minikey(entry), key)
|
78
|
+
end
|
79
|
+
nil
|
80
|
+
end
|
81
|
+
|
82
|
+
# Returns minimap as a hash of (minikey, value) pairs.
|
83
|
+
def minihash(reverse=false)
|
84
|
+
hash = {}
|
85
|
+
minimap.each do |key, value|
|
86
|
+
if reverse
|
87
|
+
hash[value] = key
|
88
|
+
else
|
89
|
+
hash[key] = value
|
90
|
+
end
|
91
|
+
end
|
92
|
+
hash
|
93
|
+
end
|
94
|
+
|
95
|
+
protected
|
96
|
+
|
97
|
+
# A hook to convert entries to minikeys. Returns the entry by default,
|
98
|
+
# or entry.minikey if the entry responds to minikey.
|
99
|
+
def entry_to_minikey(entry)
|
100
|
+
entry.respond_to?(:minikey) ? entry.minikey : entry.to_s
|
101
|
+
end
|
102
|
+
|
103
|
+
module_function
|
104
|
+
|
105
|
+
# Minimizes a set of paths to the set of shortest basepaths that unqiuely
|
106
|
+
# identify the paths. The path extension and versions are removed from
|
107
|
+
# the basepath if possible. For example:
|
108
|
+
#
|
109
|
+
# Minimap.minimize ['path/to/a.rb', 'path/to/b.rb']
|
110
|
+
# # => ['a', 'b']
|
111
|
+
#
|
112
|
+
# Minimap.minimize ['path/to/a-0.1.0.rb', 'path/to/b-0.1.0.rb']
|
113
|
+
# # => ['a', 'b']
|
114
|
+
#
|
115
|
+
# Minimap.minimize ['path/to/file.rb', 'path/to/file.txt']
|
116
|
+
# # => ['file.rb', 'file.txt']
|
117
|
+
#
|
118
|
+
# Minimap.minimize ['path-0.1/to/file.rb', 'path-0.2/to/file.rb']
|
119
|
+
# # => ['path-0.1/to/file', 'path-0.2/to/file']
|
120
|
+
#
|
121
|
+
# Minimized paths that carry their extension will always carry
|
122
|
+
# their version as well, but the converse is not true; paths
|
123
|
+
# can be minimized to carry just the version and not the path
|
124
|
+
# extension.
|
125
|
+
#
|
126
|
+
# Minimap.minimize ['path/to/a-0.1.0.rb', 'path/to/a-0.1.0.txt']
|
127
|
+
# # => ['a-0.1.0.rb', 'a-0.1.0.txt']
|
128
|
+
#
|
129
|
+
# Minimap.minimize ['path/to/a-0.1.0.rb', 'path/to/a-0.2.0.rb']
|
130
|
+
# # => ['a-0.1.0', 'a-0.2.0']
|
131
|
+
#
|
132
|
+
# If a block is given, each (path, mini-path) pair will be passed
|
133
|
+
# to it after minimization.
|
134
|
+
def minimize(paths) # :yields: path, mini_path
|
135
|
+
unless block_given?
|
136
|
+
mini_paths = []
|
137
|
+
minimize(paths) {|p, mp| mini_paths << mp }
|
138
|
+
return mini_paths
|
139
|
+
end
|
140
|
+
|
141
|
+
splits = paths.uniq.collect do |path|
|
142
|
+
extname = File.extname(path)
|
143
|
+
extname = '' if extname =~ /^\.\d+$/
|
144
|
+
base = File.basename(path.chomp(extname))
|
145
|
+
version = base =~ /(-\d+(\.\d+)*)$/ ? $1 : ''
|
146
|
+
|
147
|
+
[dirname_or_array(path), base.chomp(version), extname, version, false, path]
|
148
|
+
end
|
149
|
+
|
150
|
+
while !splits.empty?
|
151
|
+
index = 0
|
152
|
+
splits = splits.collect do |(dir, base, extname, version, flagged, path)|
|
153
|
+
index += 1
|
154
|
+
case
|
155
|
+
when !flagged && just_one?(splits, index, base)
|
156
|
+
|
157
|
+
# found just one
|
158
|
+
yield(path, base)
|
159
|
+
nil
|
160
|
+
when dir.kind_of?(Array)
|
161
|
+
|
162
|
+
# no more path segments to use, try to add
|
163
|
+
# back version and extname
|
164
|
+
if dir.empty?
|
165
|
+
dir << File.dirname(base)
|
166
|
+
base = File.basename(base)
|
167
|
+
end
|
168
|
+
|
169
|
+
case
|
170
|
+
when !version.empty?
|
171
|
+
# add back version (occurs first)
|
172
|
+
[dir, "#{base}#{version}", extname, '', false, path]
|
173
|
+
|
174
|
+
when !extname.empty?
|
175
|
+
|
176
|
+
# add back extension (occurs second)
|
177
|
+
[dir, "#{base}#{extname}", '', version, false, path]
|
178
|
+
else
|
179
|
+
|
180
|
+
# nothing more to distinguish... path is minimized (occurs third)
|
181
|
+
yield(path, min_join(dir[0], base))
|
182
|
+
nil
|
183
|
+
end
|
184
|
+
else
|
185
|
+
|
186
|
+
# shift path segment. dirname_or_array returns an
|
187
|
+
# array if this is the last path segment to shift.
|
188
|
+
[dirname_or_array(dir), min_join(File.basename(dir), base), extname, version, false, path]
|
189
|
+
end
|
190
|
+
end.compact
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
# Returns true if the mini_path matches path. Matching logic reverses
|
195
|
+
# that of minimize:
|
196
|
+
#
|
197
|
+
# * a match occurs when path ends with mini_path
|
198
|
+
# * if mini_path doesn't specify an extension, then mini_path
|
199
|
+
# must only match path up to the path extension
|
200
|
+
# * if mini_path doesn't specify a version, then mini_path
|
201
|
+
# must only match path up to the path basename (minus the
|
202
|
+
# version and extname)
|
203
|
+
#
|
204
|
+
# For example:
|
205
|
+
#
|
206
|
+
# Minimap.minimal_match?('dir/file-0.1.0.rb', 'file') # => true
|
207
|
+
# Minimap.minimal_match?('dir/file-0.1.0.rb', 'dir/file') # => true
|
208
|
+
# Minimap.minimal_match?('dir/file-0.1.0.rb', 'file-0.1.0') # => true
|
209
|
+
# Minimap.minimal_match?('dir/file-0.1.0.rb', 'file-0.1.0.rb') # => true
|
210
|
+
#
|
211
|
+
# Minimap.minimal_match?('dir/file-0.1.0.rb', 'file.rb') # => false
|
212
|
+
# Minimap.minimal_match?('dir/file-0.1.0.rb', 'file-0.2.0') # => false
|
213
|
+
# Minimap.minimal_match?('dir/file-0.1.0.rb', 'another') # => false
|
214
|
+
#
|
215
|
+
# In matching, partial basenames are not allowed but partial directories
|
216
|
+
# are allowed. Hence:
|
217
|
+
#
|
218
|
+
# Minimap.minimal_match?('dir/file-0.1.0.txt', 'file') # => true
|
219
|
+
# Minimap.minimal_match?('dir/file-0.1.0.txt', 'ile') # => false
|
220
|
+
# Minimap.minimal_match?('dir/file-0.1.0.txt', 'r/file') # => true
|
221
|
+
#
|
222
|
+
def minimal_match?(path, mini_path)
|
223
|
+
extname = non_version_extname(mini_path)
|
224
|
+
version = mini_path =~ /(-\d+(\.\d+)*)#{extname}$/ ? $1 : ''
|
225
|
+
|
226
|
+
match_path = case
|
227
|
+
when !extname.empty?
|
228
|
+
# force full match
|
229
|
+
path
|
230
|
+
when !version.empty?
|
231
|
+
# match up to version
|
232
|
+
path.chomp(non_version_extname(path))
|
233
|
+
else
|
234
|
+
# match up base
|
235
|
+
path.chomp(non_version_extname(path)).sub(/(-\d+(\.\d+)*)$/, '')
|
236
|
+
end
|
237
|
+
|
238
|
+
# key ends with pattern AND basenames of each are equal...
|
239
|
+
# the last check ensures that a full path segment has
|
240
|
+
# been specified
|
241
|
+
match_path[-mini_path.length, mini_path.length] == mini_path && File.basename(match_path) == File.basename(mini_path)
|
242
|
+
end
|
243
|
+
|
244
|
+
# utility method for minimize -- joins the
|
245
|
+
# dir and path, preventing results like:
|
246
|
+
#
|
247
|
+
# "./path"
|
248
|
+
# "//path"
|
249
|
+
#
|
250
|
+
def min_join(dir, path) # :nodoc:
|
251
|
+
case dir
|
252
|
+
when "." then path
|
253
|
+
when "/" then "/#{path}"
|
254
|
+
else "#{dir}/#{path}"
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
# utility method for minimize -- returns the
|
259
|
+
# dirname of path, or an array if the dirname
|
260
|
+
# is effectively empty.
|
261
|
+
def dirname_or_array(path) # :nodoc:
|
262
|
+
dir = File.dirname(path)
|
263
|
+
case dir
|
264
|
+
when path, '.' then []
|
265
|
+
else dir
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
# utility method for minimize -- determines if there
|
270
|
+
# is just one of the base in splits, while flagging
|
271
|
+
# all matching entries.
|
272
|
+
def just_one?(splits, index, base) # :nodoc:
|
273
|
+
just_one = true
|
274
|
+
index.upto(splits.length-1) do |i|
|
275
|
+
if splits[i][1] == base
|
276
|
+
splits[i][4] = true
|
277
|
+
just_one = false
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
just_one
|
282
|
+
end
|
283
|
+
|
284
|
+
# utility method for minimal_match -- returns a non-version
|
285
|
+
# extname, or an empty string if the path ends in a version.
|
286
|
+
def non_version_extname(path) # :nodoc:
|
287
|
+
extname = File.extname(path)
|
288
|
+
extname =~ /^\.\d+$/ ? '' : extname
|
289
|
+
end
|
290
|
+
end
|
291
|
+
end
|
292
|
+
end
|