octocatalog-diff 0.5.1

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.
Files changed (143) hide show
  1. checksums.yaml +7 -0
  2. data/.version +1 -0
  3. data/LICENSE +20 -0
  4. data/README.md +82 -0
  5. data/bin/octocatalog-diff +75 -0
  6. data/doc/advanced-bootstrap.md +33 -0
  7. data/doc/advanced-cache-dir.md +24 -0
  8. data/doc/advanced-catalog-only.md +37 -0
  9. data/doc/advanced-ci.md +13 -0
  10. data/doc/advanced-dynamic-ignores.md +123 -0
  11. data/doc/advanced-future-parser.md +11 -0
  12. data/doc/advanced-ignores.md +224 -0
  13. data/doc/advanced-output-formats.md +96 -0
  14. data/doc/advanced-output-hacks.md +45 -0
  15. data/doc/advanced-override-facts.md +67 -0
  16. data/doc/advanced-pe-enc.md +52 -0
  17. data/doc/advanced-puppet-master.md +50 -0
  18. data/doc/advanced-puppet-versions.md +9 -0
  19. data/doc/advanced-storeconfigs.md +72 -0
  20. data/doc/advanced-using-without-git.md +15 -0
  21. data/doc/advanced.md +43 -0
  22. data/doc/basic.md +70 -0
  23. data/doc/configuration-enc.md +69 -0
  24. data/doc/configuration-hiera.md +103 -0
  25. data/doc/configuration-puppetdb.md +49 -0
  26. data/doc/configuration.md +51 -0
  27. data/doc/dev/README.md +1 -0
  28. data/doc/dev/coverage.md +34 -0
  29. data/doc/dev/how-to-add-options.md +83 -0
  30. data/doc/dev/integration-tests.md +63 -0
  31. data/doc/dev/releasing.md +19 -0
  32. data/doc/installation.md +49 -0
  33. data/doc/limitations.md +34 -0
  34. data/doc/optionsref.md +947 -0
  35. data/doc/requirements.md +16 -0
  36. data/doc/roadmap.md +26 -0
  37. data/doc/similar.md +17 -0
  38. data/doc/troubleshooting.md +54 -0
  39. data/lib/octocatalog-diff.rb +12 -0
  40. data/lib/octocatalog-diff/bootstrap.rb +53 -0
  41. data/lib/octocatalog-diff/catalog-diff/cli.rb +205 -0
  42. data/lib/octocatalog-diff/catalog-diff/cli/catalogs.rb +240 -0
  43. data/lib/octocatalog-diff/catalog-diff/cli/diffs.rb +145 -0
  44. data/lib/octocatalog-diff/catalog-diff/cli/helpers/fact_override.rb +99 -0
  45. data/lib/octocatalog-diff/catalog-diff/cli/options.rb +173 -0
  46. data/lib/octocatalog-diff/catalog-diff/cli/options/basedir.rb +14 -0
  47. data/lib/octocatalog-diff/catalog-diff/cli/options/bootstrap_environment.rb +18 -0
  48. data/lib/octocatalog-diff/catalog-diff/cli/options/bootstrap_script.rb +14 -0
  49. data/lib/octocatalog-diff/catalog-diff/cli/options/bootstrap_then_exit.rb +12 -0
  50. data/lib/octocatalog-diff/catalog-diff/cli/options/bootstrapped_dirs.rb +18 -0
  51. data/lib/octocatalog-diff/catalog-diff/cli/options/cached_master_dir.rb +21 -0
  52. data/lib/octocatalog-diff/catalog-diff/cli/options/catalog_only.rb +14 -0
  53. data/lib/octocatalog-diff/catalog-diff/cli/options/color.rb +13 -0
  54. data/lib/octocatalog-diff/catalog-diff/cli/options/compare_file_text.rb +15 -0
  55. data/lib/octocatalog-diff/catalog-diff/cli/options/debug.rb +12 -0
  56. data/lib/octocatalog-diff/catalog-diff/cli/options/display_datatype_changes.rb +16 -0
  57. data/lib/octocatalog-diff/catalog-diff/cli/options/display_detail_add.rb +12 -0
  58. data/lib/octocatalog-diff/catalog-diff/cli/options/display_source_file_line.rb +12 -0
  59. data/lib/octocatalog-diff/catalog-diff/cli/options/enc.rb +31 -0
  60. data/lib/octocatalog-diff/catalog-diff/cli/options/existing_catalogs.rb +25 -0
  61. data/lib/octocatalog-diff/catalog-diff/cli/options/fact_file.rb +23 -0
  62. data/lib/octocatalog-diff/catalog-diff/cli/options/fact_override.rb +19 -0
  63. data/lib/octocatalog-diff/catalog-diff/cli/options/facts_terminus.rb +16 -0
  64. data/lib/octocatalog-diff/catalog-diff/cli/options/from_puppetdb.rb +13 -0
  65. data/lib/octocatalog-diff/catalog-diff/cli/options/header.rb +24 -0
  66. data/lib/octocatalog-diff/catalog-diff/cli/options/hiera_config.rb +18 -0
  67. data/lib/octocatalog-diff/catalog-diff/cli/options/hiera_path_strip.rb +12 -0
  68. data/lib/octocatalog-diff/catalog-diff/cli/options/hostname.rb +13 -0
  69. data/lib/octocatalog-diff/catalog-diff/cli/options/ignore.rb +24 -0
  70. data/lib/octocatalog-diff/catalog-diff/cli/options/ignore_attr.rb +16 -0
  71. data/lib/octocatalog-diff/catalog-diff/cli/options/ignore_tags.rb +23 -0
  72. data/lib/octocatalog-diff/catalog-diff/cli/options/include_tags.rb +12 -0
  73. data/lib/octocatalog-diff/catalog-diff/cli/options/master_cache_branch.rb +12 -0
  74. data/lib/octocatalog-diff/catalog-diff/cli/options/output_file.rb +15 -0
  75. data/lib/octocatalog-diff/catalog-diff/cli/options/output_format.rb +15 -0
  76. data/lib/octocatalog-diff/catalog-diff/cli/options/parallel.rb +12 -0
  77. data/lib/octocatalog-diff/catalog-diff/cli/options/parser.rb +48 -0
  78. data/lib/octocatalog-diff/catalog-diff/cli/options/pass_env_vars.rb +19 -0
  79. data/lib/octocatalog-diff/catalog-diff/cli/options/pe_enc_ssl_ca.rb +15 -0
  80. data/lib/octocatalog-diff/catalog-diff/cli/options/pe_enc_ssl_client_cert.rb +14 -0
  81. data/lib/octocatalog-diff/catalog-diff/cli/options/pe_enc_ssl_client_key.rb +14 -0
  82. data/lib/octocatalog-diff/catalog-diff/cli/options/pe_enc_token.rb +15 -0
  83. data/lib/octocatalog-diff/catalog-diff/cli/options/pe_enc_token_file.rb +17 -0
  84. data/lib/octocatalog-diff/catalog-diff/cli/options/pe_enc_url.rb +19 -0
  85. data/lib/octocatalog-diff/catalog-diff/cli/options/puppet_binary.rb +16 -0
  86. data/lib/octocatalog-diff/catalog-diff/cli/options/puppet_master.rb +16 -0
  87. data/lib/octocatalog-diff/catalog-diff/cli/options/puppet_master_api_version.rb +20 -0
  88. data/lib/octocatalog-diff/catalog-diff/cli/options/puppet_master_ssl_ca.rb +19 -0
  89. data/lib/octocatalog-diff/catalog-diff/cli/options/puppet_master_ssl_client_cert.rb +19 -0
  90. data/lib/octocatalog-diff/catalog-diff/cli/options/puppet_master_ssl_client_key.rb +19 -0
  91. data/lib/octocatalog-diff/catalog-diff/cli/options/puppetdb_ssl_ca.rb +15 -0
  92. data/lib/octocatalog-diff/catalog-diff/cli/options/puppetdb_ssl_client_cert.rb +14 -0
  93. data/lib/octocatalog-diff/catalog-diff/cli/options/puppetdb_ssl_client_key.rb +14 -0
  94. data/lib/octocatalog-diff/catalog-diff/cli/options/puppetdb_ssl_client_password.rb +14 -0
  95. data/lib/octocatalog-diff/catalog-diff/cli/options/puppetdb_ssl_client_password_file.rb +13 -0
  96. data/lib/octocatalog-diff/catalog-diff/cli/options/puppetdb_url.rb +18 -0
  97. data/lib/octocatalog-diff/catalog-diff/cli/options/quiet.rb +12 -0
  98. data/lib/octocatalog-diff/catalog-diff/cli/options/retry_failed_catalog.rb +13 -0
  99. data/lib/octocatalog-diff/catalog-diff/cli/options/safe_to_delete_cached_master_dir.rb +15 -0
  100. data/lib/octocatalog-diff/catalog-diff/cli/options/storeconfigs.rb +12 -0
  101. data/lib/octocatalog-diff/catalog-diff/cli/options/suppress_absent_file_details.rb +14 -0
  102. data/lib/octocatalog-diff/catalog-diff/cli/options/to_from_branch.rb +16 -0
  103. data/lib/octocatalog-diff/catalog-diff/cli/printer.rb +52 -0
  104. data/lib/octocatalog-diff/catalog-diff/differ.rb +615 -0
  105. data/lib/octocatalog-diff/catalog-diff/display.rb +125 -0
  106. data/lib/octocatalog-diff/catalog-diff/display/json.rb +25 -0
  107. data/lib/octocatalog-diff/catalog-diff/display/text.rb +452 -0
  108. data/lib/octocatalog-diff/catalog-util/bootstrap.rb +145 -0
  109. data/lib/octocatalog-diff/catalog-util/builddir.rb +289 -0
  110. data/lib/octocatalog-diff/catalog-util/cached_master_directory.rb +169 -0
  111. data/lib/octocatalog-diff/catalog-util/command.rb +96 -0
  112. data/lib/octocatalog-diff/catalog-util/enc.rb +77 -0
  113. data/lib/octocatalog-diff/catalog-util/enc/noop.rb +22 -0
  114. data/lib/octocatalog-diff/catalog-util/enc/pe.rb +99 -0
  115. data/lib/octocatalog-diff/catalog-util/enc/pe/v1.rb +61 -0
  116. data/lib/octocatalog-diff/catalog-util/enc/script.rb +88 -0
  117. data/lib/octocatalog-diff/catalog-util/facts.rb +89 -0
  118. data/lib/octocatalog-diff/catalog-util/fileresources.rb +83 -0
  119. data/lib/octocatalog-diff/catalog-util/git.rb +65 -0
  120. data/lib/octocatalog-diff/catalog.rb +209 -0
  121. data/lib/octocatalog-diff/catalog/computed.rb +205 -0
  122. data/lib/octocatalog-diff/catalog/json.rb +30 -0
  123. data/lib/octocatalog-diff/catalog/noop.rb +19 -0
  124. data/lib/octocatalog-diff/catalog/puppetdb.rb +82 -0
  125. data/lib/octocatalog-diff/catalog/puppetmaster.rb +121 -0
  126. data/lib/octocatalog-diff/external/pson/LICENSE +17 -0
  127. data/lib/octocatalog-diff/external/pson/README.md +20 -0
  128. data/lib/octocatalog-diff/external/pson/common.rb +370 -0
  129. data/lib/octocatalog-diff/external/pson/pure.rb +15 -0
  130. data/lib/octocatalog-diff/external/pson/pure/generator.rb +395 -0
  131. data/lib/octocatalog-diff/external/pson/pure/parser.rb +307 -0
  132. data/lib/octocatalog-diff/external/pson/version.rb +8 -0
  133. data/lib/octocatalog-diff/facts.rb +125 -0
  134. data/lib/octocatalog-diff/facts/json.rb +20 -0
  135. data/lib/octocatalog-diff/facts/puppetdb.rb +59 -0
  136. data/lib/octocatalog-diff/facts/yaml.rb +29 -0
  137. data/lib/octocatalog-diff/puppetdb.rb +163 -0
  138. data/lib/octocatalog-diff/util/colored.rb +20 -0
  139. data/lib/octocatalog-diff/util/httparty.rb +158 -0
  140. data/lib/octocatalog-diff/util/parallel.rb +170 -0
  141. data/lib/octocatalog-diff/util/puppetversion.rb +24 -0
  142. data/lib/octocatalog-diff/version.rb +7 -0
  143. metadata +386 -0
