root 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/History +3 -0
- data/MIT-LICENSE +22 -0
- data/README +14 -0
- data/lib/root.rb +268 -0
- data/lib/root/constant.rb +140 -0
- data/lib/root/constant_manifest.rb +126 -0
- data/lib/root/gems.rb +40 -0
- data/lib/root/intern.rb +36 -0
- data/lib/root/manifest.rb +168 -0
- data/lib/root/minimap.rb +88 -0
- data/lib/root/string_ext.rb +57 -0
- data/lib/root/utils.rb +391 -0
- data/lib/root/versions.rb +134 -0
- metadata +72 -0
@@ -0,0 +1,57 @@
|
|
1
|
+
class Root
|
2
|
+
|
3
|
+
# StringExt provides two common string transformations, camelize and
|
4
|
+
# underscore. StringExt is automatically included in String.
|
5
|
+
#
|
6
|
+
# Both methods are directly taken from the ActiveSupport {Inflections}[http://api.rubyonrails.org/classes/ActiveSupport/CoreExtensions/String/Inflections.html]
|
7
|
+
# module. StringExt should not cause conflicts if ActiveSupport is
|
8
|
+
# loaded alongside Root.
|
9
|
+
#
|
10
|
+
# ActiveSupport is distributed with an MIT-LICENSE:
|
11
|
+
#
|
12
|
+
# Copyright (c) 2004-2008 David Heinemeier Hansson
|
13
|
+
#
|
14
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
15
|
+
# associated documentation files (the "Software"), to deal in the Software without restriction,
|
16
|
+
# including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
17
|
+
# and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
|
18
|
+
# subject to the following conditions:
|
19
|
+
#
|
20
|
+
# The above copyright notice and this permission notice shall be included in all copies or substantial
|
21
|
+
# portions of the Software.
|
22
|
+
#
|
23
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
24
|
+
# LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
25
|
+
# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
26
|
+
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
27
|
+
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
28
|
+
#
|
29
|
+
module StringExt
|
30
|
+
|
31
|
+
# camelize converts self to UpperCamelCase. If the argument to
|
32
|
+
# camelize is set to :lower then camelize produces lowerCamelCase.
|
33
|
+
# camelize will also convert '/' to '::' which is useful for
|
34
|
+
# converting paths to namespaces.
|
35
|
+
def camelize(first_letter = :upper)
|
36
|
+
case first_letter
|
37
|
+
when :upper then self.to_s.gsub(/\/(.?)/) { "::" + $1.upcase }.gsub(/(^|_)(.)/) { $2.upcase }
|
38
|
+
when :lower then self.first + camelize[1..-1]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# The reverse of camelize. Makes an underscored, lowercase form
|
43
|
+
# from self. underscore will also change '::' to '/' to convert
|
44
|
+
# namespaces to paths.
|
45
|
+
def underscore
|
46
|
+
self.gsub(/::/, '/').
|
47
|
+
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
|
48
|
+
gsub(/([a-z\d])([A-Z])/,'\1_\2').
|
49
|
+
tr("-", "_").
|
50
|
+
downcase
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
class String # :nodoc:
|
56
|
+
include Root::StringExt
|
57
|
+
end
|
data/lib/root/utils.rb
ADDED
@@ -0,0 +1,391 @@
|
|
1
|
+
require 'root/versions'
|
2
|
+
autoload(:FileUtils, 'fileutils')
|
3
|
+
|
4
|
+
class Root
|
5
|
+
module Utils
|
6
|
+
include Versions
|
7
|
+
|
8
|
+
# Regexp to match a windows-style root filepath.
|
9
|
+
WIN_ROOT_PATTERN = /^[A-z]:\//
|
10
|
+
|
11
|
+
module_function
|
12
|
+
|
13
|
+
# Returns the filepath of path relative to dir. Both dir and path are
|
14
|
+
# expanded before the relative filepath is determined. Returns nil if
|
15
|
+
# the path is not relative to dir.
|
16
|
+
#
|
17
|
+
# relative_filepath('dir', "dir/path/to/file.txt") # => "path/to/file.txt"
|
18
|
+
#
|
19
|
+
def relative_filepath(dir, path, dir_string=Dir.pwd)
|
20
|
+
expanded_dir = File.expand_path(dir, dir_string)
|
21
|
+
expanded_path = File.expand_path(path, dir_string)
|
22
|
+
|
23
|
+
return nil unless expanded_path.index(expanded_dir) == 0
|
24
|
+
|
25
|
+
# use dir.length + 1 to remove a leading '/'. If dir.length + 1 >= expanded.length
|
26
|
+
# as in: relative_filepath('/path', '/path') then the first arg returns nil, and an
|
27
|
+
# empty string is returned
|
28
|
+
expanded_path[(expanded_dir.chomp("/").length + 1)..-1] || ""
|
29
|
+
end
|
30
|
+
|
31
|
+
# Generates a target filepath translated from the source_dir to the
|
32
|
+
# target_dir. Raises an error if the filepath is not relative to the
|
33
|
+
# source_dir.
|
34
|
+
#
|
35
|
+
# translate("/path/to/file.txt", "/path", "/another/path") # => '/another/path/to/file.txt'
|
36
|
+
#
|
37
|
+
def translate(path, source_dir, target_dir)
|
38
|
+
unless relative_path = relative_filepath(source_dir, path)
|
39
|
+
raise ArgumentError, "\n#{path}\nis not relative to:\n#{source_dir}"
|
40
|
+
end
|
41
|
+
File.join(target_dir, relative_path)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Returns the path, exchanging the extension with extname. Extname may
|
45
|
+
# optionally omit the leading period.
|
46
|
+
#
|
47
|
+
# exchange('path/to/file.txt', '.html') # => 'path/to/file.html'
|
48
|
+
# exchange('path/to/file.txt', 'rb') # => 'path/to/file.rb'
|
49
|
+
#
|
50
|
+
def exchange(path, extname)
|
51
|
+
"#{path.chomp(File.extname(path))}#{extname[0] == ?. ? '' : '.'}#{extname}"
|
52
|
+
end
|
53
|
+
|
54
|
+
# Lists all unique paths matching the input glob patterns.
|
55
|
+
def glob(*patterns)
|
56
|
+
patterns.collect do |pattern|
|
57
|
+
Dir.glob(pattern)
|
58
|
+
end.flatten.uniq
|
59
|
+
end
|
60
|
+
|
61
|
+
# Lists all unique versions of path matching the glob version patterns. If
|
62
|
+
# no patterns are specified, then all versions of path will be returned.
|
63
|
+
def vglob(path, *vpatterns)
|
64
|
+
vpatterns << "*" if vpatterns.empty?
|
65
|
+
vpatterns.collect do |vpattern|
|
66
|
+
results = Dir.glob(version(path, vpattern))
|
67
|
+
|
68
|
+
# extra work to include the default version path for any version
|
69
|
+
results << path if vpattern == "*" && File.exists?(path)
|
70
|
+
results
|
71
|
+
end.flatten.uniq
|
72
|
+
end
|
73
|
+
|
74
|
+
# Path suffix glob. Globs along the base paths for paths that match the
|
75
|
+
# specified suffix pattern.
|
76
|
+
def sglob(suffix_pattern, *base_paths)
|
77
|
+
base_paths.collect do |base|
|
78
|
+
base = File.expand_path(base)
|
79
|
+
Dir.glob(File.join(base, suffix_pattern))
|
80
|
+
end.flatten.uniq
|
81
|
+
end
|
82
|
+
|
83
|
+
# Like Dir.chdir but makes the directory, if necessary, when mkdir is
|
84
|
+
# specified. chdir raises an error for non-existant directories, as well
|
85
|
+
# as non-directory inputs.
|
86
|
+
def chdir(dir, mkdir=false, &block)
|
87
|
+
dir = File.expand_path(dir)
|
88
|
+
|
89
|
+
unless File.directory?(dir)
|
90
|
+
if !File.exists?(dir) && mkdir
|
91
|
+
FileUtils.mkdir_p(dir)
|
92
|
+
else
|
93
|
+
raise ArgumentError, "not a directory: #{dir}"
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
Dir.chdir(dir, &block)
|
98
|
+
end
|
99
|
+
|
100
|
+
# Prepares the input path by making the parent directory for path. If a
|
101
|
+
# block is given, a file is created at path and passed to it; in this
|
102
|
+
# way files with non-existant parent directories are readily made.
|
103
|
+
#
|
104
|
+
# Returns path.
|
105
|
+
def prepare(path, &block)
|
106
|
+
dirname = File.dirname(path)
|
107
|
+
FileUtils.mkdir_p(dirname) unless File.exists?(dirname)
|
108
|
+
File.open(path, "w", &block) if block_given?
|
109
|
+
path
|
110
|
+
end
|
111
|
+
|
112
|
+
# The path root type indicating windows, *nix, or some unknown style of
|
113
|
+
# filepaths (:win, :nix, :unknown).
|
114
|
+
def path_root_type
|
115
|
+
@path_root_type ||= case
|
116
|
+
when RUBY_PLATFORM =~ /mswin/ && File.expand_path(".") =~ WIN_ROOT_PATTERN then :win
|
117
|
+
when File.expand_path(".")[0] == ?/ then :nix
|
118
|
+
else :unknown
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# Returns true if the input path appears to be an expanded path, based on
|
123
|
+
# path_root_type.
|
124
|
+
#
|
125
|
+
# If root_type == :win returns true if the path matches WIN_ROOT_PATTERN.
|
126
|
+
#
|
127
|
+
# expanded?('C:/path') # => true
|
128
|
+
# expanded?('c:/path') # => true
|
129
|
+
# expanded?('D:/path') # => true
|
130
|
+
# expanded?('path') # => false
|
131
|
+
#
|
132
|
+
# If root_type == :nix, then expanded? returns true if the path begins
|
133
|
+
# with '/'.
|
134
|
+
#
|
135
|
+
# expanded?('/path') # => true
|
136
|
+
# expanded?('path') # => false
|
137
|
+
#
|
138
|
+
# Otherwise expanded? always returns nil.
|
139
|
+
def expanded?(path, root_type=path_root_type)
|
140
|
+
case root_type
|
141
|
+
when :win
|
142
|
+
path =~ WIN_ROOT_PATTERN ? true : false
|
143
|
+
when :nix
|
144
|
+
path[0] == ?/
|
145
|
+
else
|
146
|
+
nil
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
# Trivial indicates when a path does not have content to load. Returns
|
151
|
+
# true if the file at path is empty, non-existant, a directory, or nil.
|
152
|
+
def trivial?(path)
|
153
|
+
path == nil || !File.file?(path) || File.size(path) == 0
|
154
|
+
end
|
155
|
+
|
156
|
+
# Empty returns true when dir is an existing directory that has no files.
|
157
|
+
def empty?(dir)
|
158
|
+
File.directory?(dir) && (Dir.entries(dir) - ['.', '..']).empty?
|
159
|
+
end
|
160
|
+
|
161
|
+
# Minimizes a set of paths to the set of shortest basepaths that unqiuely
|
162
|
+
# identify the paths. The path extension and versions are removed from
|
163
|
+
# the basepath if possible. For example:
|
164
|
+
#
|
165
|
+
# minimize ['path/to/a.rb', 'path/to/b.rb']
|
166
|
+
# # => ['a', 'b']
|
167
|
+
#
|
168
|
+
# minimize ['path/to/a-0.1.0.rb', 'path/to/b-0.1.0.rb']
|
169
|
+
# # => ['a', 'b']
|
170
|
+
#
|
171
|
+
# minimize ['path/to/file.rb', 'path/to/file.txt']
|
172
|
+
# # => ['file.rb', 'file.txt']
|
173
|
+
#
|
174
|
+
# minimize ['path-0.1/to/file.rb', 'path-0.2/to/file.rb']
|
175
|
+
# # => ['path-0.1/to/file', 'path-0.2/to/file']
|
176
|
+
#
|
177
|
+
# Minimized paths that carry their extension will always carry
|
178
|
+
# their version as well, but the converse is not true; paths
|
179
|
+
# can be minimized to carry just the version and not the path
|
180
|
+
# extension.
|
181
|
+
#
|
182
|
+
# minimize ['path/to/a-0.1.0.rb', 'path/to/a-0.1.0.txt']
|
183
|
+
# # => ['a-0.1.0.rb', 'a-0.1.0.txt']
|
184
|
+
#
|
185
|
+
# minimize ['path/to/a-0.1.0.rb', 'path/to/a-0.2.0.rb']
|
186
|
+
# # => ['a-0.1.0', 'a-0.2.0']
|
187
|
+
#
|
188
|
+
# If a block is given, each (path, mini-path) pair will be passed
|
189
|
+
# to it after minimization.
|
190
|
+
def minimize(paths) # :yields: path, mini_path
|
191
|
+
unless block_given?
|
192
|
+
mini_paths = []
|
193
|
+
minimize(paths) {|p, mp| mini_paths << mp }
|
194
|
+
return mini_paths
|
195
|
+
end
|
196
|
+
|
197
|
+
splits = paths.uniq.collect do |path|
|
198
|
+
extname = File.extname(path)
|
199
|
+
extname = '' if extname =~ /^\.\d+$/
|
200
|
+
base = File.basename(path.chomp(extname))
|
201
|
+
version = base =~ /(-\d+(\.\d+)*)$/ ? $1 : ''
|
202
|
+
|
203
|
+
[dirname_or_array(path), base.chomp(version), extname, version, false, path]
|
204
|
+
end
|
205
|
+
|
206
|
+
while !splits.empty?
|
207
|
+
index = 0
|
208
|
+
splits = splits.collect do |(dir, base, extname, version, flagged, path)|
|
209
|
+
index += 1
|
210
|
+
case
|
211
|
+
when !flagged && just_one?(splits, index, base)
|
212
|
+
|
213
|
+
# found just one
|
214
|
+
yield(path, base)
|
215
|
+
nil
|
216
|
+
when dir.kind_of?(Array)
|
217
|
+
|
218
|
+
# no more path segments to use, try to add
|
219
|
+
# back version and extname
|
220
|
+
if dir.empty?
|
221
|
+
dir << File.dirname(base)
|
222
|
+
base = File.basename(base)
|
223
|
+
end
|
224
|
+
|
225
|
+
case
|
226
|
+
when !version.empty?
|
227
|
+
# add back version (occurs first)
|
228
|
+
[dir, "#{base}#{version}", extname, '', false, path]
|
229
|
+
|
230
|
+
when !extname.empty?
|
231
|
+
|
232
|
+
# add back extension (occurs second)
|
233
|
+
[dir, "#{base}#{extname}", '', version, false, path]
|
234
|
+
else
|
235
|
+
|
236
|
+
# nothing more to distinguish... path is minimized (occurs third)
|
237
|
+
yield(path, min_join(dir[0], base))
|
238
|
+
nil
|
239
|
+
end
|
240
|
+
else
|
241
|
+
|
242
|
+
# shift path segment. dirname_or_array returns an
|
243
|
+
# array if this is the last path segment to shift.
|
244
|
+
[dirname_or_array(dir), min_join(File.basename(dir), base), extname, version, false, path]
|
245
|
+
end
|
246
|
+
end.compact
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
# Returns true if the mini_path matches path. Matching logic reverses
|
251
|
+
# that of minimize:
|
252
|
+
#
|
253
|
+
# * a match occurs when path ends with mini_path
|
254
|
+
# * if mini_path doesn't specify an extension, then mini_path
|
255
|
+
# must only match path up to the path extension
|
256
|
+
# * if mini_path doesn't specify a version, then mini_path
|
257
|
+
# must only match path up to the path basename (minus the
|
258
|
+
# version and extname)
|
259
|
+
#
|
260
|
+
# For example:
|
261
|
+
#
|
262
|
+
# minimal_match?('dir/file-0.1.0.rb', 'file') # => true
|
263
|
+
# minimal_match?('dir/file-0.1.0.rb', 'dir/file') # => true
|
264
|
+
# minimal_match?('dir/file-0.1.0.rb', 'file-0.1.0') # => true
|
265
|
+
# minimal_match?('dir/file-0.1.0.rb', 'file-0.1.0.rb') # => true
|
266
|
+
#
|
267
|
+
# minimal_match?('dir/file-0.1.0.rb', 'file.rb') # => false
|
268
|
+
# minimal_match?('dir/file-0.1.0.rb', 'file-0.2.0') # => false
|
269
|
+
# minimal_match?('dir/file-0.1.0.rb', 'another') # => false
|
270
|
+
#
|
271
|
+
# In matching, partial basenames are not allowed but partial directories
|
272
|
+
# are allowed. Hence:
|
273
|
+
#
|
274
|
+
# minimal_match?('dir/file-0.1.0.txt', 'file') # => true
|
275
|
+
# minimal_match?('dir/file-0.1.0.txt', 'ile') # => false
|
276
|
+
# minimal_match?('dir/file-0.1.0.txt', 'r/file') # => true
|
277
|
+
#
|
278
|
+
def minimal_match?(path, mini_path)
|
279
|
+
extname = non_version_extname(mini_path)
|
280
|
+
version = mini_path =~ /(-\d+(\.\d+)*)#{extname}$/ ? $1 : ''
|
281
|
+
|
282
|
+
match_path = case
|
283
|
+
when !extname.empty?
|
284
|
+
# force full match
|
285
|
+
path
|
286
|
+
when !version.empty?
|
287
|
+
# match up to version
|
288
|
+
path.chomp(non_version_extname(path))
|
289
|
+
else
|
290
|
+
# match up base
|
291
|
+
path.chomp(non_version_extname(path)).sub(/(-\d+(\.\d+)*)$/, '')
|
292
|
+
end
|
293
|
+
|
294
|
+
# key ends with pattern AND basenames of each are equal...
|
295
|
+
# the last check ensures that a full path segment has
|
296
|
+
# been specified
|
297
|
+
match_path[-mini_path.length, mini_path.length] == mini_path && File.basename(match_path) == File.basename(mini_path)
|
298
|
+
end
|
299
|
+
|
300
|
+
# Returns the path segments for the given path, splitting along the path
|
301
|
+
# divider. Root paths are always represented by a string, if only an
|
302
|
+
# empty string.
|
303
|
+
#
|
304
|
+
# os divider example
|
305
|
+
# windows '\' split('C:\path\to\file') # => ["C:", "path", "to", "file"]
|
306
|
+
# *nix '/' split('/path/to/file') # => ["", "path", "to", "file"]
|
307
|
+
#
|
308
|
+
# The path is always expanded relative to the expand_dir; so '.' and
|
309
|
+
# '..' are resolved. However, unless expand_path == true, only the
|
310
|
+
# segments relative to the expand_dir are returned.
|
311
|
+
#
|
312
|
+
# On windows (note that expanding paths allows the use of slashes or
|
313
|
+
# backslashes):
|
314
|
+
#
|
315
|
+
# Dir.pwd # => 'C:/'
|
316
|
+
# split('path\to\..\.\to\file') # => ["C:", "path", "to", "file"]
|
317
|
+
# split('path/to/.././to/file', false) # => ["path", "to", "file"]
|
318
|
+
#
|
319
|
+
# On *nix (or more generally systems with '/' roots):
|
320
|
+
#
|
321
|
+
# Dir.pwd # => '/'
|
322
|
+
# split('path/to/.././to/file') # => ["", "path", "to", "file"]
|
323
|
+
# split('path/to/.././to/file', false) # => ["path", "to", "file"]
|
324
|
+
#
|
325
|
+
def split(path, expand_path=true, expand_dir=Dir.pwd)
|
326
|
+
path = if expand_path
|
327
|
+
File.expand_path(path, expand_dir)
|
328
|
+
else
|
329
|
+
# normalize the path by expanding it, then
|
330
|
+
# work back to the relative filepath as needed
|
331
|
+
expanded_dir = File.expand_path(expand_dir)
|
332
|
+
expanded_path = File.expand_path(path, expand_dir)
|
333
|
+
expanded_path.index(expanded_dir) != 0 ? expanded_path : relative_filepath(expanded_dir, expanded_path)
|
334
|
+
end
|
335
|
+
|
336
|
+
segments = path.scan(/[^\/]+/)
|
337
|
+
|
338
|
+
# add back the root filepath as needed on *nix
|
339
|
+
segments.unshift "" if path[0] == ?/
|
340
|
+
segments
|
341
|
+
end
|
342
|
+
|
343
|
+
# utility method for minimize -- joins the
|
344
|
+
# dir and path, preventing results like:
|
345
|
+
#
|
346
|
+
# "./path"
|
347
|
+
# "//path"
|
348
|
+
#
|
349
|
+
def min_join(dir, path) # :nodoc:
|
350
|
+
case dir
|
351
|
+
when "." then path
|
352
|
+
when "/" then "/#{path}"
|
353
|
+
else "#{dir}/#{path}"
|
354
|
+
end
|
355
|
+
end
|
356
|
+
|
357
|
+
# utility method for minimize -- returns the
|
358
|
+
# dirname of path, or an array if the dirname
|
359
|
+
# is effectively empty.
|
360
|
+
def dirname_or_array(path) # :nodoc:
|
361
|
+
dir = File.dirname(path)
|
362
|
+
case dir
|
363
|
+
when path, '.' then []
|
364
|
+
else dir
|
365
|
+
end
|
366
|
+
end
|
367
|
+
|
368
|
+
# utility method for minimize -- determines if there
|
369
|
+
# is just one of the base in splits, while flagging
|
370
|
+
# all matching entries.
|
371
|
+
def just_one?(splits, index, base) # :nodoc:
|
372
|
+
just_one = true
|
373
|
+
index.upto(splits.length-1) do |i|
|
374
|
+
if splits[i][1] == base
|
375
|
+
splits[i][4] = true
|
376
|
+
just_one = false
|
377
|
+
end
|
378
|
+
end
|
379
|
+
|
380
|
+
just_one
|
381
|
+
end
|
382
|
+
|
383
|
+
# utility method for minimal_match -- returns a non-version
|
384
|
+
# extname, or an empty string if the path ends in a version.
|
385
|
+
def non_version_extname(path) # :nodoc:
|
386
|
+
extname = File.extname(path)
|
387
|
+
extname =~ /^\.\d+$/ ? '' : extname
|
388
|
+
end
|
389
|
+
|
390
|
+
end
|
391
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
class Root
|
2
|
+
|
3
|
+
# Version provides methods for adding, removing, and incrementing versions
|
4
|
+
# at the end of filepaths. Versions are all formatted like:
|
5
|
+
# 'filepath-version.extension'.
|
6
|
+
#
|
7
|
+
module Versions
|
8
|
+
|
9
|
+
# Adds a version to the filepath. Versioned filepaths follow the format:
|
10
|
+
# 'path-version.extension'. If no version is specified, then the
|
11
|
+
# filepath is returned.
|
12
|
+
#
|
13
|
+
# version("path/to/file.txt", 1.0) # => "path/to/file-1.0.txt"
|
14
|
+
#
|
15
|
+
def version(path, version)
|
16
|
+
version = version.to_s.strip
|
17
|
+
if version.empty?
|
18
|
+
path
|
19
|
+
else
|
20
|
+
extname = File.extname(path)
|
21
|
+
path.chomp(extname) + '-' + version + extname
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Increments the version of the filepath by the specified increment.
|
26
|
+
#
|
27
|
+
# increment("path/to/file-1.0.txt", "0.0.1") # => "path/to/file-1.0.1.txt"
|
28
|
+
# increment("path/to/file.txt", 1.0) # => "path/to/file-1.0.txt"
|
29
|
+
#
|
30
|
+
def increment(path, increment)
|
31
|
+
path, version = deversion(path)
|
32
|
+
|
33
|
+
# split the version and increment into integer arrays of equal length
|
34
|
+
increment, version = [increment, version].collect do |vstr|
|
35
|
+
begin
|
36
|
+
vstr.to_s.split(/\./).collect {|v| v.to_i}
|
37
|
+
rescue
|
38
|
+
raise "Bad version or increment: #{vstr}"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
version.concat Array.new(increment.length - version.length, 0) if increment.length > version.length
|
42
|
+
|
43
|
+
# add the increment to version
|
44
|
+
0.upto(version.length-1) do |i|
|
45
|
+
version[i] += (increment[i] || 0)
|
46
|
+
end
|
47
|
+
|
48
|
+
self.version(path, version.join("."))
|
49
|
+
end
|
50
|
+
|
51
|
+
# Splits the version from the input path, then returns the path and
|
52
|
+
# version. If no version is specified, then the returned version will be
|
53
|
+
# nil.
|
54
|
+
#
|
55
|
+
# deversion("path/to/file-1.0.txt") # => ["path/to/file.txt", "1.0"]
|
56
|
+
# deversion("path/to/file.txt") # => ["path/to/file.txt", nil]
|
57
|
+
#
|
58
|
+
def deversion(path)
|
59
|
+
extname = File.extname(path)
|
60
|
+
extname = '' if extname =~ /^\.\d+$/
|
61
|
+
path =~ /^(.*)-(\d(\.?\d)*)#{extname}$/ ? [$1 + extname, $2] : [path, nil]
|
62
|
+
end
|
63
|
+
|
64
|
+
# A <=> comparison for versions. compare_versions can take strings,
|
65
|
+
# integers, or even arrays representing the parts of a version.
|
66
|
+
#
|
67
|
+
# compare_versions("1.0.0", "0.9.9") # => 1
|
68
|
+
# compare_versions(1.1, 1.1) # => 0
|
69
|
+
# compare_versions([0,9], [0,9,1]) # => -1
|
70
|
+
def compare_versions(a,b)
|
71
|
+
a, b = [a,b].collect {|item| to_integer_array(item) }
|
72
|
+
|
73
|
+
# equalize the lengths of the integer arrays
|
74
|
+
d = b.length - a.length
|
75
|
+
case
|
76
|
+
when d < 0 then b.concat Array.new(-d, 0)
|
77
|
+
when d > 0 then a.concat Array.new(d, 0)
|
78
|
+
end
|
79
|
+
|
80
|
+
a <=> b
|
81
|
+
end
|
82
|
+
|
83
|
+
# Version unique. Select the latest or earliest versions of each file
|
84
|
+
# in the array. For paths that have no version, vniq considers any
|
85
|
+
# version to beat no version. The order of paths is preserved by
|
86
|
+
# default, but the extra sort doing so may be turned off.
|
87
|
+
#
|
88
|
+
# paths = [
|
89
|
+
# "/path/to/two-0.0.1.txt",
|
90
|
+
# "/path/to/one-0.0.1.txt",
|
91
|
+
# "/path/to/one.txt",
|
92
|
+
# "/path/to/two-1.0.1.txt",
|
93
|
+
# "/path/to/three.txt"]
|
94
|
+
#
|
95
|
+
# vniq(paths)
|
96
|
+
# # => [
|
97
|
+
# # "/path/to/one-0.0.1.txt",
|
98
|
+
# # "/path/to/two-1.0.1.txt",
|
99
|
+
# # "/path/to/three.txt"]
|
100
|
+
#
|
101
|
+
def vniq(array, earliest=false, preserve_order=true)
|
102
|
+
unique = {}
|
103
|
+
array.sort.each do |path|
|
104
|
+
base, version = deversion(path)
|
105
|
+
(unique[base] ||= []) << version
|
106
|
+
end
|
107
|
+
|
108
|
+
results = []
|
109
|
+
unique.each_pair do |base, versions|
|
110
|
+
versions = versions.sort {|a, b| compare_versions(a,b) }
|
111
|
+
winner = earliest ? versions.shift : versions.pop
|
112
|
+
results << version(base, winner)
|
113
|
+
end
|
114
|
+
|
115
|
+
results = results.sort_by do |path|
|
116
|
+
array.index(path)
|
117
|
+
end if preserve_order
|
118
|
+
|
119
|
+
results
|
120
|
+
end
|
121
|
+
|
122
|
+
private
|
123
|
+
|
124
|
+
# Converts an input argument (typically a string or an array) to an
|
125
|
+
# array of integers. Splits version string on "."
|
126
|
+
def to_integer_array(arg)
|
127
|
+
arr = case arg
|
128
|
+
when Array then arg
|
129
|
+
else arg.to_s.split('.')
|
130
|
+
end
|
131
|
+
arr.collect {|i| i.to_i}
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|