root 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|