summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorAustin Ziegler <austin@zieglers.ca>2020-06-30 09:51:08 -0400
committerAustin Ziegler <austin@zieglers.ca>2020-07-01 11:53:51 -0400
commit60613d2af564a93dc5c19c8a71c3f674a5db6fdb (patch)
treed88bb489d8f8e44b323073a32195b951d9d9629b /lib
parent99f65fdc8253c3bf800bc0e17c2cbbde9ade9ba8 (diff)
downloaddiff-lcs-60613d2af564a93dc5c19c8a71c3f674a5db6fdb.tar.gz
# This is a combination of 9 commits.
# This is the 1st commit message: Fix improperly placed chunks Resolve #65 - Also add even more tests for checking `ldiff` results against `diff` results. - Fix issues with diff/ldiff output highlighted by the above tests. - Add a parameter to indicate that the hunk being processed is the _last_ hunk; this results in correct counting of the hunk size. - The misplaced chunks were happening because of an improper `.abs` on `#diff_size`, when the `.abs` needed to be on the finding of the maximum diff size. # This is the commit message #2: Ooops. Debugger # This is the commit message #3: Restore missing test - Fix some more format issues raised by the missing test. - Start fixing Rubocop formatting. # This is the commit message #4: Last RuboCop fixes # This is the commit message #5: Finalize diff-lcs 1.4 # This is the commit message #6: Fix #44 The problem here was the precedence of `or` vs `||`. Switching to `||` resulted in the expected behaviour. # This is the commit message #7: Resolve #43 # This is the commit message #8: Typo # This is the commit message #9: Resolve #35 with a comment
Diffstat (limited to 'lib')
-rw-r--r--lib/diff/lcs.rb9
-rw-r--r--lib/diff/lcs/block.rb2
-rw-r--r--lib/diff/lcs/hunk.rb147
-rw-r--r--lib/diff/lcs/ldiff.rb36
4 files changed, 132 insertions, 62 deletions
diff --git a/lib/diff/lcs.rb b/lib/diff/lcs.rb
index 9d47064..63888a1 100644
--- a/lib/diff/lcs.rb
+++ b/lib/diff/lcs.rb
@@ -49,7 +49,7 @@ module Diff; end unless defined? Diff # rubocop:disable Style/Documentation
# a x b y c z p d q
# a b c a x b y c z
module Diff::LCS
- VERSION = '1.4.3'
+ VERSION = '1.4.4'
end
require 'diff/lcs/callbacks'
@@ -60,6 +60,13 @@ module Diff::LCS # rubocop:disable Style/Documentation
# +self+ and +other+. See Diff::LCS#lcs.
#
# lcs = seq1.lcs(seq2)
+ #
+ # A note when using objects: Diff::LCS only works properly when each object
+ # can be used as a key in a Hash, which typically means that the objects must
+ # implement Object#eql? in a way that two identical values compare
+ # identically for key purposes. That is:
+ #
+ # O.new('a').eql?(O.new('a')) == true
def lcs(other, &block) #:yields self[i] if there are matched subsequences:
Diff::LCS.lcs(self, other, &block)
end
diff --git a/lib/diff/lcs/block.rb b/lib/diff/lcs/block.rb
index fe86793..430702d 100644
--- a/lib/diff/lcs/block.rb
+++ b/lib/diff/lcs/block.rb
@@ -19,7 +19,7 @@ class Diff::LCS::Block
end
def diff_size
- (@insert.size - @remove.size).abs
+ @insert.size - @remove.size
end
def op
diff --git a/lib/diff/lcs/hunk.rb b/lib/diff/lcs/hunk.rb
index d884a1b..49b520e 100644
--- a/lib/diff/lcs/hunk.rb
+++ b/lib/diff/lcs/hunk.rb
@@ -6,26 +6,38 @@ require 'diff/lcs/block'
# each block. (So if we're not using context, every hunk will contain one
# block.) Used in the diff program (bin/ldiff).
class Diff::LCS::Hunk
+ OLD_DIFF_OP_ACTION = { '+' => 'a', '-' => 'd', '!' => 'c' }.freeze #:nodoc:
+ ED_DIFF_OP_ACTION = { '+' => 'a', '-' => 'd', '!' => 'c' }.freeze #:nodoc:
+
+ private_constant :OLD_DIFF_OP_ACTION, :ED_DIFF_OP_ACTION if respond_to?(:private_constant)
+
# Create a hunk using references to both the old and new data, as well as the
# piece of data.
def initialize(data_old, data_new, piece, flag_context, file_length_difference)
# At first, a hunk will have just one Block in it
@blocks = [Diff::LCS::Block.new(piece)]
+
+ if @blocks[0].remove.empty? && @blocks[0].insert.empty?
+ fail "Cannot build a hunk from #{piece.inspect}; has no add or remove actions"
+ end
+
if String.method_defined?(:encoding)
@preferred_data_encoding = data_old.fetch(0, data_new.fetch(0, '')).encoding
end
+
@data_old = data_old
@data_new = data_new
before = after = file_length_difference
after += @blocks[0].diff_size
@file_length_difference = after # The caller must get this manually
- @max_diff_size = @blocks.map { |e| e.diff_size }.max
+ @max_diff_size = @blocks.map { |e| e.diff_size.abs }.max
+
# Save the start & end of each array. If the array doesn't exist (e.g.,
- # we're only adding items in this block), then figure out the line
- # number based on the line number of the other file and the current
- # difference in file lengths.
+ # we're only adding items in this block), then figure out the line number
+ # based on the line number of the other file and the current difference in
+ # file lengths.
if @blocks[0].remove.empty?
a1 = a2 = nil
else
@@ -55,7 +67,7 @@ class Diff::LCS::Hunk
# Change the "start" and "end" fields to note that context should be added
# to this hunk.
- attr_accessor :flag_context
+ attr_accessor :flag_context # rubocop:disable Layout/EmptyLinesAroundAttributeAccessor
undef :flag_context=
def flag_context=(context) #:nodoc: # rubocop:disable Lint/DuplicateMethods
return if context.nil? or context.zero?
@@ -101,18 +113,18 @@ class Diff::LCS::Hunk
end
# Returns a diff string based on a format.
- def diff(format)
+ def diff(format, last = false)
case format
when :old
- old_diff
+ old_diff(last)
when :unified
- unified_diff
+ unified_diff(last)
when :context
- context_diff
+ context_diff(last)
when :ed
self
when :reverse_ed, :ed_finish
- ed_diff(format)
+ ed_diff(format, last)
else
fail "Unknown diff format #{format}."
end
@@ -120,35 +132,34 @@ class Diff::LCS::Hunk
# Note that an old diff can't have any context. Therefore, we know that
# there's only one block in the hunk.
- def old_diff
+ def old_diff(_last = false)
warn 'Expecting only one block in an old diff hunk!' if @blocks.size > 1
- op_act = { '+' => 'a', '-' => 'd', '!' => 'c' }
block = @blocks[0]
# Calculate item number range. Old diff range is just like a context
# diff range, except the ranges are on one line with the action between
# them.
- s = encode("#{context_range(:old)}#{op_act[block.op]}#{context_range(:new)}\n")
+ s = encode("#{context_range(:old, ',')}#{OLD_DIFF_OP_ACTION[block.op]}#{context_range(:new, ',')}\n")
# If removing anything, just print out all the remove lines in the hunk
# which is just all the remove lines in the block.
unless block.remove.empty?
- @data_old[@start_old..@end_old].each { |e| s << encode('< ') + e + encode("\n") }
+ @data_old[@start_old..@end_old].each { |e| s << encode('< ') + e.chomp + encode("\n") }
end
s << encode("---\n") if block.op == '!'
unless block.insert.empty?
- @data_new[@start_new..@end_new].each { |e| s << encode('> ') + e + encode("\n") }
+ @data_new[@start_new..@end_new].each { |e| s << encode('> ') + e.chomp + encode("\n") }
end
s
end
private :old_diff
- def unified_diff
+ def unified_diff(last = false)
# Calculate item number range.
- s = encode("@@ -#{unified_range(:old)} +#{unified_range(:new)} @@\n")
+ s = encode("@@ -#{unified_range(:old, last)} +#{unified_range(:new, last)} @@\n")
# Outlist starts containing the hunk of the old file. Removing an item
# just means putting a '-' in front of it. Inserting an item requires
@@ -161,7 +172,14 @@ class Diff::LCS::Hunk
# file -- don't take removed items into account.
lo, hi, num_added, num_removed = @start_old, @end_old, 0, 0
- outlist = @data_old[lo..hi].map { |e| e.insert(0, encode(' ')) }
+ outlist = @data_old[lo..hi].map { |e| String.new("#{encode(' ')}#{e.chomp}") }
+
+ last_block = blocks[-1]
+
+ if last
+ old_missing_newline = missing_last_newline?(@data_old)
+ new_missing_newline = missing_last_newline?(@data_new)
+ end
@blocks.each do |block|
block.remove.each do |item|
@@ -170,67 +188,100 @@ class Diff::LCS::Hunk
outlist[offset][0, 1] = encode(op)
num_removed += 1
end
+
+ if last && block == last_block && old_missing_newline && !new_missing_newline
+ outlist << encode('\\ No newline at end of file')
+ num_removed += 1
+ end
+
block.insert.each do |item|
op = item.action.to_s # +
offset = item.position - @start_new + num_removed
- outlist[offset, 0] = encode(op) + @data_new[item.position]
+ outlist[offset, 0] = encode(op) + @data_new[item.position].chomp
num_added += 1
end
end
+ outlist << encode('\\ No newline at end of file') if last && new_missing_newline
+
s << outlist.join(encode("\n"))
+
+ s
end
private :unified_diff
- def context_diff
+ def context_diff(last = false)
s = encode("***************\n")
- s << encode("*** #{context_range(:old)} ****\n")
- r = context_range(:new)
+ s << encode("*** #{context_range(:old, ',', last)} ****\n")
+ r = context_range(:new, ',', last)
+
+ if last
+ old_missing_newline = missing_last_newline?(@data_old)
+ new_missing_newline = missing_last_newline?(@data_new)
+ end
# Print out file 1 part for each block in context diff format if there
# are any blocks that remove items
lo, hi = @start_old, @end_old
removes = @blocks.reject { |e| e.remove.empty? }
- if removes
- outlist = @data_old[lo..hi].map { |e| e.insert(0, encode(' ')) }
+
+ unless removes.empty?
+ outlist = @data_old[lo..hi].map { |e| String.new("#{encode(' ')}#{e.chomp}") }
+
+ last_block = removes[-1]
removes.each do |block|
block.remove.each do |item|
- outlist[item.position - lo].insert(0, encode(block.op)) # - or !
+ outlist[item.position - lo][0, 1] = encode(block.op) # - or !
+ end
+
+ if last && block == last_block && old_missing_newline
+ outlist << encode('\\ No newline at end of file')
end
end
- s << outlist.join("\n")
+
+ s << outlist.join(encode("\n")) << encode("\n")
end
- s << encode("\n--- #{r} ----\n")
+ s << encode("--- #{r} ----\n")
lo, hi = @start_new, @end_new
inserts = @blocks.reject { |e| e.insert.empty? }
- if inserts
- outlist = @data_new[lo..hi].collect { |e| e.insert(0, encode(' ')) }
+
+ unless inserts.empty?
+ outlist = @data_new[lo..hi].map { |e| String.new("#{encode(' ')}#{e.chomp}") }
+
+ last_block = inserts[-1]
+
inserts.each do |block|
block.insert.each do |item|
- outlist[item.position - lo].insert(0, encode(block.op)) # - or !
+ outlist[item.position - lo][0, 1] = encode(block.op) # + or !
+ end
+
+ if last && block == last_block && new_missing_newline
+ outlist << encode('\\ No newline at end of file')
end
end
- s << outlist.join("\n")
+ s << outlist.join(encode("\n"))
end
+
s
end
private :context_diff
- def ed_diff(format)
- op_act = { '+' => 'a', '-' => 'd', '!' => 'c' }
+ def ed_diff(format, _last = false)
warn 'Expecting only one block in an old diff hunk!' if @blocks.size > 1
s =
if format == :reverse_ed
- encode("#{op_act[@blocks[0].op]}#{context_range(:old)}\n")
+ encode("#{ED_DIFF_OP_ACTION[@blocks[0].op]}#{context_range(:old, ',')}\n")
else
- encode("#{context_range(:old, ' ')}#{op_act[@blocks[0].op]}\n")
+ encode("#{context_range(:old, ' ')}#{ED_DIFF_OP_ACTION[@blocks[0].op]}\n")
end
unless @blocks[0].insert.empty?
- @data_new[@start_new..@end_new].each do |e| s << e + encode("\n") end
+ @data_new[@start_new..@end_new].each do |e|
+ s << e.chomp + encode("\n")
+ end
s << encode(".\n")
end
s
@@ -239,7 +290,7 @@ class Diff::LCS::Hunk
# Generate a range of item numbers to print. Only print 1 number if the
# range has only one item in it. Otherwise, it's 'start,end'
- def context_range(mode, op = ',') # rubocop:disable Naming/UncommunicativeMethodParamName
+ def context_range(mode, op, last = false)
case mode
when :old
s, e = (@start_old + 1), (@end_old + 1)
@@ -247,6 +298,9 @@ class Diff::LCS::Hunk
s, e = (@start_new + 1), (@end_new + 1)
end
+ e -= 1 if last
+ e = 1 if e.zero?
+
s < e ? "#{s}#{op}#{e}" : e.to_s
end
private :context_range
@@ -254,7 +308,7 @@ class Diff::LCS::Hunk
# Generate a range of item numbers to print for unified diff. Print number
# where block starts, followed by number of lines in the block
# (don't print number of lines if it's 1)
- def unified_range(mode)
+ def unified_range(mode, last)
case mode
when :old
s, e = (@start_old + 1), (@end_old + 1)
@@ -262,12 +316,25 @@ class Diff::LCS::Hunk
s, e = (@start_new + 1), (@end_new + 1)
end
- length = e - s + 1
+ length = e - s + (last ? 0 : 1)
+
first = length < 2 ? e : s # "strange, but correct"
- length == 1 ? first.to_s : "#{first},#{length}"
+ length <= 1 ? first.to_s : "#{first},#{length}"
end
private :unified_range
+ def missing_last_newline?(data)
+ newline = encode("\n")
+
+ if data[-2]
+ data[-2].end_with?(newline) && !data[-1].end_with?(newline)
+ elsif data[-1]
+ !data[-1].end_with?(newline)
+ else
+ true
+ end
+ end
+
if String.method_defined?(:encoding)
def encode(literal, target_encoding = @preferred_data_encoding)
literal.encode target_encoding
diff --git a/lib/diff/lcs/ldiff.rb b/lib/diff/lcs/ldiff.rb
index 2862267..17b374c 100644
--- a/lib/diff/lcs/ldiff.rb
+++ b/lib/diff/lcs/ldiff.rb
@@ -98,24 +98,19 @@ class << Diff::LCS::Ldiff
# items we've read from each file will differ by FLD (could be 0).
file_length_difference = 0
- if @binary.nil? or @binary
- data_old = IO.read(file_old)
- data_new = IO.read(file_new)
-
- # Test binary status
- if @binary.nil?
- old_txt = data_old[0, 4096].scan(/\0/).empty?
- new_txt = data_new[0, 4096].scan(/\0/).empty?
- @binary = !old_txt or !new_txt
- end
+ data_old = IO.read(file_old)
+ data_new = IO.read(file_new)
+
+ # Test binary status
+ if @binary.nil?
+ old_txt = data_old[0, 4096].scan(/\0/).empty?
+ new_txt = data_new[0, 4096].scan(/\0/).empty?
+ @binary = !old_txt || !new_txt
+ end
- unless @binary
- data_old = data_old.split($/).map { |e| e.chomp }
- data_new = data_new.split($/).map { |e| e.chomp }
- end
- else
- data_old = IO.readlines(file_old).map { |e| e.chomp }
- data_new = IO.readlines(file_new).map { |e| e.chomp }
+ unless @binary
+ data_old = data_old.lines.to_a
+ data_new = data_new.lines.to_a
end
# diff yields lots of pieces, each of which is basically a Block object
@@ -150,20 +145,21 @@ class << Diff::LCS::Ldiff
end
diffs.each do |piece|
- begin
+ begin # rubocop:disable Style/RedundantBegin
hunk = Diff::LCS::Hunk.new(data_old, data_new, piece, @lines, file_length_difference)
file_length_difference = hunk.file_length_difference
next unless oldhunk
next if @lines.positive? and hunk.merge(oldhunk)
- output << oldhunk.diff(@format) << "\n"
+ output << oldhunk.diff(@format)
+ output << "\n" if @format == :unified
ensure
oldhunk = hunk
end
end
- last = oldhunk.diff(@format)
+ last = oldhunk.diff(@format, true)
last << "\n" if last.respond_to?(:end_with?) && !last.end_with?("\n")
output << last