unified 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|