aws-cft-tools 0.1.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.
- checksums.yaml +7 -0
- data/.editorconfig +10 -0
- data/.gitignore +52 -0
- data/.rspec +2 -0
- data/.rubocop.yml +19 -0
- data/.travis.yml +5 -0
- data/.yardopts +1 -0
- data/BEST-PRACTICES.md +136 -0
- data/CONTRIBUTING.md +38 -0
- data/Gemfile +8 -0
- data/LICENSE +15 -0
- data/README.md +118 -0
- data/Rakefile +17 -0
- data/USAGE.adoc +404 -0
- data/aws-cft-tools.gemspec +53 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/code.json +24 -0
- data/exe/aws-cft +176 -0
- data/lib/aws-cft-tools.rb +3 -0
- data/lib/aws_cft_tools.rb +31 -0
- data/lib/aws_cft_tools/aws_enumerator.rb +55 -0
- data/lib/aws_cft_tools/change.rb +66 -0
- data/lib/aws_cft_tools/client.rb +84 -0
- data/lib/aws_cft_tools/client/base.rb +40 -0
- data/lib/aws_cft_tools/client/cft.rb +93 -0
- data/lib/aws_cft_tools/client/cft/changeset_management.rb +109 -0
- data/lib/aws_cft_tools/client/cft/stack_management.rb +85 -0
- data/lib/aws_cft_tools/client/ec2.rb +136 -0
- data/lib/aws_cft_tools/client/templates.rb +84 -0
- data/lib/aws_cft_tools/deletion_change.rb +43 -0
- data/lib/aws_cft_tools/dependency_tree.rb +109 -0
- data/lib/aws_cft_tools/dependency_tree/nodes.rb +71 -0
- data/lib/aws_cft_tools/dependency_tree/variables.rb +37 -0
- data/lib/aws_cft_tools/errors.rb +25 -0
- data/lib/aws_cft_tools/runbook.rb +166 -0
- data/lib/aws_cft_tools/runbook/report.rb +30 -0
- data/lib/aws_cft_tools/runbooks.rb +16 -0
- data/lib/aws_cft_tools/runbooks/common/changesets.rb +30 -0
- data/lib/aws_cft_tools/runbooks/common/templates.rb +38 -0
- data/lib/aws_cft_tools/runbooks/deploy.rb +107 -0
- data/lib/aws_cft_tools/runbooks/deploy/reporting.rb +50 -0
- data/lib/aws_cft_tools/runbooks/deploy/stacks.rb +109 -0
- data/lib/aws_cft_tools/runbooks/deploy/templates.rb +37 -0
- data/lib/aws_cft_tools/runbooks/deploy/threading.rb +37 -0
- data/lib/aws_cft_tools/runbooks/diff.rb +28 -0
- data/lib/aws_cft_tools/runbooks/diff/context.rb +86 -0
- data/lib/aws_cft_tools/runbooks/diff/context/reporting.rb +87 -0
- data/lib/aws_cft_tools/runbooks/hosts.rb +43 -0
- data/lib/aws_cft_tools/runbooks/images.rb +43 -0
- data/lib/aws_cft_tools/runbooks/init.rb +86 -0
- data/lib/aws_cft_tools/runbooks/retract.rb +69 -0
- data/lib/aws_cft_tools/runbooks/retract/templates.rb +44 -0
- data/lib/aws_cft_tools/runbooks/stacks.rb +43 -0
- data/lib/aws_cft_tools/stack.rb +83 -0
- data/lib/aws_cft_tools/template.rb +177 -0
- data/lib/aws_cft_tools/template/dsl_context.rb +14 -0
- data/lib/aws_cft_tools/template/file_system.rb +62 -0
- data/lib/aws_cft_tools/template/metadata.rb +144 -0
- data/lib/aws_cft_tools/template/properties.rb +129 -0
- data/lib/aws_cft_tools/template_set.rb +120 -0
- data/lib/aws_cft_tools/template_set/array_methods.rb +63 -0
- data/lib/aws_cft_tools/template_set/closure.rb +77 -0
- data/lib/aws_cft_tools/template_set/dependencies.rb +55 -0
- data/lib/aws_cft_tools/template_set/each_slice_state.rb +58 -0
- data/lib/aws_cft_tools/version.rb +8 -0
- data/rubycritic.reek +3 -0
- metadata +321 -0
@@ -0,0 +1,129 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AwsCftTools
|
4
|
+
class Template
|
5
|
+
##
|
6
|
+
# Simple properties of templates.
|
7
|
+
#
|
8
|
+
module Properties
|
9
|
+
##
|
10
|
+
# Returns the list of environments allowed for the +Environment+ parameter.
|
11
|
+
#
|
12
|
+
# @return [Array<String>]
|
13
|
+
#
|
14
|
+
def allowed_environments
|
15
|
+
template.dig('Parameters', 'Environment', 'AllowedValues') || []
|
16
|
+
end
|
17
|
+
|
18
|
+
def environment?(value)
|
19
|
+
allowed_environments.include?(value)
|
20
|
+
end
|
21
|
+
|
22
|
+
##
|
23
|
+
# Returns the parameter defaults for the template.
|
24
|
+
#
|
25
|
+
def default_parameters
|
26
|
+
(template['Parameters'] || []).each_with_object({}) do |param, hash|
|
27
|
+
hash[param.first] = param.last['Default']
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
##
|
32
|
+
# Returns the role of the template as specified in the template metadata.
|
33
|
+
#
|
34
|
+
# @return [String]
|
35
|
+
#
|
36
|
+
def role
|
37
|
+
template.dig('Metadata', 'Role')
|
38
|
+
end
|
39
|
+
|
40
|
+
def role?(value)
|
41
|
+
!value || role == value
|
42
|
+
end
|
43
|
+
|
44
|
+
##
|
45
|
+
# Returns the list of regions in which the template is allowed.
|
46
|
+
#
|
47
|
+
# @note The region in which a template is deployed is available as the +AWS::Region+ pseudo-parameter.
|
48
|
+
#
|
49
|
+
# @return [Array<String>]
|
50
|
+
#
|
51
|
+
def allowed_regions
|
52
|
+
template.dig('Metadata', 'Region') || []
|
53
|
+
end
|
54
|
+
|
55
|
+
def region?(region)
|
56
|
+
!region || allowed_regions.empty? || allowed_regions.include?(region)
|
57
|
+
end
|
58
|
+
|
59
|
+
##
|
60
|
+
# Returns any templates on which this template has an explicit dependency.
|
61
|
+
#
|
62
|
+
# These explicit dependencies are combined with any dependencies implied by imported values.
|
63
|
+
#
|
64
|
+
def template_dependencies
|
65
|
+
template.dig('Metadata', 'DependsOn', 'Templates') || []
|
66
|
+
end
|
67
|
+
|
68
|
+
##
|
69
|
+
# lists the exported values from the template
|
70
|
+
#
|
71
|
+
# Note that this does substitutions of any references to template parameters.
|
72
|
+
#
|
73
|
+
# @return [Array<String>]
|
74
|
+
#
|
75
|
+
def outputs
|
76
|
+
(template['Outputs'] || []).map do |_, output|
|
77
|
+
with_substitutions(output_name(output.dig('Export', 'Name')))
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
##
|
82
|
+
# lists the imports expected by the template
|
83
|
+
#
|
84
|
+
# Note that this does substitutions of any references to template parameters.
|
85
|
+
#
|
86
|
+
# @return [Array<String>]
|
87
|
+
#
|
88
|
+
def inputs
|
89
|
+
(template['Resources'] || {})
|
90
|
+
.values
|
91
|
+
.flat_map { |resource| pull_imports(resource['Properties'] || {}) }
|
92
|
+
.uniq
|
93
|
+
.map(&method(:with_substitutions))
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
def output_name(name)
|
99
|
+
if name.is_a?(Hash)
|
100
|
+
name['Sub'] || name['Fn::Sub']
|
101
|
+
else
|
102
|
+
name
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def substitutions
|
107
|
+
@substitutions ||= begin
|
108
|
+
defaults = Hash.new { |hash, key| hash[key] = "${#{key}}" }
|
109
|
+
# need to get the default values of parameters from the template and populate those
|
110
|
+
|
111
|
+
[default_parameters, parameters].flat_map(&:to_a).each do |key, value|
|
112
|
+
defaults["${#{key}}"] = value
|
113
|
+
end
|
114
|
+
|
115
|
+
defaults
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
## expands ${param} when ${param} is defined in the parameters
|
120
|
+
## but only works with "${param}" and not [ "${param}", {param: value}]
|
121
|
+
## only substitutes when a value is provided as a parameter - otherwise, leaves it unsubsituted
|
122
|
+
def with_substitutions(string)
|
123
|
+
return string if string.is_a?(Array)
|
124
|
+
|
125
|
+
string.gsub(/(\${[^}]*})/, substitutions)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AwsCftTools
|
4
|
+
##
|
5
|
+
# Management of a set of template sources.
|
6
|
+
#
|
7
|
+
class TemplateSet < ::Array
|
8
|
+
extend Forwardable
|
9
|
+
|
10
|
+
attr_reader :known_exports
|
11
|
+
attr_reader :dependency_tree
|
12
|
+
|
13
|
+
require_relative 'template_set/array_methods'
|
14
|
+
require_relative 'template_set/closure'
|
15
|
+
require_relative 'template_set/dependencies'
|
16
|
+
|
17
|
+
include ArrayMethods
|
18
|
+
|
19
|
+
# @!method closure
|
20
|
+
# @see AwsCftTools::TemplateSet::Closure
|
21
|
+
# @!method closed_subset
|
22
|
+
# @see AwsCftTools::TemplateSet::Closure
|
23
|
+
include Closure
|
24
|
+
|
25
|
+
include Dependencies
|
26
|
+
|
27
|
+
# @!method undefined_variables
|
28
|
+
# @see AwsCftTools::DependencyTree#undefined_variables
|
29
|
+
# @!method defined_variables
|
30
|
+
# @see AwsCftTools::DependencyTree#defined_variables
|
31
|
+
def_delegators :dependency_tree, :undefined_variables, :defined_variables
|
32
|
+
|
33
|
+
##
|
34
|
+
# @param list [Array<AwsCftTools::Template>] the templates in the set
|
35
|
+
#
|
36
|
+
def initialize(list = [])
|
37
|
+
@dependency_tree = AwsCftTools::DependencyTree.new
|
38
|
+
@sorted_names = []
|
39
|
+
@known_exports = []
|
40
|
+
super(list)
|
41
|
+
|
42
|
+
list.each { |template| process_template_addition(template) }
|
43
|
+
end
|
44
|
+
|
45
|
+
# @!visibility private
|
46
|
+
def initialize_clone(other)
|
47
|
+
initialize_copy(other)
|
48
|
+
end
|
49
|
+
|
50
|
+
# @!visibility private
|
51
|
+
def initialize_copy(other)
|
52
|
+
super(other)
|
53
|
+
@dependency_tree = other.dependency_tree.clone
|
54
|
+
end
|
55
|
+
|
56
|
+
##
|
57
|
+
# Set the list of known exported values in CloudFormation
|
58
|
+
#
|
59
|
+
# @param list [Array<String>]
|
60
|
+
#
|
61
|
+
def known_exports=(list)
|
62
|
+
@known_exports |= list
|
63
|
+
list.each do |name|
|
64
|
+
@dependency_tree.exported(name)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
##
|
69
|
+
# @param filenames [Array<String>]
|
70
|
+
# @return [AwsCftTools::TemplateSet] set of templates with the given filenames
|
71
|
+
#
|
72
|
+
def templates_for(filenames)
|
73
|
+
select { |template| filenames.include?(template.filename.to_s) }
|
74
|
+
end
|
75
|
+
|
76
|
+
##
|
77
|
+
# @param template [AwsCftTools::Template]
|
78
|
+
# @return [AwsCftTools::TemplateSet] set of templates on which the given template depends
|
79
|
+
#
|
80
|
+
def dependencies_for(template)
|
81
|
+
templates_for(@dependency_tree.dependencies_for(template.filename.to_s))
|
82
|
+
end
|
83
|
+
|
84
|
+
##
|
85
|
+
# @param template [AwsCftTools::Template]
|
86
|
+
# @return [AwsCftTools::TemplateSet] set of templates that depend on the given template
|
87
|
+
#
|
88
|
+
def dependents_for(template)
|
89
|
+
templates_for(@dependency_tree.dependents_for(template.filename.to_s))
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
def resort_templates
|
95
|
+
@sorted_names = @dependency_tree.sort
|
96
|
+
sort_by! { |template| @sorted_names.index(template.filename.to_s) || -1 }
|
97
|
+
end
|
98
|
+
|
99
|
+
def process_template_outputs(template)
|
100
|
+
template.outputs.each { |name| provided(template, name) }
|
101
|
+
end
|
102
|
+
|
103
|
+
def process_template_inputs(template)
|
104
|
+
template.inputs.each { |name| required(template, name) }
|
105
|
+
end
|
106
|
+
|
107
|
+
def process_template_dependencies(template)
|
108
|
+
templates_for(template.template_dependencies).each { |other| linked(other, template) }
|
109
|
+
end
|
110
|
+
|
111
|
+
def process_template_addition(template)
|
112
|
+
process_template_inputs(template)
|
113
|
+
process_template_outputs(template)
|
114
|
+
process_template_dependencies(template)
|
115
|
+
|
116
|
+
resort_templates
|
117
|
+
self
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AwsCftTools
|
4
|
+
class TemplateSet
|
5
|
+
##
|
6
|
+
# Array methods that need to be overridden to work well with template sets.
|
7
|
+
#
|
8
|
+
module ArrayMethods
|
9
|
+
##
|
10
|
+
# create a new template set holding templates in either set without duplicates
|
11
|
+
#
|
12
|
+
# Note that this is identical to `|`.
|
13
|
+
#
|
14
|
+
# @param other [AwsCftTools::TemplateSet]
|
15
|
+
# @return [AwsCftTools::TemplateSet]
|
16
|
+
#
|
17
|
+
def +(other)
|
18
|
+
self.class.new(super(other).uniq(&:name)).tap do |union|
|
19
|
+
union.known_exports = @known_exports
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
##
|
24
|
+
# create a new template set holding templates in either set without duplicates
|
25
|
+
#
|
26
|
+
# @param other [AwsCftTools::TemplateSet]
|
27
|
+
# @return [AwsCftTools::TemplateSet]
|
28
|
+
#
|
29
|
+
def |(other)
|
30
|
+
self + other
|
31
|
+
end
|
32
|
+
|
33
|
+
##
|
34
|
+
# create a new template set holding templates in the first set not in the second
|
35
|
+
#
|
36
|
+
# @param other [AwsCftTools::TemplateSet]
|
37
|
+
# @return [AwsCftTools::TemplateSet]
|
38
|
+
#
|
39
|
+
def -(other)
|
40
|
+
forbidden_names = other.map(&:name)
|
41
|
+
clone.replace_list(
|
42
|
+
reject { |template| forbidden_names.include?(template.name) }
|
43
|
+
)
|
44
|
+
end
|
45
|
+
|
46
|
+
##
|
47
|
+
# @return [AwsCftTools::TemplateSet]
|
48
|
+
# @yield [AwsCftTools::Template]
|
49
|
+
#
|
50
|
+
def select
|
51
|
+
return unless block_given?
|
52
|
+
clone.replace_list(super)
|
53
|
+
end
|
54
|
+
|
55
|
+
protected
|
56
|
+
|
57
|
+
def replace_list(new_list)
|
58
|
+
self[0..size - 1] = new_list
|
59
|
+
self
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AwsCftTools
|
4
|
+
class TemplateSet
|
5
|
+
##
|
6
|
+
# Closure-related functions for a TemplateSet.
|
7
|
+
#
|
8
|
+
module Closure
|
9
|
+
##
|
10
|
+
# Provides the given templates and any from the set that those templates depend on.
|
11
|
+
#
|
12
|
+
# @param templates [Array<AwsCftTools::Template>]
|
13
|
+
# @return [AwsCftTools::TemplateSet]
|
14
|
+
#
|
15
|
+
def closure(templates)
|
16
|
+
templates_for(
|
17
|
+
calculate_closure(templates.filenames) { |template| @dependency_tree.dependencies_for(template) }
|
18
|
+
)
|
19
|
+
end
|
20
|
+
|
21
|
+
##
|
22
|
+
# Provides a list of filenames holding the source for the templates in a set.
|
23
|
+
#
|
24
|
+
# @return [Array<String>]
|
25
|
+
#
|
26
|
+
def filenames
|
27
|
+
map(&:filename).map(&:to_s)
|
28
|
+
end
|
29
|
+
|
30
|
+
##
|
31
|
+
# Provides the subset of the given templates that have no dependent templates outside the set.
|
32
|
+
#
|
33
|
+
# @param templates [AwsCftTools::TemplateSet]
|
34
|
+
# @return [AwsCftTools::TemplateSet]
|
35
|
+
#
|
36
|
+
def closed_subset(templates)
|
37
|
+
templates_for(@dependency_tree.closed_subset(templates.filenames))
|
38
|
+
end
|
39
|
+
|
40
|
+
##
|
41
|
+
# @param folders [Array<String>]
|
42
|
+
# @return [AwsCftTools::TemplateSet]
|
43
|
+
#
|
44
|
+
def in_folder_order(folders)
|
45
|
+
proper_ordered_set, set = folders.reduce([clone, []]) do |memo, folder|
|
46
|
+
set, proper_ordered_set = memo
|
47
|
+
selected = closure(templates_in_folder(set, folder)) - proper_ordered_set
|
48
|
+
[set - selected, proper_ordered_set | selected]
|
49
|
+
end
|
50
|
+
|
51
|
+
proper_ordered_set | set
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def templates_in_folder(set, folder)
|
57
|
+
set.select { |template| template.filename.to_s.start_with?(folder + '/') }
|
58
|
+
end
|
59
|
+
|
60
|
+
def calculate_closure(set, &block)
|
61
|
+
stack = set.clone
|
62
|
+
|
63
|
+
stack += closure_step(stack.shift, set, &block) while stack.any?
|
64
|
+
set
|
65
|
+
end
|
66
|
+
|
67
|
+
def closure_step(template, set)
|
68
|
+
[].tap do |stack|
|
69
|
+
(yield(template) - set).each do |depedency|
|
70
|
+
stack << depedency
|
71
|
+
set.unshift(depedency)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'each_slice_state'
|
4
|
+
|
5
|
+
module AwsCftTools
|
6
|
+
class TemplateSet
|
7
|
+
##
|
8
|
+
# Simple derived information about templates.
|
9
|
+
#
|
10
|
+
module Dependencies
|
11
|
+
##
|
12
|
+
# @param template [AwsCftTools::Template]
|
13
|
+
# @param variable [#to_s]
|
14
|
+
def provided(template, variable)
|
15
|
+
@dependency_tree.provided(template.filename.to_s, variable.to_s)
|
16
|
+
end
|
17
|
+
|
18
|
+
##
|
19
|
+
# @param template [AwsCftTools::Template]
|
20
|
+
# @param variable [#to_s]
|
21
|
+
def required(template, variable)
|
22
|
+
@dependency_tree.required(template.filename.to_s, variable.to_s)
|
23
|
+
end
|
24
|
+
|
25
|
+
##
|
26
|
+
# @param from [AwsCftTools::Template]
|
27
|
+
# @param to [AwsCftTools::Template]
|
28
|
+
#
|
29
|
+
def linked(from, to)
|
30
|
+
@dependency_tree.linked(from.filename.to_s, to.filename.to_s)
|
31
|
+
end
|
32
|
+
|
33
|
+
##
|
34
|
+
# Iterates through the sorted list and yields an array of templates with no unsatisfied dependencies,
|
35
|
+
# up to the maximum slice size.
|
36
|
+
#
|
37
|
+
# @param maximum_slice_size [Integer]
|
38
|
+
# @yield [Array<AwsCftTools::Template>] up to +maximum_slice_size+ templates with no unsatisfied
|
39
|
+
# dependencies
|
40
|
+
#
|
41
|
+
def each_slice(maximum_slice_size, &block)
|
42
|
+
return unless block_given?
|
43
|
+
# we want to start at the beginning and get up to <n> items for which all prior dependencies have
|
44
|
+
# already been returned in a prior call
|
45
|
+
state = EachSliceState.new(maximum_slice_size, &block)
|
46
|
+
|
47
|
+
each do |template|
|
48
|
+
state.add_template(template, @dependency_tree.dependencies_for(template.filename.to_s))
|
49
|
+
end
|
50
|
+
# catch the last templates
|
51
|
+
state.process_slice
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AwsCftTools
|
4
|
+
class TemplateSet
|
5
|
+
##
|
6
|
+
# Keeps track of state for the .each_slice(n) method.
|
7
|
+
#
|
8
|
+
class EachSliceState
|
9
|
+
##
|
10
|
+
# @param slice_size [Integer] maximum number of templates to yield at once
|
11
|
+
# @yield [Array<AwsCftTools::Template>]
|
12
|
+
#
|
13
|
+
def initialize(slice_size, &block)
|
14
|
+
@seen = []
|
15
|
+
@size = slice_size
|
16
|
+
@slice = []
|
17
|
+
@block = block
|
18
|
+
end
|
19
|
+
|
20
|
+
##
|
21
|
+
# Have all of the listed dependencies been seen in prior yields?
|
22
|
+
#
|
23
|
+
# @param deps [Array<String>]
|
24
|
+
# @return [Boolean]
|
25
|
+
#
|
26
|
+
def fulfilled?(deps)
|
27
|
+
(deps - @seen).empty?
|
28
|
+
end
|
29
|
+
|
30
|
+
###
|
31
|
+
# Add the template to the current slice and process the slice if it reaches the maximum slice size.
|
32
|
+
#
|
33
|
+
# @param template [AwsCftTools::Template]
|
34
|
+
#
|
35
|
+
def add_template(template, dependencies = [])
|
36
|
+
process_slice unless fulfilled?(dependencies)
|
37
|
+
unless fulfilled?(dependencies)
|
38
|
+
raise AwsCftTools::UnsatisfiedDependencyError, "Unable to process #{template.filename}"
|
39
|
+
end
|
40
|
+
|
41
|
+
@slice << template
|
42
|
+
|
43
|
+
process_slice if @slice.count == @size
|
44
|
+
end
|
45
|
+
|
46
|
+
###
|
47
|
+
# Pass the current slice through the block and reset for the next slice.
|
48
|
+
#
|
49
|
+
# @return [Integer] number of templates processed in this batch
|
50
|
+
#
|
51
|
+
def process_slice
|
52
|
+
@block.call(@slice) if @slice.any?
|
53
|
+
@seen |= @slice.map(&:filename).map(&:to_s)
|
54
|
+
@slice = []
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|