• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

halostatue / diff-lcs / 15813454968

23 Jun 2025 01:57AM UTC coverage: 88.366%. Remained the same
15813454968

Pull #159

github

web-flow
Merge d45cbd309 into fbabd3a7f
Pull Request #159: Bump ruby/setup-ruby from 1.244.0 to 1.245.0

533 of 797 branches covered (66.88%)

676 of 765 relevant lines covered (88.37%)

288.17 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

86.32
/lib/diff/lcs/hunk.rb
1
# frozen_string_literal: true
2

3
require "diff/lcs/block"
2✔
4

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
class Diff::LCS::Hunk
2✔
9
  OLD_DIFF_OP_ACTION = {"+" => "a", "-" => "d", "!" => "c"}.freeze # :nodoc:
2✔
10
  ED_DIFF_OP_ACTION = {"+" => "a", "-" => "d", "!" => "c"}.freeze # :nodoc:
2✔
11

12
  private_constant :OLD_DIFF_OP_ACTION, :ED_DIFF_OP_ACTION if respond_to?(:private_constant)
2!
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)
2✔
17
    # At first, a hunk will have just one Block in it
18
    @blocks = [Diff::LCS::Block.new(piece)]
20✔
19

20
    if @blocks[0].remove.empty? && @blocks[0].insert.empty?
20!
21
      fail "Cannot build a hunk from #{piece.inspect}; has no add or remove actions"
×
22
    end
23

24
    if String.method_defined?(:encoding)
20!
25
      @preferred_data_encoding = data_old.fetch(0) { data_new.fetch(0) { "" } }.encoding
22✔
26
    end
27

28
    @data_old = data_old
20✔
29
    @data_new = data_new
20✔
30
    @old_empty = data_old.empty? || (data_old.size == 1 && data_old[0].empty?)
20✔
31
    @new_empty = data_new.empty? || (data_new.size == 1 && data_new[0].empty?)
20✔
32

33
    before = after = file_length_difference
20✔
34
    after += @blocks[0].diff_size
20✔
35
    @file_length_difference = after # The caller must get this manually
20✔
36
    @max_diff_size = @blocks.map { |e| e.diff_size.abs }.max
40✔
37

38
    # Save the start & end of each array. If the array doesn't exist (e.g.,
39
    # we're only adding items in this block), then figure out the line number
40
    # based on the line number of the other file and the current difference in
41
    # file lengths.
42
    if @blocks[0].remove.empty?
20✔
43
      a1 = a2 = nil
4✔
44
    else
8✔
45
      a1 = @blocks[0].remove[0].position
16✔
46
      a2 = @blocks[0].remove[-1].position
16✔
47
    end
48

49
    if @blocks[0].insert.empty?
20✔
50
      b1 = b2 = nil
2✔
51
    else
9✔
52
      b1 = @blocks[0].insert[0].position
18✔
53
      b2 = @blocks[0].insert[-1].position
18✔
54
    end
55

56
    @start_old = a1 || (b1 - before)
20✔
57
    @start_new = b1 || (a1 + before)
20✔
58
    @end_old = a2 || (b2 - after)
20✔
59
    @end_new = b2 || (a2 + after)
20✔
60

61
    self.flag_context = flag_context
20✔
62
  end
63

64
  attr_reader :blocks
2✔
65
  attr_reader :start_old, :start_new
2✔
66
  attr_reader :end_old, :end_new
2✔
67
  attr_reader :file_length_difference
2✔
68

69
  # Change the "start" and "end" fields to note that context should be added
70
  # to this hunk.
71
  attr_accessor :flag_context
2✔
72
  undef :flag_context=
2✔
73
  def flag_context=(context) # :nodoc: # standard:disable Lint/DuplicateMethods
2✔
74
    return if context.nil? || context.zero?
20!
75

76
    add_start = (context > @start_old) ? @start_old : context
20✔
77

78
    @start_old -= add_start
20✔
79
    @start_new -= add_start
20✔
80

81
    old_size = @data_old.size
