machinery-tool 1.22.1 → 1.22.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (116) hide show
  1. checksums.yaml +4 -4
  2. data/.git_revision +1 -1
  3. data/NEWS +10 -0
  4. data/bin/machinery +1 -1
  5. data/lib/analyze_changed_config_files_diffs_task.rb +6 -6
  6. data/lib/autoyast.rb +2 -2
  7. data/lib/build_task.rb +10 -7
  8. data/lib/cli.rb +1005 -801
  9. data/lib/compare_task.rb +11 -7
  10. data/lib/comparison.rb +2 -2
  11. data/lib/config_base.rb +1 -1
  12. data/lib/config_task.rb +1 -1
  13. data/lib/containerize_task.rb +3 -3
  14. data/lib/containerized_app.rb +1 -1
  15. data/lib/copy_task.rb +1 -1
  16. data/lib/current_user.rb +1 -1
  17. data/lib/deploy_task.rb +6 -4
  18. data/lib/diff_widget.rb +67 -63
  19. data/lib/docker_system.rb +12 -8
  20. data/lib/dpkg_database.rb +1 -1
  21. data/lib/element_filter.rb +7 -4
  22. data/lib/exceptions.rb +23 -5
  23. data/lib/export_task.rb +1 -1
  24. data/lib/exporter.rb +1 -1
  25. data/lib/file_diff.rb +1 -1
  26. data/lib/file_scope.rb +1 -1
  27. data/lib/file_validator.rb +7 -4
  28. data/lib/filter.rb +97 -93
  29. data/lib/filter_option_parser.rb +2 -2
  30. data/lib/hint.rb +64 -59
  31. data/lib/html.rb +1 -1
  32. data/lib/inspect_task.rb +12 -12
  33. data/lib/inspector.rb +3 -3
  34. data/lib/json_validation_error_cleaner.rb +1 -1
  35. data/lib/json_validator.rb +4 -4
  36. data/lib/kiwi_config.rb +8 -4
  37. data/lib/list_task.rb +10 -9
  38. data/lib/local_system.rb +11 -5
  39. data/lib/logged_cheetah.rb +1 -1
  40. data/lib/man_task.rb +10 -6
  41. data/lib/managed_files_database.rb +1 -1
  42. data/lib/manifest.rb +5 -5
  43. data/lib/migration.rb +16 -10
  44. data/lib/mountpoints.rb +1 -1
  45. data/lib/move_task.rb +1 -1
  46. data/lib/remote_system.rb +7 -7
  47. data/lib/remove_task.rb +1 -1
  48. data/lib/renderer.rb +177 -172
  49. data/lib/rpm.rb +4 -4
  50. data/lib/rpm_database.rb +1 -1
  51. data/lib/scope.rb +2 -2
  52. data/lib/scope_file_access_archive.rb +1 -1
  53. data/lib/scope_file_access_flat.rb +1 -1
  54. data/lib/scope_file_store.rb +1 -1
  55. data/lib/serve_html_task.rb +6 -2
  56. data/lib/server.rb +19 -12
  57. data/lib/show_task.rb +10 -6
  58. data/lib/static_html.rb +1 -1
  59. data/lib/system.rb +10 -10
  60. data/lib/system_description.rb +14 -13
  61. data/lib/system_description_memory_store.rb +1 -1
  62. data/lib/system_description_store.rb +9 -9
  63. data/lib/tarball.rb +8 -2
  64. data/lib/upgrade_format_task.rb +11 -6
  65. data/lib/validate_task.rb +2 -2
  66. data/lib/version.rb +1 -1
  67. data/lib/workload_mapper.rb +2 -2
  68. data/lib/workload_mapper_dsl.rb +1 -1
  69. data/lib/zypper.rb +40 -17
  70. data/machinery-helper/machinery_helper.go +35 -16
  71. data/machinery-helper/version.go +1 -1
  72. data/man/generated/machinery.1.gz +0 -0
  73. data/manual/site/sitemap.xml +24 -24
  74. data/plugins/changed_config_files/changed_config_files_inspector.rb +59 -56
  75. data/plugins/changed_config_files/changed_config_files_model.rb +23 -21
  76. data/plugins/changed_config_files/changed_config_files_renderer.rb +56 -52
  77. data/plugins/changed_managed_files/changed_managed_files_inspector.rb +52 -50
  78. data/plugins/changed_managed_files/changed_managed_files_model.rb +23 -21
  79. data/plugins/changed_managed_files/changed_managed_files_renderer.rb +43 -39
  80. data/plugins/environment/environment_inspector.rb +25 -23
  81. data/plugins/environment/environment_model.rb +5 -3
  82. data/plugins/groups/groups_inspector.rb +30 -28
  83. data/plugins/groups/groups_model.rb +18 -17
  84. data/plugins/groups/groups_renderer.rb +29 -25
  85. data/plugins/os/os_inspector.rb +120 -118
  86. data/plugins/os/os_model.rb +139 -134
  87. data/plugins/os/os_renderer.rb +13 -9
  88. data/plugins/packages/packages_inspector.rb +99 -86
  89. data/plugins/packages/packages_model.rb +35 -34
  90. data/plugins/packages/packages_renderer.rb +47 -39
  91. data/plugins/patterns/patterns_inspector.rb +70 -68
  92. data/plugins/patterns/patterns_model.rb +19 -18
  93. data/plugins/patterns/patterns_renderer.rb +36 -32
  94. data/plugins/repositories/repositories_inspector.rb +162 -156
  95. data/plugins/repositories/repositories_model.rb +50 -49
  96. data/plugins/repositories/repositories_renderer.rb +48 -44
  97. data/plugins/repositories/schema/system-description-repositories.schema-v10.json +0 -1
  98. data/plugins/services/services_inspector.rb +187 -176
  99. data/plugins/services/services_model.rb +37 -36
  100. data/plugins/services/services_renderer.rb +28 -24
  101. data/plugins/unmanaged_files/unmanaged_files_inspector.rb +102 -99
  102. data/plugins/unmanaged_files/unmanaged_files_model.rb +64 -56
  103. data/plugins/unmanaged_files/unmanaged_files_renderer.rb +44 -40
  104. data/plugins/users/users_inspector.rb +67 -65
  105. data/plugins/users/users_model.rb +37 -36
  106. data/plugins/users/users_renderer.rb +31 -27
  107. data/schema/migrations/migrate1to2.rb +1 -1
  108. data/schema/migrations/migrate2to3.rb +1 -1
  109. data/schema/migrations/migrate3to4.rb +1 -1
  110. data/schema/migrations/migrate4to5.rb +1 -1
  111. data/schema/migrations/migrate5to6.rb +1 -1
  112. data/schema/migrations/migrate6to7.rb +1 -1
  113. data/schema/migrations/migrate7to8.rb +1 -1
  114. data/schema/migrations/migrate8to9.rb +1 -1
  115. data/schema/migrations/migrate9to10.rb +1 -1
  116. metadata +2 -2
