require 'chef_fs/diff' module ChefFS module CommandLine def self.diff(pattern, a_root, b_root, recurse_depth, output_mode) found_result = false ChefFS::Diff::diffable_leaves_from_pattern(pattern, a_root, b_root, recurse_depth) do |a_leaf, b_leaf| found_result = true diff = diff_leaves(a_leaf, b_leaf, output_mode) yield diff if diff != '' end if !found_result && pattern.exact_path yield "#{pattern}: No such file or directory on remote or local" end end private # Diff two known leaves (could be files or dirs) def self.diff_leaves(old_file, new_file, output_mode) result = '' # If both are directories # If old is a directory and new is a file # If old is a directory and new does not exist if old_file.dir? if new_file.dir? if output_mode != :name_only && output_mode != :name_status result << "Common subdirectories: #{old_file.path}\n" end elsif new_file.exists? if output_mode == :name_only result << "#{new_file.path_for_printing}\n" elsif output_mode == :name_status result << "T\t#{new_file.path_for_printing}\n" else result << "File #{new_file.path_for_printing} is a directory while file #{new_file.path_for_printing} is a regular file\n" end elsif new_file.parent.can_have_child?(old_file.name, old_file.dir?) if output_mode == :name_only result << "#{new_file.path_for_printing}\n" elsif output_mode == :name_status result << "D\t#{new_file.path_for_printing}\n" else result << "Only in #{old_file.parent.path_for_printing}: #{old_file.name}\n" end end # If new is a directory and old does not exist # If new is a directory and old is a file elsif new_file.dir? if old_file.exists? if output_mode == :name_only result << "#{new_file.path_for_printing}\n" elsif output_mode == :name_status result << "T\t#{new_file.path_for_printing}\n" else result << "File #{old_file.path_for_printing} is a regular file while file #{old_file.path_for_printing} is a directory\n" end elsif old_file.parent.can_have_child?(new_file.name, new_file.dir?) if output_mode == :name_only result << "#{new_file.path_for_printing}\n" elsif output_mode == :name_status result << "A\t#{new_file.path_for_printing}\n" else result << "Only in #{new_file.parent.path_for_printing}: #{new_file.name}\n" end end else # Neither is a directory, so they are diffable with file diff different, old_value, new_value = ChefFS::Diff::diff_files(old_file, new_file) if different # If one of the files doesn't exist, we only want to print the diff if the # other file *could be uploaded/downloaded*. if !old_value && !old_file.parent.can_have_child?(new_file.name, new_file.dir?) return result end if !new_value && !new_file.parent.can_have_child?(old_file.name, old_file.dir?) return result end if output_mode == :name_only result << "#{new_file.path_for_printing}\n" elsif output_mode == :name_status if !old_value result << "A\t#{new_file.path_for_printing}\n" elsif !new_value result << "D\t#{new_file.path_for_printing}\n" else result << "M\t#{new_file.path_for_printing}\n" end else old_path = old_file.path_for_printing new_path = new_file.path_for_printing result << "diff --knife #{old_path} #{new_path}\n" if !old_value result << "new file\n" old_path = "/dev/null" old_value = '' end if !new_value result << "deleted file\n" new_path = "/dev/null" new_value = '' end result << diff_text(old_path, new_path, old_value, new_value) end end end return result end def self.sort_keys(json_object) if json_object.is_a?(Array) json_object.map { |o| sort_keys(o) } elsif json_object.is_a?(Hash) new_hash = {} json_object.keys.sort.each { |key| new_hash[key] = sort_keys(json_object[key]) } new_hash else json_object end end def self.canonicalize_json(json_text) parsed_json = JSON.parse(json_text, :create_additions => false) sorted_json = sort_keys(parsed_json) JSON.pretty_generate(sorted_json) end def self.diff_text(old_path, new_path, old_value, new_value) # Reformat JSON for a nicer diff. if old_path =~ /\.json$/ begin reformatted_old_value = canonicalize_json(old_value) reformatted_new_value = canonicalize_json(new_value) old_value = reformatted_old_value new_value = reformatted_new_value rescue # If JSON parsing fails, we just won't change any values and fall back # to normal diff. end end # Copy to tempfiles before diffing # TODO don't copy things that are already in files! Or find an in-memory diff algorithm begin new_tempfile = Tempfile.new("new") new_tempfile.write(new_value) new_tempfile.close begin old_tempfile = Tempfile.new("old") old_tempfile.write(old_value) old_tempfile.close result = `diff -u #{old_tempfile.path} #{new_tempfile.path}` result = result.gsub(/^--- #{old_tempfile.path}/, "--- #{old_path}") result = result.gsub(/^\+\+\+ #{new_tempfile.path}/, "+++ #{new_path}") result ensure old_tempfile.close! end ensure new_tempfile.close! end end end end