octocatalog-diff 0.6.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (132) hide show
  1. checksums.yaml +4 -4
  2. data/.version +1 -1
  3. data/README.md +5 -2
  4. data/bin/octocatalog-diff +9 -49
  5. data/doc/CHANGELOG.md +14 -0
  6. data/doc/advanced-filter.md +59 -1
  7. data/doc/advanced-override-enc.md +54 -0
  8. data/doc/advanced-pe-enc.md +1 -1
  9. data/doc/advanced.md +2 -1
  10. data/doc/dev/api.md +5 -0
  11. data/doc/dev/api/v1.md +41 -0
  12. data/doc/dev/api/v1/calls/catalog-diff.md +209 -0
  13. data/doc/dev/api/v1/calls/catalog.md +115 -0
  14. data/doc/dev/api/v1/calls/config.md +37 -0
  15. data/doc/dev/api/v1/objects/catalog.md +127 -0
  16. data/doc/dev/api/v1/objects/diff.md +261 -0
  17. data/doc/dev/api/v1/objects/override.md +30 -0
  18. data/doc/dev/how-to-add-options.md +12 -12
  19. data/doc/optionsref.md +91 -74
  20. data/doc/versions/v1.md +22 -0
  21. data/lib/octocatalog-diff.rb +1 -8
  22. data/lib/octocatalog-diff/api/v1.rb +27 -0
  23. data/lib/octocatalog-diff/api/v1/catalog-compile.rb +40 -0
  24. data/lib/octocatalog-diff/api/v1/catalog-diff.rb +68 -0
  25. data/lib/octocatalog-diff/api/v1/catalog.rb +84 -0
  26. data/lib/octocatalog-diff/api/v1/common.rb +24 -0
  27. data/lib/octocatalog-diff/api/v1/config.rb +125 -0
  28. data/lib/octocatalog-diff/api/v1/diff.rb +194 -0
  29. data/lib/octocatalog-diff/api/v1/override.rb +103 -0
  30. data/lib/octocatalog-diff/catalog-diff/differ.rb +66 -47
  31. data/lib/octocatalog-diff/catalog-diff/display.rb +8 -2
  32. data/lib/octocatalog-diff/catalog-diff/display/json.rb +3 -2
  33. data/lib/octocatalog-diff/catalog-diff/display/legacy_json.rb +28 -0
  34. data/lib/octocatalog-diff/catalog-diff/display/text.rb +64 -9
  35. data/lib/octocatalog-diff/catalog-diff/filter.rb +45 -6
  36. data/lib/octocatalog-diff/catalog-diff/filter/absent_file.rb +65 -0
  37. data/lib/octocatalog-diff/catalog-diff/filter/compilation_dir.rb +78 -0
  38. data/lib/octocatalog-diff/catalog-diff/filter/yaml.rb +10 -7
  39. data/lib/octocatalog-diff/catalog-util/bootstrap.rb +13 -14
  40. data/lib/octocatalog-diff/catalog-util/builddir.rb +1 -0
  41. data/lib/octocatalog-diff/catalog-util/cached_master_directory.rb +2 -2
  42. data/lib/octocatalog-diff/catalog-util/enc.rb +49 -14
  43. data/lib/octocatalog-diff/catalog-util/enc/pe.rb +3 -5
  44. data/lib/octocatalog-diff/catalog-util/enc/pe/v1.rb +3 -1
  45. data/lib/octocatalog-diff/catalog-util/git.rb +36 -24
  46. data/lib/octocatalog-diff/catalog.rb +5 -9
  47. data/lib/octocatalog-diff/catalog/computed.rb +9 -1
  48. data/lib/octocatalog-diff/catalog/puppetdb.rb +4 -3
  49. data/lib/octocatalog-diff/cli.rb +195 -0
  50. data/lib/octocatalog-diff/cli/diffs.rb +40 -0
  51. data/lib/octocatalog-diff/cli/options.rb +183 -0
  52. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/basedir.rb +1 -1
  53. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/bootstrap_current.rb +1 -1
  54. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/bootstrap_environment.rb +1 -1
  55. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/bootstrap_script.rb +1 -1
  56. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/bootstrap_then_exit.rb +1 -1
  57. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/bootstrapped_dirs.rb +1 -1
  58. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/cached_master_dir.rb +1 -1
  59. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/catalog_only.rb +1 -1
  60. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/color.rb +1 -1
  61. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/command_line.rb +2 -2
  62. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/compare_file_text.rb +1 -1
  63. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/create_symlinks.rb +2 -2
  64. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/debug.rb +1 -1
  65. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/debug_bootstrap.rb +1 -1
  66. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/display_datatype_changes.rb +1 -1
  67. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/display_detail_add.rb +1 -1
  68. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/display_source_file_line.rb +1 -1
  69. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/enc.rb +1 -1
  70. data/lib/octocatalog-diff/cli/options/enc_override.rb +21 -0
  71. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/environment.rb +2 -2
  72. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/existing_catalogs.rb +1 -1
  73. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/fact_file.rb +1 -1
  74. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/fact_override.rb +2 -2
  75. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/facts_terminus.rb +1 -1
  76. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/filters.rb +5 -2
  77. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/from_puppetdb.rb +1 -1
  78. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/header.rb +1 -1
  79. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/hiera_config.rb +1 -1
  80. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/hiera_path.rb +1 -1
  81. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/hiera_path_strip.rb +1 -1
  82. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/hostname.rb +1 -1
  83. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/ignore.rb +1 -1
  84. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/ignore_attr.rb +1 -1
  85. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/ignore_tags.rb +1 -1
  86. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/include_tags.rb +1 -1
  87. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/master_cache_branch.rb +1 -1
  88. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/output_file.rb +1 -1
  89. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/output_format.rb +5 -3
  90. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/parallel.rb +1 -1
  91. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/parser.rb +1 -1
  92. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/pass_env_vars.rb +1 -1
  93. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/pe_enc_ssl_ca.rb +1 -1
  94. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/pe_enc_ssl_client_cert.rb +1 -1
  95. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/pe_enc_ssl_client_key.rb +1 -1
  96. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/pe_enc_token.rb +1 -1
  97. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/pe_enc_token_file.rb +1 -1
  98. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/pe_enc_url.rb +1 -1
  99. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/preserve_environments.rb +1 -1
  100. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/puppet_binary.rb +2 -2
  101. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/puppet_master.rb +2 -2
  102. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/puppet_master_api_version.rb +2 -2
  103. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/puppet_master_ssl_ca.rb +2 -2
  104. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/puppet_master_ssl_client_cert.rb +2 -2
  105. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/puppet_master_ssl_client_key.rb +2 -2
  106. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/puppetdb_api_version.rb +1 -1
  107. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/puppetdb_ssl_ca.rb +1 -1
  108. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/puppetdb_ssl_client_cert.rb +1 -1
  109. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/puppetdb_ssl_client_key.rb +1 -1
  110. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/puppetdb_ssl_client_password.rb +1 -1
  111. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/puppetdb_ssl_client_password_file.rb +1 -1
  112. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/puppetdb_url.rb +1 -1
  113. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/quiet.rb +1 -1
  114. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/retry_failed_catalog.rb +1 -1
  115. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/safe_to_delete_cached_master_dir.rb +1 -1
  116. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/storeconfigs.rb +1 -1
  117. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/suppress_absent_file_details.rb +2 -1
  118. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/to_from_branch.rb +1 -1
  119. data/lib/octocatalog-diff/{catalog-diff/cli → cli}/options/validate_references.rb +1 -1
  120. data/lib/octocatalog-diff/cli/printer.rb +52 -0
  121. data/lib/octocatalog-diff/errors.rb +33 -0
  122. data/lib/octocatalog-diff/facts.rb +1 -4
  123. data/lib/octocatalog-diff/facts/puppetdb.rb +8 -7
  124. data/lib/octocatalog-diff/puppetdb.rb +5 -9
  125. data/lib/octocatalog-diff/util/catalogs.rb +242 -0
  126. metadata +97 -75
  127. data/lib/octocatalog-diff/catalog-diff/cli.rb +0 -211
  128. data/lib/octocatalog-diff/catalog-diff/cli/catalogs.rb +0 -246
  129. data/lib/octocatalog-diff/catalog-diff/cli/diffs.rb +0 -147
  130. data/lib/octocatalog-diff/catalog-diff/cli/helpers/fact_override.rb +0 -100
  131. data/lib/octocatalog-diff/catalog-diff/cli/options.rb +0 -185
  132. data/lib/octocatalog-diff/catalog-diff/cli/printer.rb +0 -54
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module OctocatalogDiff
6
+ module API
7
+ module V1
8
+ # Sets up the override of a fact or ENC parameter during catalog compilation.
9
+ class Override
10
+ # Accessors
11
+ attr_reader :key, :value
12
+
13
+ # Constructor: Accepts a key and value.
14
+ # @param input [Hash] Must contain :key and :value
15
+ def initialize(input)
16
+ @key = input.fetch(:key)
17
+ @value = parsed_value(input.fetch(:value))
18
+ end
19
+
20
+ # Initialize from a parsed command line
21
+ # @param input [String] Command line parameter
22
+ # @return [OctocatalogDiff::API::V1::Override] Initialized object
23
+ def self.create_from_input(input, key = nil)
24
+ # Normally the input will be a string in the format key=(data type)value where the data
25
+ # type is optional and the parentheses are literal. Example:
26
+ # foo=1 (auto-determine data type - in this case it would be a fixnum)
27
+ # foo=(fixnum)1 (will be a fixnum)
28
+ # foo=(string)1 (will be '1' the string)
29
+ # If input is not a string, we can still construct the object if the key is given.
30
+ # That input would come directly from code and not from the command line, since inputs
31
+ # from the command line are always strings.
32
+ if key.nil? && input.is_a?(String)
33
+ unless input.include?('=')
34
+ raise ArgumentError, "Fact override '#{input}' is not in 'key=(data type)value' format"
35
+ end
36
+ k, v = input.strip.split('=', 2).map(&:strip)
37
+ new(key: k, value: v)
38
+ elsif key.nil?
39
+ message = "Define a key when the input is not a string (#{input.class} => #{input.inspect})"
40
+ raise ArgumentError, message
41
+ else
42
+ new(key: key, value: input)
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ # Guess the datatype from a particular input
49
+ # @param input [String] Input in string format
50
+ # @return [?] Output in appropriate format
51
+ def parsed_value(input)
52
+ # If data type is explicitly given
53
+ if input =~ /^\((\w+)\)(.*)$/m
54
+ datatype = Regexp.last_match(1)
55
+ value = Regexp.last_match(2)
56
+ return convert_to_data_type(datatype.downcase, value)
57
+ end
58
+
59
+ # Guess data type
60
+ return input.to_i if input =~ /^-?\d+$/
61
+ return input.to_f if input =~ /^-?\d*\.\d+$/
62
+ return true if input.casecmp('true').zero?
63
+ return false if input.casecmp('false').zero?
64
+ input
65
+ end
66
+
67
+ # Handle data type that's explicitly given
68
+ # @param datatype [String] Data type (as a string)
69
+ # @param value [String] Value given
70
+ # @return [?] Value converted to specified data type
71
+ def convert_to_data_type(datatype, value)
72
+ return value if datatype == 'string'
73
+ return parse_json(value) if datatype == 'json'
74
+ return nil if datatype == 'nil'
75
+ if datatype == 'fixnum'
76
+ return Regexp.last_match(1).to_i if value =~ /^(-?\d+)$/
77
+ raise ArgumentError, "Illegal fixnum '#{value}'"
78
+ end
79
+ if datatype == 'float'
80
+ return Regexp.last_match(1).to_f if value =~ /^(-?\d*\.\d+)$/
81
+ return Regexp.last_match(1).to_f if value =~ /^(-?\d+)$/
82
+ raise ArgumentError, "Illegal float '#{value}'"
83
+ end
84
+ if datatype == 'boolean'
85
+ return true if value.casecmp('true').zero?
86
+ return false if value.casecmp('false').zero?
87
+ raise ArgumentError, "Illegal boolean '#{value}'"
88
+ end
89
+ raise ArgumentError, "Unknown data type '#{datatype}'"
90
+ end
91
+
92
+ # Parse JSON value
93
+ # @param input [String] Input, hopefully in JSON format
94
+ # @return [?] Output data structure
95
+ def parse_json(input)
96
+ JSON.parse(input)
97
+ rescue JSON::ParserError => exc
98
+ raise JSON::ParserError, "Failed to parse JSON: input=#{input} error=#{exc}"
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -7,6 +7,7 @@ require 'set'
7
7
  require 'stringio'
