ki-repo 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,483 @@
1
+ # encoding: UTF-8
2
+
3
+ # Copyright 2012 Mikko Apo
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ module Ki
18
+
19
+ # Builds and updates ki-metada.json file based on parameters and added files
20
+ # @see VersionMetadataFile
21
+ class BuildVersionMetadataFile
22
+ attr_chain :input_dir, -> { Dir.pwd }
23
+ attr_chain :metadata_file, -> { VersionMetadataFile.new("ki-version.json") }
24
+ attr_chain :source_parameters, -> { Hash.new }
25
+ attr_chain :default_parameters, -> { {"hashes" => ["sha1"], "tags" => []} }
26
+ attr_chain :previous_dep, :require => "Define a dependency before -o or --operation"
27
+ attr_chain :shell_command, :require
28
+
29
+ def execute(ctx, args)
30
+ # opts.parse parses input parameters and fills in configuration parameters
31
+ files = opts.parse(args)
32
+ if source_parameters.size > 0
33
+ metadata_file.source(source_parameters)
34
+ end
35
+ # adds files to metadata and fills in parameters
36
+ metadata_file.add_files(input_dir, files, default_parameters)
37
+ metadata_file.save
38
+ end
39
+
40
+ def help
41
+ <<EOF
42
+ "#{shell_command}" can be used to generate version metadata files. Version metadata files
43
+ contain information about files (size, permission bits, hash checksums), version origins
44
+ and dependencies.
45
+
46
+ After version metadata file is ready, it can be imported to repository using version-import.
47
+
48
+ ### Usage
49
+
50
+ #{shell_command} <parameters> file_pattern1*.* file_pattern2*.*
51
+
52
+ ### Examples
53
+
54
+ #{shell_command} test.sh
55
+ #{shell_command} readme* -t doc
56
+ #{shell_command} -d my/component/1,name=comp,path=doc,internal -O "mv doc/test.sh helloworld.sh"
57
+ ki version-import
58
+
59
+ ### Parameters
60
+ #{opts}
61
+ EOF
62
+ end
63
+
64
+ def summary
65
+ "Create version metadata file"
66
+ end
67
+
68
+ def opts
69
+ OptionParser.new do |opts|
70
+ opts.banner = ""
71
+ opts.on("-f", "--file FILE", "Version file target") do |v|
72
+ if !defined? @input_dir
73
+ input_dir(File.dirname(v))
74
+ end
75
+ metadata_file.init_from_path(v)
76
+ end
77
+ opts.on("-i", "--input-directory INPUT-DIR", "Input directory") do |v|
78
+ input_dir(v)
79
+ end
80
+ opts.on("-v", "--version-id VERSION-ID", "Version's id") do |v|
81
+ metadata_file.version_id=v
82
+ end
83
+ ["url", "tag-url", "author", "repotype"].each do |source_param|
84
+ opts.on("--source-#{source_param} #{source_param.upcase}", "Build source parameter #{source_param}") do |v|
85
+ source_parameters[source_param]=v
86
+ end
87
+ end
88
+ opts.on("-t", "--tags TAGS", "Tag files with keywords") do |v|
89
+ default_parameters["tags"]= v.split(",").sort
90
+ end
91
+ hash_prefix = "/hashing"
92
+ hashes = KiCommand::KiExtensions.find(hash_prefix).map { |k, v| k[hash_prefix.size+1..-1] }
93
+ opts.on("--hashes HASHES", "Calculate checksums using defined hash algos. Default: sha1. Available: #{hashes.join(", ")}") do |v|
94
+ default_parameters["hashes"]= v.split(",").sort
95
+ end
96
+ opts.on("-d", "--dependency DEPENDENCY", "Dependency definition my/component/123[,name=AA][,path=aa][,internal]") do |v|
97
+ previous_dep(metadata_file.add_dependency(v))
98
+ end
99
+ opts.on("-o", "--operation OP", "Add operation to previous dependency") do |v|
100
+ previous_dep.add_operation(v.split(" "))
101
+ end
102
+ opts.on("-O", "--version-operation OP", "Add operation to version") do |v|
103
+ metadata_file.add_operation(v.split(" "))
104
+ end
105
+ end
106
+ end
107
+ end
108
+
109
+ # Tests version from repository or metadata file
110
+ # @see VersionTester
111
+ class TestVersion
112
+ attr_chain :shell_command, :require
113
+
114
+ def execute(ctx, args)
115
+ @tester = VersionTester.new.recursive(false).print(true)
116
+ ver_strs = opts.parse(args)
117
+ if ver_strs.size > 0 || @tester.recursive
118
+ @tester.ki_home(ctx.ki_home)
119
+ versions = ver_strs.map { |v| ctx.ki_home.version(v) }
120
+ else
121
+ versions = []
122
+ end
123
+ if @file
124
+ versions.unshift Version.create_version(@file, @input_dir)
125
+ end
126
+ all_ok = true
127
+ versions.each do |v|
128
+ all_ok = all_ok && @tester.test_version(v)
129
+ end
130
+ if all_ok
131
+ puts "All files ok."
132
+ end
133
+ end
134
+
135
+ def help
136
+ <<EOF
137
+ "#{shell_command}" tests versions, their files and their dependencies. Can also test version that has not been imported yet.
138
+
139
+ ### Examples
140
+
141
+ #{shell_command} -r my/product other/product
142
+ #{shell_command} -f ki-version.json -i file-directory
143
+
144
+ ### Parameters
145
+ #{opts}
146
+ EOF
147
+ end
148
+
149
+ def summary
150
+ "Tests versions and their dependencies"
151
+ end
152
+
153
+ def opts
154
+ OptionParser.new do |opts|
155
+ opts.banner = ""
156
+ opts.on("-f", "--file FILE", "Version source file. By default uses file's directory as source for binary files.'") do |v|
157
+ if @input_dir.nil?
158
+ dir = File.dirname(v)
159
+ @input_dir = dir != "." ? dir : Dir.pwd
160
+ end
161
+ @file = v
162
+ end
163
+ opts.on("-i", "--input-directory INPUT-DIR", "Binary file input directory") do |v|
164
+ @input_dir = v
165
+ end
166
+ opts.on("-r", "--recursive", "Tests version's dependencies also.'") do |v|
167
+ @tester.recursive = true
168
+ end
169
+ end
170
+ end
171
+ end
172
+
173
+ # Imports version and its files to repository
174
+ # @see VersionImporter
175
+ class ImportVersion
176
+ attr_chain :input_dir, -> { Dir.pwd }
177
+ attr_chain :file, -> { File.join(input_dir, "ki-version.json") }
178
+ attr_chain :importer, -> {}
179
+ attr_chain :shell_command, :require
180
+
181
+ def execute(ctx, args)
182
+ @importer = VersionImporter.new
183
+ opts.parse(args)
184
+ @importer.ki_home(ctx.ki_home).import(file, input_dir)
185
+ end
186
+
187
+ def help
188
+ <<EOF
189
+ "#{shell_command}" imports version and its files to repository.
190
+
191
+ Version name can be defined either during "version-build",
192
+ or generated automatically for component at import (with -c my/component) or defined to be a specific version (-v).
193
+ Can also move files (-m), test dependencies before import (-t).
194
+
195
+ ### Examples
196
+
197
+ #{shell_command} -m -t -c my/product
198
+ #{shell_command} -f ki-version.json -i file-directory
199
+
200
+ ### Parameters
201
+ #{opts}
202
+ EOF
203
+ end
204
+
205
+ def summary
206
+ "Imports version metadata and files to repository"
207
+ end
208
+
209
+ def opts
210
+ OptionParser.new do |opts|
211
+ opts.banner = ""
212
+ opts.on("-f", "--file FILE", "Version source file. By default uses file's directory as source for binary files.'") do |v|
213
+ if !defined? @input_dir
214
+ input_dir(File.dirname(v))
215
+ end
216
+ file(v)
217
+ end
218
+ opts.on("-i", "--input-directory INPUT-DIR", "Input directory") do |v|
219
+ input_dir(v)
220
+ end
221
+ opts.on("-t", "--test-recursive", "Tests version's dependencies before importing.'") do |v|
222
+ @importer.tester.recursive = true
223
+ end
224
+ opts.on("-m", "--move", "Moves files to repository'") do |v|
225
+ @importer.move_files = true
226
+ end
227
+ opts.on("-c", "--create-new-version COMPONENT", "Creates new version number for defined component'") do |c|
228
+ @importer.create_new_version = c
229
+ end
230
+ opts.on("-v", "--version-id VERSION", "Imports version with defined version id'") do |v|
231
+ @importer.specific_version_id = v
232
+ end
233
+ end
234
+ end
235
+ end
236
+
237
+ # Exports version from repository to target directory
238
+ # @see VersionExporter
239
+ class ExportVersion
240
+ attr_chain :out, -> { Dir.pwd }
241
+ attr_chain :shell_command, :require
242
+
243
+ def execute(ctx, args)
244
+ @exporter = VersionExporter.new
245
+ file_patterns = opts.parse(args)
246
+ version = file_patterns.delete_at(0)
247
+ @exporter.find_files.files(file_patterns)
248
+ @exporter.ki_home(ctx.ki_home).export(version, out)
249
+ end
250
+
251
+ def help
252
+ <<EOF
253
+ "#{shell_command}" exports version and its dependencies to target directory.
254
+
255
+ ### Usage
256
+
257
+ #{shell_command} <parameters> <file_export_pattern*.*>
258
+
259
+ ### Examples
260
+
261
+ #{shell_command} -o export-dir --tags -c bin my/product
262
+ #{shell_command} -o scripts -c -t my/admin-tools '*.sh'
263
+
264
+ ### Parameters
265
+ #{opts}
266
+ EOF
267
+ end
268
+
269
+ def summary
270
+ "Export version to a directory"
271
+ end
272
+
273
+ def opts
274
+ OptionParser.new do |opts|
275
+ opts.banner = ""
276
+ opts.on("-o", "--output-directory INPUT-DIR", "Input directory") do |v|
277
+ out(v)
278
+ end
279
+ opts.on("--tags TAGS", "Select files with matching tag") do |v|
280
+ @exporter.find_files.tags(v.split(","))
281
+ end
282
+ opts.on("-t", "--test", "Test version before export") do |v|
283
+ @exporter.test_dependencies=true
284
+ end
285
+ opts.on("-c", "--copy", "Exported files are copied instead of linked") do |v|
286
+ @exporter.copy=true
287
+ end
288
+ end
289
+ end
290
+ end
291
+
292
+ # Sets status for version
293
+ class VersionStatus
294
+ attr_chain :shell_command, :require
295
+
296
+ def execute(ctx, args)
297
+ @repository = "local"
298
+ command = args.delete_at(0)
299
+ case command
300
+ when "add"
301
+ version, key_value, *rest = args
302
+ key, value = key_value.split("=")
303
+ flags = rest.to_h("=")
304
+ repository = ctx.ki_home.repository(@repository)
305
+ repository.version(version).statuses.add_status(key, value, flags)
306
+ when "order"
307
+ component, key, values_str = args
308
+ repository = ctx.ki_home.repository(@repository)
309
+ repository.component(component).status_info.edit_data do |info|
310
+ info.cached_data[key]=values_str.split(",")
311
+ end
312
+ else
313
+ raise "Not supported '#{command}'"
314
+ end
315
+ end
316
+
317
+ def help
318
+ <<EOF
319
+ "#{shell_command}" sets status values to versions and sets status value order to component.
320
+
321
+ Status value order is used to determine which statuses match version queries:
322
+
323
+ my/component:maturity>alpha
324
+
325
+ ### Examples
326
+
327
+ #{shell_command} add my/component/1.2.3 Smoke=Green action=path/123
328
+ #{shell_command} order my/component maturity alpha,beta,gamma
329
+ EOF
330
+ end
331
+
332
+ def summary
333
+ "Add status values to version"
334
+ end
335
+ end
336
+
337
+ # Shows information about a version
338
+ class ShowVersion
339
+ attr_chain :shell_command, :require
340
+
341
+ def execute(ctx, args)
342
+ finder = ctx.ki_home.finder
343
+ versions = opts.parse(args).map { |v| finder.version(v) }
344
+ if @file
345
+ versions.unshift Version.create_version(@file, @input_dir)
346
+ end
347
+ versions.each do |ver|
348
+ VersionIterator.new.finder(finder).version(ver).iterate_versions do |version|
349
+ metadata = version.metadata
350
+ puts "Version: #{metadata.version_id}"
351
+ if metadata.source.size > 0
352
+ puts "Source: #{map_to_csl(metadata.source)}"
353
+ end
354
+ if metadata.dependencies.size > 0
355
+ puts "Dependencies(#{metadata.dependencies.size}):"
356
+ metadata.dependencies.each do |dep|
357
+ dep_data = dep.dup
358
+ dep_ops = dep_data.delete("operations")
359
+ puts "#{dep_data.delete("version_id")}: #{map_to_csl(dep_data)}"
360
+ if dep_ops && dep_ops.size > 0
361
+ puts "Depedency operations:"
362
+ dep_ops.each do |op|
363
+ puts op.join(" ")
364
+ end
365
+ end
366
+ end
367
+ end
368
+ if metadata.files.size > 0
369
+ puts "Files(#{metadata.files.size}):"
370
+ metadata.files.each do |file|
371
+ file_data = file.dup
372
+ puts "#{file_data.delete("path")} - size: #{file_data.delete("size")}, #{map_to_csl(file_data)}"
373
+ end
374
+ end
375
+ if metadata.operations.size > 0
376
+ puts "Version operations(#{metadata.operations.size}):"
377
+ metadata.operations.each do |op|
378
+ puts op.join(" ")
379
+ end
380
+ end
381
+ if @dirs
382
+ puts "Version directories: #{version.versions.map { |v| v.path }.join(", ")}"
383
+ end
384
+ if !@recursive
385
+ break
386
+ end
387
+ end
388
+ end
389
+ end
390
+
391
+ def map_to_csl(map)
392
+ map.sort.map { |k, v| "#{k}=#{Array.wrap(v).join(",")}" }.join(", ")
393
+ end
394
+
395
+ def help
396
+ <<EOF
397
+ "#{shell_command}" prints information about version or versions and their dependencies
398
+
399
+ ### Examples
400
+
401
+ #{shell_command} -r -d my/component/23 my/product/127
402
+ #{shell_command} -f ki-version.json -i binary-dir
403
+ EOF
404
+ end
405
+
406
+ def summary
407
+ "Prints information about version or versions"
408
+ end
409
+
410
+ def opts
411
+ OptionParser.new do |opts|
412
+ opts.banner = ""
413
+ opts.on("-r", "--recursive", "Shows version's dependencies.") do |v|
414
+ @recursive = true
415
+ end
416
+ opts.on("-d", "--dirs", "Shows version's directories.") do |v|
417
+ @dirs = true
418
+ end
419
+ opts.on("-f", "--file FILE", "Version source file. By default uses file's directory as source for binary files.") do |v|
420
+ if @input_dir.nil?
421
+ dir = File.dirname(v)
422
+ @input_dir = dir != "." ? dir : Dir.pwd
423
+ end
424
+ @file = v
425
+ end
426
+ opts.on("-i", "--input-directory INPUT-DIR", "Binary file input directory") do |v|
427
+ @input_dir = v
428
+ end
429
+ end
430
+ end
431
+ end
432
+
433
+ # Sets status for version
434
+ class VersionSearch
435
+ attr_chain :shell_command, :require
436
+
437
+ def execute(ctx, args)
438
+ finder = ctx.ki_home.finder
439
+ args.each do |arg|
440
+ version = finder.version(arg)
441
+ if version
442
+ puts version.version_id
443
+ else
444
+ matcher = FileRegexp.matcher(arg)
445
+ found_components = finder.components.keys.select { |name| matcher.match(name) }
446
+ if found_components.size > 0
447
+ puts "Found components(#{found_components.size}):"
448
+ puts found_components.join("\n")
449
+ else
450
+ puts "'#{arg}' does not match versions or components"
451
+ end
452
+ end
453
+ end
454
+ end
455
+
456
+ def help
457
+ <<EOF
458
+ "#{shell_command}" searches for versions and components.
459
+
460
+ ### Examples
461
+
462
+ #{shell_command} my/component
463
+ #{shell_command} my/*
464
+ EOF
465
+ end
466
+
467
+ def summary
468
+ "Searches for versions and components"
469
+ end
470
+ end
471
+
472
+ KiCommand.register_cmd("version-build", BuildVersionMetadataFile)
473
+ KiCommand.register_cmd("version-test", TestVersion)
474
+ KiCommand.register_cmd("version-import", ImportVersion)
475
+ KiCommand.register_cmd("version-export", ExportVersion)
476
+ KiCommand.register_cmd("version-status", VersionStatus)
477
+ KiCommand.register_cmd("version-show", ShowVersion)
478
+ KiCommand.register_cmd("version-search", VersionSearch)
479
+ KiCommand.register("/hashing/sha1", SHA1)
480
+ KiCommand.register("/hashing/sha2", SHA2)
481
+ KiCommand.register("/hashing/md5", MD5)
482
+
483
+ end
@@ -0,0 +1,200 @@
1
+ # encoding: UTF-8
2
+
3
+ # Copyright 2012 Mikko Apo
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ module Ki
18
+ # Collects components from multiple repositories and provides methods to find components and versions
19
+ # @see Component
20
+ # @see Version
21
+ class RepositoryFinder
22
+ attr_reader :versions
23
+ attr_reader :components
24
+
25
+ def initialize(source)
26
+ @source = source
27
+ @components = load_all_components
28
+ @versions = HashCache.new
29
+ end
30
+
31
+ # Finds matching component by name
32
+ # @return [Component] matching component
33
+ def component(component)
34
+ @components[component]
35
+ end
36
+
37
+ # Finds version matching arguments, goes through versions in chronological order from latest to oldest.
38
+ # * note: block form can be used to iterate through all available version
39
+ # Supported search formats:
40
+ # * version("test/comp") -> latest version
41
+ # * version("test/comp:Smoke=green"), version("test/comp","Smoke"=>"green") -> latest version with matching status
42
+ # * version("test/comp"){|version| ... } -> iterates all available versions and returns latest version where block returns true, block argument is a Version
43
+ # * version("test/comp:maturity!=alpha") ->
44
+ # Component can define ordering for status values: {"maturity": ["alpha","beta","gamma"]}
45
+ # * version("test/comp:maturity>alpha") -> returns first version where maturity is beta or gamma
46
+ # * version("test/comp:maturity>=alpha") -> returns first version where maturity is alpha, beta or gamma
47
+ # * version("test/comp:maturity<beta") -> returns first version where maturity is alpha
48
+ # * version("test/comp:maturity<=beta") -> returns first version where maturity is alpha or beta
49
+ # Version supports also Component and Version parameters:
50
+ # * version(my_version) -> returns my_version
51
+ # * version(my_component, "Smoke:green") -> finds Version matching other parameters
52
+ # @return [Version] matching version
53
+ def version(*args, &block)
54
+ if args.empty?
55
+ raise "no parameters!"
56
+ end
57
+ status_rules = []
58
+ component_or_version = nil
59
+ dep_navigation_arr = []
60
+ args.each do |str|
61
+ if str.kind_of?(String)
62
+ dep_nav_arr = str.split("->")
63
+ strings = dep_nav_arr.delete_at(0).split(":")
64
+ dep_navigation_arr.concat(dep_nav_arr)
65
+ if component_or_version.nil?
66
+ component_or_version = strings.delete_at(0)
67
+ end
68
+ strings.each do |s|
69
+ status_rules << s.match(/(.*?)([<=>!]+)(.*)/).captures
70
+ end
71
+ elsif str.kind_of?(Hash)
72
+ str.each_pair do |k, v|
73
+ status_rules << [k, "=", v]
74
+ end
75
+ elsif str.kind_of?(Version)
76
+ return str
77
+ elsif str.kind_of?(Component) && component_or_version.nil?
78
+ component_or_version = str.component_id
79
+ else
80
+ raise "Not supported '#{str.inspect}'"
81
+ end
82
+ end
83
+ if component = @components[component_or_version]
84
+ if status_rules.size > 0 || block
85
+ component.versions.each do |v|
86
+ ver = component.version_by_id(v.name)
87
+ ok = has_statuses(ver.statuses, status_rules, component)
88
+ if ok && block
89
+ ok = block.call(ver)
90
+ end
91
+ if ok
92
+ return ver
93
+ end
94
+ end
95
+ else
96
+ # picking latest version
97
+ version_name = component.versions.first.name
98
+ end
99
+ else
100
+ # user has defined an exact version
101
+ version_arr = component_or_version.split("/")
102
+ version_name = version_arr.delete_at(-1)
103
+ component_str = version_arr.join("/")
104
+ component = @components[component_str]
105
+ end
106
+ if component && version_name
107
+ ver = component.version_by_id(version_name)
108
+ if dep_navigation_arr
109
+ dep_navigation_arr.each do |dep_str|
110
+ dep_version_str = find_dep_by_name(ver, dep_str)
111
+ if dep_version_str.nil?
112
+ raise "Could not locate dependency '#{dep_str}' from '#{ver.version_id}'"
113
+ end
114
+ ver = version(dep_version_str)
115
+ end
116
+ end
117
+ ver
118
+ else
119
+ nil
120
+ end
121
+ end
122
+
123
+ def find_dep_by_name(ver, dep_str)
124
+ ver.metadata.dependencies.each do |dep|
125
+ if dep["name"] == dep_str
126
+ return dep["version_id"]
127
+ end
128
+ end
129
+ nil
130
+ end
131
+
132
+ def all_repositories(source=@source)
133
+ node = source
134
+ repositories = []
135
+ while (node)
136
+ repositories.concat(node.repositories.to_a)
137
+ if node.root?
138
+ break
139
+ end
140
+ node = node.parent
141
+ end
142
+ repositories
143
+ end
144
+
145
+ # Loads all Component from all repositories
146
+ # @param [KiHome] source
147
+ def load_all_components(source=@source)
148
+ components = HashCache.new
149
+ all_repositories(source).each do |info|
150
+ info.components.each do |component_info|
151
+ component = components.cache(component_info.component_id) do
152
+ Component.new.component_id(component_info.component_id).finder(self).components([])
153
+ end
154
+ component.components << component_info
155
+ end
156
+ end
157
+ components
158
+ end
159
+
160
+ # locates first matching status for the key and checks if that is ok for the block
161
+ def check_status_value(version_statuses, key, &block)
162
+ version_statuses.each do |status_key, status_value|
163
+ if status_key == key
164
+ return block.call(status_value)
165
+ end
166
+ end
167
+ false
168
+ end
169
+
170
+ # Checks if version's statuses match status_rules
171
+ def has_statuses(version_statuses_original, status_rules, component)
172
+ ret = true
173
+ if status_rules.size > 0
174
+ ret = false
175
+ version_statuses = version_statuses_original.reverse # latest first
176
+ status_info = component.status_info
177
+ # go through each rule and see if this version has matching status
178
+ status_rules.each do |key, op, value|
179
+ if order = status_info[key]
180
+ rule_value_index = order.index(value)
181
+ end
182
+ op_action = {
183
+ "=" => ->(status_value) { status_value == value },
184
+ "!=" => ->(status_value) { status_value != value },
185
+ "<" => ->(status_value) { order.index(status_value) < rule_value_index },
186
+ ">" => ->(status_value) { order.index(status_value) > rule_value_index },
187
+ ">=" => ->(status_value) { order.index(status_value) >= rule_value_index },
188
+ "<=" => ->(status_value) { order.index(status_value) <= rule_value_index }
189
+ }.fetch(op) do
190
+ raise "Not supported status operation: '#{key}#{op}#{value}'"
191
+ end
192
+ ret = check_status_value(version_statuses, key) do |status_value|
193
+ op_action.call(status_value)
194
+ end
195
+ end
196
+ end
197
+ ret
198
+ end
199
+ end
200
+ end