diff-lcs 1.1.3 → 1.5.0

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