Module: Amp::Diffs::MercurialDiff

Extended by:
MercurialDiff
Included in:
MercurialDiff
Defined in:
lib/amp/encoding/mercurial_diff.rb

Overview

MercurialDiff

Mercurial has it’s own implementation of the unified diff, because windows boxes don’t have diff -u. Plus code is faster than the shell. Lame. That’s ok, it’s pretty easy to do. And we can also add flags and change default settings.

Mainly, you’re only going to use MercurialDiff.unified_diff(). It’s usage is described below.

Constant Summary collapse

DEFAULT_OPTIONS =

These are the default options you can modify. Grab them, clone them, change them. Notice: You have to clone this when you use it, or you will be changing the default options!

{:context => 3, :text => false, :show_func => false,
:git => false, :no_dates => false, :ignore_ws => false,
:ignore_ws_amount => false, :ignore_blank_lines => false,
:pretty => false}

Instance Method Summary collapse

Instance Method Details

#add_line(input, options) ⇒ String

Given a line, returns a string that represents “adding that line” in a diff, based on the options.

Parameters:

  • input (String)

    the input line

Returns:

  • (String)

    the output line, in a format indicating it is “added”



53
54
55
# File 'lib/amp/encoding/mercurial_diff.rb', line 53

def add_line(input, options)
  options[:pretty] ? "+#{input.chomp}".green+"\n" : "+#{input}"
end

#bunidiff(t1, t2, l1, l2, header1, header2, opts = DEFAULT_OPTIONS) ⇒ Object

Helper method for creating unified diffs.

Parameters:

  • t1 (String)

    original text

  • t2 (String)

    new text

  • l1 (String)

    the original text broke into lines?

  • l2 (String)

    the new etxt broken into lines?

  • header1 (String)

    the original file’s header

  • header2 (String)

    the new file’s header

  • opts (defaults to: DEFAULT_OPTIONS)

    options for the method



263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
# File 'lib/amp/encoding/mercurial_diff.rb', line 263

def bunidiff(t1,t2, l1, l2, header1, header2, opts=DEFAULT_OPTIONS)
  header = [ "--- #{header1}\t\n", "+++ #{header2}\t\n" ]

  diff = BinaryDiff.blocks(t1,t2)
  hunk = nil
  return_hunks = []
  saved_delta = []
  delta = []
  diff.size.times do |i|
    s = (i > 0) ? diff[i-1] : {:start_a => 0, :end_a => 0, :start_b => 0, :end_b => 0}
    saved_delta += delta unless delta.empty?
    delta = []
    s1 = diff[i]
    a1 = s[:end_a]
    a2 = s1[:start_a]
    b1 = s[:end_b]
    b2 = s1[:start_b]
    
    old = (a2 == 0) ? [] : l1[a1..(a2-1)]
    newb = (b2 == 0) ? [] : l2[b1..(b2-1)] #stands for new "b"
    
    next if old.empty? && newb.empty?
    if opts[:ignore_ws] || opts[:ignore_blank_lines] || opts[:ignore_ws_amount]
      next if whitespace_clean(old.join,opts) == whitespace_clean(newb.join,opts)
    end
    
    astart = context_start(a1,opts)
    bstart = context_start(b1,opts)
    prev = nil
    if hunk
      if astart < hunk[:end_a] + opts[:context] + 1
        prev = hunk
        astart = hunk[:end_a]
        bstart = hunk[:end_b]
      else
        yield_hunk(hunk, header, l1, delta, opts) {|x| return_hunks << x}

        header = nil
      end
    end
    # move this inside previous nested if statements
    if prev
      hunk[:end_a] = a2
      hunk[:end_b] = b2
      delta = hunk[:delta]
    else
      hunk = {:start_a => astart, :end_a => a2, :start_b => bstart, :end_b => b2, :delta => delta}
    end
    
    hunk[:delta] += l1[astart..(a1-1)].map {|x| ' ' + x } if a1 > 0
    hunk[:delta] += old.map  {|x| remove_line(x, opts) }
    hunk[:delta] += newb.map {|x| add_line(x, opts) } 

  end
  saved_delta += delta
  
  yield_hunk(hunk, header, l1, saved_delta, opts) {|x| return_hunks << x} if hunk
  return_hunks