@@ -15,47 +15,48 @@
15
15
  # To contact SUSE about this file by physical or electronic mail,
16
16
  # you may find current contact information at www.suse.com
17
17
 
18
+ module Machinery
19
+ class Service < Machinery::Object
20
+ def enabled?
21
+ # systemd vs sysvinit
22
+ state == "enabled" || state == "on"
23
+ end
18
24
 
19
- class Service < Machinery::Object
20
- def enabled?
21
- # systemd vs sysvinit
22
- state == "enabled" || state == "on"
25
+ def disabled?
26
+ # systemd vs sysvinit
27
+ state == "disabled" || state == "off"
28
+ end
23
29
  end
24
30
 
25
- def disabled?
26
- # systemd vs sysvinit
27
- state == "disabled" || state == "off"
28
- end
29
- end
30
-
31
- class ServicesScope < Machinery::Array
32
- include Machinery::Scope
33
-
34
- has_attributes :init_system
35
- has_elements class: Service
36
-
37
- def compare_with(other)
38
- if self.init_system != other.init_system
39
- [self, other, nil, nil]
40
- else
41
- only_self = self - other
42
- only_other = other - self
43
- common = self & other
44
- changed = Machinery::Scope.extract_changed_elements(only_self, only_other, :name)
45
- changed = nil if changed.empty?
46
-
47
- [
48
- service_list_to_scope(only_self),
49
- service_list_to_scope(only_other),
50
- changed,
51
- service_list_to_scope(common)
52
- ].map { |e| (e && !e.empty?) ? e : nil }
31
+ class ServicesScope < Machinery::Array
32
+ include Machinery::Scope
33
+
34
+ has_attributes :init_system
35
+ has_elements class: Service
36
+
37
+ def compare_with(other)
38
+ if init_system != other.init_system
39
+ [self, other, nil, nil]
40
+ else
41
+ only_self = self - other
42
+ only_other = other - self
43
+ common = self & other
44
+ changed = Machinery::Scope.extract_changed_elements(only_self, only_other, :name)
45
+ changed = nil if changed.empty?
46
+
47
+ [
48
+ service_list_to_scope(only_self),
49
+ service_list_to_scope(only_other),
50
+ changed,
51
+ service_list_to_scope(common)
52
+ ].map { |e| e && !e.empty? ? e : nil }
53
+ end
53
54
  end
