octocatalog-diff 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
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