diff options
-rw-r--r-- | Gemfile | 12 | ||||
-rw-r--r-- | chef.gemspec | 2 | ||||
-rw-r--r-- | lib/chef/util/diff.rb | 82 | ||||
-rw-r--r-- | spec/unit/util/diff_spec.rb | 31 |
4 files changed, 56 insertions, 71 deletions
@@ -20,14 +20,14 @@ end platforms :mswin, :mingw do gem "systemu", "2.2.0" # CHEF-3718 - gem "ffi", "1.0.9" + gem "ffi", "1.3.1" gem "rdp-ruby-wmi", "0.3.1" - gem "windows-api", "0.4.0" - gem "windows-pr", "1.2.1" + gem "windows-api", "0.4.2" + gem "windows-pr", "1.2.2" gem "win32-api", "1.4.8" - gem "win32-dir", "0.3.7" - gem "win32-event", "0.5.2" - gem "win32-mutex", "0.3.1" + gem "win32-dir", "0.4.1" + gem "win32-event", "0.6.0" + gem "win32-mutex", "0.4.0" gem "win32-process", "0.6.5" gem "win32-service", "0.7.2" end diff --git a/chef.gemspec b/chef.gemspec index cbee2c0a00..0be91849c7 100644 --- a/chef.gemspec +++ b/chef.gemspec @@ -30,6 +30,8 @@ Gem::Specification.new do |s| s.add_dependency "highline", ">= 1.6.9" s.add_dependency "erubis" + s.add_dependency "diff-lcs", ">= 1.2.4" + %w(rdoc sdoc rake rack rspec_junit_formatter).each { |gem| s.add_development_dependency gem } %w(rspec-core rspec-expectations rspec-mocks).each { |gem| s.add_development_dependency gem, "~> 2.13.0" } s.add_development_dependency "chef-zero", "~> 1.4" diff --git a/lib/chef/util/diff.rb b/lib/chef/util/diff.rb index 6f76a2fabd..7c50512c06 100644 --- a/lib/chef/util/diff.rb +++ b/lib/chef/util/diff.rb @@ -15,13 +15,9 @@ # limitations under the License. # -require 'chef/mixin/shell_out' - class Chef class Util class Diff - include Chef::Mixin::ShellOut - # @todo: to_a, to_s, to_json, inspect defs, accessors for @diff and @error # @todo: move coercion to UTF-8 into to_json # @todo: replace shellout to diff -u with diff-lcs gem @@ -78,48 +74,23 @@ class Chef return "(new content is binary, diff output suppressed)" if is_binary?(new_file) begin - # -u: Unified diff format - # LC_ALL: in ruby 1.9 we want to set nil which is a magic option to mixlib-shellout to - # pass through the LC_ALL locale. in ruby 1.8 we force to 7-bit 'C' locale - # (which is the mixlib-shellout default for all rubies all the time). Chef::Log.debug("running: diff -u #{old_file} #{new_file}") - locale = ( Object.const_defined? :Encoding ) ? nil : 'C' - result = shell_out("diff -u #{old_file} #{new_file}", :env => {'LC_ALL' => locale}) - + diff_str = udiff(old_file, new_file) + rescue Exception => e # Should *not* receive this, but in some circumstances it seems that # an exception can be thrown even using shell_out instead of shell_out! return "Could not determine diff. Error: #{e.message}" end - # diff will set a non-zero return code even when there's - # valid stdout results, if it encounters something unexpected - # So as long as we have output, we'll show it. - # - # Also on some platforms (Solaris) diff outputs a single line - # when there are no differences found. Look for this line - # before analyzing diff output. - if !result.stdout.empty? && result.stdout != "No differences encountered\n" - if result.stdout.length > diff_output_threshold + if !diff_str.empty? && diff_str != "No differences encountered\n" + if diff_str.length > diff_output_threshold return "(long diff of over #{diff_output_threshold} characters, diff output suppressed)" else - diff_str = result.stdout - if Object.const_defined? :Encoding # ruby >= 1.9 - if ( diff_str.encoding == Encoding::ASCII_8BIT && - diff_str.encoding != Encoding.default_external && - RUBY_VERSION.to_f < 2.0 ) - # @todo mixlib-shellout under ruby 1.9 hands back an ASCII-8BIT encoded string, which needs to - # be fixed to the default external encoding -- this should be moved into mixlib-shellout - diff_str = diff_str.force_encoding(Encoding.default_external) - end - diff_str.encode!('UTF-8', :invalid => :replace, :undef => :replace, :replace => '?') - end + diff_str = encode_diff_for_json(diff_str) @diff = diff_str.split("\n") - @diff.delete("\\ No newline at end of file") return "(diff available)" end - elsif !result.stderr.empty? - return "Could not determine diff. Error: #{result.stderr}" else return "(no diff)" end @@ -139,6 +110,49 @@ class Chef end end + # produces a unified-output-format diff with 3 lines of context + def udiff(old_file, new_file) + diff_str = "" + file_length_difference = 0 + + old_data = IO::readlines(old_file).map { |e| e.chomp } + new_data = IO::readlines(new_file).map { |e| e.chomp } + diff_data = ::Diff::LCS.diff(old_data, new_data) + + return diff_str if old_data.empty? && new_data.empty? + return "No differences encountered\n" if diff_data.empty? + + # write diff header (standard unified format) + ft = File.stat(old_file).mtime.localtime.strftime('%Y-%m-%d %H:%M:%S.%N %z') + diff_str << "--- #{old_file}\t#{ft}\n" + ft = File.stat(new_file).mtime.localtime.strftime('%Y-%m-%d %H:%M:%S.%N %z') + diff_str << "+++ #{new_file}\t#{ft}\n" + + # loop over diff hunks. if a hunk overlaps with the last hunk, + # join them. otherwise, print out the old one. + old_hunk = hunk = nil + diff_data.each do |piece| + begin + hunk = ::Diff::LCS::Hunk.new(old_data, new_data, piece, 3, file_length_difference) + file_length_difference = hunk.file_length_difference + next unless old_hunk + next if hunk.merge(old_hunk) + diff_str << old_hunk.diff(:unified) << "\n" + ensure + old_hunk = hunk + end + end + diff_str << old_hunk.diff(:unified) << "\n" + return diff_str + end + + def encode_diff_for_json(diff_str) + if Object.const_defined? :Encoding + diff_str.encode!('UTF-8', :invalid => :replace, :undef => :replace, :replace => '?') + end + return diff_str + end + end end end diff --git a/spec/unit/util/diff_spec.rb b/spec/unit/util/diff_spec.rb index 2dadb5be56..ad1166f9fc 100644 --- a/spec/unit/util/diff_spec.rb +++ b/spec/unit/util/diff_spec.rb @@ -393,37 +393,6 @@ describe Chef::Util::Diff, :uses_diff => true do end end - describe "when errors are thrown from shell_out" do - before do - differ.stub!(:shell_out).and_raise('boom') - differ.diff(old_file, new_file) - end - - it "calling for_output should return the error message" do - expect(differ.for_output).to eql(["Could not determine diff. Error: boom"]) - end - - it "calling for_reporting should be nil" do - expect(differ.for_reporting).to be_nil - end - end - - describe "when shell_out returns stderr output" do - before do - @result = mock('result', :stdout => "", :stderr => "boom") - differ.stub!(:shell_out).and_return(@result) - differ.diff(old_file, new_file) - end - - it "calling for_output should return the error message" do - expect(differ.for_output).to eql(["Could not determine diff. Error: boom"]) - end - - it "calling for_reporting should be nil" do - expect(differ.for_reporting).to be_nil - end - end - describe "when checking if files are binary or text" do it "should identify zero-length files as text" do |