diff-lcs 1.3 → 1.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 (56) hide show
  1. checksums.yaml +5 -5
  2. data/Contributing.md +86 -48
  3. data/History.md +370 -159
  4. data/License.md +6 -4
  5. data/Manifest.txt +23 -1
  6. data/README.rdoc +10 -10
  7. data/Rakefile +109 -36
  8. data/bin/htmldiff +9 -6
  9. data/bin/ldiff +4 -1
  10. data/lib/diff/lcs/array.rb +2 -2
  11. data/lib/diff/lcs/backports.rb +9 -0
  12. data/lib/diff/lcs/block.rb +5 -5
  13. data/lib/diff/lcs/callbacks.rb +22 -17
  14. data/lib/diff/lcs/change.rb +42 -49
  15. data/lib/diff/lcs/htmldiff.rb +21 -12
  16. data/lib/diff/lcs/hunk.rb +160 -73
  17. data/lib/diff/lcs/internals.rb +57 -56
  18. data/lib/diff/lcs/ldiff.rb +63 -57
  19. data/lib/diff/lcs/string.rb +1 -1
  20. data/lib/diff/lcs.rb +226 -210
  21. data/lib/diff-lcs.rb +2 -2
  22. data/spec/change_spec.rb +58 -34
  23. data/spec/diff_spec.rb +13 -9
  24. data/spec/fixtures/aX +1 -0
  25. data/spec/fixtures/bXaX +1 -0
  26. data/spec/fixtures/ldiff/output.diff +4 -0
  27. data/spec/fixtures/ldiff/output.diff-c +7 -0
  28. data/spec/fixtures/ldiff/output.diff-e +3 -0
  29. data/spec/fixtures/ldiff/output.diff-f +3 -0
  30. data/spec/fixtures/ldiff/output.diff-u +5 -0
  31. data/spec/fixtures/ldiff/output.diff.chef +4 -0
  32. data/spec/fixtures/ldiff/output.diff.chef-c +15 -0
  33. data/spec/fixtures/ldiff/output.diff.chef-e +3 -0
  34. data/spec/fixtures/ldiff/output.diff.chef-f +3 -0
  35. data/spec/fixtures/ldiff/output.diff.chef-u +9 -0
  36. data/spec/fixtures/ldiff/output.diff.chef2 +7 -0
  37. data/spec/fixtures/ldiff/output.diff.chef2-c +20 -0
  38. data/spec/fixtures/ldiff/output.diff.chef2-d +7 -0
  39. data/spec/fixtures/ldiff/output.diff.chef2-e +7 -0
  40. data/spec/fixtures/ldiff/output.diff.chef2-f +7 -0
  41. data/spec/fixtures/ldiff/output.diff.chef2-u +16 -0
  42. data/spec/fixtures/new-chef +4 -0
  43. data/spec/fixtures/new-chef2 +17 -0
  44. data/spec/fixtures/old-chef +4 -0
  45. data/spec/fixtures/old-chef2 +14 -0
  46. data/spec/hunk_spec.rb +48 -37
  47. data/spec/issues_spec.rb +132 -21
  48. data/spec/lcs_spec.rb +3 -3
  49. data/spec/ldiff_spec.rb +74 -32
  50. data/spec/patch_spec.rb +14 -20
  51. data/spec/sdiff_spec.rb +83 -81
  52. data/spec/spec_helper.rb +146 -91
  53. data/spec/traverse_balanced_spec.rb +138 -136
  54. data/spec/traverse_sequences_spec.rb +7 -9
  55. metadata +76 -48
  56. data/autotest/discover.rb +0 -1
@@ -1,14 +1,15 @@
1
- # -*- ruby encoding: utf-8 -*-
1
+ # frozen_string_literal: true
2
2
 
3
- require 'cgi'
3
+ require "cgi"
4
4
 
5
+ # Produce a simple HTML diff view.
5
6
  class Diff::LCS::HTMLDiff
6
7
  class << self
7
- attr_accessor :can_expand_tabs #:nodoc:
8
+ attr_accessor :can_expand_tabs # :nodoc:
8
9
  end
9
10
  self.can_expand_tabs = true
10
11
 
11
- class Callbacks
12
+ class Callbacks # :nodoc:
12
13
  attr_accessor :output