8
8
 
9
9
  require_relative '../catalog'
10
+ require_relative '../errors'
10
11
  require_relative 'filter'
11
12
 
12
13
  module OctocatalogDiff
@@ -55,14 +56,12 @@ module OctocatalogDiff
55
56
  # The heavy lifting is still handled by 'hashdiff' but we're pre-simplifying the input and post-processing
56
57
  # the output to make it easier to deal with later.
57
58
  class Differ
58
- # This class is to distinguish handled errors from unhandled ones, for spec testing.
59
- class DifferError < RuntimeError
60
- end
61
-
62
59
  # Constructor
63
60
  # @param catalog1_in [OctocatalogDiff::Catalog] First catalog to compare
64
61
  # @param catalog2_in [OctocatalogDiff::Catalog] Second catalog to compare
65
62
  def initialize(opts, catalog1_in, catalog2_in)
63
+ @catalog1_raw = catalog1_in
64
+ @catalog2_raw = catalog2_in
66
65
  @catalog1 = catalog_resources(catalog1_in, 'First catalog')
67
66
  @catalog2 = catalog_resources(catalog2_in, 'Second catalog')
68
67
  @logger = opts.fetch(:logger, Logger.new(StringIO.new))
@@ -105,6 +104,32 @@ module OctocatalogDiff
105
104
  self