20✔
82

83
    add_end =
84
      if (@end_old + context) >= old_size
20✔
85
        old_size - @end_old - 1
18✔
86
      else
1✔
87
        context
2✔
88
      end
89

90
    @end_old += add_end
20✔
91
    @end_new += add_end
20✔
92
  end
93

94
  # Merges this hunk and the provided hunk together if they overlap. Returns
95
  # a truthy value so that if there is no overlap, you can know the merge
96
  # was skipped.
97
  def merge(hunk)
2✔
98
    return unless overlaps?(hunk)
2!
99

100
    @start_old = hunk.start_old
×
101
    @start_new = hunk.start_new
×
102
    blocks.unshift(*hunk.blocks)
×
103
  end
104
  alias_method :unshift, :merge
2✔
105

106
  # Determines whether there is an overlap between this hunk and the
107
  # provided hunk. This will be true if the difference between the two hunks
108
  # start or end positions is within one position of each other.
109
  def overlaps?(hunk)
2✔
110
    hunk and (((@start_old - hunk.end_old) <= 1) or
2✔
111
              ((@start_new - hunk.end_new) <= 1))
2✔
112
  end
113

114
  # Returns a diff string based on a format.
115
  def diff(format, last = false)
2✔
116
    case format
20!
117
    when :old
1✔
118
      old_diff(last)
2✔
119
    when :unified
7✔
120
      unified_diff(last)
14✔
121
    when :context
1✔
122
      context_diff(last)
2✔
123
    when :ed
×
124
      self
×
125
    when :reverse_ed, :ed_finish
1✔
126
      ed_diff(format, last)
2✔
127
    else
×
128
      fail "Unknown diff format #{format}."
×
129
    end
130
  end
131

132
  # Note that an old diff can't have any context. Therefore, we know that
133
  # there's only one block in the hunk.
134
  def old_diff(last = false)
2✔
135
    warn "Expecting only one block in an old diff hunk!" if @blocks.size > 1
2!
136

137
    block = @blocks[0]
2✔
138

139
    if last
2!
140
      old_missing_newline = !@old_empty && missing_last_newline?(@data_old)
×
141
      new_missing_newline = !@new_empty && missing_last_newline?(@data_new)
×
142
    end
143

144
    # Calculate item number range. Old diff range is just like a context
145
    # diff range, except the ranges are on one line with the action between
146
    # them.
147
    s = encode("#{context_range(:old, ",")}#{OLD_DIFF_OP_ACTION[block.op]}#{context_range(:new, ",")}\n")
2✔
148
    # If removing anything, just print out all the remove lines in the hunk
149
    # which is just all the remove lines in the block.
150
    unless block.remove.empty?
2!
151
      @data_old[@start_old..@end_old].each { |e| s << encode("< ") + e.chomp + encode("\n") }
4✔
152
    end
153

154
    s << encode("\\ No newline at end of file\n") if old_missing_newline && !new_missing_newline
2!
155
    s << encode("---\n") if block.op == "!"
2!
156

157
    unless block.insert.empty?
2!
158
      @data_new[@start_new..@end_new].each { |e| s << encode("> ") + e.chomp + encode("\n") }
4✔
159
    end
160

161
    s << encode("\\ No newline at end of file\n") if new_missing_newline && !old_missing_newline
2!
162

163
    s
2✔
164
  end
165
  private :old_diff
2✔
166

167
  def unified_diff(last = false)
2✔
168
    # Calculate item number range.
169
    s = encode("@@ -#{unified_range(:old)} +#{unified_range(:new)} @@\n")
14✔
170

171
    # Outlist starts containing the hunk of the old file. Removing an item
172
    # just means putting a '-' in front of it. Inserting an item requires
173
    # getting it from the new file and splicing it in. We splice in
174
    # +num_added+ items. Remove blocks use +num_added+ because splicing
175
    # changed the length of outlist.
176
    #
177
    # We remove +num_removed+ items. Insert blocks use +num_removed+