13
14
  attr_accessor :match_class
14
15
  attr_accessor :only_a_class
@@ -25,7 +26,7 @@ class Diff::LCS::HTMLDiff
25
26
 
26
27
  def htmlize(element, css_class)
27
28
  element = "&nbsp;" if element.empty?
28
- %Q|<pre class="#{__send__(css_class)}">#{element}</pre>\n|
29
+ %(<pre class="#{__send__(css_class)}">#{element}</pre>\n)
29
30
  end
30
31
  private :htmlize
31
32
 
@@ -45,13 +46,16 @@ class Diff::LCS::HTMLDiff
45
46
  end
46
47
  end
47
48
 
49
+ # standard:disable Style/HashSyntax
48
50
  DEFAULT_OPTIONS = {
49
51
  :expand_tabs => nil,
50
52
  :output => nil,
51
53
  :css => nil,
52
- :title => nil,
53
- }
54
+ :title => nil
55
+ }.freeze
56
+ # standard:enable Style/HashSyntax
54
57
 
58
+ # standard:disable Layout/HeredocIndentation
55
59
  DEFAULT_CSS = <<-CSS
56
60
  body { margin: 0; }
57
61
  .diff
@@ -85,18 +89,19 @@ pre
85
89
  }
86
90
  h1 { margin-left: 2em; }
87
91
  CSS
92
+ # standard:enable Layout/HeredocIndentation
88
93
 
89
94
  def initialize(left, right, options = nil)
90
- @left = left
91
- @right = right
92
- @options = options
95
+ @left = left
96
+ @right = right
97
+ @options = options
93
98
 
94
99
  @options = DEFAULT_OPTIONS.dup if @options.nil?
95
100
  end
96
101
 
97
102
  def verify_options
98
103
  @options[:expand_tabs] ||= 4
99
- @options[:expand_tabs] = 4 if @options[:expand_tabs] < 0
104
+ @options[:expand_tabs] = 4 if @options[:expand_tabs].negative?
100
105
 
101
106
  @options[:output] ||= $stdout
102
107
 
@@ -111,7 +116,7 @@ h1 { margin-left: 2em; }
111
116
  def run
112
117
  verify_options
113
118
 
114
- if @options[:expand_tabs] > 0 && self.class.can_expand_tabs
119
+ if @options[:expand_tabs].positive? && self.class.can_expand_tabs
115
120
  formatter = Text::Format.new
116
121
  formatter.tabstop = @options[:expand_tabs]
117
122
 
@@ -122,6 +127,7 @@ h1 { margin-left: 2em; }
122
127
  @left.map! { |line| CGI.escapeHTML(line.chomp) }
123
128
  @right.map! { |line| CGI.escapeHTML(line.chomp) }
124
129
 
130
+ # standard:disable Layout/HeredocIndentation
125
131
  @options[:output] << <<-OUTPUT
126
132
  <html>
127
133
  <head>
@@ -136,14 +142,17 @@ h1 { margin-left: 2em; }
136
142
  <span class="only_b">Only in New</span></p>
137
143
  <div class="diff">
138
144
  OUTPUT
145
+ # standard:enable Layout/HeredocIndentation
139
146
 
140
147
  callbacks = Callbacks.new(@options[:output])
141
148
  Diff::LCS.traverse_sequences(@left, @right, callbacks)
142
149
 
150
+ # standard:disable Layout/HeredocIndentation
143
151
  @options[:output] << <<-OUTPUT
144
152
  </div>
145
153
  </body>
146
154
  </html>
147
155
  OUTPUT
156
+ # standard:enable Layout/HeredocIndentation
148
157
  end
149
158
  end
data/lib/diff/lcs/hunk.rb CHANGED
@@ -1,30 +1,42 @@
1
- # -*- ruby encoding: utf-8 -*-
1
+ # frozen_string_literal: true
2
2
 
3
- require 'diff/lcs/block'
3
+ require "diff/lcs/block"
4
4
 