106
105
  end
107
106
 
107
+ # Handle --ignore-tags option, the ability to tag resources within modules/manifests and
108
+ # have catalog-diff ignore them.
109
+ def ignore_tags
110
+ return unless @opts[:ignore_tags].is_a?(Array) && @opts[:ignore_tags].any?
111
+
112
+ # Go through the "to" catalog and identify any resources that have been tagged with one or more
113
+ # specified "ignore tags." Add any such items to the ignore list. The 'to' catalog has the authoritative
114
+ # list of dynamic ignores.
115
+ @catalog2_raw.resources.each do |resource|
116
+ next unless tagged_for_ignore?(resource)
117
+ ignore(type: resource['type'], title: resource['title'])
118
+ @logger.debug "Ignoring type='#{resource['type']}', title='#{resource['title']}' based on tag in to-catalog"
119
+ end
120
+
121
+ # Go through the "from" catalog and identify any resources that have been tagged with one or more
122
+ # specified "ignore tags." Only mark the resources for ignoring if they do not appear in the 'to'
123
+ # catalog, thereby allowing the 'to' catalog to be the authoritative ignore list. This allows deleted
124
+ # items that were previously ignored to continue to be ignored.
125
+ @catalog1_raw.resources.each do |resource|
126
+ next if @catalog2_raw.resource(type: resource['type'], title: resource['title'])
127
+ next unless tagged_for_ignore?(resource)
128
+ ignore(type: resource['type'], title: resource['title'])
129
+ @logger.debug "Ignoring type='#{resource['type']}', title='#{resource['title']}' based on tag in from-catalog"
130
+ end
131
+ end
132
+
108
133
  # Return catalog1 with filter_and_cleanups applied.
