unified 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +33 -0
- data/Rakefile +1 -0
- data/lib/unified.rb +8 -0
- data/lib/unified/chunk.rb +88 -0
- data/lib/unified/diff.rb +74 -0
- data/lib/unified/line.rb +21 -0
- data/lib/unified/parser.rb +53 -0
- data/lib/unified/transformer.rb +25 -0
- data/lib/unified/version.rb +3 -0
- data/spec/chunk_spec.rb +134 -0
- data/spec/diff_spec.rb +203 -0
- data/spec/examples/invalid/no_chunk_header.diff +10 -0
- data/spec/examples/invalid/no_content.diff +0 -0
- data/spec/examples/invalid/no_modifications.diff +11 -0
- data/spec/examples/invalid/no_modified_file_header.diff +6 -0
- data/spec/examples/invalid/no_original_file_header.diff +6 -0
- data/spec/examples/valid/.DS_Store +0 -0
- data/spec/examples/valid/commit_revision.diff +5 -0
- data/spec/examples/valid/modified_filename.diff +7 -0
- data/spec/examples/valid/more_additions.diff +7 -0
- data/spec/examples/valid/more_deletions.diff +10 -0
- data/spec/examples/valid/multi_chunk.diff +19 -0
- data/spec/examples/valid/new_file.diff +8 -0
- data/spec/examples/valid/no_newline.diff +29 -0
- data/spec/examples/valid/null_git.diff +8 -0
- data/spec/examples/valid/removed_file.diff +8 -0
- data/spec/examples/valid/simple.diff +11 -0
- data/spec/examples/valid/simple_git.diff +29 -0
- data/spec/line_spec.rb +45 -0
- data/spec/parser_spec.rb +344 -0
- data/spec/spec_helper.rb +21 -0
- data/spec/support/matchers/parse_matchers.rb +53 -0
- data/spec/transformer_spec.rb +82 -0
- data/unified.gemspec +26 -0
- metadata +173 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Matthew Williams
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# Unified
|
2
|
+
|
3
|
+
Unified is a gem for parsing unified diff files into usable diff files.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'unified'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install unified
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
To parse a diff file:
|
22
|
+
|
23
|
+
Unified::Diff.parse!(diff_content)
|
24
|
+
|
25
|
+
Where diff_content is a valid unified diff string.
|
26
|
+
|
27
|
+
## Contributing
|
28
|
+
|
29
|
+
1. Fork it
|
30
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
31
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
32
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
33
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/lib/unified.rb
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
module Unified
|
2
|
+
class Chunk
|
3
|
+
|
4
|
+
attr_reader :original_line_number
|
5
|
+
attr_reader :modified_line_number
|
6
|
+
attr_reader :lines
|
7
|
+
attr_reader :section_header
|
8
|
+
|
9
|
+
def initialize(attrs = {})
|
10
|
+
@original_line_number = attrs[:original]
|
11
|
+
@modified_line_number = attrs[:modified]
|
12
|
+
@section_header = attrs[:section_header]
|
13
|
+
@lines = attrs[:lines].map {|s| Unified::Line.new(s) }
|
14
|
+
end
|
15
|
+
|
16
|
+
def total_number_of_lines
|
17
|
+
@number_of_lines ||= @lines.size
|
18
|
+
end
|
19
|
+
|
20
|
+
def number_of_deleted_lines
|
21
|
+
@number_of_added_lines ||= number_of_lines_by_type(:deletion?)
|
22
|
+
end
|
23
|
+
|
24
|
+
def number_of_added_lines
|
25
|
+
@number_of_deleted_lines ||= number_of_lines_by_type(:addition?)
|
26
|
+
end
|
27
|
+
|
28
|
+
def number_of_unchanged_lines
|
29
|
+
@number_of_unchanged_lines ||= number_of_lines_by_type(:unchanged?)
|
30
|
+
end
|
31
|
+
|
32
|
+
def number_of_original_lines
|
33
|
+
@number_of_original_lines ||= number_of_unchanged_lines + number_of_deleted_lines
|
34
|
+
end
|
35
|
+
|
36
|
+
def number_of_modified_lines
|
37
|
+
@number_of_modified_lines ||= number_of_unchanged_lines + number_of_added_lines
|
38
|
+
end
|
39
|
+
|
40
|
+
def header
|
41
|
+
parts = "@@"
|
42
|
+
parts << " -#{@original_line_number}"
|
43
|
+
parts << ",#{number_of_original_lines}" unless number_of_original_lines == 1
|
44
|
+
parts << " +#{@modified_line_number}"
|
45
|
+
parts << ",#{number_of_modified_lines}" unless number_of_modified_lines == 1
|
46
|
+
parts << " @@"
|
47
|
+
parts << " #{@section_header}" unless @section_header.nil?
|
48
|
+
parts
|
49
|
+
end
|
50
|
+
|
51
|
+
def to_s
|
52
|
+
header + "\n" + @lines.join("\n")
|
53
|
+
end
|
54
|
+
|
55
|
+
# Iterator for lines passing |line, original_line_number, modified_line_number| as block arguments
|
56
|
+
def each_line
|
57
|
+
original_line_number = @original_line_number
|
58
|
+
modified_line_number = @modified_line_number
|
59
|
+
|
60
|
+
@lines.each do |line|
|
61
|
+
if line.addition?
|
62
|
+
yield line, nil, modified_line_number
|
63
|
+
modified_line_number += 1
|
64
|
+
elsif line.deletion?
|
65
|
+
yield line, original_line_number, nil
|
66
|
+
original_line_number += 1
|
67
|
+
else
|
68
|
+
yield line, original_line_number, modified_line_number
|
69
|
+
original_line_number += 1
|
70
|
+
modified_line_number += 1
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def ==(another)
|
76
|
+
@modified_line_number == another.modified_line_number and
|
77
|
+
@original_line_number == another.original_line_number and
|
78
|
+
@section_header == another.section_header and
|
79
|
+
@lines = another.lines
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def number_of_lines_by_type(type)
|
85
|
+
@lines.inject(0) {|total, line| line.send(type) ? total + 1 : total}
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
data/lib/unified/diff.rb
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
module Unified
|
2
|
+
class ParseError < StandardError; end
|
3
|
+
class Diff
|
4
|
+
|
5
|
+
attr_reader :original_filename
|
6
|
+
attr_reader :modified_filename
|
7
|
+
attr_reader :original_revision
|
8
|
+
attr_reader :modified_revision
|
9
|
+
attr_reader :chunks
|
10
|
+
|
11
|
+
def initialize(attrs={})
|
12
|
+
@original_filename = attrs[:original_filename]
|
13
|
+
@modified_filename = attrs[:modified_filename]
|
14
|
+
@original_revision = attrs[:original_revision]
|
15
|
+
@modified_revision = attrs[:modified_revision]
|
16
|
+
@chunks = attrs[:chunks]
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.parse!(content)
|
20
|
+
begin
|
21
|
+
Unified::Transformer.new.apply(Unified::Parser.new.parse(content))
|
22
|
+
rescue StandardError => e
|
23
|
+
raise Unified::ParseError, e.message
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def total_number_of_lines
|
28
|
+
@total_number_of_lines ||= number_of_added_lines + number_of_deleted_lines + number_of_unchanged_lines
|
29
|
+
end
|
30
|
+
|
31
|
+
def number_of_added_lines
|
32
|
+
@number_of_added_lines ||= @chunks.inject(0) {|total, chunk| total + chunk.number_of_added_lines}
|
33
|
+
end
|
34
|
+
|
35
|
+
def number_of_deleted_lines
|
36
|
+
@number_of_deleted_lines ||= @chunks.inject(0) {|total, chunk| total + chunk.number_of_deleted_lines}
|
37
|
+
end
|
38
|
+
|
39
|
+
def number_of_unchanged_lines
|
40
|
+
@number_of_unchanged_lines ||= @chunks.inject(0) {|total, chunk| total + chunk.number_of_unchanged_lines}
|
41
|
+
end
|
42
|
+
|
43
|
+
def number_of_modified_lines
|
44
|
+
@number_of_modified_lines ||= number_of_deleted_lines + number_of_added_lines
|
45
|
+
end
|
46
|
+
|
47
|
+
def proportion_of_deleted_lines
|
48
|
+
@proportion_of_deleted_lines ||= ((100.0 * number_of_deleted_lines / number_of_modified_lines)).round
|
49
|
+
end
|
50
|
+
|
51
|
+
def proportion_of_added_lines
|
52
|
+
@proportion_of_added_lines ||= 100 - proportion_of_deleted_lines
|
53
|
+
end
|
54
|
+
|
55
|
+
def header
|
56
|
+
"--- #{@original_filename}\t#{@original_revision}\n+++ #{modified_filename}\t#{@modified_revision}"
|
57
|
+
end
|
58
|
+
|
59
|
+
def to_s
|
60
|
+
str = [header]
|
61
|
+
@chunks.each do |chunk|
|
62
|
+
str << chunk.to_s
|
63
|
+
end
|
64
|
+
str.join("\n")
|
65
|
+
end
|
66
|
+
|
67
|
+
def each_chunk
|
68
|
+
@chunks.each do |chunk|
|
69
|
+
yield chunk
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
end
|
data/lib/unified/line.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
module Unified
|
2
|
+
class Line < String
|
3
|
+
|
4
|
+
def addition?
|
5
|
+
@addition ||= first_character == "+"
|
6
|
+
end
|
7
|
+
|
8
|
+
def deletion?
|
9
|
+
@deletion ||= first_character == "-"
|
10
|
+
end
|
11
|
+
|
12
|
+
def unchanged?
|
13
|
+
@unchanged ||= first_character == " "
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
def first_character
|
18
|
+
@first_character ||= self[0]
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'parslet'
|
2
|
+
|
3
|
+
module Unified
|
4
|
+
class Parser < Parslet::Parser
|
5
|
+
rule(:minus) { str('-') }
|
6
|
+
rule(:plus) { str('+') }
|
7
|
+
rule(:space) { str(' ') }
|
8
|
+
rule(:space?) { str(' ').maybe }
|
9
|
+
rule(:digit) { match['0-9'] }
|
10
|
+
rule(:digits) { digit.repeat(1) }
|
11
|
+
rule(:tab) { match['\\t'] }
|
12
|
+
rule(:newline) { match['\\r'].maybe >> match['\\n'] }
|
13
|
+
rule(:text_until_tab) { (tab.absent? >> newline.absent? >> any).repeat.as(:string) }
|
14
|
+
rule(:rest_of_line) { (newline.absent? >> any).repeat }
|
15
|
+
|
16
|
+
rule(:original_file_marker) { str('--- ') }
|
17
|
+
rule(:modified_file_marker) { str('+++ ') }
|
18
|
+
rule(:revision) { rest_of_line.as(:string) }
|
19
|
+
rule(:header_information) { text_until_tab.as(:filename) >> (tab >> revision.as(:revision)).maybe }
|
20
|
+
|
21
|
+
rule(:original_file_header) { original_file_marker >> header_information.as(:original) >> newline }
|
22
|
+
rule(:modified_file_header) { modified_file_marker >> header_information.as(:modified) >> newline }
|
23
|
+
|
24
|
+
rule(:chunk_marker) { str('@@') }
|
25
|
+
rule(:line_numbers) do
|
26
|
+
digits.as(:line_number) >> (str(',') >> digits).maybe
|
27
|
+
end
|
28
|
+
rule(:chunk_header) do
|
29
|
+
chunk_marker >> space >>
|
30
|
+
minus >> line_numbers.as(:original) >> space >>
|
31
|
+
plus >> line_numbers.as(:modified) >> space >>
|
32
|
+
chunk_marker >> (space >> rest_of_line.as(:string).as(:section_heading)).maybe >>
|
33
|
+
newline
|
34
|
+
end
|
35
|
+
|
36
|
+
rule(:added_line) { plus >> rest_of_line }
|
37
|
+
rule(:deleted_line) { minus >> rest_of_line }
|
38
|
+
rule(:unchanged_line) { space >> rest_of_line }
|
39
|
+
rule(:no_newline_notice) { str('\') }
|
40
|
+
rule(:line) { (added_line | deleted_line | unchanged_line | no_newline_notice).as(:string) >> newline.maybe }
|
41
|
+
|
42
|
+
rule(:chunk) do
|
43
|
+
(chunk_header >> line.as(:line).repeat(1).as(:lines)).as(:chunk)
|
44
|
+
end
|
45
|
+
|
46
|
+
rule(:unified_diff_header) { original_file_header >> modified_file_header }
|
47
|
+
rule(:unified_diff) do
|
48
|
+
(unified_diff_header.as(:diff_header) >>
|
49
|
+
chunk.repeat(1).as(:chunks)).as(:diff)
|
50
|
+
end
|
51
|
+
root(:unified_diff)
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require "unified/chunk"
|
2
|
+
require "unified/diff"
|
3
|
+
require "unified/line"
|
4
|
+
|
5
|
+
module Unified
|
6
|
+
class Transformer < Parslet::Transform
|
7
|
+
rule(:string => simple(:s)) { String(s) }
|
8
|
+
rule(:line_number => simple(:n)) { Integer(n) }
|
9
|
+
rule(:line => subtree(:line)) { Line.new line }
|
10
|
+
rule(:chunk => subtree(:chunk)) { Chunk.new chunk }
|
11
|
+
rule(:original => subtree(:original), :modified => subtree(:modified)) do
|
12
|
+
{
|
13
|
+
:original_filename => original[:filename],
|
14
|
+
:modified_filename => modified[:filename],
|
15
|
+
:original_revision => original[:revision],
|
16
|
+
:modified_revision => modified[:revision]
|
17
|
+
}
|
18
|
+
end
|
19
|
+
rule(:diff_header => subtree(:header), :chunks => subtree(:chunks)) do
|
20
|
+
header[:chunks] = chunks
|
21
|
+
header
|
22
|
+
end
|
23
|
+
rule(:diff => subtree(:d)) { Diff.new d }
|
24
|
+
end
|
25
|
+
end
|
data/spec/chunk_spec.rb
ADDED
@@ -0,0 +1,134 @@
|
|
1
|
+
require "unified/chunk"
|
2
|
+
|
3
|
+
describe "Unified::Chunk" do
|
4
|
+
let(:lines) { [" Line 1", "+Line 2", "-Line 3", "+Line4", "-Line5", " Line6"] }
|
5
|
+
let(:chunk_without_section_header) { Unified::Chunk.new original: 1, modified: 2, lines: lines }
|
6
|
+
let(:chunk) { Unified::Chunk.new original: 1, modified: 2, lines: lines, section_header: "A section header" }
|
7
|
+
describe "#original_line_number" do
|
8
|
+
it "returns the correct number" do
|
9
|
+
chunk.original_line_number.should == 1
|
10
|
+
end
|
11
|
+
end
|
12
|
+
describe "#modified_line_number" do
|
13
|
+
it "returns the correct number" do
|
14
|
+
chunk.modified_line_number.should == 2
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
describe "#total_number_of_lines" do
|
19
|
+
it "returns the number of lines" do
|
20
|
+
chunk.total_number_of_lines.should == lines.length
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe "#number_of_deleted_lines" do
|
25
|
+
it "returns the number of lines beginning with '-'" do
|
26
|
+
chunk.number_of_deleted_lines.should == 2
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe "#number_of_added_lines" do
|
31
|
+
it "returns the number of lines beginning with '+'" do
|
32
|
+
chunk.number_of_added_lines.should == 2
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe "#number_of_unchanged_lines" do
|
37
|
+
it "returns the number of lines beginning with ' '" do
|
38
|
+
chunk.number_of_unchanged_lines.should == 2
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe "#number_of_original_lines" do
|
43
|
+
it "returns the number of lines within the original chunk" do
|
44
|
+
chunk.number_of_original_lines.should == 4
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
describe "#number_of_modified_lines" do
|
49
|
+
it "returns the number of lines within the modified chunk" do
|
50
|
+
chunk.number_of_modified_lines.should == 4
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
describe "#section_heading" do
|
55
|
+
it "returns nil when not provided" do
|
56
|
+
chunk_without_section_header.section_header.should be_nil
|
57
|
+
end
|
58
|
+
it "returns the correct value when it is provided" do
|
59
|
+
chunk.section_header.should == "A section header"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
describe "#each_line" do
|
64
|
+
it "iterates over each line" do
|
65
|
+
count = 0
|
66
|
+
chunk.each_line do |line|
|
67
|
+
count += 1
|
68
|
+
end
|
69
|
+
|
70
|
+
expect(count).to eq lines.length
|
71
|
+
end
|
72
|
+
|
73
|
+
it "passes the line as block argument" do
|
74
|
+
index = 0
|
75
|
+
chunk.each_line do |line|
|
76
|
+
expect(line).to be_a_kind_of Unified::Line
|
77
|
+
line.should == lines[index]
|
78
|
+
index += 1
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
it "passes the original line number as a block argument" do
|
83
|
+
expected_line_numbers = [1, nil, 2, nil, 3, 4]
|
84
|
+
index = 0
|
85
|
+
chunk.each_line do |line, original_line_number|
|
86
|
+
original_line_number.should == expected_line_numbers[index]
|
87
|
+
index += 1
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
it "passes the modified line number as a block argument" do
|
92
|
+
expected_line_numbers = [2, 3, nil, 4, nil, 5]
|
93
|
+
index = 0
|
94
|
+
chunk.each_line do |line, original_line_number, modified_line_number|
|
95
|
+
modified_line_number.should == expected_line_numbers[index]
|
96
|
+
index += 1
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
describe "#==" do
|
102
|
+
it "returns true when both line numbers and all lines are equal" do
|
103
|
+
another_chunk = Unified::Chunk.new original: 1, modified: 2, lines: lines, section_header: "A section header"
|
104
|
+
chunk.should == another_chunk
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
describe "#header" do
|
109
|
+
it "returns a valid chunk header" do
|
110
|
+
chunk_without_section_header.header.should == "@@ -1,4 +2,4 @@"
|
111
|
+
end
|
112
|
+
it "returns a valid chunk header with section heading" do
|
113
|
+
chunk.header.should == "@@ -1,4 +2,4 @@ A section header"
|
114
|
+
end
|
115
|
+
it "does not display number of lines if it is 1" do
|
116
|
+
chunk = Unified::Chunk.new original: 1, modified: 1, lines: [" Line 1"]
|
117
|
+
chunk.header.should == "@@ -1 +1 @@"
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
describe "#to_s" do
|
122
|
+
it "outputs the chunk header followed by each line" do
|
123
|
+
chunk.to_s.should == <<-EOF.strip
|
124
|
+
@@ -1,4 +2,4 @@ A section header
|
125
|
+
Line 1
|
126
|
+
+Line 2
|
127
|
+
-Line 3
|
128
|
+
+Line4
|
129
|
+
-Line5
|
130
|
+
Line6
|
131
|
+
EOF
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|