54
- end
55
55
 
56
- private
56
+ private
57
57
 
58
- def service_list_to_scope(services)
59
- self.class.new(services, init_system: init_system) unless services.elements.empty?
58
+ def service_list_to_scope(services)
59
+ self.class.new(services, init_system: init_system) unless services.elements.empty?
60
+ end
60
61
  end
61
62
  end
@@ -15,38 +15,42 @@
15
15
  # To contact SUSE about this file by physical or electronic mail,
16
16
  # you may find current contact information at www.suse.com
17
17
 
18
- class ServicesRenderer < Renderer
19
- def content(description)
20
- return unless description["services"]
18
+ module Machinery
19
+ class Ui
20
+ class ServicesRenderer < Machinery::Ui::Renderer
21
+ def content(description)
22
+ return unless description["services"]
21
23
 
22
- if description["services"].elements.empty?
23
- puts "There are no services."
24
- end
24
+ if description["services"].elements.empty?
25
+ puts "There are no services."
26
+ end
25
27
 
26
- list do
27
- description["services"].each do |p|
28
- item "#{p.name}: #{p.state}"
28
+ list do
29
+ description["services"].each do |p|
30
+ item "#{p.name}: #{p.state}"
31
+ end
32
+ end
29
33
  end
30
- end
31
- end
32
34
 
33
- def display_name
34
- "Services"
35
- end
35
+ def display_name
36
+ "Services"
37
+ end
36
38
 
37
- def compare_content_changed(changed_elements)
38
- list do
39
- changed_elements.each do |one, two|
40
- changes = []
41
- relevant_attributes = one.attributes.keys
39
+ def compare_content_changed(changed_elements)
40
+ list do
41
+ changed_elements.each do |one, two|
42
+ changes = []
43
+ relevant_attributes = one.attributes.keys
42
44
 
43
- relevant_attributes.each do |attribute|
44
- if one[attribute] != two[attribute]
45
- changes << "#{attribute}: #{one[attribute]} <> #{two[attribute]}"
45
+ relevant_attributes.each do |attribute|
46
+ if one[attribute] != two[attribute]
47
+ changes << "#{attribute}: #{one[attribute]} <> #{two[attribute]}"
48
+ end
49
+ end
50
+
51
+ item "#{one.name} (#{changes.join(", ")})"
46
52
  end
47
53
  end
48
-
49
- item "#{one.name} (#{changes.join(", ")})"
50
54
  end
51
55
  end
52
56
  end
@@ -15,125 +15,128 @@
15
15
  # To contact SUSE about this file by physical or electronic mail,
16
16
  # you may find current contact information at www.suse.com
17
17
 
18
- class UnmanagedFilesInspector < Inspector
19
- has_priority 100
20
-
21
- # checks if all required binaries are present
22
- def check_requirements(check_tar)
23
- @system.check_requirement(["rpm", "dpkg"], "--version")
24
- @system.check_create_archive_dependencies if check_tar
25
- end
26
-
27
- def initialize(system, description)
28
- @system = system
29
- @description = description
30
- end
31
-
32
- def inspect(filter, options = {})
33
- do_extract = options[:extract_unmanaged_files]
34
- check_requirements(do_extract)
18
+ module Machinery
19
+ class UnmanagedFilesInspector < Machinery::Inspector
20
+ has_priority 100
21
+
22
+ # checks if all required binaries are present
23
+ def check_requirements(check_tar)
24
+ @system.check_requirement(["rpm", "dpkg"], "--version")
25
+ @system.check_create_archive_dependencies if check_tar
26
+ end
35
27
 
36
- scope = UnmanagedFilesScope.new
28
+ def initialize(system, description)
29
+ @system = system
30
+ @description = description
31
+ end
37
32
 