109
134
  # This is in the public section because it's called from spec tests as well
110
135
  # as being called internally.
@@ -123,6 +148,20 @@ module OctocatalogDiff
123
148
 
124
149
  private
125
150
 
151
+ # Determine if a resource is tagged with any ignore-tag.
152
+ # @param resource [Hash] The resource
153
+ # @return [Boolean] true if tagged for ignore, false if not
154
+ def tagged_for_ignore?(resource)
155
+ return false unless @opts[:ignore_tags].is_a?(Array)
156
+ return false unless resource.key?('tags') && resource['tags'].is_a?(Array)
157
+ @opts[:ignore_tags].each do |tag|
158
+ # tag_with_type will be like: 'ignored_catalog_diff__mymodule__mytype'
159
+ tag_with_type = [tag, resource['type'].downcase.gsub(/\W/, '_')].join('__')
160
+ return true if resource['tags'].include?(tag) || resource['tags'].include?(tag_with_type)
161
+ end
162
+ false
163
+ end
164
+
126
165
  # Actually perform the catalog diff. This implements the 3-part algorithm described in the
127
166
  # comment block at the top of this file.
128
167
  def catdiff
@@ -150,18 +189,31 @@ module OctocatalogDiff
150
189
  # Remove resources that have been explicitly ignored