5
- # A Hunk is a group of Blocks which overlap because of the context
6
- # surrounding each block. (So if we're not using context, every hunk will
7
- # contain one block.) Used in the diff program (bin/diff).
5
+ # A Hunk is a group of Blocks which overlap because of the context surrounding
6
+ # each block. (So if we're not using context, every hunk will contain one
7
+ # block.) Used in the diff program (bin/ldiff).
8
8
  class Diff::LCS::Hunk
9
- # Create a hunk using references to both the old and new data, as well as
10
- # the piece of data.
9
+ OLD_DIFF_OP_ACTION = {"+" => "a", "-" => "d", "!" => "c"}.freeze # :nodoc:
10
+ ED_DIFF_OP_ACTION = {"+" => "a", "-" => "d", "!" => "c"}.freeze # :nodoc:
11
+
12
+ private_constant :OLD_DIFF_OP_ACTION, :ED_DIFF_OP_ACTION if respond_to?(:private_constant)
13
+
14
+ # Create a hunk using references to both the old and new data, as well as the
15
+ # piece of data.
11
16
  def initialize(data_old, data_new, piece, flag_context, file_length_difference)
12
17
  # At first, a hunk will have just one Block in it
13
- @blocks = [ Diff::LCS::Block.new(piece) ]
18
+ @blocks = [Diff::LCS::Block.new(piece)]
19
+
20
+ if @blocks[0].remove.empty? && @blocks[0].insert.empty?
21
+ fail "Cannot build a hunk from #{piece.inspect}; has no add or remove actions"
22
+ end
23
+
14
24
  if String.method_defined?(:encoding)
15
- @preferred_data_encoding = data_old.fetch(0, data_new.fetch(0,'') ).encoding
25
+ @preferred_data_encoding = data_old.fetch(0) { data_new.fetch(0, "") }.encoding
16
26
  end
27
+
17
28
  @data_old = data_old
18
29
  @data_new = data_new
19
30
 
20
31
  before = after = file_length_difference
21
32
  after += @blocks[0].diff_size
22
33
  @file_length_difference = after # The caller must get this manually
34
+ @max_diff_size = @blocks.map { |e| e.diff_size.abs }.max
23
35
 
24
36
  # Save the start & end of each array. If the array doesn't exist (e.g.,
25
- # we're only adding items in this block), then figure out the line
26
- # number based on the line number of the other file and the current
27
- # difference in file lengths.
37
+ # we're only adding items in this block), then figure out the line number
38
+ # based on the line number of the other file and the current difference in
39
+ # file lengths.
28
40
  if @blocks[0].remove.empty?
29
41
  a1 = a2 = nil
30
42
  else
@@ -41,8 +53,8 @@ class Diff::LCS::Hunk
41
53
 
42
54
  @start_old = a1 || (b1 - before)
43
55
  @start_new = b1 || (a1 + before)
44
- @end_old = a2 || (b2 - after)
45
- @end_new = b2 || (a2 + after)
56
+ @end_old = a2 || (b2 - after)
57
+ @end_new = b2 || (a2 + after)
46
58
 
47
59
  self.flag_context = flag_context
48
60
  end
@@ -55,19 +67,26 @@ class Diff::LCS::Hunk
55
67
  # Change the "start" and "end" fields to note that context should be added
56
68
  # to this hunk.
57
69
  attr_accessor :flag_context
58
- undef :flag_context=;
59
- def flag_context=(context) #:nodoc:
60
- return if context.nil? or context.zero?
70
+ undef :flag_context=
71
+ def flag_context=(context) # :nodoc: # standard:disable Lint/DuplicateMethods
72
+ return if context.nil? || context.zero?
61
73
 
62
74
  add_start = (context > @start_old) ? @start_old : context
75
+
63
76
  @start_old -= add_start
64
77
  @start_new -= add_start
65
78
 
66
- if (@end_old + context) > @data_old.size
67
- add_end = @data_old.size - @end_old
68
- else
69
- add_end = context
70
- end
79
+ old_size = @data_old.size
80
+
81
+ add_end =
82
+ if (@end_old + context) > old_size
83
+ old_size - @end_old
84
+ else
85
+ context
86
+ end
87
+
88
+ add_end = @max_diff_size if add_end >= old_size
89
+
71
90
  @end_old += add_end
72
91
  @end_new += add_end
73
92
  end
@@ -76,13 +95,11 @@ class Diff::LCS::Hunk
76
95
  # a truthy value so that if there is no overlap, you can know the merge
