Ruby unified diff using tmp files

This is admittedly ghetto, but I needed a super simple small-scale solution to creating unified diffs based off large blocks of text. The easiest way to do this was by creating temp file and using the native diff command to do the comparison.

I’ve used this method for a really small snippets app for our intranet, which can be found on GitHub.

require 'tempfile'

# TODO: do some pre-comparison on a and b so we can skip creating temp files and all that jazz.
# NOTE: this is so ghetto ;)

module Diffable
  def diff(b, options = {})
    Diff.new(self, b, options).diff
  end
end

class Diff  
  attr_reader :output, :changed
  alias_method :changed?, :changed
  alias_method :to_s, :output
  
  def initialize(a, b, options = {})
    @a = a.to_str
    @b = b.to_str
    @options = options
    @changed = false
    @output = ''
  end

  def diff
    file_a = string_to_file('a', @a).path
    file_b = string_to_file('b', @b).path
    @output = format_output(`diff --unified=-1 #{file_a} #{file_b}`)
    @changed = !@output.strip.empty?
    self
  end
  
  def to_html(options = {})
    diff.output.each_line.inject("") do |output, line|
      output + \
        case line[0]
        when '@'
          "<div class='#{options[:meta_class] || 'diff_meta'}'>#{line}</div>"
        when '-'
          "<div class='#{options[:sub_class] || 'diff_sub'}'>#{line}</div>"
        when '+'
          "<div class='#{options[:add_class] || 'diff_add'}'>#{line}</div>"
        else
          line
        end
    end
  end

  def to_s
    output
  end
    
private  

  def string_to_file(key, data)
    Tempfile.open("#{key}.tmp_diff") do |file|
      file << data.to_str
    end
  end

  def format_output(output)
    output.gsub! /\-{3}.+/, "--- #{@options[:mine]}" if @options[:mine]
    output.gsub! /\+{3}.+/, "+++ #{@options[:theirs]}" if @options[:theirs]
    output
  end
end

class String
  include Diffable
end

Usage

require 'diff'
require 'pp'

original = "
Lorem ipsum dolor sit amet, consectetur adipiscing elit. 
Donec convallis urna nec magna ornare sit amet molestie leo dapibus. 
Maecenas augue tortor, eleifend sed consectetur consectetur, aliquet et risus. 
Suspendisse eu tellus ac quam adipiscing placerat. 
In a erat quis dui euismod interdum. 
In sapien lacus, suscipit sit amet faucibus sit amet, malesuada vehicula purus. 
Etiam in odio ut urna viverra rhoncus. 
Etiam ultricies tellus a metus adipiscing rutrum. 
"

modified = "
Donec convallis urna nec magna ornare sit amet molestie leo dapibus. 
Lorem ipsum dolor sit amet, consectetur adipiscing elit. 
Etiam in odio ut urna viverra rhoncus. 
Etiam ultricies tellus a metus adipiscing rutrum. 
Suspendisse eu tellus ac quam adipiscing placerat. 
In sapien lacus, suscipit sit amet faucibus sit amet, malesuada vehicula purus. 
In a erat quis dui euismod interdum. 
Maecenas augue tortor, eleifend sed consectetur consectetur, aliquet et risus. 
"

puts original.diff(modified, :mine => 'original', :theirs => 'modified')

__END__

--- original
+++ modified
@@ -1,9 +1,9 @@
 
-Lorem ipsum dolor sit amet, consectetur adipiscing elit. 
 Donec convallis urna nec magna ornare sit amet molestie leo dapibus. 
-Maecenas augue tortor, eleifend sed consectetur consectetur, aliquet et risus. 
-Suspendisse eu tellus ac quam adipiscing placerat. 
-In a erat quis dui euismod interdum. 
-In sapien lacus, suscipit sit amet faucibus sit amet, malesuada vehicula purus. 
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. 
 Etiam in odio ut urna viverra rhoncus. 
 Etiam ultricies tellus a metus adipiscing rutrum. 
+Suspendisse eu tellus ac quam adipiscing placerat. 
+In sapien lacus, suscipit sit amet faucibus sit amet, malesuada vehicula purus. 
+In a erat quis dui euismod interdum. 
+Maecenas augue tortor, eleifend sed consectetur consectetur, aliquet et risus. 

View Gist

It can use some work (definitely use some work), but it gets the job done. I’ve also tried to fit this diff library into the build, but it wasn’t what I was looking for at the time.