151
190
  filter_diffs_for_ignored_items(result)
152
191
 
153
- # If a file has ensure => absent, there are certain parameters that don't matter anymore. Filter
154
- # out any such parameters from the result array.
155
- filter_diffs_for_absent_files(result) if @opts[:suppress_absent_file_details]
192
+ # Legacy options which are now filters
193
+ @opts[:filters] ||= []
194
+ add_element_to_array(@opts[:filters], 'CompilationDir')
195
+ add_element_to_array(@opts[:filters], 'AbsentFile') if @opts[:suppress_absent_file_details]
156
196
 
157
197
  # Apply any additional pluggable filters.
158
- OctocatalogDiff::CatalogDiff::Filter.apply_filters(result, @opts[:filters])
198
+ filter_opts = {
199
+ logger: @logger,
200
+ from_compilation_dir: @catalog1_raw.compilation_dir,
201
+ to_compilation_dir: @catalog2_raw.compilation_dir
202
+ }
203
+ OctocatalogDiff::CatalogDiff::Filter.apply_filters(result, @opts[:filters], filter_opts) if @opts[:filters].any?
159
204
 
160
205
  # That's it!
161
206
  @logger.debug "Exiting catdiff; change count: #{result.size}"
162
207
  result
163
208
  end
164
209
 
210
+ # Add an element to an array if it doesn't already exist in that array
211
+ # @param array_in [Array] Array to have element added (**mutated** by this method)
212
+ # @param element [?] Element to add
213
+ def add_element_to_array(array_in, element)
214
+ array_in << element unless array_in.include?(element)
215
+ end
216
+
165
217
  # Filter the differences for any items that were ignored, by some combination of type, title, and
166
218
  # attribute. This modifies the array itself by selecting only items that do not meet the ignored
167
219
  # filter.
@@ -169,43 +221,6 @@ module OctocatalogDiff
169
221
  result.reject! { |item| ignored?(item) }
170
222
  end
171
223
 
172
- # If a file has ensure => absent, there are certain parameters that don't matter anymore. Filter
173
- # out any such parameters from the result array.
174
- # @param result [Array] Diff result list (modified by this method)
175
- def filter_diffs_for_absent_files(result)
176
- @logger.debug "Entering filter_diffs_for_absent_files with #{result.size} diffs"
177
-
178
- # Scan for files in the result that are file resources with ensure => absent.
179
- absent_files = Set.new
180
- result.each do |diff|
181
- next unless diff[0] == '~' || diff[0] == '!'
182
- next unless diff[1] =~ /^File\f([^\f]+)\fparameters\fensure$/
183
- next unless ['absent', 'false', false].include?(diff[3])
184
- absent_files.add Regexp.last_match(1)
185
- end
186
-
187
- # If there are any absent files, remove all diffs referencing that file, except for
188
- # the change to 'ensure'.
189
- if absent_files.any?
190
- keep = %w(ensure backup force provider)
191
- result.map! do |diff|
192
- if (diff[0] == '!' || diff[0] == '~') && diff[1] =~ /^File\f(.+)\fparameters\f(.+)$/
193
- if absent_files.include?(Regexp.last_match(1)) && !keep.include?(Regexp.last_match(2))
194
- @logger.debug "Removing file=#{Regexp.last_match(1)} parameter=#{Regexp.last_match(2)} for absent file"
195
- nil
196
- else
197
- diff
198
- end
199
- else
200
- diff
201
- end
202
- end
203
- result.compact!
204
- end
205
-
206
- @logger.debug "Exiting filter_diffs_for_absent_files with #{result.size} diffs"
207
- end
208
-
209
224
  # Pre-processing of a catalog.