77
96
  # was skipped.
78
97
  def merge(hunk)
79
- if overlaps?(hunk)
80
- @start_old = hunk.start_old
81
- @start_new = hunk.start_new
82
- blocks.unshift(*hunk.blocks)
83
- else
84
- nil
85
- end
98
+ return unless overlaps?(hunk)
99
+
100
+ @start_old = hunk.start_old
101
+ @start_new = hunk.start_new
102
+ blocks.unshift(*hunk.blocks)
86
103
  end
87
104
  alias_method :unshift, :merge
88
105
 
@@ -95,47 +112,53 @@ class Diff::LCS::Hunk
95
112
  end
96
113
 
97
114
  # Returns a diff string based on a format.
98
- def diff(format)
115
+ def diff(format, last = false)
99
116
  case format
100
117
  when :old
101
- old_diff
118
+ old_diff(last)
102
119
  when :unified
103
- unified_diff
120
+ unified_diff(last)
104
121
  when :context
105
- context_diff
122
+ context_diff(last)
106
123
  when :ed
107
124
  self
108
125
  when :reverse_ed, :ed_finish
109
- ed_diff(format)
126
+ ed_diff(format, last)
110
127
  else
111
- raise "Unknown diff format #{format}."
128
+ fail "Unknown diff format #{format}."
112
129
  end
113
130
  end
114
131
 
115
132
  # Note that an old diff can't have any context. Therefore, we know that
116
133
  # there's only one block in the hunk.
117
- def old_diff
134
+ def old_diff(_last = false)
118
135
  warn "Expecting only one block in an old diff hunk!" if @blocks.size > 1
119
- op_act = { "+" => 'a', "-" => 'd', "!" => "c" }
120
136
 
121
137
  block = @blocks[0]
122
138
 
123
139
  # Calculate item number range. Old diff range is just like a context
124
140
  # diff range, except the ranges are on one line with the action between
125
141
  # them.
126
- s = encode("#{context_range(:old)}#{op_act[block.op]}#{context_range(:new)}\n")
142
+ s = encode("#{context_range(:old, ",")}#{OLD_DIFF_OP_ACTION[block.op]}#{context_range(:new, ",")}\n")
127
143
  # If removing anything, just print out all the remove lines in the hunk
128
144
  # which is just all the remove lines in the block.
129
- @data_old[@start_old .. @end_old].each { |e| s << encode("< ") + e + encode("\n") } unless block.remove.empty?
145
+ unless block.remove.empty?
146
+ @data_old[@start_old..@end_old].each { |e| s << encode("< ") + e.chomp + encode("\n") }
147
+ end
148
+
130
149
  s << encode("---\n") if block.op == "!"
131
- @data_new[@start_new .. @end_new].each { |e| s << encode("> ") + e + encode("\n") } unless block.insert.empty?
150
+
151
+ unless block.insert.empty?
152
+ @data_new[@start_new..@end_new].each { |e| s << encode("> ") + e.chomp + encode("\n") }
153
+ end
154
+
132
155
  s
133
156
  end
134
157
  private :old_diff
135
158
 
136
- def unified_diff
159
+ def unified_diff(last = false)
137
160
  # Calculate item number range.
138
- s = encode("@@ -#{unified_range(:old)} +#{unified_range(:new)} @@\n")
161
+ s = encode("@@ -#{unified_range(:old, last)} +#{unified_range(:new, last)} @@\n")
139
162
 
140
163
  # Outlist starts containing the hunk of the old file. Removing an item
141
164
  # just means putting a '-' in front of it. Inserting an item requires
@@ -148,75 +171,122 @@ class Diff::LCS::Hunk
148
171
  # file -- don't take removed items into account.
149
172
  lo, hi, num_added, num_removed = @start_old, @end_old, 0, 0
150
173
 
151
- outlist = @data_old[lo .. hi].map { |e| e.insert(0, encode(' ')) }
174
+ # standard:disable Performance/UnfreezeString
175
+ outlist = @data_old[lo..hi].map { |e| String.new("#{encode(" ")}#{e.chomp}") }
176
+ # standard:enable Performance/UnfreezeString
177
+
178
+ last_block = blocks[-1]
179
+
180
+ if last
181
+ old_missing_newline = missing_last_newline?(@data_old)
182
+ new_missing_newline = missing_last_newline?(@data_new)
183
+ end
152
184
 