38
- file_store_tmp = @description.scope_file_store("unmanaged_files.tmp")
39
- file_store_final = @description.scope_file_store("unmanaged_files")
33
+ def inspect(filter, options = {})
34
+ do_extract = options[:extract_unmanaged_files]
35
+ check_requirements(do_extract)
40
36
 
41
- scope.scope_file_store = file_store_tmp
37
+ scope = UnmanagedFilesScope.new
42
38
 
43
- file_filter = filter.element_filter_for("/unmanaged_files/name").dup if filter
44
- file_filter ||= ElementFilter.new("/unmanaged_files/name")
45
- file_filter.add_matchers("=", @description.store.base_path)
39
+ file_store_tmp = @description.scope_file_store("unmanaged_files.tmp")
40
+ file_store_final = @description.scope_file_store("unmanaged_files")
46
41
 
47
- # Add a recursive pendant to each ignored element
48
- file_filter.matchers.each do |operator, matchers|
49
- file_filter.add_matchers(operator, matchers.map { |entry| File.join(entry, "/*") })
50
- end
42
+ scope.scope_file_store = file_store_tmp
51
43
 
52
- helper = MachineryHelper.new(@system)
44
+ file_filter = filter.element_filter_for("/unmanaged_files/name").dup if filter
45
+ file_filter ||= Machinery::ElementFilter.new("/unmanaged_files/name")
46
+ file_filter.add_matchers("=", @description.store.base_path)
53
47
 
54
- if helper_usable?(helper)
55
- helper_options = {}
56
- helper_options[:do_extract] = do_extract
57
- helper_options[:extract_metadata] = options[:extract_metadata]
48
+ # Add a recursive pendant to each ignored element
49
+ file_filter.matchers.each do |operator, matchers|
50
+ file_filter.add_matchers(operator, matchers.map { |entry| File.join(entry, "/*") })
51
+ end
58
52
 
59
- run_helper_inspection(helper, file_filter, file_store_tmp, file_store_final,
60
- scope, helper_options)
61
- else
62
- raise Machinery::Errors::MissingRequirement.new(
63
- "There is no machinery-helper available for the remote system architecture #{@system.arch}."
64
- )
65
- end
66
- end
53
+ helper = MachineryHelper.new(@system)
67
54
 
68
- def helper_usable?(helper)
69
- helper.can_help?
70
- end
55
+ if helper_usable?(helper)
56
+ helper_options = {}
57
+ helper_options[:do_extract] = do_extract
58
+ helper_options[:extract_metadata] = options[:extract_metadata]
71
59
 
