tap 0.12.4 → 0.17.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 +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
|