178
    # because their item numbers -- corresponding to positions in the NEW
179
    # file -- don't take removed items into account.
180
    lo, hi, num_added, num_removed = @start_old, @end_old, 0, 0
14✔
181

182
    # standard:disable Performance/UnfreezeString
183
    outlist = @data_old[lo..hi].map { |e| String.new("#{encode(" ")}#{e.chomp}") }
46✔
184
    # standard:enable Performance/UnfreezeString
185

186
    last_block = blocks[-1]
14✔
187

188
    if last
14✔
189
      old_missing_newline = !@old_empty && missing_last_newline?(@data_old)
4✔
190
      new_missing_newline = !@new_empty && missing_last_newline?(@data_new)
4✔
191
    end
192

193
    @blocks.each do |block|
14✔
194
      block.remove.each do |item|
14✔
195
        op = item.action.to_s # -
12✔
196
        offset = item.position - lo + num_added
12✔
197
        outlist[offset][0, 1] = encode(op)
12✔
198
        num_removed += 1
12✔
199
      end
200

201
      if last && block == last_block && old_missing_newline && !new_missing_newline
14!
202
        outlist << encode('\\ No newline at end of file')
×
203
        num_removed += 1
×
204
      end
205

206
      block.insert.each do |item|
14✔
207
        op = item.action.to_s # +
18✔
208
        offset = item.position - @start_new + num_removed
18✔
209
        outlist[offset, 0] = encode(op) + @data_new[item.position].chomp
18✔
210
        num_added += 1
18✔
211
      end
212
    end
213

214
    outlist << encode('\\ No newline at end of file') if last && new_missing_newline
14✔
215

216
    s << outlist.join(encode("\n"))
14✔
217

218
    s
14✔
219
  end
220
  private :unified_diff
2✔
221

222
  def context_diff(last = false)
2✔
223
    s = encode("***************\n")
2✔
224
    s << encode("*** #{context_range(:old, ",")} ****\n")
2✔
225
    r = context_range(:new, ",")
2✔
226

227
    if last
2!
228
      old_missing_newline = missing_last_newline?(@data_old)
×
229
      new_missing_newline = missing_last_newline?(@data_new)
×
230
    end
231

232
    # Print out file 1 part for each block in context diff format if there
233
    # are any blocks that remove items
234
    lo, hi = @start_old, @end_old
2✔
235
    removes = @blocks.reject { |e| e.remove.empty? }
4✔
236

237
    unless removes.empty?
2!
238
      # standard:disable Performance/UnfreezeString
1✔
239
      outlist = @data_old[lo..hi].map { |e| String.new("#{encode("  ")}#{e.chomp}") }
4✔
240
      # standard:enable Performance/UnfreezeString
241

242
      last_block = removes[-1]
2✔
243

244
      removes.each do |block|
2✔
245
        block.remove.each do |item|
2✔
246
          outlist[item.position - lo][0, 1] = encode(block.op) # - or !
2✔
247
        end
248

249
        if last && block == last_block && old_missing_newline
2!
250
          outlist << encode('\\ No newline at end of file')
×
251
        end
252
      end
253

254
      s << outlist.join(encode("\n")) << encode("\n")
2✔
255
    end
256

257
    s << encode("--- #{r} ----\n")
2✔
258
    lo, hi = @start_new, @end_new
2✔
259
    inserts = @blocks.reject { |e| e.insert.empty? }
4✔
260

261
    unless inserts.empty?
2!
262
      # standard:disable Performance/UnfreezeString
1✔
263
      outlist = @data_new[lo..hi].map { |e| String.new("#{encode("  ")}#{e.chomp}") }
4✔
264
      # standard:enable Performance/UnfreezeString
265

266
      last_block = inserts[-1]
2✔
267

268
      inserts.each do |block|
2✔
269
        block.insert.each do |item|
2✔
270
          outlist[item.position - lo][0, 1] = encode(block.op) # + or !
2✔
271
        end
272

273
        if last && block == last_block && new_missing_newline
