unified 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. data/.gitignore +17 -0
  2. data/.rspec +2 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +33 -0
  6. data/Rakefile +1 -0
  7. data/lib/unified.rb +8 -0
  8. data/lib/unified/chunk.rb +88 -0
  9. data/lib/unified/diff.rb +74 -0
  10. data/lib/unified/line.rb +21 -0
  11. data/lib/unified/parser.rb +53 -0
  12. data/lib/unified/transformer.rb +25 -0
  13. data/lib/unified/version.rb +3 -0
  14. data/spec/chunk_spec.rb +134 -0
  15. data/spec/diff_spec.rb +203 -0
  16. data/spec/examples/invalid/no_chunk_header.diff +10 -0
  17. data/spec/examples/invalid/no_content.diff +0 -0
  18. data/spec/examples/invalid/no_modifications.diff +11 -0
  19. data/spec/examples/invalid/no_modified_file_header.diff +6 -0
  20. data/spec/examples/invalid/no_original_file_header.diff +6 -0
  21. data/spec/examples/valid/.DS_Store +0 -0
  22. data/spec/examples/valid/commit_revision.diff +5 -0
  23. data/spec/examples/valid/modified_filename.diff +7 -0
  24. data/spec/examples/valid/more_additions.diff +7 -0
  25. data/spec/examples/valid/more_deletions.diff +10 -0
  26. data/spec/examples/valid/multi_chunk.diff +19 -0
  27. data/spec/examples/valid/new_file.diff +8 -0
  28. data/spec/examples/valid/no_newline.diff +29 -0
  29. data/spec/examples/valid/null_git.diff +8 -0
  30. data/spec/examples/valid/removed_file.diff +8 -0
  31. data/spec/examples/valid/simple.diff +11 -0
  32. data/spec/examples/valid/simple_git.diff +29 -0
  33. data/spec/line_spec.rb +45 -0
  34. data/spec/parser_spec.rb +344 -0
  35. data/spec/spec_helper.rb +21 -0
  36. data/spec/support/matchers/parse_matchers.rb +53 -0
  37. data/spec/transformer_spec.rb +82 -0
  38. data/unified.gemspec +26 -0
  39. metadata +173 -0
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format documentation
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in unified.gemspec
4
+ gemspec
@@ -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.
@@ -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
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,8 @@
1
+ require "unified/version"
2
+
3
+ require "unified/parser"
4
+ require "unified/transformer"
5
+
6
+ require "unified/diff"
7
+ require "unified/chunk"
8
+ require "unified/line"
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,3 @@
1
+ module Unified
2
+ VERSION = "1.0.0"
3
+ end
@@ -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