@@ -0,0 +1,125 @@
1
+ require_relative 'differ'
2
+ require_relative 'display/json'
3
+ require_relative 'display/text'
4
+
5
+ module OctocatalogDiff
6
+ module CatalogDiff
7
+ # Prepare a display of the results from a catalog-diff. Intended that this will contain utility
8
+ # methods but call out to a OctocatalogDiff::CatalogDiff::Display::<something> class to display in
9
+ # the desired format.
10
+ class Display
11
+ # Display the diff in some specified format.
12
+ # @param diff_in [OctocatalogDiff::CatalogDiff::Differ | Array<Diff results>] Diff to display
13
+ # @param options [Hash] Consisting of:
14
+ # - :header [String] => Header (can be :default to construct header)
15
+ # - :display_source_file_line [Boolean] => Display manifest filename and line number where declared
16
+ # - :compilation_from_dir [String] => Directory where 'from' catalog was compiled
17
+ # - :compilation_to_dir [String] => Directory where 'to' catalog was compiled
18
+ # - :display_detail_add [Boolean] => Set true to display parameters of newly added resources
19
+ # @param logger [Logger] Logger object
20
+ # @return [String] Text output for provided diff
21
+ def self.output(diff_in, options = {}, logger = nil)
22
+ diff = diff_in.is_a?(OctocatalogDiff::CatalogDiff::Differ) ? diff_in.diff : diff_in
23
+ raise ArgumentError, "text_output requires Array<Diff results>; passed in #{diff_in.class}" unless diff.is_a?(Array)
24
+
25
+ # req_format means 'requested format' because 'format' has a built-in meaning to Ruby
26
+ req_format = options.fetch(:format, :color_text)
27
+
28
+ # Options hash to pass to display method
29
+ opts = {}
30
+ opts[:header] = header(options)
31
+ opts[:display_source_file_line] = options.fetch(:display_source_file_line, false)
32
+ opts[:compilation_from_dir] = options[:compilation_from_dir] || nil
33
+ opts[:compilation_to_dir] = options[:compilation_to_dir] || nil
34
+ opts[:display_detail_add] = options.fetch(:display_detail_add, false)
35
+ opts[:display_datatype_changes] = options.fetch(:display_datatype_changes, false)
36
+
37
+ # Call appropriate display method
38
+ case req_format
39
+ when :json
40
+ logger.debug 'Generating JSON output' if logger
41
+ OctocatalogDiff::CatalogDiff::Display::Json.generate(diff, opts, logger)
42
+ when :text
43
+ logger.debug 'Generating non-colored text output' if logger
44
+ OctocatalogDiff::CatalogDiff::Display::Text.generate(diff, opts.merge(color: false), logger)
45
+ when :color_text
46
+ logger.debug 'Generating colored text output' if logger
47
+ OctocatalogDiff::CatalogDiff::Display::Text.generate(diff, opts.merge(color: true), logger)
48
+ else
49
+ raise ArgumentError, "Unrecognized text format '#{req_format}'"
50
+ end
51
+ end
52
+
53
+ # Utility method!
54
+ # Construct the header for diffs
55
+ # Default is diff <old_branch_name>/<node_name> <new_branch_name>/<node_name>
56
+ # @param opts [Hash] Options hash from CLI
57
+ # @return [String] Header in indicated format
58
+ def self.header(opts)
59
+ return nil if opts[:no_header]
60
+ return opts[:header] unless opts[:header] == :default
61
+ node = opts.fetch(:node, 'node')
62
+ from_br = opts.fetch(:from_env, 'a')
63
+ to_br = opts.fetch(:to_env, 'b')
64
+ from_br = 'current' if from_br == '.'
65
+ to_br = 'current' if to_br == '.'
66
+ "diff #{from_br}/#{node} #{to_br}/#{node}"
67
+ end
68
+
69
+ # Utility method!
70
+ # Go through the 'diff' array, filtering out ignored items and classifying each change
71
+ # as an addition (+), subtraction (-), change (~), or nested change (!). This creates
72
+ # hashes for each type of change that are consumed later for ordering purposes.
73
+ # @param diff [Array<Diff results>] The diff which *must* be in this format
74
+ # @return [Array<Hash of adds, Hash of removes, Hash of changes, Hash of nested] Processed results
75
+ def self.parse_diff_array_into_categorized_hashes(diff)
76
+ only_in_old = {}
77
+ only_in_new = {}
78
+ changed = {}
79
+ diff.each do |diff_obj|
80
+ (type, title, the_rest) = diff_obj[1].split(/\f/, 3)
81
+ key = "#{type}[#{title}]"
82
+ if ['-', '+'].include?(diff_obj[0])
83
+ only_in_old[key] = { diff: diff_obj[2], loc: diff_obj[3] } if diff_obj[0] == '-'
84
+ only_in_new[key] = { diff: diff_obj[2], loc: diff_obj[3] } if diff_obj[0] == '+'
85
+ elsif ['~', '!'].include?(diff_obj[0])
86
+ # HashDiff reports these as diffs for some reason
87
+ next if diff_obj[2].nil? && diff_obj[3].nil?
88
+
89
+ # This turns "foo\fbar\fbaz" into hash['foo']['bar']['baz']
90
+ result = the_rest.split(/\f/).reverse.inject(old: diff_obj[2], new: diff_obj[3]) { |a, e| { e => a } }
91
+
92
+ # Assign to appropriate variable
93
+ diff = changed.key?(key) ? changed[key][:diff] : {}
94
+ simple_deep_merge!(diff, result)
95
+ changed[key] = { diff: diff, old_loc: diff_obj[4], new_loc: diff_obj[5] }
96
+ else
97
+ raise "Unrecognized diff symbol '#{diff_obj[0]}' in #{diff_obj.inspect}"
98
+ end
99
+ end
100
+ [only_in_new, only_in_old, changed]
101
+ end
102
+
103
+ # Utility Method!
104
+ # Deep merge two hashes. (The 'deep_merge' gem seems to de-duplicate arrays so this is a reinvention
105
+ # of the wheel, but a simpler wheel that does just exactly what we need.)
106
+ # @param hash1 [Hash] First object
107
+ # @param hash2 [Hash] Second object
108
+ def self.simple_deep_merge!(hash1, hash2)
109
+ raise ArgumentError, 'First argument to simple_deep_merge must be a hash' unless hash1.is_a?(Hash)
110
+ raise ArgumentError, 'Second argument to simple_deep_merge must be a hash' unless hash2.is_a?(Hash)
111
+ hash2.each do |k, v|
112
+ if v.is_a?(Hash) && hash1[k].is_a?(Hash)
113
+ # We can only merge a hash with a hash. If hash1[k] is something other than a hash, say for example
114
+ # a string, then the merging is NOT invoked and hash1[k] gets directly overwritten in the `else` clause.
115
+ # Also if hash1[k] is nil, it falls through to the `else` clause where it gets set directly to the result
116
+ # hash without needless iterations.
117
+ simple_deep_merge!(hash1[k], v)
118
+ else
119
+ hash1[k] = v
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,25 @@
1
+ require_relative '../display'
2
+
3
+ require 'json'
4
+
5
+ module OctocatalogDiff
6
+ module CatalogDiff
7
+ class Display
8
+ # Display the output from a diff in JSON format.
9
+ class Json < OctocatalogDiff::CatalogDiff::Display
10
+ # Generate JSON representation of the 'diff' suitable for further analysis.
11
+ # @param diff [Array<Diff results>] The diff which *must* be in this format
12
+ # @param options [Hash] Options which are:
13
+ # - :header => [String] Header to print; no header is printed if not specified
14
+ # @param _logger [Logger] Not used here
15
+ def self.generate(diff, options = {}, _logger = nil)
16
+ result = {
17
+ 'diff' => diff
18
+ }
19
+ result['header'] = options[:header] unless options[:header].nil?
20
+ result.to_json
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,452 @@
1
+ require_relative '../display'
2
+ require_relative '../../util/colored.rb'
3
+
4
+ require 'diffy'
5
+ require 'json'
6
+
7
+ module OctocatalogDiff
8
+ module CatalogDiff
9
+ class Display
10
+ # Display the output from a diff in text format. Uses the 'diffy' gem to provide diffs in
11
+ # blocks of text. Formats results in a logical Puppet output.
12
+ class Text < OctocatalogDiff::CatalogDiff::Display
13
+ SEPARATOR = '*******************************************'.freeze
14
+
15
+ # Generate the text representation of the 'diff' suitable for rendering in a console or log.
16
+ # @param diff [Array<Diff results>] The diff which *must* be in this format
17
+ # @param options_in [Hash] Options which are:
18
+ # - :color => [Boolean] True or false, whether to use color codes
19
+ # - :header => [String] Header to print; no header is printed if not specified
20
+ # - :display_source_file_line [Boolean] True or false, print filename and line (if known)
21
+ # - :display_detail_add [Boolean] If true, print details of any added resources
22
+ # @param logger [Logger] Logger object
23
+ # @return [Array<String>] Results
24
+ def self.generate(diff, options_in = {}, logger = nil)
25
+ # Empty?
26
+ return [] if diff.empty?
27
+
28
+ # We may modify this for temporary local use, but don't want to pass these changes
29
+ # back to the rest of the program.
30
+ options = options_in.dup
31
+
32
+ # Enable color support if requested...
33
+ String.colors_enabled = options.fetch(:color, true)
34
+
35
+ previous_diffy_default_format = Diffy::Diff.default_format
36
+ Diffy::Diff.default_format = options.fetch(:color, true) ? :color : :text
37
+
38
+ # Strip out differences or update display where string matches but data type differs.
39
+ # For example, 28 (the integer) and "28" (the string) have identical string
40
+ # representations, but are different data types. Same for nil vs. "".
41
+ adjust_for_display_datatype_changes(diff, options[:display_datatype_changes], logger)
42
+
43
+ # Call the utility method to sort changes into their respective types
44
+ only_in_new, only_in_old, changed = parse_diff_array_into_categorized_hashes(diff)
45
+ sorted_list = only_in_old.keys | only_in_new.keys | changed.keys
46
+ sorted_list.sort!
47
+ unless logger.nil?
48
+ logger.debug "Added resources: #{only_in_new.keys.count}"
49
+ logger.debug "Removed resources: #{only_in_old.keys.count}"
50
+ logger.debug "Changed resources: #{changed.keys.count}"
51
+ end
52
+
53
+ # Run through the list to build the result
54
+ result = []
55
+ sorted_list.each do |item|
56
+ # Print the header if needed
57
+ unless options[:header].nil?
58
+ result << options[:header] unless options[:header].empty?
59
+ result << SEPARATOR
60
+ options[:header] = nil
61
+ end
62
+
63
+ # A removed item appears only in the old hash.
64
+ if only_in_old.key?(item)
65
+ result.concat display_removed_item(
66
+ item: item,
67
+ old_loc: only_in_old[item][:loc],
68
+ options: options,
69
+ logger: logger
70
+ )
71
+
72
+ # An added item appears only in the new hash.
73
+ elsif only_in_new.key?(item)
74
+ result.concat display_added_item(
75
+ item: item,
76
+ new_loc: only_in_new[item][:loc],
77
+ diff: only_in_new[item][:diff],
78
+ options: options,
79
+ logger: logger
80
+ )
81
+
82
+ # A change can appear either in the change hash, the nested hash, or both.
83
+ # Therefore, changes and nested changes are combined for display.
84
+ elsif changed.key?(item)
85
+ result.concat display_changed_or_nested_item(
86
+ item: item,
87
+ old_loc: changed[item][:old_loc],
88
+ new_loc: changed[item][:new_loc],
89
+ diff: changed[item][:diff],
90
+ options: options,
91
+ logger: logger
92
+ )
93
+
94
+ # An unrecognized change throws an error. This indicates a bug.
95
+ else
96
+ # :nocov:
97
+ raise "BUG (please report): Unable to determine diff type of item: #{item.inspect}"
98
+ # :nocov:
99
+ end
100
+ result << SEPARATOR
101
+ end
102
+
103
+ # Reset the global color-related flags
104
+ String.colors_enabled = false
105
+ Diffy::Diff.default_format = previous_diffy_default_format
106
+
107
+ # The end
108
+ result
109
+ end
110
+
111
+ # Display a changed or nested item
112
+ # @param item [String] Item (type+title) that has changed
113
+ # @param old_loc [Hash] File and line number of location in "from" catalog
114
+ # @param new_loc [Hash] File and line number of location in "to" catalog
115
+ # @param diff [Hash] Difference hash
116
+ # @param options [Hash] Display options
117
+ # @param logger [Logger] Logger object
118
+ # @return [Array] Lines of text
119
+ def self.display_changed_or_nested_item(opts = {})
120
+ item = opts.fetch(:item)
121
+ old_loc = opts.fetch(:old_loc)
122
+ new_loc = opts.fetch(:new_loc)
123
+ diff = opts.fetch(:diff)
124
+ options = opts.fetch(:options)
125
+ logger = opts[:logger]
126
+
127
+ result = []
128
+ info_hash = { item: item, result: result, old_loc: old_loc, new_loc: new_loc, options: options, logger: logger }
129
+ add_source_file_line_info(info_hash)
130
+ result << " #{item} =>"
131
+ diff.keys.sort.each { |key| result.concat hash_diff(diff[key], 1, key, true) }
132
+ result
133
+ end
134
+
135
+ # Display a removed item
136
+ # @param item [String] Item (type+title) that has changed
137
+ # @param old_loc [Hash] File and line number of location in "from" catalog
138
+ # @param options [Hash] Display options
139
+ # @param logger [Logger] Logger object
140
+ # @return [Array] Lines of text
141
+ def self.display_removed_item(opts = {})
142
+ item = opts.fetch(:item)
143
+ old_loc = opts.fetch(:old_loc)
144
+ options = opts.fetch(:options)
145
+ logger = opts[:logger]
146
+
147
+ result = []
148
+ add_source_file_line_info(item: item, result: result, old_loc: old_loc, options: options, logger: logger)
149
+ result << "- #{item}".red
150
+ end
151
+
152
+ # Display an added item
153
+ # @param item [String] Item (type+title) that has changed
154
+ # @param new_loc [Hash] File and line number of location in "to" catalog
155
+ # @param diff [Hash] Difference hash
156
+ # @param options [Hash] Display options
157
+ # @param logger [Logger] Logger object
158
+ # @return [Array] Lines of text
159
+ def self.display_added_item(opts = {})
160
+ item = opts.fetch(:item)
161
+ new_loc = opts.fetch(:new_loc)
162
+ diff = opts.fetch(:diff)
163
+ options = opts.fetch(:options)
164
+ logger = opts[:logger]
165
+
166
+ result = []
167
+ add_source_file_line_info(item: item, result: result, new_loc: new_loc, options: options, logger: logger)
168
+ if options[:display_detail_add] && diff.key?('parameters')
169
+ result << "+ #{item} =>".green
170
+ result << ' parameters =>'.green
171
+ result.concat(
172
+ diff_two_hashes_with_diffy(
173
+ depth: 1,
174
+ hash2: Hash[diff['parameters'].sort], # Should work with somewhat older rubies too
175
+ limit: 80,
176
+ strip_diff: true
177
+ ).map(&:green)
178
+ )
179
+ else
180
+ result << "+ #{item}".green
181
+ if diff.key?('parameters') && logger && !options[:display_detail_add_notice_printed]
182
+ logger.info 'Note: you can use --display-detail-add to view details of added resources'
183
+ options[:display_detail_add_notice_printed] = true
184
+ end
185
+ end
186
+
187
+ result
188
+ end
189
+
190
+ # Generate info about the source of the change. Pass in parameters as a hash with indicated names.
191
+ # @param item [Hash] Item that is added/removed/changed
192
+ # @param result [Array] Result array (modified by this method)
193
+ # @param old_loc [Hash] Old location hash { file => ..., line => ... }
194
+ # @param new_loc [Hash] New location hash { file => ..., line => ... }
195
+ # @param options [Hash] Options hash
196
+ # @param logger [Logger] Logger object
197
+ def self.add_source_file_line_info(opts = {})
198
+ item = opts.fetch(:item)
199
+ result = opts.fetch(:result)
200
+ old_loc = opts[:old_loc]
201
+ new_loc = opts[:new_loc]
202
+ options = opts.fetch(:options, {})
203
+ logger = opts[:logger]
204
+
205
+ # Initialize any currently undefined settings
206
+ empty_hash = { 'file' => nil, 'line' => nil }
207
+ old_loc ||= empty_hash
208
+ new_loc ||= empty_hash
209
+ return if old_loc == empty_hash && new_loc == empty_hash
210
+
211
+ # Convert old_loc and new_loc to strings
212
+ old_loc_string = loc_string(old_loc, options[:compilation_from_dir], logger)
213
+ new_loc_string = loc_string(new_loc, options[:compilation_to_dir], logger)
214
+
215
+ # Debug log information and build up local_result with printable changes
216
+ local_result = []
217
+ if old_loc == new_loc || new_loc == empty_hash || old_loc_string == new_loc_string
218
+ logger.debug "#{item} @ #{old_loc_string || 'nil'}" if logger
219
+ local_result << " #{old_loc_string}".cyan unless old_loc_string.nil?
220
+ elsif old_loc == empty_hash
221
+ logger.debug "#{item} @ #{new_loc_string || 'nil'}" if logger
222
+ local_result << " #{new_loc_string}".cyan unless new_loc_string.nil?
223
+ else
224
+ logger.debug "#{item} -@ #{old_loc_string} +@ #{new_loc_string}" if logger
225
+ local_result << "- #{old_loc_string}".cyan
226
+ local_result << "+ #{new_loc_string}".cyan
227
+ end
228
+
229
+ # Only modify result if option to display source file and line is enabled
230
+ result.concat local_result if options[:display_source_file_line]
231
+ end
232
+
233
+ # Convert { file => ..., line => ... } to displayable string
234
+ # @param loc [Hash] file => ..., line => ... hash
235
+ # @param compilation_dir [String] Compilation directory
236
+ # @param logger [Logger] Logger object
237
+ # @return [String] Location string
238
+ def self.loc_string(loc, compilation_dir, logger)
239
+ return nil if loc.nil? || !loc.is_a?(Hash) || loc['file'].nil? || loc['line'].nil?
240
+ result = "#{loc['file']}:#{loc['line']}"
241
+ if compilation_dir
242
+ rex = Regexp.new('^' + Regexp.escape(compilation_dir + '/'))
243
+ result_new = result.sub(rex, '')
244
+ if result_new != result
245
+ logger.debug "Removed compilation directory in #{result} -> #{result_new}" if logger
246
+ result = result_new
247
+ end
248
+ end
249
+ result
250
+ end
251
+
252
+ # Get the diff of two long strings. Call the 'diffy' gem for this.
253
+ # @param string1 [String] First string (-)
254
+ # @param string2 [String] Second string (+)
255
+ # @param depth [Fixnum] Depth, for correct indentation
256
+ # @return Array<String> Displayable result
257
+ def self.diff_two_strings_with_diffy(string1, string2, depth)
258
+ # prevent 'No newline at end of file' for single line strings
259
+ string1 += "\n" unless string1 =~ /\n/
260
+ string2 += "\n" unless string2 =~ /\n/
261
+ diff = Diffy::Diff.new(string1, string2, context: 2, include_diff_info: true).to_s.split("\n")
262
+ diff.shift # Remove first line of diff info (filename that makes no sense)
263
+ diff.shift # Remove second line of diff info (filename that makes no sense)
264
+ diff.map { |x| left_pad(2 * depth + 2, x) }
265
+ end
266
+
267
+ # Get the diff of two hashes. Call the 'diffy' gem for this.
268
+ # @param hash1 [Hash] First hash (-)
269
+ # @param hash1 [Hash] Second hash (+)
270
+ # @param depth [Fixnum] Depth, for correct indentation
271
+ # @param limit [Fixnum] Maximum string length
272
+ # @param strip_diff [Boolean] Strip leading +/-/" "
273
+ # @return Array<String> Displayable result
274
+ def self.diff_two_hashes_with_diffy(opts = {})
275
+ depth = opts.fetch(:depth, 0)
276
+ hash1 = opts.fetch(:hash1, {})
277
+ hash2 = opts.fetch(:hash2, {})
278
+ limit = opts[:limit]
279
+ strip_diff = opts.fetch(:strip_diff, false)
280
+
281
+ json_old = stringify_for_diffy(hash1)
282
+ json_new = stringify_for_diffy(hash2)
283
+
284
+ # If stripping the diff, we need to make sure diffy does not colorize the output, so that
285
+ # there are not color codes in the output to deal with.
286
+ diff = if strip_diff
287
+ Diffy::Diff.new(json_old, json_new, context: 0).to_s(:text).split("\n")
288
+ else
289
+ Diffy::Diff.new(json_old, json_new, context: 0).to_s.split("\n")
290
+ end
291
+ raise "Diffy diff empty for string: #{json_old}" if diff.empty?
292
+
293
+ # This is the array that is returned
294
+ diff.map do |x|
295
+ x = x[2..-1] if strip_diff # Drop first 2 characters: '+ ', '- ', or ' '
296
+ truncate_string(left_pad(2 * depth + 2, x), limit)
297
+ end
298
+ end
299
+
300
+ # Limit length of a string
301
+ # @param str [String] String
302
+ # @param limit [Fixnum] Limit (0=unlimited)
303
+ # @return [String] Truncated string
304
+ def self.truncate_string(str, limit)
305
+ return str if limit.nil? || str.length <= limit
306
+ "#{str[0..limit]}..."
307
+ end
308
+
309
+ # Get the diff between two hashes. This is recursive-aware.
310
+ # @param obj [diff object] diff object
311
+ # @param depth [Fixnum] Depth of nesting, used for indentation
312
+ # @return Array<String> Printable diff outputs
313
+ def self.hash_diff(obj, depth, key_in, nested = false)
314
+ result = []
315
+ result << left_pad(2 * depth, " #{key_in} =>")
316
+ if obj.key?(:old) && obj.key?(:new)
317
+ if nested && obj[:old].is_a?(Hash) && obj[:new].is_a?(Hash)
318
+ # Nested hashes will be stringified and then use 'diffy'
319
+ result.concat diff_two_hashes_with_diffy(depth: depth, hash1: obj[:old], hash2: obj[:new])
320
+ elsif obj[:old].is_a?(String) && obj[:new].is_a?(String) && (obj[:old] =~ /\n/ || obj[:new] =~ /\n/)
321
+ # Multi-line strings will be split and then use 'diffy' to mimic the
322
+ # output seen when using "diff" on the command line
323
+ result.concat diff_two_strings_with_diffy(obj[:old], obj[:new], depth)
324
+ else
325
+ # Stuff we don't recognize will be converted to a string and printed
326
+ # with '+' and '-' unless the object resolves to an empty string.
327
+ result.concat diff_at_depth(depth, obj[:old], obj[:new])
328
+ end
329
+ else
330
+ obj.keys.sort.each { |key| result.concat hash_diff(obj[key], 1 + depth, key, nested) }
331
+ end
332
+ result
333
+ end
334
+
335
+ # Get the diff between two arbitrary objects
336
+ # @param depth [Fixnum] Depth of nesting, used for indentation
337
+ # @param old_obj [?] Old object
338
+ # @param new_obj [?] New object
339
+ # @return Array<String> Diff output
340
+ def self.diff_at_depth(depth, old_obj, new_obj)
341
+ old_s = old_obj.to_s
342
+ new_s = new_obj.to_s
343
+ result = []
344
+ result << left_pad(2 * depth + 2, "- #{old_s}").red unless old_s == ''
345
+ result << left_pad(2 * depth + 2, "+ #{new_s}").green unless new_s == ''
346
+ result
347
+ end
348
+
349
+ # Utility Method!
350
+ # Indent a given text string with a certain number of spaces
351
+ # @param spaces [Fixnum] Number of spaces
352
+ # @param text [String] Text
353
+ def self.left_pad(spaces, text = '')
354
+ [' ' * spaces, text].join('')
355
+ end
356
+
357
+ # Utility Method!
358
+ # Given an arbitrary object, convert it into a string for use by 'diffy'.
359
+ # This basically exists so we can do something prettier than just calling .inspect or .to_s
360
+ # on object types we anticipate seeing, while not failing entirely on other object types.
361
+ # @param obj [?] Object to be stringified
362
+ # @return [String] String representation of object for diffy
363
+ def self.stringify_for_diffy(obj)
364
+ return JSON.pretty_generate(obj) if [Hash, Array].include?(obj.class)
365
+ return '""' if obj.is_a?(String) && obj == ''
366
+ return obj if [String, Fixnum, Float].include?(obj.class)
367
+ "#{obj.class}: #{obj.inspect}"
368
+ end
369
+
370
+ # Utility Method!
371
+ # Implement the --display-datatype-changes option by:
372
+ # - Removing string-equivalent differences when option == false
373
+ # - Updating display of string-equivalent differences when option == true
374
+ # @param diff [Array<Diff Objects>] Difference array
375
+ # @param option [Boolean] Selected behavior; see description
376
+ # @param logger [Logger] Logger object
377
+ def self.adjust_for_display_datatype_changes(diff, option, logger = nil)
378
+ diff.map! do |diff_obj|
379
+ if diff_obj[0] == '+' || diff_obj[0] == '-'
380
+ diff_obj[2] = 'undef' if diff_obj[2].nil?
381
+ diff_obj
382
+ else
383
+ x2, x3 = _adjust_for_display_datatype(diff_obj[2], diff_obj[3], option, logger)
384
+ if x2.nil? && x3.nil?
385
+ # Delete this! Return nil and compact! will get rid of them.
386
+ msg = "Adjust display for #{diff_obj[1].gsub(/\f/, '::')}: " \
387
+ "#{diff_obj[2].inspect} != #{diff_obj[3].inspect} DELETED"
388
+ logger.debug(msg) if logger
389
+ nil
390
+ elsif x2 == diff_obj[2] && x3 == diff_obj[3]
391
+ # Neither object changed
392
+ diff_obj
393
+ else
394
+ # Adjust the display and return modified object
395
+ msg = "Adjust display for #{diff_obj[1].gsub(/\f/, '::')}: " \
396
+ "#{diff_obj[2].inspect} -> #{x2}; "\
397
+ "#{diff_obj[3].inspect} -> #{x3}"
398
+ logger.debug(msg) if logger
399
+ diff_obj[2] = x2
400
+ diff_obj[3] = x3
401
+ diff_obj
402
+ end
403
+ end
404
+ end
405
+ diff.compact!
406
+ end
407
+
408
+ # Utility Method!
409
+ # Called by adjust_for_display_datatype_changes to compare an old value
410
+ # to a new value and adjust as appropriate.
411
+ # @param obj1 [?] First object
412
+ # @param obj2 [?] Second object
413
+ # @param option [Boolean] Selected behavior; see adjust_for_display_datatype_changes
414
+ # @return [<String, String> or <?, ?>] Updated values of objects
415
+ def self._adjust_for_display_datatype(obj1, obj2, option, logger)
416
+ # If not string-equal, return to leave untouched
417
+ return [obj1, obj2] unless obj1.to_s == obj2.to_s
418
+
419
+ # Delete if option to display these is false
420
+ return [nil, nil] unless option
421
+
422
+ # Delete if both objects are nil
423
+ return [nil, nil] if obj1.nil? && obj2.nil?
424
+
425
+ # If one is nil and the other is the empty string...
426
+ return ['undef', '""'] if obj1.nil?
427
+ return ['""', 'undef'] if obj2.nil?
428
+
429
+ # If one is an integer and the other is a string
430
+ return [obj1, "\"#{obj2}\""] if obj1.is_a?(Fixnum) && obj2.is_a?(String)
431
+ return ["\"#{obj1}\"", obj2] if obj1.is_a?(String) && obj2.is_a?(Fixnum)
432
+
433
+ # True and false
434
+ return [obj1, "\"#{obj2}\""] if obj1.is_a?(TrueClass) && obj2.is_a?(String)
435
+ return [obj1, "\"#{obj2}\""] if obj1.is_a?(FalseClass) && obj2.is_a?(String)
436
+ return ["\"#{obj1}\"", obj2] if obj1.is_a?(String) && obj2.is_a?(TrueClass)
437
+ return ["\"#{obj1}\"", obj2] if obj1.is_a?(String) && obj2.is_a?(FalseClass)
438
+
439
+ # Unhandled case - warn about it and then return inputs untouched
440
+ # Note: If you encounter this, please report it so we can add a handler.
441
+ # :nocov:
442
+ msg = "In _adjust_for_display_datatype, objects '#{obj1.inspect}' (#{obj1.class}) and"\
443
+ " '#{obj2.inspect}' (#{obj2.class}) have identical string representations but"\
444
+ ' formatting is not implemented to update display.'
445
+ logger.warn(msg) if logger
446
+ [obj1, obj2]
447
+ # :nocov:
448
+ end
449
+ end
450
+ end
451
+ end
452
+ end