end

#context_end(l, len, options) ⇒ Object

Starts a block ending context for a change - part of the unified diff format.



195
196
197
198
199
# File 'lib/amp/encoding/mercurial_diff.rb', line 195

def context_end(l, len, options)
  ret = l + options[:context]
  ret = len if ret > len
  ret
end

#context_start(l, options) ⇒ Object

Starts a block starting context for a change - part of the unified diff format.



204
205
206
207
208
# File 'lib/amp/encoding/mercurial_diff.rb', line 204

def context_start(l, options)
  ret = l - options[:context]
  return 0 if ret < 0
  ret
end

#date_tag(date, fn1, addtab = true, options = DEFAULT_OPTIONS) ⇒ Object

Creates a date tag appropriate for diffs. Not all diff types use dates though (namely git, apparently), so the options matter.

Parameters:

  • date (Time)

    the time that we want to make a spiffy date line for

  • fn1 (String)

    the filename of the file being stamped. Only used if the addtab option is on.

  • addtab (Boolean) (defaults to: true)

    (false) whether or not to add a tab in the line or not. Only used if we’re in git mode or no-date mode.

  • options (defaults to: DEFAULT_OPTIONS)

    the options to use while creating the date line.

  • [Boolean] (Hash)

    a customizable set of options

Options Hash (options):

  • :nodates (Boolean) — default: false

    should we never print dates?



98
99
100
101
102
# File 'lib/amp/encoding/mercurial_diff.rb', line 98

def date_tag(date, fn1, addtab = true, options = DEFAULT_OPTIONS)
  return "\t#{date.to_diff}\n" if !(options[:git]) && !(options[:nodates])
  return "\t\n" if addtab && fn1 =~ / /
  return "\n"
end

#diff_line(revisions, a, b, options = DEFAULT_OPTIONS) ⇒ Object

Creates a header or something? Not sure what this is used for, no code references it. I think it’s for git or something. eh.



70
71
72
73
74
75
76
77
78
79
80
81
82
83
# File 'lib/amp/encoding/mercurial_diff.rb', line 70

def diff_line(revisions, a, b, options=DEFAULT_OPTIONS)
  options = DEFAULT_OPTIONS.merge options
  parts = ['diff']
  
  parts << '--git' if options[:git]
  parts << revisions.map {|r| "-r #{r}"}.join(' ') if revisions && !options[:git]
  if options[:git]
    parts << "a/#{a}"
    parts << "b/#{b}"
  else
    parts << a
  end
  parts.join(' ') + "\n"
end

#get_matching_blocks(a, b) ⇒ [Hash]

Gets the matching blocks between the two texts.

Parameters:

  • a (String)

    the original text

  • b (String)

    the final text

Returns:

  • ([Hash])

    The blocks of changes between the two



354
355
356
357
358
359
# File 'lib/amp/encoding/mercurial_diff.rb', line 354

def get_matching_blocks(a, b)
  an = a.split_lines_better
  bn = b.split_lines_better
  
  SequenceMatcher.new(an, bn).get_matching_blocks
end

#patch(a, bin) ⇒ Object

Applies the patch bin to the text a.

Parameters:

  • a (String)

    the text to patch

  • bin (String)

    the binary patch to apply



344
345
346
# File 'lib/amp/encoding/mercurial_diff.rb', line 344

def patch(a, bin)
  MercurialPatch.apply_patches(a, [bin])
end

#patch_text(binary) ⇒ Object

Unpacks a binary-compressed patch.

Parameters:

  • binary (String)

    the packed binary text to unpack



