ydiffy 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +5 -0
- data/.rspec +2 -0
- data/.travis.yml +5 -0
- data/CHANGELOG +40 -0
- data/CONTRIBUTORS +13 -0
- data/Gemfile +7 -0
- data/LICENSE +19 -0
- data/README.md +334 -0
- data/Rakefile +11 -0
- data/lib/diffy.rb +13 -0
- data/lib/diffy/css.rb +34 -0
- data/lib/diffy/diff.rb +171 -0
- data/lib/diffy/format.rb +37 -0
- data/lib/diffy/html_formatter.rb +135 -0
- data/lib/diffy/split_diff.rb +49 -0
- data/lib/diffy/version.rb +3 -0
- data/spec/demo_app.rb +46 -0
- data/spec/diffy_spec.rb +676 -0
- data/ydiffy.gemspec +23 -0
- metadata +93 -0
data/lib/diffy.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'tempfile'
|
2
|
+
require 'erb'
|
3
|
+
require 'rbconfig'
|
4
|
+
|
5
|
+
module Diffy
|
6
|
+
WINDOWS = (RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/)
|
7
|
+
end
|
8
|
+
require 'open3' unless Diffy::WINDOWS
|
9
|
+
require File.join(File.dirname(__FILE__), 'diffy', 'format')
|
10
|
+
require File.join(File.dirname(__FILE__), 'diffy', 'html_formatter')
|
11
|
+
require File.join(File.dirname(__FILE__), 'diffy', 'diff')
|
12
|
+
require File.join(File.dirname(__FILE__), 'diffy', 'split_diff')
|
13
|
+
require File.join(File.dirname(__FILE__), 'diffy', 'css')
|
data/lib/diffy/css.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
module Diffy
|
2
|
+
CSS = <<-STYLE
|
3
|
+
.diff{overflow:auto;}
|
4
|
+
.diff ul{background:#fff;overflow:auto;font-size:13px;list-style:none;margin:0;padding:0;display:table;width:100%;}
|
5
|
+
.diff del, .diff ins{display:block;text-decoration:none;}
|
6
|
+
.diff li{padding:0; display:table-row;margin: 0;height:1em;}
|
7
|
+
.diff li.ins{background:#dfd; color:#080}
|
8
|
+
.diff li.del{background:#fee; color:#b00}
|
9
|
+
.diff li:hover{background:#ffc}
|
10
|
+
/* try 'whitespace:pre;' if you don't want lines to wrap */
|
11
|
+
.diff del, .diff ins, .diff span{white-space:pre-wrap;font-family:courier;}
|
12
|
+
.diff del strong{font-weight:normal;background:#fcc;}
|
13
|
+
.diff ins strong{font-weight:normal;background:#9f9;}
|
14
|
+
.diff li.diff-comment { display: none; }
|
15
|
+
.diff li.diff-block-info { background: none repeat scroll 0 0 gray; }
|
16
|
+
STYLE
|
17
|
+
|
18
|
+
CSS_COLORBLIND_1 = <<-STYLE
|
19
|
+
.diff{overflow:auto;}
|
20
|
+
.diff ul{background:#fff;overflow:auto;font-size:13px;list-style:none;margin:0;padding:0;display:table;width:100%;}
|
21
|
+
.diff del, .diff ins{display:block;text-decoration:none;}
|
22
|
+
.diff li{padding:0; display:table-row;margin: 0;height:1em;}
|
23
|
+
.diff li.ins{background:#ddf; color:#008}
|
24
|
+
.diff li.del{background:#fee; color:#b00}
|
25
|
+
.diff li:hover{background:#ffc}
|
26
|
+
/* try 'whitespace:pre;' if you don't want lines to wrap */
|
27
|
+
.diff del, .diff ins, .diff span{white-space:pre-wrap;font-family:courier;}
|
28
|
+
.diff del strong{font-weight:normal;background:#fcc;}
|
29
|
+
.diff ins strong{font-weight:normal;background:#99f;}
|
30
|
+
.diff li.diff-comment { display: none; }
|
31
|
+
.diff li.diff-block-info { background: none repeat scroll 0 0 gray; }
|
32
|
+
STYLE
|
33
|
+
|
34
|
+
end
|
data/lib/diffy/diff.rb
ADDED
@@ -0,0 +1,171 @@
|
|
1
|
+
module Diffy
|
2
|
+
class Diff
|
3
|
+
ORIGINAL_DEFAULT_OPTIONS = {
|
4
|
+
:diff => '-U 10000',
|
5
|
+
:source => 'strings',
|
6
|
+
:include_diff_info => false,
|
7
|
+
:include_plus_and_minus_in_html => false,
|
8
|
+
:context => nil,
|
9
|
+
:allow_empty_diff => true,
|
10
|
+
}
|
11
|
+
|
12
|
+
class << self
|
13
|
+
attr_writer :default_format
|
14
|
+
def default_format
|
15
|
+
@default_format ||= :text
|
16
|
+
end
|
17
|
+
|
18
|
+
# default options passed to new Diff objects
|
19
|
+
attr_writer :default_options
|
20
|
+
def default_options
|
21
|
+
@default_options ||= ORIGINAL_DEFAULT_OPTIONS.dup
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
include Enumerable
|
26
|
+
attr_reader :string1, :string2, :options
|
27
|
+
|
28
|
+
# supported options
|
29
|
+
# +:diff+:: A cli options string passed to diff
|
30
|
+
# +:source+:: Either _strings_ or _files_. Determines whether string1
|
31
|
+
# and string2 should be interpreted as strings or file paths.
|
32
|
+
# +:include_diff_info+:: Include diff header info
|
33
|
+
# +:include_plus_and_minus_in_html+:: Show the +, -, ' ' at the
|
34
|
+
# beginning of lines in html output.
|
35
|
+
def initialize(string1, string2, options = {})
|
36
|
+
@options = self.class.default_options.merge(options)
|
37
|
+
if ! ['strings', 'files'].include?(@options[:source])
|
38
|
+
raise ArgumentError, "Invalid :source option #{@options[:source].inspect}. Supported options are 'strings' and 'files'."
|
39
|
+
end
|
40
|
+
@string1, @string2 = string1, string2
|
41
|
+
end
|
42
|
+
|
43
|
+
def diff
|
44
|
+
@diff ||= begin
|
45
|
+
paths = case options[:source]
|
46
|
+
when 'strings'
|
47
|
+
[tempfile(string1), tempfile(string2)]
|
48
|
+
when 'files'
|
49
|
+
[string1, string2]
|
50
|
+
end
|
51
|
+
|
52
|
+
if WINDOWS
|
53
|
+
# don't use open3 on windows
|
54
|
+
cmd = sprintf '"%s" %s %s', diff_bin, diff_options.join(' '), paths.map { |s| %("#{s}") }.join(' ')
|
55
|
+
diff = `#{cmd}`
|
56
|
+
else
|
57
|
+
diff = Open3.popen3(diff_bin, *(diff_options + paths)) { |i, o, e| o.read }
|
58
|
+
end
|
59
|
+
diff.force_encoding('ASCII-8BIT') if diff.respond_to?(:valid_encoding?) && !diff.valid_encoding?
|
60
|
+
if diff =~ /\A\s*\Z/ && !options[:allow_empty_diff]
|
61
|
+
diff = case options[:source]
|
62
|
+
when 'strings' then string1
|
63
|
+
when 'files' then File.read(string1)
|
64
|
+
end.gsub(/^/, " ")
|
65
|
+
end
|
66
|
+
diff
|
67
|
+
end
|
68
|
+
ensure
|
69
|
+
# unlink the tempfiles explicitly now that the diff is generated
|
70
|
+
if defined? @tempfiles # to avoid Ruby warnings about undefined ins var.
|
71
|
+
Array(@tempfiles).each do |t|
|
72
|
+
begin
|
73
|
+
# check that the path is not nil and file still exists.
|
74
|
+
# REE seems to be very agressive with when it magically removes
|
75
|
+
# tempfiles
|
76
|
+
t.unlink if t.path && File.exist?(t.path)
|
77
|
+
rescue => e
|
78
|
+
warn "#{e.class}: #{e}"
|
79
|
+
warn e.backtrace.join("\n")
|
80
|
+
end
|
81
|
+
end
|
82
|
+
@tempfiles = []
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def each
|
87
|
+
lines = case @options[:include_diff_info]
|
88
|
+
when false then diff.split("\n").reject{|x| x =~ /^(---|\+\+\+|@@|\\\\)/ }.map {|line| line + "\n" }
|
89
|
+
when true then diff.split("\n").map {|line| line + "\n" }
|
90
|
+
end
|
91
|
+
if block_given?
|
92
|
+
lines.each{|line| yield line}
|
93
|
+
else
|
94
|
+
lines.to_enum
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def each_chunk
|
99
|
+
old_state = nil
|
100
|
+
chunks = inject([]) do |cc, line|
|
101
|
+
state = line.each_char.first
|
102
|
+
if state == old_state
|
103
|
+
cc.last << line
|
104
|
+
else
|
105
|
+
cc.push line.dup
|
106
|
+
end
|
107
|
+
old_state = state
|
108
|
+
cc
|
109
|
+
end
|
110
|
+
|
111
|
+
if block_given?
|
112
|
+
chunks.each{|chunk| yield chunk }
|
113
|
+
else
|
114
|
+
chunks.to_enum
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def tempfile(string)
|
119
|
+
t = Tempfile.new('diffy')
|
120
|
+
# ensure tempfiles aren't unlinked when GC runs by maintaining a
|
121
|
+
# reference to them.
|
122
|
+
@tempfiles ||=[]
|
123
|
+
@tempfiles.push(t)
|
124
|
+
t.print(string)
|
125
|
+
t.flush
|
126
|
+
t.close
|
127
|
+
t.path
|
128
|
+
end
|
129
|
+
|
130
|
+
def to_s(format = nil)
|
131
|
+
format ||= self.class.default_format
|
132
|
+
formats = Format.instance_methods(false).map{|x| x.to_s}
|
133
|
+
if formats.include? format.to_s
|
134
|
+
enum = self
|
135
|
+
enum.extend Format
|
136
|
+
enum.send format
|
137
|
+
else
|
138
|
+
raise ArgumentError,
|
139
|
+
"Format #{format.inspect} not found in #{formats.inspect}"
|
140
|
+
end
|
141
|
+
end
|
142
|
+
private
|
143
|
+
|
144
|
+
@@bin = nil
|
145
|
+
def diff_bin
|
146
|
+
return @@bin if @@bin
|
147
|
+
|
148
|
+
if @@bin = ENV['DIFFY_DIFF']
|
149
|
+
# system() trick from Minitest
|
150
|
+
raise "Can't execute diff program '#@@bin'" unless system(@@bin, __FILE__, __FILE__)
|
151
|
+
return @@bin
|
152
|
+
end
|
153
|
+
|
154
|
+
diffs = ['diff', 'ldiff']
|
155
|
+
diffs.first << '.exe' if WINDOWS # ldiff does not have exe extension
|
156
|
+
@@bin = diffs.find { |name| system(name, __FILE__, __FILE__) }
|
157
|
+
|
158
|
+
if @@bin.nil?
|
159
|
+
raise "Can't find a diff executable in PATH #{ENV['PATH']}"
|
160
|
+
end
|
161
|
+
|
162
|
+
@@bin
|
163
|
+
end
|
164
|
+
|
165
|
+
# options pass to diff program
|
166
|
+
def diff_options
|
167
|
+
Array(options[:context] ? "-U #{options[:context]}" : options[:diff])
|
168
|
+
end
|
169
|
+
|
170
|
+
end
|
171
|
+
end
|
data/lib/diffy/format.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
module Diffy
|
2
|
+
module Format
|
3
|
+
# ANSI color output suitable for terminal output
|
4
|
+
def color
|
5
|
+
map do |line|
|
6
|
+
case line
|
7
|
+
when /^(---|\+\+\+|\\\\)/
|
8
|
+
"\033[90m#{line.chomp}\033[0m"
|
9
|
+
when /^\+/
|
10
|
+
"\033[32m#{line.chomp}\033[0m"
|
11
|
+
when /^-/
|
12
|
+
"\033[31m#{line.chomp}\033[0m"
|
13
|
+
when /^@@/
|
14
|
+
"\033[36m#{line.chomp}\033[0m"
|
15
|
+
else
|
16
|
+
line.chomp
|
17
|
+
end
|
18
|
+
end.join("\n") + "\n"
|
19
|
+
end
|
20
|
+
|
21
|
+
# Basic text output
|
22
|
+
def text
|
23
|
+
to_a.join
|
24
|
+
end
|
25
|
+
|
26
|
+
# Basic html output which does not attempt to highlight the changes
|
27
|
+
# between lines, and is more performant.
|
28
|
+
def html_simple
|
29
|
+
HtmlFormatter.new(self, options).to_s
|
30
|
+
end
|
31
|
+
|
32
|
+
# Html output which does inline highlighting of changes between two lines.
|
33
|
+
def html
|
34
|
+
HtmlFormatter.new(self, options.merge(:highlight_words => true)).to_s
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
module Diffy
|
2
|
+
class HtmlFormatter
|
3
|
+
|
4
|
+
def initialize(diff, options = {})
|
5
|
+
@diff = diff
|
6
|
+
@options = options
|
7
|
+
end
|
8
|
+
|
9
|
+
def to_s
|
10
|
+
if @options[:highlight_words]
|
11
|
+
wrap_lines(highlighted_words)
|
12
|
+
else
|
13
|
+
wrap_lines(@diff.map{|line| wrap_line(ERB::Util.h(line))})
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
def wrap_line(line)
|
19
|
+
cleaned = clean_line(line)
|
20
|
+
case line
|
21
|
+
when /^(---|\+\+\+|\\\\)/
|
22
|
+
' <li class="diff-comment"><span>' + line.chomp + '</span></li>'
|
23
|
+
when /^\+/
|
24
|
+
' <li class="ins"><ins>' + cleaned + '</ins></li>'
|
25
|
+
when /^-/
|
26
|
+
' <li class="del"><del>' + cleaned + '</del></li>'
|
27
|
+
when /^ /
|
28
|
+
' <li class="unchanged"><span>' + cleaned + '</span></li>'
|
29
|
+
when /^@@/
|
30
|
+
' <li class="diff-block-info"><span>' + line.chomp + '</span></li>'
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# remove +/- or wrap in html
|
35
|
+
def clean_line(line)
|
36
|
+
if @options[:include_plus_and_minus_in_html]
|
37
|
+
line.sub(/^(.)/, '<span class="symbol">\1</span>')
|
38
|
+
else
|
39
|
+
line.sub(/^./, '')
|
40
|
+
end.chomp
|
41
|
+
end
|
42
|
+
|
43
|
+
def wrap_lines(lines)
|
44
|
+
if lines.empty?
|
45
|
+
%'<div class="diff"></div>'
|
46
|
+
else
|
47
|
+
%'<div class="diff">\n <ul>\n#{lines.join("\n")}\n </ul>\n</div>\n'
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def highlighted_words
|
52
|
+
chunks = @diff.each_chunk.
|
53
|
+
reject{|c| c == ''"\n"}
|
54
|
+
|
55
|
+
processed = []
|
56
|
+
lines = chunks.each_with_index.map do |chunk1, index|
|
57
|
+
next if processed.include? index
|
58
|
+
processed << index
|
59
|
+
chunk1 = chunk1
|
60
|
+
chunk2 = chunks[index + 1]
|
61
|
+
if not chunk2
|
62
|
+
next ERB::Util.h(chunk1)
|
63
|
+
end
|
64
|
+
|
65
|
+
dir1 = chunk1.each_char.first
|
66
|
+
dir2 = chunk2.each_char.first
|
67
|
+
case [dir1, dir2]
|
68
|
+
when ['-', '+']
|
69
|
+
if chunk1.each_char.take(3).join("") =~ /^(---|\+\+\+|\\\\)/ and
|
70
|
+
chunk2.each_char.take(3).join("") =~ /^(---|\+\+\+|\\\\)/
|
71
|
+
ERB::Util.h(chunk1)
|
72
|
+
else
|
73
|
+
line_diff = Diffy::Diff.new(
|
74
|
+
split_characters(chunk1),
|
75
|
+
split_characters(chunk2),
|
76
|
+
Diffy::Diff::ORIGINAL_DEFAULT_OPTIONS
|
77
|
+
)
|
78
|
+
hi1 = reconstruct_characters(line_diff, '-')
|
79
|
+
hi2 = reconstruct_characters(line_diff, '+')
|
80
|
+
processed << (index + 1)
|
81
|
+
[hi1, hi2]
|
82
|
+
end
|
83
|
+
else
|
84
|
+
ERB::Util.h(chunk1)
|
85
|
+
end
|
86
|
+
end.flatten
|
87
|
+
lines.map{|line| line.each_line.map(&:chomp).to_a if line }.flatten.compact.
|
88
|
+
map{|line|wrap_line(line) }.compact
|
89
|
+
end
|
90
|
+
|
91
|
+
def split_characters(chunk)
|
92
|
+
chunk.gsub(/^./, '').each_line.map do |line|
|
93
|
+
chars = line.sub(/([\r\n]$)/, '').split('')
|
94
|
+
# add escaped newlines
|
95
|
+
chars << '\n'
|
96
|
+
chars.map{|chr| ERB::Util.h(chr) }
|
97
|
+
end.flatten.join("\n") + "\n"
|
98
|
+
end
|
99
|
+
|
100
|
+
def reconstruct_characters(line_diff, type)
|
101
|
+
enum = line_diff.each_chunk.to_a
|
102
|
+
enum.each_with_index.map do |l, i|
|
103
|
+
re = /(^|\\n)#{Regexp.escape(type)}/
|
104
|
+
case l
|
105
|
+
when re
|
106
|
+
highlight(l)
|
107
|
+
when /^ /
|
108
|
+
if i > 1 and enum[i+1] and l.each_line.to_a.size < 4
|
109
|
+
highlight(l)
|
110
|
+
else
|
111
|
+
l.gsub(/^./, '').gsub("\n", '').
|
112
|
+
gsub('\r', "\r").gsub('\n', "\n")
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end.join('').split("\n").map do |l|
|
116
|
+
type + l.gsub('</strong><strong>' , '')
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def highlight(lines)
|
121
|
+
"<strong>" +
|
122
|
+
lines.
|
123
|
+
# strip diff tokens (e.g. +,-,etc.)
|
124
|
+
gsub(/(^|\\n)./, '').
|
125
|
+
# mark line boundaries from higher level line diff
|
126
|
+
# html is all escaped so using brackets should make this safe.
|
127
|
+
gsub('\n', '<LINE_BOUNDARY>').
|
128
|
+
# join characters back by stripping out newlines
|
129
|
+
gsub("\n", '').
|
130
|
+
# close and reopen strong tags. we don't want inline elements
|
131
|
+
# spanning block elements which get added later.
|
132
|
+
gsub('<LINE_BOUNDARY>',"</strong>\n<strong>") + "</strong>"
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Diffy
|
2
|
+
class SplitDiff
|
3
|
+
def initialize(left, right, options = {})
|
4
|
+
@format = options[:format] || Diffy::Diff.default_format
|
5
|
+
|
6
|
+
formats = Format.instance_methods(false).map { |x| x.to_s }
|
7
|
+
unless formats.include?(@format.to_s)
|
8
|
+
fail ArgumentError, "Format #{format.inspect} is not a valid format"
|
9
|
+
end
|
10
|
+
|
11
|
+
@diff = Diffy::Diff.new(left, right, options).to_s(@format)
|
12
|
+
@left_diff, @right_diff = split
|
13
|
+
end
|
14
|
+
|
15
|
+
%w(left right).each do |direction|
|
16
|
+
define_method direction do
|
17
|
+
instance_variable_get("@#{direction}_diff")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def split
|
24
|
+
[split_left, split_right]
|
25
|
+
end
|
26
|
+
|
27
|
+
def split_left
|
28
|
+
case @format
|
29
|
+
when :color
|
30
|
+
@diff.gsub(/\033\[32m\+(.*)\033\[0m\n/, '')
|
31
|
+
when :html, :html_simple
|
32
|
+
@diff.gsub(%r{\s+<li class="ins"><ins>(.*)</ins></li>}, '')
|
33
|
+
when :text
|
34
|
+
@diff.gsub(/^\+(.*)\n/, '')
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def split_right
|
39
|
+
case @format
|
40
|
+
when :color
|
41
|
+
@diff.gsub(/\033\[31m\-(.*)\033\[0m\n/, '')
|
42
|
+
when :html, :html_simple
|
43
|
+
@diff.gsub(%r{\s+<li class="del"><del>(.*)</del></li>}, '')
|
44
|
+
when :text
|
45
|
+
@diff.gsub(/^-(.*)\n/, '')
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|