purdypatch 0.0.1

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 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: []