210
225
  # - Remove 'before' and 'require' from parameters
211
226
  # - Sort 'tags' array, or remove the tags array if tags are being ignored
@@ -368,7 +383,11 @@ module OctocatalogDiff
368
383
  rule = rule_in.dup
369
384
 
370
385
  # Type matches?
371
- return false unless rule[:type] == '*' || rule[:type].casecmp(hsh[:type]).zero?
386
+ if rule[:type].is_a?(Regexp)
387
+ return false unless hsh[:type].match(rule[:type])
388
+ elsif rule[:type].is_a?(String)
389
+ return false unless rule[:type] == '*' || rule[:type].casecmp(hsh[:type]).zero?
390
+ end
372
391
 
373
392
  # Title matches? (Support regexp and string)
374
393
  if rule[:title].is_a?(Regexp)
@@ -594,7 +613,7 @@ module OctocatalogDiff
594
613
  # @return [Hash] Internal simplified hash object
595
614
  def catalog_resources(catalog_in, name = 'Passed catalog')
596
615
  return catalog_in.resources if catalog_in.is_a?(OctocatalogDiff::Catalog)
597
- raise DifferError, "#{name} is not a valid catalog (input datatype: #{catalog_in.class})"
616
+ raise OctocatalogDiff::Errors::DifferError, "#{name} is not a valid catalog (input datatype: #{catalog_in.class})"
598
617
  end
599
618
 
600
619
  # Turn array of resources into a hash by serialized keys. For consistency with 'hashdiff'
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative '../api/v1/diff'
3
4
  require_relative 'differ'
4
5
  require_relative 'display/json'
6
+ require_relative 'display/legacy_json'
5
7
  require_relative 'display/text'
6
8
 
7
9
  module OctocatalogDiff
@@ -21,8 +23,9 @@ module OctocatalogDiff
21
23
  # @param logger [Logger] Logger object
22
24
  # @return [String] Text output for provided diff
23
25
  def self.output(diff_in, options = {}, logger = nil)
24
- diff = diff_in.is_a?(OctocatalogDiff::CatalogDiff::Differ) ? diff_in.diff : diff_in
25
- raise ArgumentError, "text_output requires Array<Diff results>; passed in #{diff_in.class}" unless diff.is_a?(Array)
26
+ diff_x = diff_in.is_a?(OctocatalogDiff::CatalogDiff::Differ) ? diff_in.diff : diff_in
27
+ raise ArgumentError, "text_output requires Array<Diff results>; passed in #{diff_in.class}" unless diff_x.is_a?(Array)
28
+ diff = diff_x.map { |x| OctocatalogDiff::API::V1::Diff.factory(x) }
26
29
 
27
30
  # req_format means 'requested format' because 'format' has a built-in meaning to Ruby
28
31
  req_format = options.fetch(:format, :color_text)
@@ -41,6 +44,9 @@ module OctocatalogDiff
41
44
  when :json
42
45
  logger.debug 'Generating JSON output' if logger
43
46
  OctocatalogDiff::CatalogDiff::Display::Json.generate(diff, opts, logger)
47
+ when :legacy_json
48
+ logger.debug 'Generating Legacy JSON output' if logger
49
+ OctocatalogDiff::CatalogDiff::Display::LegacyJson.generate(diff, opts, logger)
44
50
  when :text