72
- def run_helper_inspection(helper, filter, file_store_tmp, file_store_final, scope, options)
73
- begin
74
- helper.inject_helper
75
- unless helper.has_compatible_version?
76
- raise Machinery::Errors::UnsupportedHelperVersion.new(
77
- "Error: machinery-helper is not compatible with this Machinery version." \
78
- "\nTry to reinstall the package or gem to fix the issue."
60
+ run_helper_inspection(helper, file_filter, file_store_tmp, file_store_final,
61
+ scope, helper_options)
62
+ else
63
+ raise Machinery::Errors::MissingRequirement.new(
64
+ "There is no machinery-helper available for the "\
65
+ "remote system architecture #{@system.arch}."
79
66
  )
80
67
  end
68
+ end
81
69
 
82
- args = []
83
- args.push("--extract-metadata") if options[:extract_metadata] || options[:do_extract]
84
-
85
- helper.run_helper(scope, *args)
86
- scope.delete_if { |f| filter.matches?(f.name) }
87
-
88
- if options[:do_extract]
89
- mount_points = MountPoints.new(@system)
90
- excluded_trees = mount_points.remote + mount_points.special
91
-
92
- file_store_tmp.remove
93
- file_store_tmp.create
94
-
95
- files = scope.select { |f| f.file? || f.link? }.map(&:name)
96
- scope.retrieve_files_from_system_as_archive(@system, files, [])
97
- show_extraction_progress(files.count)
70
+ def helper_usable?(helper)
71
+ helper.can_help?
72
+ end
98
73
 
99
- scope.retrieve_trees_from_system_as_archive(@system,
100
- scope.select(&:directory?).map(&:name), excluded_trees) do |count|
101
- show_extraction_progress(files.count + count)
74
+ def run_helper_inspection(helper, filter, file_store_tmp, file_store_final, scope, options)
75
+ begin
76
+ helper.inject_helper
77
+ unless helper.has_compatible_version?
78
+ raise Machinery::Errors::UnsupportedHelperVersion.new(
79
+ "Error: machinery-helper is not compatible with this Machinery version." \
80
+ "\nTry to reinstall the package or gem to fix the issue."
81
+ )
102
82
  end
103
83
 
104
- file_store_final.remove
105
- file_store_tmp.rename(file_store_final.store_name)
106
- scope.scope_file_store = file_store_final
107
- scope.extracted = true
108
- scope.has_metadata = true
109
- else
110
- file_store_final.remove
111
- scope.extracted = false
112
- scope.has_metadata = !!options[:extract_metadata]
84
+ args = []
85
+ args.push("--extract-metadata") if options[:extract_metadata] || options[:do_extract]
86
+
87
+ helper.run_helper(scope, *args)
88
+ scope.delete_if { |f| filter.matches?(f.name) }
89
+
90
+ if options[:do_extract]
91
+ mount_points = Machinery::MountPoints.new(@system)
92
+ excluded_trees = mount_points.remote + mount_points.special
93
+
94
+ file_store_tmp.remove
95
+ file_store_tmp.create
96
+
97
+ files = scope.select { |f| f.file? || f.link? }.map(&:name)
98
+ scope.retrieve_files_from_system_as_archive(@system, files, [])
99
+ show_extraction_progress(files.count)
100
+
101
+ scope.retrieve_trees_from_system_as_archive(@system,
102
+ scope.select(&:directory?).map(&:name), excluded_trees) do |count|
103
+ show_extraction_progress(files.count + count)
104
+ end
105
+
106
+ file_store_final.remove
107
+ file_store_tmp.rename(file_store_final.store_name)
108
+ scope.scope_file_store = file_store_final
109
+ scope.extracted = true
110
+ scope.has_metadata = true
111
+ else
112
+ file_store_final.remove
113
+ scope.extracted = false
114
+ scope.has_metadata = !!options[:extract_metadata]
115
+ end
116
+ ensure
117
+ helper.remove_helper
113
118
  end
114
- ensure
115
- helper.remove_helper
116
- end
117
119
 
118
- @description["unmanaged_files"] = scope
119
- end
120
+ @description["unmanaged_files"] = scope
121
+ end
120
122
 
121
- def summary
122
- "#{@description.unmanaged_files.extracted ? "Extracted" : "Found"} " +
123
- Machinery.pluralize(
124
- @description.unmanaged_files.count,
125
- "%d unmanaged file or tree.",
126
- "%d unmanaged files and/or trees."
127
- )
128
- end
123
+ def summary
124
+ "#{@description.unmanaged_files.extracted ? "Extracted" : "Found"} " +
125
+ Machinery.pluralize(
126
+ @description.unmanaged_files.count,
127
+ "%d unmanaged file or tree.",
128
+ "%d unmanaged files and/or trees."
129
+ )
130
+ end
129
131
 
130
- private
132
+ private
131
133
 
132
- def show_extraction_progress(count)
133
- progress = Machinery.pluralize(
134
- count, " -> Extracted %d unmanaged file or tree...",
135
- " -> Extracted %d unmanaged files and/or trees..."
136
- )
137
- Machinery::Ui.progress(progress)
134
+ def show_extraction_progress(count)
135
+ progress = Machinery.pluralize(
136
+ count, " -> Extracted %d unmanaged file or tree...",
137
+ " -> Extracted %d unmanaged files and/or trees..."
138
+ )
139
+ Machinery::Ui.progress(progress)
140
+ end
138
141
  end
139
142
  end
@@ -15,78 +15,86 @@
15
15
  # To contact SUSE about this file by physical or electronic mail,
16
16
  # you may find current contact information at www.suse.com
17
17
 
18
- class UnmanagedFile < Machinery::SystemFile
19
- end
18
+ module Machinery
19
+ class UnmanagedFile < Machinery::SystemFile
20
+ end
20
21
 
21
- class UnmanagedFileList < Machinery::Array
22
- end
22
+ class UnmanagedFileList < Machinery::Array
23
+ end
23
24
 
24
- class UnmanagedFilesScope < FileScope
25
- include Machinery::Scope
26
- include ScopeFileAccessArchive
25
+ class UnmanagedFilesScope < Machinery::FileScope
26
+ include Machinery::Scope
27
+ include Machinery::ScopeFileAccessArchive
27
28
 
28
- has_attributes :extracted, :has_metadata
29
- has_elements class: UnmanagedFile
29
+ has_attributes :extracted, :has_metadata
30
+ has_elements class: UnmanagedFile
30
31
 
31
- def compare_with(other)
32
- if extracted != other.extracted
33
- Machinery::Ui.warn("Warning: Comparing extracted with unextracted" \
34
- " unmanaged files. Only common attributes are considered.")
35
- end
32
+ def compare_with(other)
33
+ if extracted != other.extracted
34
+ Machinery::Ui.warn("Warning: Comparing extracted with unextracted" \
35
+ " unmanaged files. Only common attributes are considered.")
36
+ end
36
37
 
37
- self_hash = elements.inject({}) { |hash, e| hash[e.name] = e; hash }
38
- other_hash = other.elements.inject({}) { |hash, e| hash[e.name] = e; hash }
38
+ self_hash = elements.inject({}) do |hash, e|
39
+ hash[e.name] = e
40
+ hash
41
+ end
42
+ other_hash = other.elements.inject({}) do |hash, e|
43
+ hash[e.name] = e
44
+ hash
45
+ end
46
+
47
+ both = []
48
+ only_self = []
49
+ elements.each do |element|
50
+ if other_hash.key?(element.name) && files_match(element, other_hash[element.name])
51
+ both << element
52
+ else
53
+ only_self << element
54
+ end
55
+ end
56
+ only_other = other.elements.reject do |element|
57
+ self_hash.key?(element.name) && files_match(element, self_hash[element.name])
58
+ end
59
+ changed = Machinery::Scope.extract_changed_elements(only_self, only_other, :name)
39
60
 
40
- both = []
41
- only_self = []
42
- elements.each do |element|
43
- if other_hash.key?(element.name) && files_match(element, other_hash[element.name])
44
- both << element
61
+ if attributes == other.attributes
62
+ common_attributes = attributes
45
63
  else
46
- only_self << element
64
+ only_self_attributes = attributes
65
+ only_other_attributes = other.attributes
47
66
  end
48
- end
49
- only_other = other.elements.reject do |element|
50
- self_hash.key?(element.name) && files_match(element, self_hash[element.name])
51
- end
52
- changed = Machinery::Scope.extract_changed_elements(only_self, only_other, :name)
53
67
 
54
- if self.attributes == other.attributes
55
- common_attributes = self.attributes
56
- else
57
- only_self_attributes = self.attributes
58
- only_other_attributes = other.attributes
59
- end
68
+ comparison = [
69
+ self.class.new(only_self, only_self_attributes || {}),
70
+ self.class.new(only_other, only_other_attributes || {}),
71
+ changed
72
+ ].map { |e| e.empty? ? nil : e }
60
73
 
61
- comparison = [
62
- self.class.new(only_self, only_self_attributes || {}),
63
- self.class.new(only_other, only_other_attributes || {}),
64
- changed
65
- ].map { |e| e.empty? ? nil : e }
74
+ common = if both.empty? && common_attributes && common_attributes.empty?
75
+ nil
76
+ else
77
+ self.class.new(both, common_attributes || {})
78
+ end
66
79
 
67
- common = if both.empty? && common_attributes && common_attributes.empty?
68
- nil
69
- else
70
- self.class.new(both, common_attributes || {})
80
+ comparison.push(common)
71
81
  end
72
82
 
73
- comparison.push(common)
74
- end
75
-
76
- def contains_metadata?
77
- @metadata ||= has_metadata || elements.any?(&:user)
78
- end
83
+ def contains_metadata?
84
+ @metadata ||= has_metadata || elements.any?(&:user)
85
+ end
79
86
 
80
- def has_subdir_counts?
81
- @has_subdirs ||= elements.any?(&:files)
82
- end
87
+ def has_subdir_counts?
88
+ @has_subdirs ||= elements.any?(&:files)
89
+ end
83
90
 
84
- private
91
+ private
85
92
 
86
- def files_match(a, b)
87
- common_attributes = a.attributes.keys & b.attributes.keys
88
- common_attributes.all? do |attribute|
89
- a[attribute] == b[attribute]
93
+ def files_match(a, b)
94
+ common_attributes = a.attributes.keys & b.attributes.keys
95
+ common_attributes.all? do |attribute|
96
+ a[attribute] == b[attribute]
97
+ end
90
98
  end
91
99
  end
92
100
  end