2!
274
          outlist << encode('\\ No newline at end of file')
×
275
        end
276
      end
277
      s << outlist.join(encode("\n"))
2✔
278
    end
279

280
    s
2✔
281
  end
282
  private :context_diff
2✔
283

284
  def ed_diff(format, last)
2✔
285
    warn "Expecting only one block in an old diff hunk!" if @blocks.size > 1
2!
286
    if last
2!
287
      # ed script doesn't support well incomplete lines
×
288
      warn "<old_file>: No newline at end of file\n" if !@old_empty && missing_last_newline?(@data_old)
×
289
      warn "<new_file>: No newline at end of file\n" if !@new_empty && missing_last_newline?(@data_new)
×
290

291
      if @blocks[0].op == "!"
×
292
        return +"" if @blocks[0].changes[0].element == @blocks[0].changes[1].element + "\n"
×
293
        return +"" if @blocks[0].changes[0].element + "\n" == @blocks[0].changes[1].element
×
294
      end
295
    end
296

297
    s =
298
      if format == :reverse_ed
2!
299
        encode("#{ED_DIFF_OP_ACTION[@blocks[0].op]}#{context_range(:old, " ")}\n")
2✔
300
      else
×
301
        encode("#{context_range(:old, ",")}#{ED_DIFF_OP_ACTION[@blocks[0].op]}\n")
×
302
      end
303

304
    unless @blocks[0].insert.empty?
2!
305
      @data_new[@start_new..@end_new].each do |e|
2✔
306
        s << e.chomp + encode("\n")
2✔
307
      end
308
      s << encode(".\n")
2✔
309
    end
310
    s
2✔
311
  end
312
  private :ed_diff
2✔
313

314
  # Generate a range of item numbers to print. Only print 1 number if the
315
  # range has only one item in it. Otherwise, it's 'start,end'
316
  def context_range(mode, op)
2✔
317
    case mode
10!
318
    when :old
3✔
319
      s, e = (@start_old + 1), (@end_old + 1)
6✔
320
    when :new
2✔
321
      s, e = (@start_new + 1), (@end_new + 1)
4✔
322
    end
323

324
    (s < e) ? "#{s}#{op}#{e}" : e.to_s
10!
325
  end
326
  private :context_range
2✔
327

328
  # Generate a range of item numbers to print for unified diff. Print number
329
  # where block starts, followed by number of lines in the block
330
  # (don't print number of lines if it's 1)
331
  def unified_range(mode)
2✔
332
    case mode
28!
333
    when :old
7✔
334
      return "0,0" if @old_empty
14✔
335
      s, e = (@start_old + 1), (@end_old + 1)
12✔
336
    when :new
7✔
337
      return "0,0" if @new_empty
14!
338
      s, e = (@start_new + 1), (@end_new + 1)
14✔
339
    end
340

341
    length = e - s + 1
26✔
342

343
    (length <= 1) ? e.to_s : "#{s},#{length}"
26✔
344
  end
345
  private :unified_range
2✔
346

347
  def missing_last_newline?(data)
2✔
348
    newline = encode("\n")
8✔
349

350
    if data[-2]
8✔
351
      data[-2].end_with?(newline) && !data[-1].end_with?(newline)
4✔
352
    elsif data[-1]
4!
353
      !data[-1].end_with?(newline)
4✔
354
    else
×
355
      true
×
356
    end
357
  end
358

359
  if String.method_defined?(:encoding)
2!
360
    def encode(literal, target_encoding = @preferred_data_encoding)
2✔
361
      literal.encode target_encoding
138✔
362
    end
363

364
    def encode_as(string, *args)
2✔
365
      args.map { |arg| arg.encode(string.encoding) }
×
366
    end
367
  else
×
368
    def encode(literal, _target_encoding = nil)
×
369
      literal
×
370
    end
371

372
    def encode_as(_string, *args)
×
373
      args
×
374
    end
375
  end
376

377
  private :encode
2✔
378
  private :encode_as
2✔
379
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc