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/spec/parser_spec.rb
ADDED
@@ -0,0 +1,344 @@
|
|
1
|
+
require "unified/parser"
|
2
|
+
require "spec_helper"
|
3
|
+
|
4
|
+
# Hashes used to validate captures
|
5
|
+
header_without_revision = {
|
6
|
+
:filename => {:string => "Filename"},
|
7
|
+
}
|
8
|
+
header_with_revision = {
|
9
|
+
:revision => {:string => "revision"}
|
10
|
+
}.merge(header_without_revision)
|
11
|
+
|
12
|
+
original_file_header = {
|
13
|
+
:original => header_with_revision
|
14
|
+
}
|
15
|
+
|
16
|
+
modified_file_header = {
|
17
|
+
:modified => header_with_revision
|
18
|
+
}
|
19
|
+
|
20
|
+
describe "Unified::Parser" do
|
21
|
+
let(:parser) { Unified::Parser.new }
|
22
|
+
subject { parser }
|
23
|
+
|
24
|
+
it { should parse_all valid_diffs }
|
25
|
+
it { should_not parse_any invalid_diffs }
|
26
|
+
|
27
|
+
describe "#minus" do
|
28
|
+
subject { parser.minus }
|
29
|
+
|
30
|
+
it { should parse "-" }
|
31
|
+
it { should_not parse_empty_string }
|
32
|
+
it { should_not parse "+" }
|
33
|
+
end
|
34
|
+
|
35
|
+
describe "#plus" do
|
36
|
+
subject { parser.plus }
|
37
|
+
|
38
|
+
it { should parse "+" }
|
39
|
+
it { should_not parse_empty_string }
|
40
|
+
it { should_not parse "-" }
|
41
|
+
end
|
42
|
+
|
43
|
+
describe "#digit" do
|
44
|
+
subject { parser.digit }
|
45
|
+
|
46
|
+
it { should parse "9" }
|
47
|
+
it { should parse "0" }
|
48
|
+
|
49
|
+
it { should_not parse "a" }
|
50
|
+
it { should_not parse_empty_string }
|
51
|
+
it { should_not parse "2141" }
|
52
|
+
end
|
53
|
+
|
54
|
+
describe "#digits" do
|
55
|
+
subject { parser.digits }
|
56
|
+
|
57
|
+
it { should parse "123" }
|
58
|
+
it { should parse "1" }
|
59
|
+
it { should_not parse "a" }
|
60
|
+
it { should_not parse_empty_string }
|
61
|
+
|
62
|
+
end
|
63
|
+
|
64
|
+
describe "#space" do
|
65
|
+
subject { parser.space }
|
66
|
+
|
67
|
+
it { should parse " " }
|
68
|
+
it { should parse "\s" }
|
69
|
+
it { should_not parse_empty_string }
|
70
|
+
it { should_not parse "some content"}
|
71
|
+
it { should_not parse " " }
|
72
|
+
it { should_not parse "\t" }
|
73
|
+
end
|
74
|
+
|
75
|
+
describe "#space?" do
|
76
|
+
subject { parser.space? }
|
77
|
+
|
78
|
+
it { should parse " " }
|
79
|
+
it { should parse_empty_string }
|
80
|
+
it { should_not parse "a" }
|
81
|
+
it { should_not parse " " }
|
82
|
+
end
|
83
|
+
|
84
|
+
describe "#tab" do
|
85
|
+
subject { parser.tab }
|
86
|
+
|
87
|
+
it { should parse "\t" }
|
88
|
+
it { should_not parse " " }
|
89
|
+
end
|
90
|
+
|
91
|
+
describe "#newline" do
|
92
|
+
subject { parser.newline }
|
93
|
+
|
94
|
+
it { should parse "\n" }
|
95
|
+
it { should parse "\r\n" }
|
96
|
+
|
97
|
+
it { should_not parse_empty_string }
|
98
|
+
it { should_not parse "\n\n" }
|
99
|
+
end
|
100
|
+
|
101
|
+
describe "#rest_of_line" do
|
102
|
+
subject { parser.rest_of_line }
|
103
|
+
|
104
|
+
it { should parse "a long string of content" }
|
105
|
+
it { should parse "strings with\t(special_characters)" }
|
106
|
+
it { should parse_empty_string }
|
107
|
+
it { should_not parse "strings ending with a newline\n" }
|
108
|
+
end
|
109
|
+
|
110
|
+
describe "#text_until_tab" do
|
111
|
+
subject { parser.text_until_tab }
|
112
|
+
|
113
|
+
it { should parse "a string with names in" }
|
114
|
+
it { should_not parse "a string with a tab\t" }
|
115
|
+
|
116
|
+
it "should capture filename as {:string => 'filename'}" do
|
117
|
+
expect(subject.parse("String until tab")).to eq :string => "String until tab"
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
describe "#header_information" do
|
122
|
+
subject { parser.header_information }
|
123
|
+
|
124
|
+
it { should parse "This/is/a/filename" }
|
125
|
+
it { should parse "Filename" }
|
126
|
+
it { should parse "Filename\trevision" }
|
127
|
+
|
128
|
+
it "should capture header as {:filename => 'filename', :revision => 'revision'}" do
|
129
|
+
|
130
|
+
|
131
|
+
expect(subject.parse("Filename")).to eq header_without_revision
|
132
|
+
expect(subject.parse("Filename\trevision")).to eq header_with_revision
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
describe "#original_file_marker" do
|
137
|
+
subject { parser.original_file_marker }
|
138
|
+
|
139
|
+
it { should parse "--- " }
|
140
|
+
it { should_not parse_empty_string }
|
141
|
+
it { should_not parse "-- " }
|
142
|
+
it { should_not parse "---" }
|
143
|
+
it { should_not parse "---- " }
|
144
|
+
end
|
145
|
+
|
146
|
+
describe "#modified_file_marker" do
|
147
|
+
subject { parser.modified_file_marker }
|
148
|
+
|
149
|
+
it { should parse "+++ " }
|
150
|
+
it { should_not parse_empty_string }
|
151
|
+
it { should_not parse "++ " }
|
152
|
+
it { should_not parse "+++" }
|
153
|
+
it { should_not parse "++++ " }
|
154
|
+
end
|
155
|
+
|
156
|
+
describe "#original_file_header" do
|
157
|
+
subject { parser.original_file_header }
|
158
|
+
|
159
|
+
it { should parse "--- Filename\t2002-02-21 23:30:39.942229878 -0800\n" }
|
160
|
+
it { should parse "--- Filename\t2002-02-21 23:30:39.942229878 -0800\r\n" }
|
161
|
+
it { should parse "--- Filename\t(working copy)\n" }
|
162
|
+
it { should parse "--- Filename\t(working copy)\r\n" }
|
163
|
+
it { should parse "--- Filename\n" }
|
164
|
+
|
165
|
+
it { should_not parse "--- Filename\t(working copy)"}
|
166
|
+
|
167
|
+
it "should capture all header information" do
|
168
|
+
expect(subject.parse("--- Filename\trevision\n")).to eq original_file_header
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
describe "#modified_file_header" do
|
173
|
+
subject { parser.modified_file_header }
|
174
|
+
|
175
|
+
it { should parse "+++ Filename\t2002-02-21 23:30:39.942229878 -0800\n" }
|
176
|
+
it { should parse "+++ Filename\t2002-02-21 23:30:39.942229878 -0800\r\n" }
|
177
|
+
it { should parse "+++ Filename\t(working copy)\n" }
|
178
|
+
it { should parse "+++ Filename\t(working copy)\r\n" }
|
179
|
+
it { should parse "+++ Filename\n"}
|
180
|
+
|
181
|
+
it { should_not parse "+++ Filename\t(working copy)"}
|
182
|
+
|
183
|
+
it "should capture all header information" do
|
184
|
+
expect(subject.parse("+++ Filename\trevision\n")).to eq modified_file_header
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
describe "#chunk_marker" do
|
189
|
+
subject { parser.chunk_marker }
|
190
|
+
|
191
|
+
it { should parse "@@" }
|
192
|
+
it { should_not parse "@" }
|
193
|
+
it { should_not parse " @@" }
|
194
|
+
it { should_not parse "@@ " }
|
195
|
+
end
|
196
|
+
|
197
|
+
describe "#line_numbers" do
|
198
|
+
subject { parser.line_numbers }
|
199
|
+
|
200
|
+
it { should parse "1" }
|
201
|
+
it { should parse "1,5" }
|
202
|
+
it { should parse "101,101" }
|
203
|
+
|
204
|
+
it "captures line numbers as {:line_number => 'digits'}" do
|
205
|
+
expect(subject.parse("1,5")).to eq :line_number => "1"
|
206
|
+
expect(subject.parse("101,101")).to eq :line_number => "101"
|
207
|
+
expect(subject.parse("1")).to eq :line_number => "1"
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
describe "#chunk_header" do
|
212
|
+
subject { parser.chunk_header }
|
213
|
+
|
214
|
+
it { should parse "@@ -1 +1 @@\n" }
|
215
|
+
it { should parse "@@ -1,5 +1,5 @@\n" }
|
216
|
+
it { should parse "@@ -101,15 +102,14 @@ Some optional heading\n" }
|
217
|
+
|
218
|
+
it "captures all required information" do
|
219
|
+
expect(subject.parse("@@ -1 +1 @@\n")).to eq({
|
220
|
+
:original => {
|
221
|
+
:line_number => "1"
|
222
|
+
},
|
223
|
+
:modified => {
|
224
|
+
:line_number => "1"
|
225
|
+
}
|
226
|
+
})
|
227
|
+
|
228
|
+
expect(subject.parse("@@ -101,15 +102,14 @@ Some optional heading\n")).to eq({
|
229
|
+
:original => {
|
230
|
+
:line_number => "101"
|
231
|
+
},
|
232
|
+
:modified => {
|
233
|
+
:line_number => "102"
|
234
|
+
},
|
235
|
+
:section_heading => { string: "Some optional heading" }
|
236
|
+
})
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
describe "#added_line" do
|
241
|
+
subject { parser.added_line }
|
242
|
+
|
243
|
+
it { should parse "+This is an additional line" }
|
244
|
+
it { should_not parse " +This is not a valid additional line" }
|
245
|
+
it { should parse "+++Even though this looks like a header, it should parse" }
|
246
|
+
end
|
247
|
+
|
248
|
+
describe "#deleted_line" do
|
249
|
+
subject { parser.deleted_line }
|
250
|
+
|
251
|
+
it { should parse "-This is a deleted line" }
|
252
|
+
it { should_not parse " -This is not a valid deleted line" }
|
253
|
+
it { should parse "---Even though this looks like a header, it should parse" }
|
254
|
+
end
|
255
|
+
|
256
|
+
describe "#unchanged_line" do
|
257
|
+
subject { parser.unchanged_line }
|
258
|
+
|
259
|
+
it { should parse " this line is unchanged" }
|
260
|
+
it { should_not parse "This has no leading space" }
|
261
|
+
it { should_not parse_empty_string }
|
262
|
+
end
|
263
|
+
|
264
|
+
describe "#no_newline_notice" do
|
265
|
+
subject { parser.no_newline_notice }
|
266
|
+
|
267
|
+
it { should parse "\" }
|
268
|
+
end
|
269
|
+
|
270
|
+
describe "#line" do
|
271
|
+
subject { parser.line }
|
272
|
+
|
273
|
+
it { should parse "+This is an additional line" }
|
274
|
+
it { should parse "-This is a deleted line" }
|
275
|
+
it { should parse " this line is unchanged" }
|
276
|
+
it { should parse "\" }
|
277
|
+
|
278
|
+
it "captures all line content as {:string => 'content'}" do
|
279
|
+
expect(subject.parse("+This is an additional line")).to eq :string => "+This is an additional line"
|
280
|
+
expect(subject.parse("-This is a deleted line")).to eq :string => "-This is a deleted line"
|
281
|
+
expect(subject.parse(" this line is unchanged")).to eq :string => " this line is unchanged"
|
282
|
+
expect(subject.parse("\")).to eq :string => "\"
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
describe "#chunk" do
|
287
|
+
subject { parser.chunk }
|
288
|
+
valid_chunk = "@@ -1 +1 @@\n+addition\n-deletion\n no change"
|
289
|
+
a_chunk_with_no_lines = "@@ -1 +1 @@\n"
|
290
|
+
a_chunk_with_no_header = "+addition\n-deletion\n no change"
|
291
|
+
|
292
|
+
it { should parse valid_chunk }
|
293
|
+
it { should_not parse a_chunk_with_no_lines }
|
294
|
+
it { should_not parse a_chunk_with_no_header }
|
295
|
+
|
296
|
+
context "captures" do
|
297
|
+
let(:capture) { subject.parse valid_chunk }
|
298
|
+
it "captures as chunk" do
|
299
|
+
capture.should include :chunk
|
300
|
+
end
|
301
|
+
|
302
|
+
it "captures lines" do
|
303
|
+
capture[:chunk].should include :lines
|
304
|
+
capture[:chunk][:lines].size.should == 3
|
305
|
+
capture[:chunk][:lines].each do |line|
|
306
|
+
line.should include :line
|
307
|
+
end
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
end
|
312
|
+
|
313
|
+
describe "#unified_diff_header" do
|
314
|
+
subject { parser.unified_diff_header }
|
315
|
+
|
316
|
+
it { should parse "--- Filename\n+++ Filename\n" }
|
317
|
+
it { should_not parse "--- Missing header\n" }
|
318
|
+
it { should_not parse "\n+++ Missing header\n"}
|
319
|
+
end
|
320
|
+
|
321
|
+
describe "#unified_diff" do
|
322
|
+
subject { parser.unified_diff }
|
323
|
+
|
324
|
+
it { should parse valid_diffs.first }
|
325
|
+
it { should_not parse invalid_diffs.first }
|
326
|
+
|
327
|
+
context "capturing all necessary information" do
|
328
|
+
let(:result) { subject.parse valid_diffs.first }
|
329
|
+
|
330
|
+
it "captures as a diff hash" do
|
331
|
+
result.should include(:diff)
|
332
|
+
end
|
333
|
+
|
334
|
+
it "captures a diff header" do
|
335
|
+
result[:diff].should include(:diff_header)
|
336
|
+
end
|
337
|
+
|
338
|
+
it "captures chunks" do
|
339
|
+
result[:diff].should include(:chunks)
|
340
|
+
result[:diff][:chunks].size.should == 1
|
341
|
+
end
|
342
|
+
end
|
343
|
+
end
|
344
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
RSpec.configure do |config|
|
2
|
+
config.treat_symbols_as_metadata_keys_with_true_values = true
|
3
|
+
config.run_all_when_everything_filtered = true
|
4
|
+
config.filter_run :focus
|
5
|
+
|
6
|
+
config.order = 'random'
|
7
|
+
end
|
8
|
+
|
9
|
+
Dir[File.dirname(__FILE__) + "/support/**/*.rb"].each {|f| require f}
|
10
|
+
|
11
|
+
def load_diffs_from(dir)
|
12
|
+
Dir.glob(File.dirname(__FILE__) + '/' + dir + '/*.diff').map {|f| File.read(f) }
|
13
|
+
end
|
14
|
+
|
15
|
+
def valid_diffs
|
16
|
+
load_diffs_from("examples/valid")
|
17
|
+
end
|
18
|
+
|
19
|
+
def invalid_diffs
|
20
|
+
load_diffs_from("examples/invalid")
|
21
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
RSpec::Matchers.define :parse do |string_to_parse|
|
2
|
+
match do |parser|
|
3
|
+
begin
|
4
|
+
parser.parse(string_to_parse)
|
5
|
+
rescue Parslet::ParseFailed => e
|
6
|
+
# @exception = e
|
7
|
+
@exception = e.cause.ascii_tree
|
8
|
+
false
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
failure_message_for_should do |parser|
|
13
|
+
@exception + "\n\n" + string_to_parse
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
RSpec::Matchers.define :parse_empty_string do
|
18
|
+
match do |parser|
|
19
|
+
begin
|
20
|
+
parser.parse("")
|
21
|
+
rescue Parslet::ParseFailed => e
|
22
|
+
@exception = e.cause.ascii_tree
|
23
|
+
false
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
RSpec::Matchers.define :parse_all do |diffs|
|
29
|
+
match do |parser|
|
30
|
+
begin
|
31
|
+
diffs.each do |diff|
|
32
|
+
parser.parse(diff)
|
33
|
+
end
|
34
|
+
rescue Parslet::ParseFailed => e
|
35
|
+
@exception = e.cause.ascii_tree
|
36
|
+
false
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# TODO Can you have aliases for RSpec matchers?
|
42
|
+
RSpec::Matchers.define :parse_any do |diffs|
|
43
|
+
match do |parser|
|
44
|
+
begin
|
45
|
+
diffs.each do |diff|
|
46
|
+
parser.parse(diff)
|
47
|
+
end
|
48
|
+
rescue Parslet::ParseFailed => e
|
49
|
+
@exception = e.cause.ascii_tree
|
50
|
+
false
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require "unified/transformer"
|
2
|
+
require "spec_helper"
|
3
|
+
|
4
|
+
|
5
|
+
|
6
|
+
describe "Unified::Transformer" do
|
7
|
+
def lines
|
8
|
+
[" Line 1", " Line 2", " Line 3", "-Line 5", "-Line 6", "-Line 7", "+Line 4", "+Line 5"]
|
9
|
+
end
|
10
|
+
|
11
|
+
def build_chunk_hash(original_line_number = 1, modified_line_number = 1, header = nil)
|
12
|
+
{:chunk=>{:original=>original_line_number, :modified=>modified_line_number, :section_heading=>header, :lines=>lines}}
|
13
|
+
end
|
14
|
+
let(:transformer) { Unified::Transformer.new }
|
15
|
+
|
16
|
+
context "simple values" do
|
17
|
+
it "transforms a string value" do
|
18
|
+
expect(transformer.apply({:string => "a string"})).to eq("a string")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
context "Lines" do
|
23
|
+
it "transforms a line into a Unified::Line object" do
|
24
|
+
expect(transformer.apply({:line => {:string => "+A line"}})).to eq(Unified::Line.new("+A line"))
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
context "Chunks" do
|
29
|
+
let(:chunk) { build_chunk_hash }
|
30
|
+
it "transforms a chunk into a Unified::Chunk object" do
|
31
|
+
expect(transformer.apply(chunk)).to eq(Unified::Chunk.new(original: 1, modified: 1, section_heading: "A section heading", lines: lines))
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
describe "Diff header" do
|
36
|
+
let(:header) { {:original=>{:filename=>"Original", :revision=>"OrigRev"}, :modified=>{:filename=>"Modified", :revision=>"ModRev"}} }
|
37
|
+
it "transforms a diff header into filenames and revisions" do
|
38
|
+
expect(transformer.apply(header)).to eq({
|
39
|
+
original_filename: "Original",
|
40
|
+
modified_filename: "Modified",
|
41
|
+
original_revision: "OrigRev",
|
42
|
+
modified_revision: "ModRev"
|
43
|
+
})
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# context "Diffs" do
|
48
|
+
# let(:chunks) { [build_chunk_hash, build_chunk_hash(101, 101)] }
|
49
|
+
# let(:diff_hash) do
|
50
|
+
# {
|
51
|
+
# :diff => {
|
52
|
+
# :diff_header => {
|
53
|
+
# :original => {
|
54
|
+
# :filename=>"Original",
|
55
|
+
# :revision=>"OriginalRevision"
|
56
|
+
# },
|
57
|
+
# :modified => {
|
58
|
+
# :filename=>"Modified",
|
59
|
+
# :revision=>"ModifiedRevision"
|
60
|
+
# }
|
61
|
+
# },
|
62
|
+
# :chunks => chunks
|
63
|
+
# }
|
64
|
+
# }
|
65
|
+
# end
|
66
|
+
# let(:diff) {
|
67
|
+
# Unified::Diff.new(
|
68
|
+
# original_filename: "Original",
|
69
|
+
# original_revision: "OriginalRevision",
|
70
|
+
# modified_filename: "Modified",
|
71
|
+
# modified_revision: "ModifiedRevision",
|
72
|
+
# chunks: [
|
73
|
+
# Unified::Chunk.new(original: 1, modified: 1, lines: lines),
|
74
|
+
# Unified::Chunk.new(original: 101, modified: 101, lines: lines)
|
75
|
+
# ]
|
76
|
+
# )
|
77
|
+
# }
|
78
|
+
# it "transforms a diff into a Unified::Diff object" do
|
79
|
+
# expect(transformer.apply(diff_hash)).to eq(diff)
|
80
|
+
# end
|
81
|
+
# end
|
82
|
+
end
|