purdypatch 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -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/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in purdypatch.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013, Nathan de Vries
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without
5
+ modification, are permitted provided that the following conditions are met:
6
+
7
+ * Redistributions of source code must retain the above copyright notice,
8
+ this list of conditions and the following disclaimer.
9
+ * Redistributions in binary form must reproduce the above copyright
10
+ notice, this list of conditions and the following disclaimer in the
11
+ documentation and/or other materials provided with the distribution.
12
+
13
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
14
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
16
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
17
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
18
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
19
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
20
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
21
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
22
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/README.md ADDED
@@ -0,0 +1,18 @@
1
+ Purdy Patch
2
+ ===========
3
+
4
+ This gem provides a `purdypatch` tool for improving the format of `git-format-patch` emails, making them easier for reviewers to read and reply to in a decentralized, email-based review system.
5
+
6
+ It uses the incredibly useful [PrettyPatch formatter](http://svn.webkit.org/repository/webkit/trunk/Websites/bugs.webkit.org/PrettyPatch/), which is used by the patch review system on [bugs.webkit.org](https://bugs.webkit.org).
7
+
8
+ To get started, simply run `gem install purdypatch` to install the tool.
9
+
10
+ Once you have `purdypatch` installed, here's a simple workflow for sending out a patch for review:
11
+
12
+ $ git format-patch --to="Doge <sohip@suchclass.com>" --attach HEAD~1
13
+ $ purdypatch *.patch
14
+ $ git send-email *.purdypatch
15
+
16
+ According to [Devil’s Dictionary of Programming](http://programmingisterrible.com/post/65781074112/devils-dictionary-of-programming), `purdypatch` is simple, opinionated *and* lightweight.
17
+
18
+ Enjoy.
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/bin/purdypatch ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'purdypatch'
4
+
5
+ PurdyPatch::CLI.start(*ARGV)
@@ -0,0 +1,818 @@
1
+ require 'cgi'
2
+ require 'diff'
3
+ require 'open3'
4
+ require 'open-uri'
5
+ require 'pp'
6
+ require 'set'
7
+ require 'tempfile'
8
+
9
+ module PrettyPatch
10
+
11
+ public
12
+
13
+ GIT_PATH = "git"
14
+
15
+ def self.prettify(string)
16
+ $last_prettify_file_count = -1
17
+ $last_prettify_part_count = { "remove" => 0, "add" => 0, "shared" => 0, "binary" => 0, "extract-error" => 0 }
18
+ string = normalize_line_ending(string)
19
+ str = "#{HEADER}<body>\n"
20
+
21
+ # Just look at the first line to see if it is an SVN revision number as added
22
+ # by webkit-patch for git checkouts.
23
+ $svn_revision = 0
24
+ string.each_line do |line|
25
+ match = /^Subversion\ Revision: (\d*)$/.match(line)
26
+ unless match.nil?
27
+ str << "<span class='revision'>#{match[1]}</span>\n"
28
+ $svn_revision = match[1].to_i;
29
+ end
30
+ break
31
+ end
32
+
33
+ fileDiffs = FileDiff.parse(string)
34
+
35
+ $last_prettify_file_count = fileDiffs.length
36
+ str << fileDiffs.collect{ |diff| diff.to_html }.join
37
+ str << "</body></html>"
38
+ end
39
+
40
+ def self.filename_from_diff_header(line)
41
+ DIFF_HEADER_FORMATS.each do |format|
42
+ match = format.match(line)
43
+ return match[1] unless match.nil?
44
+ end
45
+ nil
46
+ end
47
+
48
+ def self.diff_header?(line)
49
+ RELAXED_DIFF_HEADER_FORMATS.any? { |format| line =~ format }
50
+ end
51
+
52
+ private
53
+ DIFF_HEADER_FORMATS = [
54
+ /^Index: (.*)\r?$/,
55
+ /^diff --git "?a\/.+"? "?b\/(.+)"?\r?$/,
56
+ /^\+\+\+ ([^\t]+)(\t.*)?\r?$/
57
+ ]
58
+
59
+ RELAXED_DIFF_HEADER_FORMATS = [
60
+ /^Index:/,
61
+ /^diff/
62
+ ]
63
+
64
+ BINARY_FILE_MARKER_FORMAT = /^Cannot display: file marked as a binary type.$/
65
+
66
+ IMAGE_FILE_MARKER_FORMAT = /^svn:mime-type = image\/png$/
67
+
68
+ GIT_INDEX_MARKER_FORMAT = /^index ([0-9a-f]{40})\.\.([0-9a-f]{40})/
69
+
70
+ GIT_BINARY_FILE_MARKER_FORMAT = /^GIT binary patch$/
71
+
72
+ GIT_BINARY_PATCH_FORMAT = /^(literal|delta) \d+$/
73
+
74
+ GIT_LITERAL_FORMAT = /^literal \d+$/
75
+
76
+ GIT_DELTA_FORMAT = /^delta \d+$/
77
+
78
+ START_OF_BINARY_DATA_FORMAT = /^[0-9a-zA-Z\+\/=]{20,}/ # Assume 20 chars without a space is base64 binary data.
79
+
80
+ START_OF_SECTION_FORMAT = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@\s*(.*)/
81
+
82
+ START_OF_EXTENT_STRING = "%c" % 0
83
+ END_OF_EXTENT_STRING = "%c" % 1
84
+
85
+ # We won't search for intra-line diffs in lines longer than this length, to avoid hangs. See <http://webkit.org/b/56109>.
86
+ MAXIMUM_INTRALINE_DIFF_LINE_LENGTH = 10000
87
+
88
+ SMALLEST_EQUAL_OPERATION = 3
89
+
90
+ OPENSOURCE_TRAC_URL = "http://trac.webkit.org/"
91
+
92
+ OPENSOURCE_DIRS = Set.new %w[
93
+ Examples
94
+ LayoutTests
95
+ PerformanceTests
96
+ Source
97
+ Tools
98
+ WebKitLibraries
99
+ Websites
100
+ ]
101
+
102
+ IMAGE_CHECKSUM_ERROR = "INVALID: Image lacks a checksum. This will fail with a MISSING error in run-webkit-tests. Always generate new png files using run-webkit-tests."
103
+
104
+ def self.normalize_line_ending(s)
105
+ if RUBY_VERSION >= "1.9"
106
+ # Transliteration table from http://stackoverflow.com/a/6609998
107
+ transliteration_table = { '\xc2\x82' => ',', # High code comma
108
+ '\xc2\x84' => ',,', # High code double comma
109
+ '\xc2\x85' => '...', # Tripple dot
110
+ '\xc2\x88' => '^', # High carat
111
+ '\xc2\x91' => '\x27', # Forward single quote
112
+ '\xc2\x92' => '\x27', # Reverse single quote
113
+ '\xc2\x93' => '\x22', # Forward double quote
114
+ '\xc2\x94' => '\x22', # Reverse double quote
115
+ '\xc2\x95' => ' ',
116
+ '\xc2\x96' => '-', # High hyphen
117
+ '\xc2\x97' => '--', # Double hyphen
118
+ '\xc2\x99' => ' ',
119
+ '\xc2\xa0' => ' ',
120
+ '\xc2\xa6' => '|', # Split vertical bar
121
+ '\xc2\xab' => '<<', # Double less than
122
+ '\xc2\xbb' => '>>', # Double greater than
123
+ '\xc2\xbc' => '1/4', # one quarter
124
+ '\xc2\xbd' => '1/2', # one half
125
+ '\xc2\xbe' => '3/4', # three quarters
126
+ '\xca\xbf' => '\x27', # c-single quote
127
+ '\xcc\xa8' => '', # modifier - under curve
128
+ '\xcc\xb1' => '' # modifier - under line
129
+ }
130
+ encoded_string = s.force_encoding('UTF-8').encode('UTF-16', :invalid => :replace, :replace => '', :fallback => transliteration_table).encode('UTF-8')
131
+ encoded_string.gsub /\r\n?/, "\n"
132
+ else
133
+ s.gsub /\r\n?/, "\n"
134
+ end
135
+ end
136
+
137
+ def self.find_url_and_path(file_path)
138
+ # Search file_path from the bottom up, at each level checking whether
139
+ # we've found a directory we know exists in the source tree.
140
+
141
+ dirname, basename = File.split(file_path)
142
+ dirname.split(/\//).reverse.inject(basename) do |path, directory|
143
+ path = directory + "/" + path
144
+
145
+ return [OPENSOURCE_TRAC_URL, path] if OPENSOURCE_DIRS.include?(directory)
146
+
147
+ path
148
+ end
149
+
150
+ [nil, file_path]
151
+ end
152
+
153
+ def self.linkifyFilename(filename)
154
+ url, pathBeneathTrunk = find_url_and_path(filename)
155
+
156
+ url.nil? ? filename : "<a href='#{url}browser/trunk/#{pathBeneathTrunk}'>#{filename}</a>"
157
+ end
158
+
159
+
160
+ HEADER =<<EOF
161
+ <html>
162
+ <head>
163
+ <meta charset='utf-8'>
164
+ <style>
165
+ :link, :visited {
166
+ text-decoration: none;
167
+ border-bottom: 1px dotted;
168
+ }
169
+
170
+ :link {
171
+ color: #039;
172
+ }
173
+
174
+ .FileDiff {
175
+ background-color: #f8f8f8;
176
+ border: 1px solid #ddd;
177
+ font-family: monospace;
178
+ margin: 1em 0;
179
+ position: relative;
180
+ }
181
+
182
+ h1 {
183
+ color: #333;
184
+ font-family: sans-serif;
185
+ font-size: 1em;
186
+ margin-left: 0.5em;
187
+ display: table-cell;
188
+ width: 100%;
189
+ padding: 0.5em;
190
+ }
191
+
192
+ h1 :link, h1 :visited {
193
+ color: inherit;
194
+ }
195
+
196
+ h1 :hover {
197
+ color: #555;
198
+ background-color: #eee;
199
+ }
200
+
201
+ .DiffLinks {
202
+ float: right;
203
+ }
204
+
205
+ .FileDiffLinkContainer {
206
+ opacity: 0;
207
+ display: table-cell;
208
+ padding-right: 0.5em;
209
+ white-space: nowrap;
210
+ }
211
+
212
+ .DiffSection {
213
+ background-color: white;
214
+ border: solid #ddd;
215
+ border-width: 1px 0px;
216
+ }
217
+
218
+ .ExpansionLine, .LineContainer {
219
+ white-space: nowrap;
220
+ }
221
+
222
+ .sidebyside .DiffBlockPart.add:first-child {
223
+ float: right;
224
+ }
225
+
226
+ .LineSide,
227
+ .sidebyside .DiffBlockPart.remove,
228
+ .sidebyside .DiffBlockPart.add {
229
+ display:inline-block;
230
+ width: 50%;
231
+ vertical-align: top;
232
+ }
233
+
234
+ .sidebyside .resizeHandle {
235
+ width: 5px;
236
+ height: 100%;
237
+ cursor: move;
238
+ position: absolute;
239
+ top: 0;
240
+ left: 50%;
241
+ }
242
+
243
+ .sidebyside .resizeHandle:hover {
244
+ background-color: grey;
245
+ opacity: 0.5;
246
+ }
247
+
248
+ .sidebyside .DiffBlockPart.remove .to,
249
+ .sidebyside .DiffBlockPart.add .from {
250
+ display: none;
251
+ }
252
+
253
+ .lineNumber, .expansionLineNumber {
254
+ border-bottom: 1px solid #998;
255
+ border-right: 1px solid #ddd;
256
+ color: #444;
257
+ display: inline-block;
258
+ padding: 1px 5px 0px 0px;
259
+ text-align: right;
260
+ vertical-align: bottom;
261
+ width: 3em;
262
+ }
263
+
264
+ .lineNumber {
265
+ background-color: #eed;
266
+ }
267
+
268
+ .expansionLineNumber {
269
+ background-color: #eee;
270
+ }
271
+
272
+ pre, .text {
273
+ padding-left: 5px;
274
+ white-space: pre-wrap;
275
+ word-wrap: break-word;
276
+ }
277
+
278
+ .image {
279
+ border: 2px solid black;
280
+ }
281
+
282
+ .context, .context .lineNumber {
283
+ color: #849;
284
+ background-color: #fef;
285
+ }
286
+
287
+ .Line.add, .FileDiff .add {
288
+ background-color: #dfd;
289
+ }
290
+
291
+ .Line.add ins {
292
+ background-color: #9e9;
293
+ text-decoration: none;
294
+ }
295
+
296
+ .Line.remove, .FileDiff .remove {
297
+ background-color: #fdd;
298
+ }
299
+
300
+ .Line.remove del {
301
+ background-color: #e99;
302
+ text-decoration: none;
303
+ }
304
+
305
+ div:focus {
306
+ outline: 1px solid blue;
307
+ outline-offset: -1px;
308
+ }
309
+
310
+ .revision {
311
+ display: none;
312
+ }
313
+
314
+ .clear_float {
315
+ clear: both;
316
+ }
317
+ </style>
318
+ </head>
319
+ EOF
320
+
321
+ def self.revisionOrDescription(string)
322
+ case string
323
+ when /\(revision \d+\)/
324
+ /\(revision (\d+)\)/.match(string)[1]
325
+ when /\(.*\)/
326
+ /\((.*)\)/.match(string)[1]
327
+ end
328
+ end
329
+
330
+ def self.has_image_suffix(filename)
331
+ filename =~ /\.(png|jpg|gif)$/
332
+ end
333
+
334
+ class FileDiff
335
+ def initialize(lines)
336
+ @filename = PrettyPatch.filename_from_diff_header(lines[0].chomp)
337
+ startOfSections = 1
338
+ for i in 0...lines.length
339
+ case lines[i]
340
+ when /^--- /
341
+ @from = PrettyPatch.revisionOrDescription(lines[i])
342
+ when /^\+\+\+ /
343
+ @filename = PrettyPatch.filename_from_diff_header(lines[i].chomp) if @filename.nil?
344
+ @to = PrettyPatch.revisionOrDescription(lines[i])
345
+ startOfSections = i + 1
346
+ break
347
+ when BINARY_FILE_MARKER_FORMAT
348
+ @binary = true
349
+ if (IMAGE_FILE_MARKER_FORMAT.match(lines[i + 1]) or PrettyPatch.has_image_suffix(@filename)) then
350
+ @image = true
351
+ startOfSections = i + 2
352
+ for x in startOfSections...lines.length
353
+ # Binary diffs often have property changes listed before the actual binary data. Skip them.
354
+ if START_OF_BINARY_DATA_FORMAT.match(lines[x]) then
355
+ startOfSections = x
356
+ break
357
+ end
358
+ end
359
+ end
360
+ break
361
+ when GIT_INDEX_MARKER_FORMAT
362
+ @git_indexes = [$1, $2]
363
+ when GIT_BINARY_FILE_MARKER_FORMAT
364
+ @binary = true
365
+ if (GIT_BINARY_PATCH_FORMAT.match(lines[i + 1]) and PrettyPatch.has_image_suffix(@filename)) then
366
+ @git_image = true
367
+ startOfSections = i + 1
368
+ end
369
+ break
370
+ end
371
+ end
372
+ lines_with_contents = lines[startOfSections...lines.length]
373
+ @sections = DiffSection.parse(lines_with_contents) unless @binary
374
+ if @image and not lines_with_contents.empty?
375
+ @image_url = "data:image/png;base64," + lines_with_contents.join
376
+ @image_checksum = FileDiff.read_checksum_from_png(lines_with_contents.join.unpack("m").join)
377
+ elsif @git_image
378
+ begin
379
+ raise "index line is missing" unless @git_indexes
380
+
381
+ chunks = nil
382
+ for i in 0...lines_with_contents.length
383
+ if lines_with_contents[i] =~ /^$/
384
+ chunks = [lines_with_contents[i + 1 .. -1], lines_with_contents[0 .. i]]
385
+ break
386
+ end
387
+ end
388
+
389
+ raise "no binary chunks" unless chunks
390
+
391
+ from_filepath = FileDiff.extract_contents_of_from_revision(@filename, chunks[0], @git_indexes[0])
392
+ to_filepath = FileDiff.extract_contents_of_to_revision(@filename, chunks[1], @git_indexes[1], from_filepath, @git_indexes[0])
393
+ filepaths = from_filepath, to_filepath
394
+
395
+ binary_contents = filepaths.collect { |filepath| File.exists?(filepath) ? File.read(filepath) : nil }
396
+ @image_urls = binary_contents.collect { |content| (content and not content.empty?) ? "data:image/png;base64," + [content].pack("m") : nil }
397
+ @image_checksums = binary_contents.collect { |content| FileDiff.read_checksum_from_png(content) }
398
+ rescue
399
+ $last_prettify_part_count["extract-error"] += 1
400
+ @image_error = "Exception raised during decoding git binary patch:<pre>#{CGI.escapeHTML($!.to_s + "\n" + $!.backtrace.join("\n"))}</pre>"
401
+ ensure
402
+ File.unlink(from_filepath) if (from_filepath and File.exists?(from_filepath))
403
+ File.unlink(to_filepath) if (to_filepath and File.exists?(to_filepath))
404
+ end
405
+ end
406
+ nil
407
+ end
408
+
409
+ def image_to_html
410
+ if not @image_url then
411
+ return "<span class='text'>Image file removed</span>"
412
+ end
413
+
414
+ image_checksum = ""
415
+ if @image_checksum
416
+ image_checksum = @image_checksum
417
+ elsif @filename.include? "-expected.png" and @image_url
418
+ image_checksum = IMAGE_CHECKSUM_ERROR
419
+ end
420
+
421
+ return "<p>" + image_checksum + "</p><img class='image' src='" + @image_url + "' />"
422
+ end
423
+
424
+ def to_html
425
+ str = "<div class='FileDiff'>\n"
426
+ str += "<h1>#{PrettyPatch.linkifyFilename(@filename)}</h1>\n"
427
+ if @image then
428
+ str += self.image_to_html
429
+ elsif @git_image then
430
+ if @image_error
431
+ str += @image_error
432
+ else
433
+ for i in (0...2)
434
+ image_url = @image_urls[i]
435
+ image_checksum = @image_checksums[i]
436
+
437
+ style = ["remove", "add"][i]
438
+ str += "<p class=\"#{style}\">"
439
+
440
+ if image_checksum
441
+ str += image_checksum
442
+ elsif @filename.include? "-expected.png" and image_url
443
+ str += IMAGE_CHECKSUM_ERROR
444
+ end
445
+
446
+ str += "<br>"
447
+
448
+ if image_url
449
+ str += "<img class='image' src='" + image_url + "' />"
450
+ else
451
+ str += ["</p>Added", "</p>Removed"][i]
452
+ end
453
+ end
454
+ end
455
+ elsif @binary then
456
+ $last_prettify_part_count["binary"] += 1
457
+ str += "<span class='text'>Binary file, nothing to see here</span>"
458
+ else
459
+ str += @sections.collect{ |section| section.to_html }.join("<br>\n") unless @sections.nil?
460
+ end
461
+
462
+ if @from then
463
+ str += "<span class='revision'>" + @from + "</span>"
464
+ end
465
+
466
+ str += "</div>\n"
467
+ end
468
+
469
+ def self.parse(string)
470
+ haveSeenDiffHeader = false
471
+ linesForDiffs = []
472
+ string.each_line do |line|
473
+ if (PrettyPatch.diff_header?(line))
474
+ linesForDiffs << []
475
+ haveSeenDiffHeader = true
476
+ elsif (!haveSeenDiffHeader && line =~ /^--- /)
477
+ linesForDiffs << []
478
+ haveSeenDiffHeader = false
479
+ end
480
+ linesForDiffs.last << line unless linesForDiffs.last.nil?
481
+ end
482
+
483
+ linesForDiffs.collect { |lines| FileDiff.new(lines) }
484
+ end
485
+
486
+ def self.read_checksum_from_png(png_bytes)
487
+ # Ruby 1.9 added the concept of string encodings, so to avoid treating binary data as UTF-8,
488
+ # we can force the encoding to binary at this point.
489
+ if RUBY_VERSION >= "1.9"
490
+ png_bytes.force_encoding('binary')
491
+ end
492
+ match = png_bytes && png_bytes.match(/tEXtchecksum\0([a-fA-F0-9]{32})/)
493
+ match ? match[1] : nil
494
+ end
495
+
496
+ def self.git_new_file_binary_patch(filename, encoded_chunk, git_index)
497
+ return <<END
498
+ diff --git a/#{filename} b/#{filename}
499
+ new file mode 100644
500
+ index 0000000000000000000000000000000000000000..#{git_index}
501
+ GIT binary patch
502
+ #{encoded_chunk.join("")}literal 0
503
+ HcmV?d00001
504
+
505
+ END
506
+ end
507
+
508
+ def self.git_changed_file_binary_patch(to_filename, from_filename, encoded_chunk, to_git_index, from_git_index)
509
+ return <<END
510
+ diff --git a/#{from_filename} b/#{to_filename}
511
+ copy from #{from_filename}
512
+ +++ b/#{to_filename}
513
+ index #{from_git_index}..#{to_git_index}
514
+ GIT binary patch
515
+ #{encoded_chunk.join("")}literal 0
516
+ HcmV?d00001
517
+
518
+ END
519
+ end
520
+
521
+ def self.get_svn_uri(repository_path)
522
+ "http://svn.webkit.org/repository/webkit/!svn/bc/" + $svn_revision.to_s + "/trunk/" + (repository_path)
523
+ end
524
+
525
+ def self.get_new_temp_filepath_and_name
526
+ tempfile = Tempfile.new("PrettyPatch")
527
+ filepath = tempfile.path + '.bin'
528
+ filename = File.basename(filepath)
529
+ return filepath, filename
530
+ end
531
+
532
+ def self.download_from_revision_from_svn(repository_path)
533
+ filepath, filename = get_new_temp_filepath_and_name
534
+ svn_uri = get_svn_uri(repository_path)
535
+ open(filepath, 'wb') do |to_file|
536
+ to_file << open(svn_uri) { |from_file| from_file.read }
537
+ end
538
+ return filepath
539
+ end
540
+
541
+ def self.run_git_apply_on_patch(output_filepath, patch)
542
+ # Apply the git binary patch using git-apply.
543
+ cmd = GIT_PATH + " apply --directory=" + File.dirname(output_filepath)
544
+ stdin, stdout, stderr = *Open3.popen3(cmd)
545
+ begin
546
+ stdin.puts(patch)
547
+ stdin.close
548
+
549
+ error = stderr.read
550
+ if error != ""
551
+ error = "Error running " + cmd + "\n" + "with patch:\n" + patch[0..500] + "...\n" + error
552
+ end
553
+ raise error if error != ""
554
+ ensure
555
+ stdin.close unless stdin.closed?
556
+ stdout.close
557
+ stderr.close
558
+ end
559
+ end
560
+
561
+ def self.extract_contents_from_git_binary_literal_chunk(encoded_chunk, git_index)
562
+ filepath, filename = get_new_temp_filepath_and_name
563
+ patch = FileDiff.git_new_file_binary_patch(filename, encoded_chunk, git_index)
564
+ run_git_apply_on_patch(filepath, patch)
565
+ return filepath
566
+ end
567
+
568
+ def self.extract_contents_from_git_binary_delta_chunk(from_filepath, from_git_index, encoded_chunk, to_git_index)
569
+ to_filepath, to_filename = get_new_temp_filepath_and_name
570
+ from_filename = File.basename(from_filepath)
571
+ patch = FileDiff.git_changed_file_binary_patch(to_filename, from_filename, encoded_chunk, to_git_index, from_git_index)
572
+ run_git_apply_on_patch(to_filepath, patch)
573
+ return to_filepath
574
+ end
575
+
576
+ def self.extract_contents_of_from_revision(repository_path, encoded_chunk, git_index)
577
+ # For literal encoded, simply reconstruct.
578
+ if GIT_LITERAL_FORMAT.match(encoded_chunk[0])
579
+ return extract_contents_from_git_binary_literal_chunk(encoded_chunk, git_index)
580
+ end
581
+ # For delta encoded, download from svn.
582
+ if GIT_DELTA_FORMAT.match(encoded_chunk[0])
583
+ return download_from_revision_from_svn(repository_path)
584
+ end
585
+ raise "Error: unknown git patch encoding"
586
+ end
587
+
588
+ def self.extract_contents_of_to_revision(repository_path, encoded_chunk, git_index, from_filepath, from_git_index)
589
+ # For literal encoded, simply reconstruct.
590
+ if GIT_LITERAL_FORMAT.match(encoded_chunk[0])
591
+ return extract_contents_from_git_binary_literal_chunk(encoded_chunk, git_index)
592
+ end
593
+ # For delta encoded, reconstruct using delta and previously constructed 'from' revision.
594
+ if GIT_DELTA_FORMAT.match(encoded_chunk[0])
595
+ return extract_contents_from_git_binary_delta_chunk(from_filepath, from_git_index, encoded_chunk, git_index)
596
+ end
597
+ raise "Error: unknown git patch encoding"
598
+ end
599
+ end
600
+
601
+ class DiffBlock
602
+ attr_accessor :parts
603
+
604
+ def initialize(container)
605
+ @parts = []
606
+ container << self
607
+ end
608
+
609
+ def to_html
610
+ str = "<div class='DiffBlock'>\n"
611
+ str += @parts.collect{ |part| part.to_html }.join
612
+ str += "<div class='clear_float'></div></div>\n"
613
+ end
614
+ end
615
+
616
+ class DiffBlockPart
617
+ attr_reader :className
618
+ attr :lines
619
+
620
+ def initialize(className, container)
621
+ $last_prettify_part_count[className] += 1
622
+ @className = className
623
+ @lines = []
624
+ container.parts << self
625
+ end
626
+
627
+ def to_html
628
+ str = "<div class='DiffBlockPart %s'>\n" % @className
629
+ str += @lines.collect{ |line| line.to_html }.join
630
+ # Don't put white-space after this so adjacent inline-block DiffBlockParts will not wrap.
631
+ str += "</div>"
632
+ end
633
+ end
634
+
635
+ class DiffSection
636
+ def initialize(lines)
637
+ lines.length >= 1 or raise "DiffSection.parse only received %d lines" % lines.length
638
+
639
+ matches = START_OF_SECTION_FORMAT.match(lines[0])
640
+
641
+ if matches
642
+ from, to = [matches[1].to_i, matches[3].to_i]
643
+ if matches[2] and matches[4]
644
+ from_end = from + matches[2].to_i
645
+ to_end = to + matches[4].to_i
646
+ end
647
+ end
648
+
649
+ @blocks = []
650
+ diff_block = nil
651
+ diff_block_part = nil
652
+
653
+ for line in lines[1...lines.length]
654
+ startOfLine = line =~ /^[-\+ ]/ ? 1 : 0
655
+ text = line[startOfLine...line.length].chomp
656
+ case line[0]
657
+ when ?-
658
+ if (diff_block_part.nil? or diff_block_part.className != 'remove')
659
+ diff_block = DiffBlock.new(@blocks)
660
+ diff_block_part = DiffBlockPart.new('remove', diff_block)
661
+ end
662
+
663
+ diff_block_part.lines << CodeLine.new(from, nil, text)
664
+ from += 1 unless from.nil?
665
+ when ?+
666
+ if (diff_block_part.nil? or diff_block_part.className != 'add')
667
+ # Put add lines that immediately follow remove lines into the same DiffBlock.
668
+ if (diff_block.nil? or diff_block_part.className != 'remove')
669
+ diff_block = DiffBlock.new(@blocks)
670
+ end
671
+
672
+ diff_block_part = DiffBlockPart.new('add', diff_block)
673
+ end
674
+
675
+ diff_block_part.lines << CodeLine.new(nil, to, text)
676
+ to += 1 unless to.nil?
677
+ else
678
+ if (diff_block_part.nil? or diff_block_part.className != 'shared')
679
+ diff_block = DiffBlock.new(@blocks)
680
+ diff_block_part = DiffBlockPart.new('shared', diff_block)
681
+ end
682
+
683
+ diff_block_part.lines << CodeLine.new(from, to, text)
684
+ from += 1 unless from.nil?
685
+ to += 1 unless to.nil?
686
+ end
687
+
688
+ break if from_end and to_end and from == from_end and to == to_end
689
+ end
690
+
691
+ changes = [ [ [], [] ] ]
692
+ for block in @blocks
693
+ for block_part in block.parts
694
+ for line in block_part.lines
695
+ if (!line.fromLineNumber.nil? and !line.toLineNumber.nil?) then
696
+ changes << [ [], [] ]
697
+ next
698
+ end
699
+ changes.last.first << line if line.toLineNumber.nil?
700
+ changes.last.last << line if line.fromLineNumber.nil?
701
+ end
702
+ end
703
+ end
704
+
705
+ for change in changes
706
+ next unless change.first.length == change.last.length
707
+ for i in (0...change.first.length)
708
+ from_text = change.first[i].text
709
+ to_text = change.last[i].text
710
+ next if from_text.length > MAXIMUM_INTRALINE_DIFF_LINE_LENGTH or to_text.length > MAXIMUM_INTRALINE_DIFF_LINE_LENGTH
711
+ raw_operations = HTMLDiff::DiffBuilder.new(from_text, to_text).operations
712
+ operations = []
713
+ back = 0
714
+ raw_operations.each_with_index do |operation, j|
715
+ if operation.action == :equal and j < raw_operations.length - 1
716
+ length = operation.end_in_new - operation.start_in_new
717
+ if length < SMALLEST_EQUAL_OPERATION
718
+ back = length
719
+ next
720
+ end
721
+ end
722
+ operation.start_in_old -= back
723
+ operation.start_in_new -= back
724
+ back = 0
725
+ operations << operation
726
+ end
727
+ change.first[i].operations = operations
728
+ change.last[i].operations = operations
729
+ end
730
+ end
731
+
732
+ @blocks.unshift(ContextLine.new(matches[5])) unless matches.nil? || matches[5].empty?
733
+ end
734
+
735
+ def to_html
736
+ str = "<div class='DiffSection'>\n"
737
+ str += @blocks.collect{ |block| block.to_html }.join
738
+ str += "</div>\n"
739
+ end
740
+
741
+ def self.parse(lines)
742
+ linesForSections = lines.inject([[]]) do |sections, line|
743
+ sections << [] if line =~ /^@@/
744
+ sections.last << line
745
+ sections
746
+ end
747
+
748
+ linesForSections.delete_if { |lines| lines.nil? or lines.empty? }
749
+ linesForSections.collect { |lines| DiffSection.new(lines) }
750
+ end
751
+ end
752
+
753
+ class Line
754
+ attr_reader :fromLineNumber
755
+ attr_reader :toLineNumber
756
+ attr_reader :text
757
+
758
+ def initialize(from, to, text)
759
+ @fromLineNumber = from
760
+ @toLineNumber = to
761
+ @text = text
762
+ end
763
+
764
+ def text_as_html
765
+ CGI.escapeHTML(text)
766
+ end
767
+
768
+ def classes
769
+ lineClasses = ["Line", "LineContainer"]
770
+ lineClasses << ["add"] unless @toLineNumber.nil? or !@fromLineNumber.nil?
771
+ lineClasses << ["remove"] unless @fromLineNumber.nil? or !@toLineNumber.nil?
772
+ lineClasses
773
+ end
774
+
775
+ def to_html
776
+ markedUpText = self.text_as_html
777
+ str = "<div class='%s'>\n" % self.classes.join(' ')
778
+ str += "<span class='from lineNumber'>%s</span><span class='to lineNumber'>%s</span>" %
779
+ [@fromLineNumber.nil? ? '&nbsp;' : @fromLineNumber,
780
+ @toLineNumber.nil? ? '&nbsp;' : @toLineNumber] unless @fromLineNumber.nil? and @toLineNumber.nil?
781
+ str += "<span class='text'>%s</span>\n" % markedUpText
782
+ str += "</div>\n"
783
+ end
784
+ end
785
+
786
+ class CodeLine < Line
787
+ attr :operations, true
788
+
789
+ def text_as_html
790
+ html = []
791
+ tag = @fromLineNumber.nil? ? "ins" : "del"
792
+ if @operations.nil? or @operations.empty?
793
+ return CGI.escapeHTML(@text)
794
+ end
795
+ @operations.each do |operation|
796
+ start = @fromLineNumber.nil? ? operation.start_in_new : operation.start_in_old
797
+ eend = @fromLineNumber.nil? ? operation.end_in_new : operation.end_in_old
798
+ escaped_text = CGI.escapeHTML(@text[start...eend])
799
+ if eend - start === 0 or operation.action === :equal
800
+ html << escaped_text
801
+ else
802
+ html << "<#{tag}>#{escaped_text}</#{tag}>"
803
+ end
804
+ end
805
+ html.join
806
+ end
807
+ end
808
+
809
+ class ContextLine < Line
810
+ def initialize(context)
811
+ super("@", "@", context)
812
+ end
813
+
814
+ def classes
815
+ super << "context"
816
+ end
817
+ end
818
+ end
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/ruby
2
+
3
+ require 'test/unit'
4
+ require 'open-uri'
5
+ require 'PrettyPatch'
6
+
7
+ # Note: internet connection is needed to run this test suite.
8
+
9
+ class PrettyPatch_test < Test::Unit::TestCase
10
+ class Info
11
+ TITLE = 0
12
+ FILE = 1
13
+ ADD = 2
14
+ REMOVE = 3
15
+ SHARED = 4
16
+ end
17
+
18
+ PATCHES = {
19
+ 20510 => ["Single change", 1, 1, 0, 2],
20
+ 20528 => ["No 'Index' or 'diff' in patch header", 1, 4, 3, 7],
21
+ 21151 => ["Leading '/' in the path of files", 4, 9, 1, 16],
22
+ # Binary files use shared blocks, there are three in 30488.
23
+ 30488 => ["Quoted filenames in git diff", 23, 28, 25, 64 + 3],
24
+ 23920 => ["Mac line ending", 3, 3, 0, 5],
25
+ 39615 => ["Git signature", 2, 2, 0, 3],
26
+ 80852 => ["Changes one line plus ChangeLog", 2, 2, 1, 4],
27
+ 83127 => ["Only add stuff", 2, 2, 0, 3],
28
+ 85071 => ["Adds and removes from a file plus git signature", 2, 5, 3, 9],
29
+ 106368 => ["Images with git delta binary patch", 69, 8, 23, 10],
30
+ }
31
+
32
+ def get_patch_uri(id)
33
+ "https://bugs.webkit.org/attachment.cgi?id=" + id.to_s
34
+ end
35
+
36
+ def get_patch(id)
37
+ result = nil
38
+ patch_uri = get_patch_uri(id)
39
+ begin
40
+ result = open(patch_uri) { |f| result = f.read }
41
+ rescue => exception
42
+ assert(false, "Fail to get patch " + patch_uri)
43
+ end
44
+ result
45
+ end
46
+
47
+ def check_one_patch(id, info)
48
+ patch = get_patch(id)
49
+ description = get_patch_uri(id)
50
+ description += " (" + info[Info::TITLE] + ")" unless info[Info::TITLE].nil?
51
+ puts "Testing " + description
52
+ pretty = nil
53
+ assert_nothing_raised("Crash while prettifying " + description) {
54
+ pretty = PrettyPatch.prettify(patch)
55
+ }
56
+ assert(pretty, "Empty result while prettifying " + description)
57
+ assert_equal(info[Info::FILE], $last_prettify_file_count, "Wrong number of files changed in " + description)
58
+ assert_equal(info[Info::ADD], $last_prettify_part_count["add"], "Wrong number of 'add' parts in " + description)
59
+ assert_equal(info[Info::REMOVE], $last_prettify_part_count["remove"], "Wrong number of 'remove' parts in " + description)
60
+ assert_equal(info[Info::SHARED], $last_prettify_part_count["shared"], "Wrong number of 'shared' parts in " + description)
61
+ assert_equal(0, $last_prettify_part_count["binary"], "Wrong number of 'binary' parts in " + description)
62
+ assert_equal(0, $last_prettify_part_count["extract-error"], "Wrong number of 'extract-error' parts in " + description)
63
+ return pretty
64
+ end
65
+
66
+ def test_patches
67
+ PATCHES.each { |id, info| check_one_patch(id, info) }
68
+ end
69
+
70
+ def test_images_without_checksum
71
+ pretty = check_one_patch(144064, ["Images without checksums", 10, 5, 4, 8])
72
+ matches = pretty.match("INVALID: Image lacks a checksum.")
73
+ # FIXME: This should match, but there's a bug when running the tests where the image data
74
+ # doesn't get properly written out to the temp files, so there is no image and we don't print
75
+ # the warning that the image is missing its checksum.
76
+ assert(!matches, "Should have invalid checksums")
77
+ # FIXME: This should only have 4 invalid images, but due to the above tempfile issue, there are 0.
78
+ assert_equal(0, pretty.scan(/INVALID\: Image lacks a checksum\./).size)
79
+ end
80
+
81
+ def test_new_image
82
+ pretty = check_one_patch(145881, ["New image", 19, 36, 19, 56])
83
+ matches = pretty.match("INVALID: Image lacks a checksum.")
84
+ assert(!matches, "Should not have invalid checksums")
85
+ end
86
+
87
+ def test_images_correctly_without_checksum_git
88
+ pretty = check_one_patch(101620, ["Images correctly without checksums git", 7, 15, 10, 26])
89
+ matches = pretty.match("INVALID: Image lacks a checksum.")
90
+ assert(!matches, "Png should lack a checksum without an error.")
91
+ end
92
+
93
+ def test_images_correctly_without_checksum_svn
94
+ pretty = check_one_patch(31202, ["Images correctly without checksums svn", 4, 4, 1, 4])
95
+ matches = pretty.match("INVALID: Image lacks a checksum.")
96
+ assert(!matches, "Png should lack a checksum without an error.")
97
+ end
98
+
99
+ end
@@ -0,0 +1,164 @@
1
+ module HTMLDiff
2
+
3
+ Match = Struct.new(:start_in_old, :start_in_new, :size)
4
+ class Match
5
+ def end_in_old
6
+ self.start_in_old + self.size
7
+ end
8
+
9
+ def end_in_new
10
+ self.start_in_new + self.size
11
+ end
12
+ end
13
+
14
+ Operation = Struct.new(:action, :start_in_old, :end_in_old, :start_in_new, :end_in_new)
15
+
16
+ class DiffBuilder
17
+
18
+ def initialize(old_version, new_version)
19
+ @old_version, @new_version = old_version, new_version
20
+ split_inputs_to_words
21
+ index_new_words
22
+ end
23
+
24
+ def split_inputs_to_words
25
+ @old_words = explode(@old_version)
26
+ @new_words = explode(@new_version)
27
+ end
28
+
29
+ def index_new_words
30
+ @word_indices = Hash.new { |h, word| h[word] = [] }
31
+ @new_words.each_with_index { |word, i| @word_indices[word] << i }
32
+ end
33
+
34
+ def operations
35
+ position_in_old = position_in_new = 0
36
+ operations = []
37
+
38
+ matches = matching_blocks
39
+ # an empty match at the end forces the loop below to handle the unmatched tails
40
+ # I'm sure it can be done more gracefully, but not at 23:52
41
+ matches << Match.new(@old_words.length, @new_words.length, 0)
42
+
43
+ matches.each_with_index do |match, i|
44
+ match_starts_at_current_position_in_old = (position_in_old == match.start_in_old)
45
+ match_starts_at_current_position_in_new = (position_in_new == match.start_in_new)
46
+
47
+ action_upto_match_positions =
48
+ case [match_starts_at_current_position_in_old, match_starts_at_current_position_in_new]
49
+ when [false, false]
50
+ :replace
51
+ when [true, false]
52
+ :insert
53
+ when [false, true]
54
+ :delete
55
+ else
56
+ # this happens if the first few words are same in both versions
57
+ :none
58
+ end
59
+
60
+ if action_upto_match_positions != :none
61
+ operation_upto_match_positions =
62
+ Operation.new(action_upto_match_positions,
63
+ position_in_old, match.start_in_old,
64
+ position_in_new, match.start_in_new)
65
+ operations << operation_upto_match_positions
66
+ end
67
+ if match.size != 0
68
+ match_operation = Operation.new(:equal,
69
+ match.start_in_old, match.end_in_old,
70
+ match.start_in_new, match.end_in_new)
71
+ operations << match_operation
72
+ end
73
+
74
+ position_in_old = match.end_in_old
75
+ position_in_new = match.end_in_new
76
+ end
77
+
78
+ operations
79
+ end
80
+
81
+ def matching_blocks
82
+ matching_blocks = []
83
+ recursively_find_matching_blocks(0, @old_words.size, 0, @new_words.size, matching_blocks)
84
+ matching_blocks
85
+ end
86
+
87
+ def recursively_find_matching_blocks(start_in_old, end_in_old, start_in_new, end_in_new, matching_blocks)
88
+ match = find_match(start_in_old, end_in_old, start_in_new, end_in_new)
89
+ if match
90
+ if start_in_old < match.start_in_old and start_in_new < match.start_in_new
91
+ recursively_find_matching_blocks(
92
+ start_in_old, match.start_in_old, start_in_new, match.start_in_new, matching_blocks)
93
+ end
94
+ matching_blocks << match
95
+ if match.end_in_old < end_in_old and match.end_in_new < end_in_new
96
+ recursively_find_matching_blocks(
97
+ match.end_in_old, end_in_old, match.end_in_new, end_in_new, matching_blocks)
98
+ end
99
+ end
100
+ end
101
+
102
+ def find_match(start_in_old, end_in_old, start_in_new, end_in_new)
103
+
104
+ best_match_in_old = start_in_old
105
+ best_match_in_new = start_in_new
106
+ best_match_size = 0
107
+
108
+ match_length_at = Hash.new { |h, index| h[index] = 0 }
109
+
110
+ start_in_old.upto(end_in_old - 1) do |index_in_old|
111
+
112
+ new_match_length_at = Hash.new { |h, index| h[index] = 0 }
113
+
114
+ @word_indices[@old_words[index_in_old]].each do |index_in_new|
115
+ next if index_in_new < start_in_new
116
+ break if index_in_new >= end_in_new
117
+
118
+ new_match_length = match_length_at[index_in_new - 1] + 1
119
+ new_match_length_at[index_in_new] = new_match_length
120
+
121
+ if new_match_length > best_match_size
122
+ best_match_in_old = index_in_old - new_match_length + 1
123
+ best_match_in_new = index_in_new - new_match_length + 1
124
+ best_match_size = new_match_length
125
+ end
126
+ end
127
+ match_length_at = new_match_length_at
128
+ end
129
+
130
+ # best_match_in_old, best_match_in_new, best_match_size = add_matching_words_left(
131
+ # best_match_in_old, best_match_in_new, best_match_size, start_in_old, start_in_new)
132
+ # best_match_in_old, best_match_in_new, match_size = add_matching_words_right(
133
+ # best_match_in_old, best_match_in_new, best_match_size, end_in_old, end_in_new)
134
+
135
+ return (best_match_size != 0 ? Match.new(best_match_in_old, best_match_in_new, best_match_size) : nil)
136
+ end
137
+
138
+ def add_matching_words_left(match_in_old, match_in_new, match_size, start_in_old, start_in_new)
139
+ while match_in_old > start_in_old and
140
+ match_in_new > start_in_new and
141
+ @old_words[match_in_old - 1] == @new_words[match_in_new - 1]
142
+ match_in_old -= 1
143
+ match_in_new -= 1
144
+ match_size += 1
145
+ end
146
+ [match_in_old, match_in_new, match_size]
147
+ end
148
+
149
+ def add_matching_words_right(match_in_old, match_in_new, match_size, end_in_old, end_in_new)
150
+ while match_in_old + match_size < end_in_old and
151
+ match_in_new + match_size < end_in_new and
152
+ @old_words[match_in_old + match_size] == @new_words[match_in_new + match_size]
153
+ match_size += 1
154
+ end
155
+ [match_in_old, match_in_new, match_size]
156
+ end
157
+
158
+ def explode(sequence)
159
+ sequence.is_a?(String) ? sequence.split(//) : sequence
160
+ end
161
+
162
+ end # of class Diff Builder
163
+
164
+ end
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'optparse'
4
+ require 'pathname'
5
+ require 'webrick/htmlutils'
6
+
7
+ $LOAD_PATH << Pathname.new(__FILE__).dirname.realpath.to_s
8
+
9
+ require 'PrettyPatch'
10
+
11
+ BACKTRACE_SEPARATOR = "\n\tfrom "
12
+
13
+ options = { :html_exceptions => false }
14
+ OptionParser.new do |opts|
15
+ opts.banner = "Usage: #{File.basename($0)} [options] [patch-file]"
16
+
17
+ opts.separator ""
18
+
19
+ opts.on("--html-exceptions", "Print exceptions to stdout as HTML") { |h| options[:html_exceptions] = h }
20
+ end.parse!
21
+
22
+ patch_data = nil
23
+ if ARGV.length == 0 || ARGV[0] == '-' then
24
+ patch_data = $stdin.read
25
+ else
26
+ File.open(ARGV[0]) { |file| patch_data = file.read }
27
+ end
28
+
29
+ begin
30
+ puts PrettyPatch.prettify(patch_data)
31
+ rescue => exception
32
+ raise unless options[:html_exceptions]
33
+
34
+ backtrace = exception.backtrace
35
+ backtrace[0] += ": " + exception + " (" + exception.class.to_s + ")"
36
+ print "<pre>\n", WEBrick::HTMLUtils::escape(backtrace.join(BACKTRACE_SEPARATOR)), "\n</pre>\n"
37
+ end
@@ -0,0 +1,3 @@
1
+ module PurdyPatch
2
+ VERSION = "0.0.1"
3
+ end
data/lib/purdypatch.rb ADDED
@@ -0,0 +1,44 @@
1
+ require 'mail'
2
+ require 'pathname'
3
+ require 'purdypatch/version'
4
+ require 'prettypatch/PrettyPatch'
5
+
6
+ module PurdyPatch
7
+ class CLI
8
+ def self.start(*args)
9
+ if args.empty?
10
+ puts "purdypatch expects one or more files created via `git format-patch --attach [ <since> | <revision range> ]`."
11
+ exit
12
+ end
13
+
14
+ args.each do |arg|
15
+ patch_mail_path = Pathname.new(arg)
16
+ next unless patch_mail_path.exist?
17
+
18
+ mail = Mail.read(patch_mail_path)
19
+ parts = mail.parts.dup
20
+ purdified = false
21
+ parts.each_with_index do |part, i|
22
+ next unless part.attachment? && part.content_type =~ %r(text/x-patch)
23
+
24
+ purdy_part = Mail::Part.new do
25
+ content_type 'text/html; charset=UTF-8'
26
+ body PrettyPatch.prettify(part.body.decoded)
27
+ end
28
+ mail.parts.insert(i, purdy_part)
29
+ mail.header['X-Formatter'] = "PurdyPatch-#{PurdyPatch::VERSION}"
30
+ purdified = true
31
+ end
32
+
33
+ if purdified
34
+ purdy_patch_mail_path = patch_mail_path.sub_ext('.purdypatch')
35
+ File.open(purdy_patch_mail_path, File::CREAT | File::TRUNC | File::RDWR) do |f|
36
+ f.write mail.to_s
37
+ end
38
+ puts %Q(Purdified patch email saved to "#{purdy_patch_mail_path}".)
39
+ end
40
+
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,20 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'purdypatch/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "purdypatch"
8
+ gem.version = PurdyPatch::VERSION
9
+ gem.authors = ["Nathan de Vries"]
10
+ gem.email = ["nathan@atnan.com"]
11
+ gem.description = %q{A tool for improving the format of git-format-patch emails, making them easier to review.}
12
+ gem.summary = %q{Make your git-format-patch emails all purdy like.}
13
+ gem.homepage = "https://github.com/atnan/purdypatch"
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.require_paths = ["lib", "lib/prettypatch"]
18
+
19
+ gem.add_dependency "mail"
20
+ end
metadata ADDED
@@ -0,0 +1,77 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: purdypatch
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Nathan de Vries
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-11-07 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: mail
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ description: A tool for improving the format of git-format-patch emails, making them
31
+ easier to review.
32
+ email:
33
+ - nathan@atnan.com
34
+ executables:
35
+ - purdypatch
36
+ extensions: []
37
+ extra_rdoc_files: []
38
+ files:
39
+ - .gitignore
40
+ - Gemfile
41
+ - LICENSE.txt
42
+ - README.md
43
+ - Rakefile
44
+ - bin/purdypatch
45
+ - lib/prettypatch/PrettyPatch.rb
46
+ - lib/prettypatch/PrettyPatch_test.rb
47
+ - lib/prettypatch/diff.rb
48
+ - lib/prettypatch/prettify.rb
49
+ - lib/purdypatch.rb
50
+ - lib/purdypatch/version.rb
51
+ - purdypatch.gemspec
52
+ homepage: https://github.com/atnan/purdypatch
53
+ licenses: []
54
+ post_install_message:
55
+ rdoc_options: []
56
+ require_paths:
57
+ - lib
58
+ - lib/prettypatch
59
+ required_ruby_version: !ruby/object:Gem::Requirement
60
+ none: false
61
+ requirements:
62
+ - - ! '>='
63
+ - !ruby/object:Gem::Version
64
+ version: '0'
65
+ required_rubygems_version: !ruby/object:Gem::Requirement
66
+ none: false
67
+ requirements:
68
+ - - ! '>='
69
+ - !ruby/object:Gem::Version
70
+ version: '0'
71
+ requirements: []
72
+ rubyforge_project:
73
+ rubygems_version: 1.8.23
74
+ signing_key:
75
+ specification_version: 3
76
+ summary: Make your git-format-patch emails all purdy like.
77
+ test_files: []