153
185
  @blocks.each do |block|
154
186
  block.remove.each do |item|
155
- op = item.action.to_s # -
187
+ op = item.action.to_s # -
156
188
  offset = item.position - lo + num_added
157
189
  outlist[offset][0, 1] = encode(op)
158
190
  num_removed += 1
159
191
  end
192
+
193
+ if last && block == last_block && old_missing_newline && !new_missing_newline
194
+ outlist << encode('\')
195
+ num_removed += 1
196
+ end
197
+
160
198
  block.insert.each do |item|
161
- op = item.action.to_s # +
199
+ op = item.action.to_s # +
162
200
  offset = item.position - @start_new + num_removed
163
- outlist[offset, 0] = encode(op) + @data_new[item.position]
201
+ outlist[offset, 0] = encode(op) + @data_new[item.position].chomp
164
202
  num_added += 1
165
203
  end
166
204
  end
167
205
 
206
+ outlist << encode('\') if last && new_missing_newline
207
+
168
208
  s << outlist.join(encode("\n"))
209
+
210
+ s
169
211
  end
170
212
  private :unified_diff
171
213
 
172
- def context_diff
214
+ def context_diff(last = false)
173
215
  s = encode("***************\n")
174
- s << encode("*** #{context_range(:old)} ****\n")
175
- r = context_range(:new)
216
+ s << encode("*** #{context_range(:old, ",", last)} ****\n")
217
+ r = context_range(:new, ",", last)
218
+
219
+ if last
220
+ old_missing_newline = missing_last_newline?(@data_old)
221
+ new_missing_newline = missing_last_newline?(@data_new)
222
+ end
176
223
 
177
224
  # Print out file 1 part for each block in context diff format if there
178
225
  # are any blocks that remove items
179
226
  lo, hi = @start_old, @end_old
180
- removes = @blocks.select { |e| not e.remove.empty? }
181
- if removes
182
- outlist = @data_old[lo .. hi].map { |e| e.insert(0, encode(' ')) }
227
+ removes = @blocks.reject { |e| e.remove.empty? }
228
+
229
+ unless removes.empty?
230
+ # standard:disable Performance/UnfreezeString
231
+ outlist = @data_old[lo..hi].map { |e| String.new("#{encode(" ")}#{e.chomp}") }
232
+ # standard:enable Performance/UnfreezeString
233
+
234
+ last_block = removes[-1]
183
235
 
184
236
  removes.each do |block|
185
237
  block.remove.each do |item|
186
238
  outlist[item.position - lo][0, 1] = encode(block.op) # - or !
187
239
  end
240
+
241
+ if last && block == last_block && old_missing_newline
242
+ outlist << encode('\')
243
+ end
188
244
  end
189
- s << outlist.join("\n")
245
+
246
+ s << outlist.join(encode("\n")) << encode("\n")
190
247
  end
191
248
 
192
- s << encode("\n--- #{r} ----\n")
249
+ s << encode("--- #{r} ----\n")
193
250
  lo, hi = @start_new, @end_new
194
- inserts = @blocks.select { |e| not e.insert.empty? }
195
- if inserts
196
- outlist = @data_new[lo .. hi].collect { |e| e.insert(0, encode(' ')) }
251
+ inserts = @blocks.reject { |e| e.insert.empty? }
252
+
253
+ unless inserts.empty?
254
+ # standard:disable Performance/UnfreezeString
255
+ outlist = @data_new[lo..hi].map { |e| String.new("#{encode(" ")}#{e.chomp}") }
256
+ # standard:enable Performance/UnfreezeString
257
+
258
+ last_block = inserts[-1]
259
+
197
260
  inserts.each do |block|
198
261
  block.insert.each do |item|
199
262
  outlist[item.position - lo][0, 1] = encode(block.op) # + or !
200
263
  end
264
+
265
+ if last && block == last_block && new_missing_newline
266
+ outlist << encode('\')
267
+ end
201
268
  end
202
- s << outlist.join("\n")
269
+ s << outlist.join(encode("\n"))
203
270
  end
271
+
204
272
  s
205
273
  end
206
274
  private :context_diff
207
275
 
208
- def ed_diff(format)
209
- op_act = { "+" => 'a', "-" => 'd', "!" => "c" }
276
+ def ed_diff(format, _last = false)
210
277
  warn "Expecting only one block in an old diff hunk!" if @blocks.size > 1
211
278
 
212
- if format == :reverse_ed
213
- s = encode("#{op_act[@blocks[0].op]}#{context_range(:old)}\n")
214
- else
215
- s = encode("#{context_range(:old, ' ')}#{op_act[@blocks[0].op]}\n")
216
- end
279
+ s =
280
+ if format == :reverse_ed
281
+ encode("#{ED_DIFF_OP_ACTION[@blocks[0].op]}#{context_range(:old, ",")}\n")
282
+ else
283
+ encode("#{context_range(:old, " ")}#{ED_DIFF_OP_ACTION[@blocks[0].op]}\n")
284
+ end
217
285
 
218
286
  unless @blocks[0].insert.empty?
219
- @data_new[@start_new .. @end_new].each { |e| s << e + encode("\n") }
287
+ @data_new[@start_new..@end_new].each do |e|
288
+ s << e.chomp + encode("\n")
289
+ end
220
290
  s << encode(".\n")
221
291
  end
222
292
  s
@@ -225,7 +295,7 @@ class Diff::LCS::Hunk
225
295
 
226
296
  # Generate a range of item numbers to print. Only print 1 number if the
227
297
  # range has only one item in it. Otherwise, it's 'start,end'
228
- def context_range(mode, op = ',')
298
+ def context_range(mode, op, last = false)
229
299
  case mode
230
300
  when :old
231
301
  s, e = (@start_old + 1), (@end_old + 1)
@@ -233,14 +303,17 @@ class Diff::LCS::Hunk
233
303
  s, e = (@start_new + 1), (@end_new + 1)
234
304
  end
235
305
 
236
- (s < e) ? "#{s}#{op}#{e}" : "#{e}"
306
+ e -= 1 if last
307
+ e = 1 if e.zero?
308
+
309
+ (s < e) ? "#{s}#{op}#{e}" : e.to_s
237
310
  end
238
311
  private :context_range
239
312
 
240
313
  # Generate a range of item numbers to print for unified diff. Print number
241
314
  # where block starts, followed by number of lines in the block
242
315
  # (don't print number of lines if it's 1)
243
- def unified_range(mode)
316
+ def unified_range(mode, last)
244
317
  case mode
245
318
  when :old
246
319
  s, e = (@start_old + 1), (@end_old + 1)
@@ -248,12 +321,25 @@ class Diff::LCS::Hunk
248
321
  s, e = (@start_new + 1), (@end_new + 1)
249
322
  end
250
323
 
251
- length = e - s + 1
324
+ length = e - s + (last ? 0 : 1)
325
+
252
326
  first = (length < 2) ? e : s # "strange, but correct"
253
- (length == 1) ? "#{first}" : "#{first},#{length}"
327
+ (length <= 1) ? first.to_s : "#{first},#{length}"
254
328
  end
255
329
  private :unified_range
256
330
 
331
+ def missing_last_newline?(data)
332
+ newline = encode("\n")
333
+
334
+ if data[-2]
335
+ data[-2].end_with?(newline) && !data[-1].end_with?(newline)
336
+ elsif data[-1]
337
+ !data[-1].end_with?(newline)
338
+ else
339
+ true
340
+ end
341
+ end
342
+
257
343
  if String.method_defined?(:encoding)
258
344
  def encode(literal, target_encoding = @preferred_data_encoding)
259
345
  literal.encode target_encoding
@@ -263,10 +349,11 @@ class Diff::LCS::Hunk
263
349
  args.map { |arg| arg.encode(string.encoding) }
264
350
  end
265
351
  else
266
- def encode(literal, target_encoding = nil)
352
+ def encode(literal, _target_encoding = nil)
267
353
  literal
268
354
  end
269
- def encode_as(string, *args)
355
+
356
+ def encode_as(_string, *args)
270
357
  args
271
358
  end
272
359
  end