327
328
329
330
331
332
333
334
335
336
337
# File 'lib/amp/encoding/mercurial_diff.rb', line 327

def patch_text(binary)
  pos = 0
  t = []
  while pos < binary.size
    p1, p2, l = binary[pos..(pos+11)].unpack("NNN")
    pos += 12
    t << binary[pos..(pos + l - 1)]
    pos += l
  end
  t.join
end

#remove_line(input, options) ⇒ String

Given a line, returns a string that represents “removing that line” in a diff, based on the options.

Parameters:

  • input (String)

    the input line

Returns:

  • (String)

    the output line, in a format indicating it is “removed”



63
64
65
# File 'lib/amp/encoding/mercurial_diff.rb', line 63

def remove_line(input, options)
  options[:pretty] ? "-#{input.chomp}".red+"\n" : "-#{input}"
end

#text_diff(a, b) ⇒ Object

Returns a text diff between a and b. This returns the packed, binary kind of diff.



373
374
375
# File 'lib/amp/encoding/mercurial_diff.rb', line 373

def text_diff a,b
  BinaryDiff.bdiff a,b
end

#trivial_diff_header(length) ⇒ String

Returns the obvious header for when we create a new file

Parameters:

  • length (Fixnum)

    the length of the file

Returns:

  • (String)

    the obvious header



366
367
368
# File 'lib/amp/encoding/mercurial_diff.rb', line 366

def trivial_diff_header(length)
  [0, 0, length].pack("NNN")
end

#unified_diff(a, ad, b, bd, fn1, fn2, r = nil, options = DEFAULT_OPTIONS) ⇒ Object

Returns a unified diff based on the 2 blocks of text, their modification times, their filenames, and the options.

This is a self-contained replacement for diffs.

Parameters:

  • a

    the original text

  • ad (Time)

    the modification timestamp for the old file

  • b

    the new text

  • bd (Time)

    the modification timestamp for the new file

  • fn1

    the old filename

  • fn2

    the new filename

  • r (defaults to: nil)

    not sure what this does

  • options (defaults to: DEFAULT_OPTIONS)

    the options we will be using. There’s a lot of settings, see the descriptions for #whitespace_clean and #date_tag.



119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
# File 'lib/amp/encoding/mercurial_diff.rb', line 119

def unified_diff(a, ad, b, bd, fn1, fn2, r=nil, options=DEFAULT_OPTIONS)
  return "" if (a.nil? || a.empty?) && (b.nil? || b.empty?)
  epoch = Time.at(0)
  if !options[:text] && (!a.nil? && a.binary? || !b.nil? && b.binary?)
    return "" if a.any? && b.any? && a.size == b.size && a == b #DERR
    l = ["Binary file #{fn1} has changed\n"]
  elsif a.nil? || a.empty?
    b = b.split_lines_better
    header = []
    if options[:pretty]
      l1 = a.nil? ? "Added file " : "Changed file "
      l1 += "#{fn2} at #{date_tag(bd,fn1,true,options)}"
      l1 = l1.cyan
      header << l1
    else
      if a.nil?
        header << "--- /dev/null#{date_tag(epoch, fn1, false, options)}"
      else
        header << "--- #{"a/" + fn1}#{date_tag(ad,fn1,true,options)}"
      end
      header << "+++ #{"b/" + fn2}#{date_tag(bd,fn1,true,options)}"
      header << "@@ -0,0 +1,#{b.size} @@\n"
    end
    l = header + (b.map {|line| add_line(line, options)})
  elsif b.nil? || b.empty?
    a = b.split_lines_better
    header = []
    if options[:pretty]
      l1 = b.nil? ? "Removed file " : "Changed file "
      l1 += "#{fn2} at #{date_tag(bd,fn1,true,options)}"
      l1 = l1.cyan
      header << l1
    else
      header << "--- #{"a/" + fn1}#{date_tag(ad,fn1,true,options)}"
      if b.nil?
        header << "+++ /dev/null#{date_tag(epoch, fn1, false, options)}"
      else
        header << "+++ #{"b/" + fn2}#{date_tag(bd,fn1,true,options)}"
      end
      header << "@@ -1,#{a.size} +0,0 @@\n"
    end
    l = header + (a.map {|line| remove_line(line, options)})
  else
    al = a.split_lines_better
    bl = b.split_lines_better
    l = bunidiff(a, b, al, bl, "a/"+fn1, "b/"+fn2, options)
    return "" if l.nil? || l.empty?
    if options[:pretty]
      l.shift
      if fn1 == fn2
        l[0] = "Changed file #{fn1.cyan} at #{date_tag(bd,fn1,true,options).lstrip}"
      else
        l[0] = "Moved file from #{fn1.cyan} to #{fn2.cyan}"
      end
    else
      l[0] = "#{l[0][0 .. -3]}#{date_tag(ad,fn1,true,options)}"
      l[1] = "#{l[1][0 .. -3]}#{date_tag(bd,fn1,true,options)}"
    end
  end
  
  l.size.times do |ln|
    if l[ln][-1,1] != "\n"
      l[ln] << "\n\\ No newline at end of file\n"
    end
  end
  
  if r
    l.unshift diff_line(r, fn1, fn2, options)
  end
  
  l.join
