File: diff.rb

package info (click to toggle)
ruby-diffy 3.4.4-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 216 kB
  • sloc: ruby: 1,049; makefile: 2
file content (175 lines) | stat: -rw-r--r-- 5,061 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
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
module Diffy
  class Diff
    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 << self
      attr_writer :default_format
      def default_format
        @default_format ||= :text
      end

      # default options passed to new Diff objects
      attr_writer :default_options
      def default_options
        @default_options ||= ORIGINAL_DEFAULT_OPTIONS.dup
      end

    end
    include Enumerable
    attr_reader :string1, :string2, :options

    # 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.
    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

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

        diff, _stderr, _process_status = Open3.capture3(diff_bin, *(diff_options + @paths))
        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

    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

    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

    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

    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
    private

    @@bin = nil
    def diff_bin
      return @@bin if @@bin

      if @@bin = ENV['DIFFY_DIFF']
        # system() trick from Minitest
        raise "Can't execute diff program '#@@bin'" unless system(@@bin, __FILE__, __FILE__)
        return @@bin
      end

      diffs = ['diff', 'ldiff']
      diffs.first << '.exe' if WINDOWS  # ldiff does not have exe extension
      @@bin = diffs.find { |name| system(name, __FILE__, __FILE__) }

      if @@bin.nil?
        raise "Can't find a diff executable in PATH #{ENV['PATH']}"
      end

      @@bin
    end

    # options pass to diff program
    def diff_options
      Array(options[:context] ? "-U#{options[:context]}" : options[:diff])
    end

  end
end