45
51
  logger.debug 'Generating non-colored text output' if logger
46
52
  OctocatalogDiff::CatalogDiff::Display::Text.generate(diff, opts.merge(color: false), logger)
@@ -7,7 +7,8 @@ require 'json'
7
7
  module OctocatalogDiff
8
8
  module CatalogDiff
9
9
  class Display
10
- # Display the output from a diff in JSON format.
10
+ # Display the output from a diff in JSON format. This is the new format, used in octocatalog-diff
11
+ # 1.x, where each diff is represented by an hash with named keys.
11
12
  class Json < OctocatalogDiff::CatalogDiff::Display
12
13
  # Generate JSON representation of the 'diff' suitable for further analysis.
13
14
  # @param diff [Array<Diff results>] The diff which *must* be in this format
@@ -16,7 +17,7 @@ module OctocatalogDiff
16
17
  # @param _logger [Logger] Not used here
17
18
  def self.generate(diff, options = {}, _logger = nil)
18
19
  result = {
19
- 'diff' => diff
20
+ 'diff' => diff.map(&:to_h_with_string_keys)
20
21
  }
21
22
  result['header'] = options[:header] unless options[:header].nil?
22
23
  result.to_json
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../display'
4
+
5
+ require 'json'
6
+
7
+ module OctocatalogDiff
8
+ module CatalogDiff
9
+ class Display
10
+ # Display the output from a diff in JSON format. This is the legacy format, used in octocatalog-diff
11
+ # 0.x, where each diff is represented by an array.
12
+ class LegacyJson < OctocatalogDiff::CatalogDiff::Display
13
+ # Generate JSON representation of the 'diff' suitable for further analysis.
14
+ # @param diff [Array<Diff results>] The diff which *must* be in this format
15
+ # @param options [Hash] Options which are:
16
+ # - :header => [String] Header to print; no header is printed if not specified
17
+ # @param _logger [Logger] Not used here
18
+ def self.generate(diff, options = {}, _logger = nil)
19
+ result = {
20
+ 'diff' => diff.map(&:raw)
21
+ }
22
+ result['header'] = options[:header] unless options[:header].nil?
23
+ result.to_json
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -257,13 +257,67 @@ module OctocatalogDiff
257
257
  # @param depth [Fixnum] Depth, for correct indentation
258
258
  # @return Array<String> Displayable result
259
259
  def self.diff_two_strings_with_diffy(string1, string2, depth)
260
- # prevent 'No newline at end of file' for single line strings
261
- string1 += "\n" unless string1 =~ /\n/
262
- string2 += "\n" unless string2 =~ /\n/
260
+ # Single line strings?
261
+ if single_lines?(string1, string2)
262
+ string1, string2 = add_trailing_newlines(string1, string2)
263
+ diff = Diffy::Diff.new(string1, string2, context: 2, include_diff_info: false).to_s.split("\n")
264
+ return diff.map { |x| left_pad(2 * depth + 2, make_trailing_whitespace_visible(adjust_position_of_plus_minus(x))) }
265
+ end
266
+
267
+ # Multiple line strings
268
+ string1, string2 = add_trailing_newlines(string1, string2)
263
269
  diff = Diffy::Diff.new(string1, string2, context: 2, include_diff_info: true).to_s.split("\n")
264
270
  diff.shift # Remove first line of diff info (filename that makes no sense)
265
271
  diff.shift # Remove second line of diff info (filename that makes no sense)
