Class: Diffy::Diff

Inherits:
Object
  • Object
show all
Includes:
Enumerable
Defined in:
lib/diffy/diff.rb

Constant Summary collapse

ORIGINAL_DEFAULT_OPTIONS =
{
  :diff => '-U10000',
  :source => 'strings',
  :include_diff_info => false,
  :include_plus_and_minus_in_html => false,
  :context => nil,
  :allow_empty_diff => true,
}

Class Attribute Summary collapse

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(string1, string2, options = {}) ⇒ Diff

supported options

:diff

A cli options string passed to diff

:source

Either strings or files. Determines whether string1 and string2 should be interpreted as strings or file paths.

:include_diff_info

Include diff header info

:include_plus_and_minus_in_html

Show the +, -, ‘ ’ at the beginning of lines in html output.



35
36
37
38
39
40
41
# File 'lib/diffy/diff.rb', line 35

def initialize(string1, string2, options = {})
  @options = self.class.default_options.merge(options)
  if ! ['strings', 'files'].include?(@options[:source])
    raise ArgumentError, "Invalid :source option #{@options[:source].inspect}. Supported options are 'strings' and 'files'."
  end
  @string1, @string2 = string1, string2
end

Class Attribute Details

.default_formatObject



14
15
16
# File 'lib/diffy/diff.rb', line 14

def default_format
  @default_format ||= :text
end

.default_optionsObject



20
21
22
# File 'lib/diffy/diff.rb', line 20

def default_options
  @default_options ||= ORIGINAL_DEFAULT_OPTIONS.dup
end

Instance Attribute Details

#optionsObject (readonly)

Returns the value of attribute options.



26
27
28
# File 'lib/diffy/diff.rb', line 26

def options
  @options
end

#string1Object (readonly)

Returns the value of attribute string1.



26
27
28
# File 'lib/diffy/diff.rb', line 26

def string1
  @string1
end

#string2Object (readonly)

Returns the value of attribute string2.



26
27
28
# File 'lib/diffy/diff.rb', line 26

def string2
  @string2
end

Instance Method Details

#diffObject



43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# File 'lib/diffy/diff.rb', line 43

def diff
  @diff ||= begin
    @paths = case options[:source]
      when 'strings'
        [tempfile(string1), tempfile(string2)]
      when 'files'
        [string1, string2]
      end

    if WINDOWS
      # don't use open3 on windows
      cmd = sprintf '"%s" %s %s', diff_bin, diff_options.join(' '), @paths.map { |s| %("#{s}") }.join(' ')
      diff = `#{cmd}`
    else
      diff = Open3.popen3(diff_bin, *(diff_options + @paths)) { |i, o, e| o.read }
    end
    diff.force_encoding('ASCII-8BIT') if diff.respond_to?(:valid_encoding?) && !diff.valid_encoding?
    if diff =~ /\A\s*\Z/ && !options[:allow_empty_diff]
      diff = case options[:source]
      when 'strings' then string1
      when 'files' then File.read(string1)
      end.gsub(/^/, " ")
    end
    diff
  end
ensure
  # unlink the tempfiles explicitly now that the diff is generated
  if defined? @tempfiles # to avoid Ruby warnings about undefined ins var.
    Array(@tempfiles).each do |t|
      begin
        # check that the path is not nil and file still exists.
        # REE seems to be very agressive with when it magically removes
        # tempfiles
        t.unlink if t.path && File.exist?(t.path)
      rescue => e
        warn "#{e.class}: #{e}"
        warn e.backtrace.join("\n")
      end
    end
  end
end

#eachObject



85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/diffy/diff.rb', line 85

def each
  lines = case @options[:include_diff_info]
  when false
    # this "primes" the diff and sets up the paths we'll reference below.
    diff

    # caching this regexp improves the performance of the loop by a
    # considerable amount.
    regexp = /^(--- "?#{@paths[0]}"?|\+\+\+ "?#{@paths[1]}"?|@@|\\\\)/

    diff.split("\n").reject{|x| x =~ regexp }.map {|line| line + "\n" }

  when true
    diff.split("\n").map {|line| line + "\n" }
  end

  if block_given?
    lines.each{|line| yield line}
  else
    lines.to_enum
  end
end

#each_chunkObject



108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/diffy/diff.rb', line 108

def each_chunk
  old_state = nil
  chunks = inject([]) do |cc, line|
    state = line.each_char.first
    if state == old_state
      cc.last << line
    else
      cc.push line.dup
    end
    old_state = state
    cc
  end

  if block_given?
    chunks.each{|chunk| yield chunk }
  else
    chunks.to_enum
  end
end

#tempfile(string) ⇒ Object



128
129
130
131
132
133
134
135
136
137
138
# File 'lib/diffy/diff.rb', line 128

def tempfile(string)
  t = Tempfile.new('diffy')
  # ensure tempfiles aren't unlinked when GC runs by maintaining a
  # reference to them.
  @tempfiles ||=[]
  @tempfiles.push(t)
  t.print(string)
  t.flush
  t.close
  t.path
end

#to_s(format = nil) ⇒ Object



140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/diffy/diff.rb', line 140

def to_s(format = nil)
  format ||= self.class.default_format
  formats = Format.instance_methods(false).map{|x| x.to_s}
  if formats.include? format.to_s
    enum = self
    enum.extend Format
    enum.send format
  else
    raise ArgumentError,
      "Format #{format.inspect} not found in #{formats.inspect}"
  end
end