end

#whitespace_clean(text, options = DEFAULT_OPTIONS) ⇒ Object

Clear up whitespace in the text if we have any options relating to getting rid of whitespace.

Parameters:

  • text (String)

    the text to modify

  • options (Hash) (defaults to: DEFAULT_OPTIONS)

    the options to use when deciding how to clean text

  • [Boolean] (Hash)

    a customizable set of options



36
37
38
39
40
41
42
43
44
45
# File 'lib/amp/encoding/mercurial_diff.rb', line 36

def whitespace_clean(text, options=DEFAULT_OPTIONS)
  if options[:ignore_ws]
    text.gsub!(/[ \t]+/, "") #warnings made me use parens
  elsif options[:ignore_ws_amount]
    text.gsub!(/[ \t]+/, ' ')
    text.gsub!(/[ \t]+\n/, "\n")
  end
  text.gsub!(/\n+/, '') if options[:ignore_blank_lines]
  text
end

#yield_hunk(hunk, header, l1, delta, options) ⇒ Object

Given a hunk of changes, yield each line we need to write to the diff.

Parameters:

  • hunk (Hash)

    specifies a block of lines that changed between the two files.

  • header

    the header for the block, if we have one.

  • l1

    the original lines - used for context (unified diff format)

  • delta

    the lines that have changed thus far

  • options

    settings for the unified diff action. unused mostly here.



219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
# File 'lib/amp/encoding/mercurial_diff.rb', line 219

def yield_hunk(hunk, header, l1, delta, options)
  header.each {|x| yield x} if header && header.any?
  delta = hunk[:delta]
  astart, a2, bstart, b2 = hunk[:start_a], hunk[:end_a], hunk[:start_b], hunk[:end_b]
  aend = context_end(a2,l1.size,options)
  alen = aend - astart
  blen = b2 - bstart + aend - a2
  
  # i seriously don't know what this does.
  func = ""
  if options[:show_func]
    (astart - 1).downto(0) do |x|
      t = l1[x].rstrip
      if t =~ /\w/
        func = ' ' + t[0 .. 39]
        break
      end
    end
  end
  
  # yield the header
  if options[:pretty]
    yield "From original lines #{astart + 1}-#{alen+astart+1}".yellow + "\n"
  else
    yield "@@ -%d,%d +%d,%d @@%s\n" % [astart + 1, alen,
                                      bstart + 1, blen, func]
  end
                                     
  # then yield each line of changes
  delta.each {|x| yield x}
  # then yield some context or something?
  a2.upto(aend-1) {|x| yield ' ' + l1[x] }
end