266
- diff.map { |x| left_pad(2 * depth + 2, x) }
272
+ diff.map { |x| left_pad(2 * depth + 2, make_trailing_whitespace_visible(x)) }
273
+ end
274
+
275
+ # Determine if two incoming strings are single lines. Returns true if both
276
+ # incoming strings are single lines, false otherwise.
277
+ # @param string_1 [String] First string
278
+ # @param string_2 [String] Second string
279
+ # @return [Boolean] Whether both incoming strings are single lines
280
+ def self.single_lines?(string_1, string_2)
281
+ string_1.strip !~ /\n/ && string_2.strip !~ /\n/
282
+ end
283
+
284
+ # Add "\n" to the end of both strings, only if both strings are lacking it.
285
+ # This prevents "\" for single string comparison.
286
+ # @param string_1 [String] First string
287
+ # @param string_2 [String] Second string
288
+ # @return [Array<String>] Adjusted string_1, string_2
289
+ def self.add_trailing_newlines(string_1, string_2)
290
+ return [string_1, string_2] unless string_1 !~ /\n\Z/ && string_2 !~ /\n\Z/
291
+ [string_1 + "\n", string_2 + "\n"]
292
+ end
293
+
294
+ # Adjust the space after of the `-` / `+` in the diff for single line diffs.
295
+ # Diffy prints diffs with no space between the `-` / `+` in the text, but for
296
+ # single lines it's easier to read with that space added.
297
+ # @param string_in [String] Input string, which is a line of a diff from diffy
298
+ # @return [String] Modified string
299
+ def self.adjust_position_of_plus_minus(string_in)
300
+ string_in.sub(/\A(\e\[\d+m)?([\-\+])/, '\1\2 ')
301
+ end
302
+
303
+ # Convert trailing whitespace to underscore for display purposes. Also convert special
304
+ # whitespace (\r, \n, \t, ...) to character representation.
305
+ # @param string_in [String] Input string, which might contain trailing whitespace
306
+ # @return [String] Modified string
307
+ def self.make_trailing_whitespace_visible(string_in)
308
+ return string_in unless string_in =~ /\A((?:.|\n)*?)(\s+)(\e\[0m)?\Z/
309
+ beginning = Regexp.last_match(1)
310
+ trailing_space = Regexp.last_match(2)
311
+ end_escape = Regexp.last_match(3)
312
+
313
+ # Trailing space adjustment for line endings
314
+ trailing_space.gsub! "\n", '\n'
315
+ trailing_space.gsub! "\r", '\r'
316
+ trailing_space.gsub! "\t", '\t'
317
+ trailing_space.gsub! "\f", '\f'
318
+ trailing_space.tr! ' ', '_'
319
+
320
+ [beginning, trailing_space, end_escape].join('')
267
321
  end
268
322
 
269
323
  # Get the diff of two hashes. Call the 'diffy' gem for this.
@@ -319,9 +373,9 @@ module OctocatalogDiff
319
373
  if nested && obj[:old].is_a?(Hash) && obj[:new].is_a?(Hash)
320
374
  # Nested hashes will be stringified and then use 'diffy'
321
375
  result.concat diff_two_hashes_with_diffy(depth: depth, hash1: obj[:old], hash2: obj[:new])
322
- elsif obj[:old].is_a?(String) && obj[:new].is_a?(String) && (obj[:old] =~ /\n/ || obj[:new] =~ /\n/)
323
- # Multi-line strings will be split and then use 'diffy' to mimic the
324
- # output seen when using "diff" on the command line
376
+ elsif obj[:old].is_a?(String) && obj[:new].is_a?(String)
377
+ # Strings will use 'diffy' to mimic the output seen when using
378
+ # "diff" on the command line.
325
379
  result.concat diff_two_strings_with_diffy(obj[:old], obj[:new], depth)
326
380
  else
327
381
  # Stuff we don't recognize will be converted to a string and printed
@@ -395,8 +449,9 @@ module OctocatalogDiff
395
449
  else
396
450
  # Adjust the display and return modified object
397
451
  msg = "Adjust display for #{diff_obj[1].gsub(/\f/, '::')}: " \
398
- "#{diff_obj[2].inspect} -> #{x2}; "\
399
- "#{diff_obj[3].inspect} -> #{x3}"
452
+ "old=#{x2.inspect} new=#{x3.inspect} "\
453
+ "(extra debugging: #{diff_obj[2].inspect} -> #{x2}; "\
454
+ "#{diff_obj[3].inspect} -> #{x3})"
400
455
  logger.debug(msg) if logger
401
456
  diff_obj[2] = x2
402
457
  diff_obj[3] = x3