differ 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +99 -0
- data/VERSION.yml +4 -0
- data/lib/differ.rb +83 -0
- data/lib/differ/change.rb +30 -0
- data/lib/differ/diff.rb +95 -0
- data/lib/differ/format/ascii.rb +27 -0
- data/lib/differ/format/color.rb +27 -0
- data/lib/differ/format/html.rb +27 -0
- data/lib/differ/string.rb +12 -0
- data/spec/differ/change_spec.rb +82 -0
- data/spec/differ/diff_spec.rb +298 -0
- data/spec/differ/format/ascii_spec.rb +18 -0
- data/spec/differ/format/color_spec.rb +18 -0
- data/spec/differ/format/html_spec.rb +18 -0
- data/spec/differ/string_spec.rb +38 -0
- data/spec/differ_spec.rb +512 -0
- data/spec/spec_helper.rb +29 -0
- metadata +72 -0
data/README.rdoc
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
= Differ
|
2
|
+
|
3
|
+
As streams of text swirled before the young man's eyes, his mind swam with
|
4
|
+
thoughts of many things. They would have to wait, however, as he focussed his
|
5
|
+
full concentration on the shifting patterns ahead of him. A glint of light
|
6
|
+
reflecting off a piece of buried code caught his eye and any hope he had was
|
7
|
+
lost. For the very moment he glanced aside, the landscape became Different.
|
8
|
+
The young man gave a small sigh and trudged onward in solemn resignation,
|
9
|
+
fated to wander the desolate codebanks in perpetuity.
|
10
|
+
|
11
|
+
Differ is a flexible, pure-Ruby diff library, suitable for use in both command
|
12
|
+
line scripts and web applications. The flexibility comes from the fact that
|
13
|
+
diffs can be built at completely arbitrary levels of granularity (some common
|
14
|
+
ones are built-in), and can be output in a variety of formats.
|
15
|
+
|
16
|
+
== Installation
|
17
|
+
|
18
|
+
sudo gem install differ
|
19
|
+
|
20
|
+
== Usage
|
21
|
+
|
22
|
+
There are a number of ways to use Differ, depending on your situation and needs.
|
23
|
+
|
24
|
+
@original = "Epic lolcat fail!"
|
25
|
+
@current = "Epic wolfman fail!"
|
26
|
+
|
27
|
+
You can call the Differ module directly.
|
28
|
+
|
29
|
+
require 'differ'
|
30
|
+
|
31
|
+
There are a number of built-in diff methods to choose from...
|
32
|
+
|
33
|
+
@diff = Differ.diff_by_line(@current, @original)
|
34
|
+
# => "{"Epic lolcat fail!" >> "Epic wolfman fail!"}"
|
35
|
+
|
36
|
+
@diff = Differ.diff_by_word(@current, @original)
|
37
|
+
# => "Epic {"lolcat" >> "wolfman"} fail!"
|
38
|
+
|
39
|
+
@diff = Differ.diff_by_char(@current, @original)
|
40
|
+
# => "Epic {+"wo"}l{-"olcat "}f{+"m"}a{+"n fa"}il!"
|
41
|
+
|
42
|
+
... or call #diff directly and supply your own boundary string!
|
43
|
+
|
44
|
+
@diff = Differ.diff(@current, @original) # implicitly by line!
|
45
|
+
# => "{"Epic lolcat fail!" >> "Epic wolfman fail!"}"
|
46
|
+
|
47
|
+
@diff = Differ.diff(@current, @original, 'i')
|
48
|
+
# => "Epi{"c lolcat fa" >> "c wolfman fa"}il"
|
49
|
+
|
50
|
+
If you would like something a little more inline...
|
51
|
+
|
52
|
+
require 'differ/string'
|
53
|
+
|
54
|
+
@diff = @current.diff(@original) # implicitly by line!
|
55
|
+
# => "{"Epic lolcat fail!" >> "Epic wolfman fail!"}"
|
56
|
+
|
57
|
+
... or a lot more inline...
|
58
|
+
|
59
|
+
@diff = (@current - @original) # implicitly by line!
|
60
|
+
# => "{"Epic lolcat fail!" >> "Epic wolfman fail!"}"
|
61
|
+
|
62
|
+
$; = ' '
|
63
|
+
@diff = (@current - @original)
|
64
|
+
# => "Epic {"lolcat" >> "wolfman"} fail!"
|
65
|
+
|
66
|
+
... we've pretty much got you covered.
|
67
|
+
|
68
|
+
=== Output Formatting
|
69
|
+
|
70
|
+
Need a different output format? We've got a few of those too.
|
71
|
+
|
72
|
+
Differ.format = :ascii # <- Default
|
73
|
+
Differ.format = :color
|
74
|
+
Differ.format = :html
|
75
|
+
|
76
|
+
Differ.format = MyCustomFormatModule
|
77
|
+
|
78
|
+
Don't want to change the system-wide default for only a single diff output?
|
79
|
+
Yeah, me either.
|
80
|
+
|
81
|
+
@diff = (@current - @original)
|
82
|
+
@diff.format_as(:color)
|
83
|
+
|
84
|
+
== Copyright
|
85
|
+
|
86
|
+
Copyright (c) 2009 Pieter Vande Bruggen.
|
87
|
+
|
88
|
+
(The GIFT License, v1)
|
89
|
+
|
90
|
+
Permission is hereby granted to use this software and/or its source code for
|
91
|
+
whatever purpose you should choose. Seriously, go nuts. Use it for your personal
|
92
|
+
RSS feed reader, your wildly profitable social network, or your mission to Mars.
|
93
|
+
|
94
|
+
I don't care, it's yours. Change the name on it if you want -- in fact, if you
|
95
|
+
start significantly changing what it does, I'd rather you did! Make it your own
|
96
|
+
little work of art, complete with a stylish flowing signature in the corner. All
|
97
|
+
I really did was give you the canvas. And my blessing.
|
98
|
+
|
99
|
+
Know always right from wrong, and let others see your good works.
|
data/VERSION.yml
ADDED
data/lib/differ.rb
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'differ/change'
|
2
|
+
require 'differ/diff'
|
3
|
+
require 'differ/format/ascii'
|
4
|
+
require 'differ/format/color'
|
5
|
+
require 'differ/format/html'
|
6
|
+
|
7
|
+
module Differ
|
8
|
+
class << self
|
9
|
+
|
10
|
+
def diff(target, source, separator = "\n")
|
11
|
+
old_sep, $; = $;, separator
|
12
|
+
|
13
|
+
target = target.split(separator)
|
14
|
+
source = source.split(separator)
|
15
|
+
|
16
|
+
$; = '' if separator.is_a? Regexp
|
17
|
+
|
18
|
+
@diff = Diff.new
|
19
|
+
advance(target, source) until source.empty? || target.empty?
|
20
|
+
@diff.insert(*target) || @diff.delete(*source)
|
21
|
+
return @diff
|
22
|
+
ensure
|
23
|
+
$; = old_sep
|
24
|
+
end
|
25
|
+
|
26
|
+
def diff_by_char(to, from)
|
27
|
+
diff(to, from, '')
|
28
|
+
end
|
29
|
+
|
30
|
+
def diff_by_word(to, from)
|
31
|
+
diff(to, from, /\b/)
|
32
|
+
end
|
33
|
+
|
34
|
+
def diff_by_line(to, from)
|
35
|
+
diff(to, from, "\n")
|
36
|
+
end
|
37
|
+
|
38
|
+
def format=(f)
|
39
|
+
@format = format_for(f)
|
40
|
+
end
|
41
|
+
|
42
|
+
def format
|
43
|
+
return @format || Format::Ascii
|
44
|
+
end
|
45
|
+
|
46
|
+
def format_for(f)
|
47
|
+
case f
|
48
|
+
when Module then f
|
49
|
+
when :ascii then Format::Ascii
|
50
|
+
when :color then Format::Color
|
51
|
+
when :html then Format::HTML
|
52
|
+
when nil then nil
|
53
|
+
else raise "Unknown format type #{f.inspect}"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
def advance(target, source)
|
59
|
+
del, add = source.shift, target.shift
|
60
|
+
|
61
|
+
prioritize_insert = target.length > source.length
|
62
|
+
insert = target.index(del)
|
63
|
+
delete = source.index(add)
|
64
|
+
|
65
|
+
if del == add
|
66
|
+
@diff.same(add)
|
67
|
+
elsif insert && prioritize_insert
|
68
|
+
change(:insert, target.unshift(add), insert)
|
69
|
+
elsif delete
|
70
|
+
change(:delete, source.unshift(del), delete)
|
71
|
+
elsif insert && !prioritize_insert
|
72
|
+
change(:insert, target.unshift(add), insert)
|
73
|
+
else
|
74
|
+
@diff.insert(add) && @diff.delete(del)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def change(method, array, index)
|
79
|
+
@diff.send(method, *array.slice!(0..index))
|
80
|
+
@diff.same(array.shift)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Differ
|
2
|
+
class Change # :nodoc:
|
3
|
+
attr_accessor :insert, :delete
|
4
|
+
def initialize(options = {})
|
5
|
+
@insert = options[:insert] || ''
|
6
|
+
@delete = options[:delete] || ''
|
7
|
+
end
|
8
|
+
|
9
|
+
def insert?
|
10
|
+
!@insert.empty?
|
11
|
+
end
|
12
|
+
|
13
|
+
def delete?
|
14
|
+
!@delete.empty?
|
15
|
+
end
|
16
|
+
|
17
|
+
def change?
|
18
|
+
!@insert.empty? && !@delete.empty?
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_s
|
22
|
+
Differ.format.format(self)
|
23
|
+
end
|
24
|
+
alias :inspect :to_s
|
25
|
+
|
26
|
+
def ==(other)
|
27
|
+
self.insert == other.insert && self.delete == other.delete
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/differ/diff.rb
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
module Differ
|
2
|
+
class Diff
|
3
|
+
def initialize
|
4
|
+
@raw = []
|
5
|
+
end
|
6
|
+
|
7
|
+
def same(*str)
|
8
|
+
return if str.empty?
|
9
|
+
if @raw.last.is_a? String
|
10
|
+
@raw.last << sep
|
11
|
+
elsif @raw.last.is_a? Change
|
12
|
+
if @raw.last.change?
|
13
|
+
@raw << sep
|
14
|
+
else
|
15
|
+
change = @raw.pop
|
16
|
+
if change.insert? && @raw.last
|
17
|
+
@raw.last << sep if change.insert.sub!(/^#{Regexp.quote(sep)}/, '')
|
18
|
+
end
|
19
|
+
if change.delete? && @raw.last
|
20
|
+
@raw.last << sep if change.delete.sub!(/^#{Regexp.quote(sep)}/, '')
|
21
|
+
end
|
22
|
+
@raw << change
|
23
|
+
|
24
|
+
@raw.last.insert << sep if @raw.last.insert?
|
25
|
+
@raw.last.delete << sep if @raw.last.delete?
|
26
|
+
@raw << ''
|
27
|
+
end
|
28
|
+
else
|
29
|
+
@raw << ''
|
30
|
+
end
|
31
|
+
@raw.last << str.join(sep)
|
32
|
+
end
|
33
|
+
|
34
|
+
def delete(*str)
|
35
|
+
return if str.empty?
|
36
|
+
if @raw.last.is_a? Change
|
37
|
+
change = @raw.pop
|
38
|
+
if change.insert? && @raw.last
|
39
|
+
@raw.last << sep if change.insert.sub!(/^#{Regexp.quote(sep)}/, '')
|
40
|
+
end
|
41
|
+
change.delete << sep if change.delete?
|
42
|
+
else
|
43
|
+
change = Change.new(:delete => @raw.empty? ? '' : sep)
|
44
|
+
end
|
45
|
+
|
46
|
+
@raw << change
|
47
|
+
@raw.last.delete << str.join(sep)
|
48
|
+
end
|
49
|
+
|
50
|
+
def insert(*str)
|
51
|
+
return if str.empty?
|
52
|
+
if @raw.last.is_a? Change
|
53
|
+
change = @raw.pop
|
54
|
+
if change.delete? && @raw.last
|
55
|
+
@raw.last << sep if change.delete.sub!(/^#{Regexp.quote(sep)}/, '')
|
56
|
+
end
|
57
|
+
change.insert << sep if change.insert?
|
58
|
+
else
|
59
|
+
change = Change.new(:insert => @raw.empty? ? '' : sep)
|
60
|
+
end
|
61
|
+
|
62
|
+
@raw << change
|
63
|
+
@raw.last.insert << str.join(sep)
|
64
|
+
end
|
65
|
+
|
66
|
+
def ==(other)
|
67
|
+
@raw == other.raw_array
|
68
|
+
end
|
69
|
+
|
70
|
+
def to_s
|
71
|
+
@raw.to_s
|
72
|
+
end
|
73
|
+
|
74
|
+
def format_as(f)
|
75
|
+
f = Differ.format_for(f)
|
76
|
+
@raw.inject('') do |sum, part|
|
77
|
+
part = case part
|
78
|
+
when String then part
|
79
|
+
when Change then f.format(part)
|
80
|
+
end
|
81
|
+
sum << part
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
protected
|
86
|
+
def raw_array
|
87
|
+
@raw
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
def sep
|
92
|
+
"#{$;}"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Differ
|
2
|
+
module Format
|
3
|
+
module Ascii
|
4
|
+
class << self
|
5
|
+
def format(change)
|
6
|
+
(change.change? && as_change(change)) ||
|
7
|
+
(change.delete? && as_delete(change)) ||
|
8
|
+
(change.insert? && as_insert(change)) ||
|
9
|
+
''
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
def as_insert(change)
|
14
|
+
"{+#{change.insert.inspect}}"
|
15
|
+
end
|
16
|
+
|
17
|
+
def as_delete(change)
|
18
|
+
"{-#{change.delete.inspect}}"
|
19
|
+
end
|
20
|
+
|
21
|
+
def as_change(change)
|
22
|
+
"{#{change.delete.inspect} >> #{change.insert.inspect}}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Differ
|
2
|
+
module Format
|
3
|
+
module Color
|
4
|
+
class << self
|
5
|
+
def format(change)
|
6
|
+
(change.change? && as_change(change)) ||
|
7
|
+
(change.delete? && as_delete(change)) ||
|
8
|
+
(change.insert? && as_insert(change)) ||
|
9
|
+
''
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
def as_insert(change)
|
14
|
+
"\033[32m#{change.insert}\033[0m"
|
15
|
+
end
|
16
|
+
|
17
|
+
def as_delete(change)
|
18
|
+
"\033[31m#{change.delete}\033[0m"
|
19
|
+
end
|
20
|
+
|
21
|
+
def as_change(change)
|
22
|
+
as_delete(change) << as_insert(change)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Differ
|
2
|
+
module Format
|
3
|
+
module HTML
|
4
|
+
class << self
|
5
|
+
def format(change)
|
6
|
+
(change.change? && as_change(change)) ||
|
7
|
+
(change.delete? && as_delete(change)) ||
|
8
|
+
(change.insert? && as_insert(change)) ||
|
9
|
+
''
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
def as_insert(change)
|
14
|
+
%Q{<ins class="differ">#{change.insert}</ins>}
|
15
|
+
end
|
16
|
+
|
17
|
+
def as_delete(change)
|
18
|
+
%Q{<del class="differ">#{change.delete}</del>}
|
19
|
+
end
|
20
|
+
|
21
|
+
def as_change(change)
|
22
|
+
as_delete(change) << as_insert(change)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Differ::Change do
|
4
|
+
before(:each) do
|
5
|
+
@format = Module.new { def self.format(c); end }
|
6
|
+
Differ.format = @format
|
7
|
+
end
|
8
|
+
|
9
|
+
describe '(empty)' do
|
10
|
+
before(:each) do
|
11
|
+
@change = Differ::Change.new()
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'should have a default insert' do
|
15
|
+
@change.insert.should == ''
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'should have a default delete' do
|
19
|
+
@change.delete.should == ''
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'should stringify to ""' do
|
23
|
+
@format.should_receive(:format).once.and_return('')
|
24
|
+
@change.to_s.should == ''
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe '(insert only)' do
|
29
|
+
before(:each) do
|
30
|
+
@change = Differ::Change.new(:insert => 'foo')
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'should populate the :insert parameter' do
|
34
|
+
@change.insert.should == 'foo'
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'should have a default delete' do
|
38
|
+
@change.delete.should == ''
|
39
|
+
end
|
40
|
+
|
41
|
+
it { (@change).should be_an_insert }
|
42
|
+
end
|
43
|
+
|
44
|
+
describe '(delete only)' do
|
45
|
+
before(:each) do
|
46
|
+
@change = Differ::Change.new(:delete => 'bar')
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'should have a default :insert' do
|
50
|
+
@change.insert.should == ''
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'should populate the :delete parameter' do
|
54
|
+
@change.delete.should == 'bar'
|
55
|
+
end
|
56
|
+
|
57
|
+
it { (@change).should be_a_delete }
|
58
|
+
end
|
59
|
+
|
60
|
+
describe '(both insert and delete)' do
|
61
|
+
before(:each) do
|
62
|
+
@change = Differ::Change.new(:insert => 'foo', :delete => 'bar')
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'should populate the :insert parameter' do
|
66
|
+
@change.insert.should == 'foo'
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'should populate the :delete parameter' do
|
70
|
+
@change.delete.should == 'bar'
|
71
|
+
end
|
72
|
+
|
73
|
+
it { (@change).should be_an_insert }
|
74
|
+
it { (@change).should be_a_delete }
|
75
|
+
it { (@change).should be_a_change }
|
76
|
+
end
|
77
|
+
|
78
|
+
it "should stringify via the current format's #format method" do
|
79
|
+
@format.should_receive(:format).once
|
80
|
+
Differ::Change.new.to_s
|
81
|
+
end
|
82
|
+
end
|