382b4969d836cdea025e5909682af81e1ce05dfe
[WebKit-https.git] / Websites / bugs.webkit.org / PrettyPatch / PrettyPatch.rb
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         # Newly added images get two diffs with svn 1.7; toss the first one.
36         deleteIndices = []
37         for i in 1...fileDiffs.length
38             prev = i - 1
39             if fileDiffs[prev].image and not fileDiffs[prev].image_url and fileDiffs[i].image and fileDiffs[i].image_url and fileDiffs[prev].filename == fileDiffs[i].filename
40                 deleteIndices.unshift(prev)
41             end
42         end
43         deleteIndices.each{ |i| fileDiffs.delete_at(i) }
44
45         $last_prettify_file_count = fileDiffs.length
46         str << fileDiffs.collect{ |diff| diff.to_html }.join
47         str << "</body></html>"
48     end
49
50     def self.filename_from_diff_header(line)
51         DIFF_HEADER_FORMATS.each do |format|
52             match = format.match(line)
53             return match[1] unless match.nil?
54         end
55         nil
56     end
57
58     def self.diff_header?(line)
59         RELAXED_DIFF_HEADER_FORMATS.any? { |format| line =~ format }
60     end
61
62 private
63     DIFF_HEADER_FORMATS = [
64         /^Index: (.*)\r?$/,
65         /^diff --git "?a\/.+"? "?b\/(.+)"?\r?$/,
66         /^\+\+\+ ([^\t]+)(\t.*)?\r?$/
67     ]
68
69     RELAXED_DIFF_HEADER_FORMATS = [
70         /^Index:/,
71         /^diff/
72     ]
73
74     RENAME_FROM = /^rename from (.*)/
75
76     SVN_BINARY_FILE_MARKER_FORMAT = /^Cannot display: file marked as a binary type.$/
77
78     SVN_IMAGE_FILE_MARKER_FORMAT = /^svn:mime-type = image\/png$/
79
80     SVN_PROPERTY_CHANGES_FORMAT = /^Property changes on: (.*)/
81
82     GIT_INDEX_MARKER_FORMAT = /^index ([0-9a-f]{40})\.\.([0-9a-f]{40})/
83
84     GIT_BINARY_FILE_MARKER_FORMAT = /^GIT binary patch$/
85
86     GIT_BINARY_PATCH_FORMAT = /^(literal|delta) \d+$/
87
88     GIT_LITERAL_FORMAT = /^literal \d+$/
89
90     GIT_DELTA_FORMAT = /^delta \d+$/
91
92     SVN_START_OF_BINARY_DATA_FORMAT = /^[0-9a-zA-Z\+\/=]{20,}/ # Assume 20 chars without a space is base64 binary data.
93
94     START_OF_SECTION_FORMAT = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@\s*(.*)/
95
96     START_OF_EXTENT_STRING = "%c" % 0
97     END_OF_EXTENT_STRING = "%c" % 1
98
99     # We won't search for intra-line diffs in lines longer than this length, to avoid hangs. See <http://webkit.org/b/56109>.
100     MAXIMUM_INTRALINE_DIFF_LINE_LENGTH = 10000
101
102     SMALLEST_EQUAL_OPERATION = 3
103
104     OPENSOURCE_TRAC_URL = "http://trac.webkit.org/"
105
106     OPENSOURCE_DIRS = Set.new %w[
107         Examples
108         LayoutTests
109         PerformanceTests
110         Source
111         Tools
112         WebKitLibraries
113         Websites
114     ]
115
116     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."
117
118     def self.normalize_line_ending(s)
119         if RUBY_VERSION >= "1.9"
120             # Transliteration table from http://stackoverflow.com/a/6609998
121             transliteration_table = { '\xc2\x82' => ',',        # High code comma
122                                       '\xc2\x84' => ',,',       # High code double comma
123                                       '\xc2\x85' => '...',      # Tripple dot
124                                       '\xc2\x88' => '^',        # High carat
125                                       '\xc2\x91' => '\x27',     # Forward single quote
126                                       '\xc2\x92' => '\x27',     # Reverse single quote
127                                       '\xc2\x93' => '\x22',     # Forward double quote
128                                       '\xc2\x94' => '\x22',     # Reverse double quote
129                                       '\xc2\x95' => ' ',
130                                       '\xc2\x96' => '-',        # High hyphen
131                                       '\xc2\x97' => '--',       # Double hyphen
132                                       '\xc2\x99' => ' ',
133                                       '\xc2\xa0' => ' ',
134                                       '\xc2\xa6' => '|',        # Split vertical bar
135                                       '\xc2\xab' => '<<',       # Double less than
136                                       '\xc2\xbb' => '>>',       # Double greater than
137                                       '\xc2\xbc' => '1/4',      # one quarter
138                                       '\xc2\xbd' => '1/2',      # one half
139                                       '\xc2\xbe' => '3/4',      # three quarters
140                                       '\xca\xbf' => '\x27',     # c-single quote
141                                       '\xcc\xa8' => '',         # modifier - under curve
142                                       '\xcc\xb1' => ''          # modifier - under line
143                                    }
144             encoded_string = s.force_encoding('UTF-8').encode('UTF-16', :invalid => :replace, :replace => '', :fallback => transliteration_table).encode('UTF-8')
145             encoded_string.gsub /\r\n?/, "\n"
146         else
147             s.gsub /\r\n?/, "\n"
148         end
149     end
150
151     def self.find_url_and_path(file_path)
152         # Search file_path from the bottom up, at each level checking whether
153         # we've found a directory we know exists in the source tree.
154
155         dirname, basename = File.split(file_path)
156         dirname.split(/\//).reverse.inject(basename) do |path, directory|
157             path = directory + "/" + path
158
159             return [OPENSOURCE_TRAC_URL, path] if OPENSOURCE_DIRS.include?(directory)
160
161             path
162         end
163
164         [nil, file_path]
165     end
166
167     def self.linkifyFilename(filename, force)
168         if force
169           "<a href='#{OPENSOURCE_TRAC_URL}browser/trunk/#{filename}'>#{filename}</a>"
170         else
171           url, pathBeneathTrunk = find_url_and_path(filename)
172           url.nil? ? filename : "<a href='#{url}browser/trunk/#{pathBeneathTrunk}'>#{filename}</a>"
173         end
174     end
175
176
177     HEADER =<<EOF
178 <html>
179 <head>
180 <meta charset='utf-8'>
181 <style>
182 :link, :visited {
183     text-decoration: none;
184     border-bottom: 1px dotted;
185 }
186
187 :link {
188     color: #039;
189 }
190
191 .FileDiff {
192     background-color: #f8f8f8;
193     border: 1px solid #ddd;
194     font-family: monospace;
195     margin: 1em 0;
196     position: relative;
197 }
198
199 h1 {
200     color: #333;
201     font-family: sans-serif;
202     font-size: 1em;
203     margin-left: 0.5em;
204     display: inline;
205     width: 100%;
206     padding: 0.5em;
207 }
208
209 h1 :link, h1 :visited {
210     color: inherit;
211 }
212
213 h1 :hover {
214     color: #555;
215     background-color: #eee;
216 }
217
218 .DiffLinks {
219     float: right;
220 }
221
222 .FileDiffLinkContainer {
223     opacity: 0;
224     display: table-cell;
225     padding-right: 0.5em;
226     white-space: nowrap;
227 }
228
229 .DiffSection {
230     background-color: white;
231     border: solid #ddd;
232     border-width: 1px 0px;
233 }
234
235 .ExpansionLine, .LineContainer {
236     white-space: nowrap;
237 }
238
239 .sidebyside .DiffBlockPart.add:first-child {
240     float: right;
241 }
242
243 .LineSide,
244 .sidebyside .DiffBlockPart.remove,
245 .sidebyside .DiffBlockPart.add {
246     display:inline-block;
247     width: 50%;
248     vertical-align: top;
249 }
250
251 .sidebyside .resizeHandle {
252     width: 5px;
253     height: 100%;
254     cursor: move;
255     position: absolute;
256     top: 0;
257     left: 50%;
258 }
259
260 .sidebyside .resizeHandle:hover {
261     background-color: grey;
262     opacity: 0.5;
263 }
264
265 .sidebyside .DiffBlockPart.remove .to,
266 .sidebyside .DiffBlockPart.add .from {
267     display: none;
268 }
269
270 .lineNumber, .expansionLineNumber {
271     border-bottom: 1px solid #998;
272     border-right: 1px solid #ddd;
273     color: #444;
274     display: inline-block;
275     padding: 1px 5px 0px 0px;
276     text-align: right;
277     vertical-align: bottom;
278     width: 3em;
279 }
280
281 .lineNumber {
282   background-color: #eed;
283 }
284
285 .expansionLineNumber {
286   background-color: #eee;
287 }
288
289 pre, .text {
290     padding-left: 5px;
291     white-space: pre-wrap;
292     word-wrap: break-word;
293 }
294
295 .image {
296     border: 2px solid black;
297 }
298
299 .context, .context .lineNumber {
300     color: #849;
301     background-color: #fef;
302 }
303
304 .Line.add, .FileDiff .add {
305     background-color: #dfd;
306 }
307
308 .Line.add ins {
309     background-color: #9e9;
310     text-decoration: none;
311 }
312
313 .Line.remove, .FileDiff .remove {
314     background-color: #fdd;
315 }
316
317 .Line.remove del {
318     background-color: #e99;
319     text-decoration: none;
320 }
321
322 /* Support for inline comments */
323
324 .author {
325   font-style: italic;
326 }
327
328 .comment {
329   position: relative;
330 }
331
332 .comment textarea {
333   height: 6em;
334 }
335
336 .overallComments textarea {
337   height: 2em;
338   max-width: 100%;
339   min-width: 200px;
340 }
341
342 .comment textarea, .overallComments textarea {
343   display: block;
344   width: 100%;
345 }
346
347 .overallComments .open {
348   -webkit-transition: height .2s;
349   height: 4em;
350 }
351
352 #statusBubbleContainer.wrap {
353   display: block;
354 }
355
356 #toolbar {
357   display: -webkit-flex;
358   display: -moz-flex;
359   padding: 3px;
360   left: 0;
361   right: 0;
362   border: 1px solid #ddd;
363   background-color: #eee;
364   font-family: sans-serif;
365   position: fixed;
366   bottom: 0;
367 }
368
369 #toolbar .actions {
370   float: right;
371 }
372
373 .winter {
374   position: fixed;
375   z-index: 5;
376   left: 0;
377   right: 0;
378   top: 0;
379   bottom: 0;
380   background-color: black;
381   opacity: 0.8;
382 }
383
384 .inactive {
385   display: none;
386 }
387
388 .lightbox {
389   position: fixed;
390   z-index: 6;
391   left: 10%;
392   right: 10%;
393   top: 10%;
394   bottom: 10%;
395   background: white;
396 }
397
398 .lightbox iframe {
399   width: 100%;
400   height: 100%;
401 }
402
403 .commentContext .lineNumber {
404   background-color: yellow;
405 }
406
407 .selected .lineNumber {
408   background-color: #69F;
409   border-bottom-color: #69F;
410   border-right-color: #69F;
411 }
412
413 .ExpandLinkContainer {
414   opacity: 0;
415   border-top: 1px solid #ddd;
416   border-bottom: 1px solid #ddd;
417 }
418
419 .ExpandArea {
420   margin: 0;
421 }
422
423 .ExpandText {
424   margin-left: 0.67em;
425 }
426
427 .LinkContainer {
428   font-family: sans-serif;
429   font-size: small;
430   font-style: normal;
431   -webkit-transition: opacity 0.5s;
432 }
433
434 .LinkContainer a {
435   border: 0;
436 }
437
438 .LinkContainer label:after,
439 .LinkContainer a:after {
440   content: " | ";
441   color: black;
442 }
443
444 .LinkContainer a:last-of-type:after {
445   content: "";
446 }
447
448 .LinkContainer label {
449   color: #039;
450 }
451
452 .help {
453  color: gray;
454  font-style: italic;
455 }
456
457 #message {
458   font-size: small;
459   font-family: sans-serif;
460 }
461
462 .commentStatus {
463   font-style: italic;
464 }
465
466 .comment, .previousComment, .frozenComment {
467   background-color: #ffd;
468 }
469
470 .overallComments {
471   -webkit-flex: 1;
472   -moz-flex: 1;
473   margin-right: 3px;
474 }
475
476 .previousComment, .frozenComment {
477   border: inset 1px;
478   padding: 5px;
479   white-space: pre-wrap;
480 }
481
482 .comment button {
483   width: 6em;
484 }
485
486 div:focus {
487   outline: 1px solid blue;
488   outline-offset: -1px;
489 }
490
491 .statusBubble {
492   /* The width/height get set to the bubble contents via postMessage on browsers that support it. */
493   width: 460px;
494   height: 20px;
495   margin: 2px 2px 0 0;
496   border: none;
497   vertical-align: middle;
498 }
499
500 .revision {
501   display: none;
502 }
503
504 .autosave-state {
505   position: absolute;
506   right: 0;
507   top: -1.3em;
508   padding: 0 3px;
509   outline: 1px solid #DDD;
510   color: #8FDF5F;
511   font-size: small;   
512   background-color: #EEE;
513 }
514
515 .autosave-state:empty {
516   outline: 0px;
517 }
518 .autosave-state.saving {
519   color: #E98080;
520 }
521
522 .clear_float {
523     clear: both;
524 }
525 </style>
526 <script src="https://webkit.org/ajax/libs/jquery/jquery-1.4.2.min.js"></script> 
527 <script src="code-review.js?version=48"></script>
528 </head>
529 EOF
530
531     def self.revisionOrDescription(string)
532         case string
533         when /\(revision \d+\)/
534             /\(revision (\d+)\)/.match(string)[1]
535         when /\(.*\)/
536             /\((.*)\)/.match(string)[1]
537         end
538     end
539
540     def self.has_image_suffix(filename)
541         filename =~ /\.(png|jpg|gif)$/
542     end
543
544     class FileDiff
545         attr_reader :filename
546         attr_reader :image
547         attr_reader :image_url
548
549         def initialize(lines)
550             @filename = PrettyPatch.filename_from_diff_header(lines[0].chomp)
551             startOfSections = 1
552             for i in 0...lines.length
553                 case lines[i]
554                 when /^--- /
555                     @from = PrettyPatch.revisionOrDescription(lines[i])
556                 when /^\+\+\+ /
557                     @filename = PrettyPatch.filename_from_diff_header(lines[i].chomp) if @filename.nil?
558                     @to = PrettyPatch.revisionOrDescription(lines[i])
559                     startOfSections = i + 1
560
561                     # Check for 'property' patch, then image data, since svn 1.7 creates a fake patch for property changes.
562                     if /^$/.match(lines[startOfSections]) and SVN_PROPERTY_CHANGES_FORMAT.match(lines[startOfSections + 1]) then
563                         startOfSections += 2
564                         for x in startOfSections...lines.length
565                             next if not /^$/.match(lines[x])
566                             if SVN_START_OF_BINARY_DATA_FORMAT.match(lines[x + 1]) then
567                                 startOfSections = x + 1
568                                 @binary = true
569                                 @image = true
570                                 break
571                             end
572                         end
573                     end
574                     break
575                 when SVN_BINARY_FILE_MARKER_FORMAT
576                     @binary = true
577                     if (SVN_IMAGE_FILE_MARKER_FORMAT.match(lines[i + 1]) or PrettyPatch.has_image_suffix(@filename)) then
578                         @image = true
579                         startOfSections = i + 2
580                         for x in startOfSections...lines.length
581                             # Binary diffs often have property changes listed before the actual binary data.  Skip them.
582                             if SVN_START_OF_BINARY_DATA_FORMAT.match(lines[x]) then
583                                 startOfSections = x
584                                 break
585                             end
586                         end
587                     end
588                     break
589                 when GIT_INDEX_MARKER_FORMAT
590                     @git_indexes = [$1, $2]
591                 when GIT_BINARY_FILE_MARKER_FORMAT
592                     @binary = true
593                     if (GIT_BINARY_PATCH_FORMAT.match(lines[i + 1]) and PrettyPatch.has_image_suffix(@filename)) then
594                         @git_image = true
595                         startOfSections = i + 1
596                     end
597                     break
598                 when RENAME_FROM
599                     @renameFrom = RENAME_FROM.match(lines[i])[1]
600                 end
601             end
602             lines_with_contents = lines[startOfSections...lines.length]
603             @sections = DiffSection.parse(lines_with_contents) unless @binary
604             if @image and not lines_with_contents.empty?
605                 @image_url = "data:image/png;base64," + lines_with_contents.join
606                 @image_checksum = FileDiff.read_checksum_from_png(lines_with_contents.join.unpack("m").join)
607             elsif @git_image
608                 begin
609                     raise "index line is missing" unless @git_indexes
610
611                     chunks = nil
612                     for i in 0...lines_with_contents.length
613                         if lines_with_contents[i] =~ /^$/
614                             chunks = [lines_with_contents[i + 1 .. -1], lines_with_contents[0 .. i]]
615                             break
616                         end
617                     end
618
619                     raise "no binary chunks" unless chunks
620
621                     from_filepath = FileDiff.extract_contents_of_from_revision(@filename, chunks[0], @git_indexes[0])
622                     to_filepath = FileDiff.extract_contents_of_to_revision(@filename, chunks[1], @git_indexes[1], from_filepath, @git_indexes[0])
623                     filepaths = from_filepath, to_filepath
624
625                     binary_contents = filepaths.collect { |filepath| File.exists?(filepath) ? File.read(filepath) : nil }
626                     @image_urls = binary_contents.collect { |content| (content and not content.empty?) ? "data:image/png;base64," + [content].pack("m") : nil }
627                     @image_checksums = binary_contents.collect { |content| FileDiff.read_checksum_from_png(content) }
628                 rescue
629                     $last_prettify_part_count["extract-error"] += 1
630                     @image_error = "Exception raised during decoding git binary patch:<pre>#{CGI.escapeHTML($!.to_s + "\n" + $!.backtrace.join("\n"))}</pre>"
631                 ensure
632                     File.unlink(from_filepath) if (from_filepath and File.exists?(from_filepath))
633                     File.unlink(to_filepath) if (to_filepath and File.exists?(to_filepath))
634                 end
635             end
636             nil
637         end
638
639         def image_to_html
640             if not @image_url then
641                 return "<span class='text'>Image file removed</span>"
642             end
643
644             image_checksum = ""
645             if @image_checksum
646                 image_checksum = @image_checksum
647             elsif @filename.include? "-expected.png" and @image_url
648                 image_checksum = IMAGE_CHECKSUM_ERROR
649             end
650
651             return "<p>" + image_checksum + "</p><img class='image' src='" + @image_url + "' />"
652         end
653
654         def to_html
655             str = "<div class='FileDiff'>\n"
656             if @renameFrom
657                 str += "<h1>#{@filename}</h1>"
658                 str += "was renamed from"
659                 str += "<h1>#{PrettyPatch.linkifyFilename(@renameFrom.to_s, true)}</h1>"
660             else
661                 str += "<h1>#{PrettyPatch.linkifyFilename(@filename, false)}</h1>\n"
662             end
663             if @image then
664                 str += self.image_to_html
665             elsif @git_image then
666                 if @image_error
667                     str += @image_error
668                 else
669                     for i in (0...2)
670                         image_url = @image_urls[i]
671                         image_checksum = @image_checksums[i]
672
673                         style = ["remove", "add"][i]
674                         str += "<p class=\"#{style}\">"
675
676                         if image_checksum
677                             str += image_checksum
678                         elsif @filename.include? "-expected.png" and image_url
679                             str += IMAGE_CHECKSUM_ERROR
680                         end
681
682                         str += "<br>"
683
684                         if image_url
685                             str += "<img class='image' src='" + image_url + "' />"
686                         else
687                             str += ["</p>Added", "</p>Removed"][i]
688                         end
689                     end
690                 end
691             elsif @binary then
692                 $last_prettify_part_count["binary"] += 1
693                 str += "<span class='text'>Binary file, nothing to see here</span>"
694             else
695                 str += @sections.collect{ |section| section.to_html }.join("<br>\n") unless @sections.nil?
696             end
697
698             if @from then
699                 str += "<span class='revision'>" + @from + "</span>"
700             end
701
702             str += "</div>\n"
703         end
704
705         def self.parse(string)
706             haveSeenDiffHeader = false
707             linesForDiffs = []
708             line_array = string.lines.to_a
709             line_array.each_with_index do |line, index|
710                 if (PrettyPatch.diff_header?(line))
711                     linesForDiffs << []
712                     haveSeenDiffHeader = true
713                 elsif (!haveSeenDiffHeader && line =~ /^--- / && line_array[index + 1] =~ /^\+\+\+ /)
714                     linesForDiffs << []
715                     haveSeenDiffHeader = false
716                 end
717                 linesForDiffs.last << line unless linesForDiffs.last.nil?
718             end
719
720             linesForDiffs.collect { |lines| FileDiff.new(lines) }
721         end
722
723         def self.read_checksum_from_png(png_bytes)
724             # Ruby 1.9 added the concept of string encodings, so to avoid treating binary data as UTF-8,
725             # we can force the encoding to binary at this point.
726             if RUBY_VERSION >= "1.9"
727                 png_bytes.force_encoding('binary')
728             end
729             match = png_bytes && png_bytes.match(/tEXtchecksum\0([a-fA-F0-9]{32})/)
730             match ? match[1] : nil
731         end
732
733         def self.git_new_file_binary_patch(filename, encoded_chunk, git_index)
734             return <<END
735 diff --git a/#{filename} b/#{filename}
736 new file mode 100644
737 index 0000000000000000000000000000000000000000..#{git_index}
738 GIT binary patch
739 #{encoded_chunk.join("")}literal 0
740 HcmV?d00001
741
742 END
743         end
744
745         def self.git_changed_file_binary_patch(to_filename, from_filename, encoded_chunk, to_git_index, from_git_index)
746             return <<END
747 diff --git a/#{from_filename} b/#{to_filename}
748 copy from #{from_filename}
749 +++ b/#{to_filename}
750 index #{from_git_index}..#{to_git_index}
751 GIT binary patch
752 #{encoded_chunk.join("")}literal 0
753 HcmV?d00001
754
755 END
756         end
757
758         def self.get_svn_uri(repository_path)
759             "http://svn.webkit.org/repository/webkit/!svn/bc/" + $svn_revision.to_s + "/trunk/" + (repository_path)
760         end
761
762         def self.get_new_temp_filepath_and_name
763             tempfile = Tempfile.new("PrettyPatch")
764             filepath = tempfile.path + '.bin'
765             filename = File.basename(filepath)
766             return filepath, filename
767         end
768
769         def self.download_from_revision_from_svn(repository_path)
770             filepath, filename = get_new_temp_filepath_and_name
771             svn_uri = get_svn_uri(repository_path)
772             open(filepath, 'wb') do |to_file|
773                 to_file << open(svn_uri) { |from_file| from_file.read }
774             end
775             return filepath
776         end
777
778         def self.run_git_apply_on_patch(output_filepath, patch)
779             # Apply the git binary patch using git-apply.
780             cmd = GIT_PATH + " apply"
781             # Check if we need to pass --unsafe-paths (git >= 2.3.3)
782             helpcmd = GIT_PATH + " help apply"
783             stdin, stdout, stderr = *Open3.popen3(helpcmd)
784             begin
785                 if stdout.read().include? "--unsafe-paths"
786                     cmd += " --unsafe-paths"
787                 end
788             end
789             cmd += " --directory=" + File.dirname(output_filepath)
790             stdin, stdout, stderr = *Open3.popen3(cmd)
791             begin
792                 stdin.puts(patch)
793                 stdin.close
794
795                 error = stderr.read
796                 if error != ""
797                     error = "Error running " + cmd + "\n" + "with patch:\n" + patch[0..500] + "...\n" + error
798                 end
799                 raise error if error != ""
800             ensure
801                 stdin.close unless stdin.closed?
802                 stdout.close
803                 stderr.close
804             end
805         end
806
807         def self.extract_contents_from_git_binary_literal_chunk(encoded_chunk, git_index)
808             filepath, filename = get_new_temp_filepath_and_name
809             patch = FileDiff.git_new_file_binary_patch(filename, encoded_chunk, git_index)
810             run_git_apply_on_patch(filepath, patch)
811             return filepath
812         end
813
814         def self.extract_contents_from_git_binary_delta_chunk(from_filepath, from_git_index, encoded_chunk, to_git_index)
815             to_filepath, to_filename = get_new_temp_filepath_and_name
816             from_filename = File.basename(from_filepath)
817             patch = FileDiff.git_changed_file_binary_patch(to_filename, from_filename, encoded_chunk, to_git_index, from_git_index)
818             run_git_apply_on_patch(to_filepath, patch)
819             return to_filepath
820         end
821
822         def self.extract_contents_of_from_revision(repository_path, encoded_chunk, git_index)
823             # For literal encoded, simply reconstruct.
824             if GIT_LITERAL_FORMAT.match(encoded_chunk[0])
825                 return extract_contents_from_git_binary_literal_chunk(encoded_chunk, git_index)
826             end
827             #  For delta encoded, download from svn.
828             if GIT_DELTA_FORMAT.match(encoded_chunk[0])
829                 return download_from_revision_from_svn(repository_path)
830             end
831             raise "Error: unknown git patch encoding"
832         end
833
834         def self.extract_contents_of_to_revision(repository_path, encoded_chunk, git_index, from_filepath, from_git_index)
835             # For literal encoded, simply reconstruct.
836             if GIT_LITERAL_FORMAT.match(encoded_chunk[0])
837                 return extract_contents_from_git_binary_literal_chunk(encoded_chunk, git_index)
838             end
839             # For delta encoded, reconstruct using delta and previously constructed 'from' revision.
840             if GIT_DELTA_FORMAT.match(encoded_chunk[0])
841                 return extract_contents_from_git_binary_delta_chunk(from_filepath, from_git_index, encoded_chunk, git_index)
842             end
843             raise "Error: unknown git patch encoding"
844         end
845     end
846
847     class DiffBlock
848         attr_accessor :parts
849
850         def initialize(container)
851             @parts = []
852             container << self
853         end
854
855         def to_html
856             str = "<div class='DiffBlock'>\n"
857             str += @parts.collect{ |part| part.to_html }.join
858             str += "<div class='clear_float'></div></div>\n"
859         end
860     end
861
862     class DiffBlockPart
863         attr_reader :className
864         attr :lines
865
866         def initialize(className, container)
867             $last_prettify_part_count[className] += 1
868             @className = className
869             @lines = []
870             container.parts << self
871         end
872
873         def to_html
874             str = "<div class='DiffBlockPart %s'>\n" % @className
875             str += @lines.collect{ |line| line.to_html }.join
876             # Don't put white-space after this so adjacent inline-block DiffBlockParts will not wrap.
877             str += "</div>"
878         end
879     end
880
881     class DiffSection
882         def initialize(lines)
883             lines.length >= 1 or raise "DiffSection.parse only received %d lines" % lines.length
884
885             matches = START_OF_SECTION_FORMAT.match(lines[0])
886
887             if matches
888                 from, to = [matches[1].to_i, matches[3].to_i]
889                 if matches[2] and matches[4]
890                     from_end = from + matches[2].to_i
891                     to_end = to + matches[4].to_i
892                 end
893             end
894
895             @blocks = []
896             diff_block = nil
897             diff_block_part = nil
898
899             for line in lines[1...lines.length]
900                 startOfLine = line =~ /^[-\+ ]/ ? 1 : 0
901                 text = line[startOfLine...line.length].chomp
902                 case line[0]
903                 when ?-
904                     if (diff_block_part.nil? or diff_block_part.className != 'remove')
905                         diff_block = DiffBlock.new(@blocks)
906                         diff_block_part = DiffBlockPart.new('remove', diff_block)
907                     end
908
909                     diff_block_part.lines << CodeLine.new(from, nil, text)
910                     from += 1 unless from.nil?
911                 when ?+
912                     if (diff_block_part.nil? or diff_block_part.className != 'add')
913                         # Put add lines that immediately follow remove lines into the same DiffBlock.
914                         if (diff_block.nil? or diff_block_part.className != 'remove')
915                             diff_block = DiffBlock.new(@blocks)
916                         end
917
918                         diff_block_part = DiffBlockPart.new('add', diff_block)
919                     end
920
921                     diff_block_part.lines << CodeLine.new(nil, to, text)
922                     to += 1 unless to.nil?
923                 else
924                     if (diff_block_part.nil? or diff_block_part.className != 'shared')
925                         diff_block = DiffBlock.new(@blocks)
926                         diff_block_part = DiffBlockPart.new('shared', diff_block)
927                     end
928
929                     diff_block_part.lines << CodeLine.new(from, to, text)
930                     from += 1 unless from.nil?
931                     to += 1 unless to.nil?
932                 end
933
934                 break if from_end and to_end and from == from_end and to == to_end
935             end
936
937             changes = [ [ [], [] ] ]
938             for block in @blocks
939                 for block_part in block.parts
940                     for line in block_part.lines
941                         if (!line.fromLineNumber.nil? and !line.toLineNumber.nil?) then
942                             changes << [ [], [] ]
943                             next
944                         end
945                         changes.last.first << line if line.toLineNumber.nil?
946                         changes.last.last << line if line.fromLineNumber.nil?
947                     end
948                 end
949             end
950
951             for change in changes
952                 next unless change.first.length == change.last.length
953                 for i in (0...change.first.length)
954                     from_text = change.first[i].text
955                     to_text = change.last[i].text
956                     next if from_text.length > MAXIMUM_INTRALINE_DIFF_LINE_LENGTH or to_text.length > MAXIMUM_INTRALINE_DIFF_LINE_LENGTH
957                     raw_operations = HTMLDiff::DiffBuilder.new(from_text, to_text).operations
958                     operations = []
959                     back = 0
960                     raw_operations.each_with_index do |operation, j|
961                         if operation.action == :equal and j < raw_operations.length - 1
962                            length = operation.end_in_new - operation.start_in_new
963                            if length < SMALLEST_EQUAL_OPERATION
964                                back = length
965                                next
966                            end
967                         end
968                         operation.start_in_old -= back
969                         operation.start_in_new -= back
970                         back = 0
971                         operations << operation
972                     end
973                     change.first[i].operations = operations
974                     change.last[i].operations = operations
975                 end
976             end
977
978             @blocks.unshift(ContextLine.new(matches[5])) unless matches.nil? || matches[5].empty?
979         end
980
981         def to_html
982             str = "<div class='DiffSection'>\n"
983             str += @blocks.collect{ |block| block.to_html }.join
984             str += "</div>\n"
985         end
986
987         def self.parse(lines)
988             linesForSections = lines.inject([[]]) do |sections, line|
989                 sections << [] if line =~ /^@@/
990                 sections.last << line
991                 sections
992             end
993
994             linesForSections.delete_if { |lines| lines.nil? or lines.empty? }
995             linesForSections.collect { |lines| DiffSection.new(lines) }
996         end
997     end
998
999     class Line
1000         attr_reader :fromLineNumber
1001         attr_reader :toLineNumber
1002         attr_reader :text
1003
1004         def initialize(from, to, text)
1005             @fromLineNumber = from
1006             @toLineNumber = to
1007             @text = text
1008         end
1009
1010         def text_as_html
1011             CGI.escapeHTML(text)
1012         end
1013
1014         def classes
1015             lineClasses = ["Line", "LineContainer"]
1016             lineClasses << ["add"] unless @toLineNumber.nil? or !@fromLineNumber.nil?
1017             lineClasses << ["remove"] unless @fromLineNumber.nil? or !@toLineNumber.nil?
1018             lineClasses
1019         end
1020
1021         def to_html
1022             markedUpText = self.text_as_html
1023             str = "<div class='%s'>\n" % self.classes.join(' ')
1024             str += "<span class='from lineNumber'>%s</span><span class='to lineNumber'>%s</span>" %
1025                    [@fromLineNumber.nil? ? '&nbsp;' : @fromLineNumber,
1026                     @toLineNumber.nil? ? '&nbsp;' : @toLineNumber] unless @fromLineNumber.nil? and @toLineNumber.nil?
1027             str += "<span class='text'>%s</span>\n" % markedUpText
1028             str += "</div>\n"
1029         end
1030     end
1031
1032     class CodeLine < Line
1033         attr :operations, true
1034
1035         def text_as_html
1036             html = []
1037             tag = @fromLineNumber.nil? ? "ins" : "del"
1038             if @operations.nil? or @operations.empty?
1039                 return CGI.escapeHTML(@text)
1040             end
1041             @operations.each do |operation|
1042                 start = @fromLineNumber.nil? ? operation.start_in_new : operation.start_in_old
1043                 eend = @fromLineNumber.nil? ? operation.end_in_new : operation.end_in_old
1044                 escaped_text = CGI.escapeHTML(@text[start...eend])
1045                 if eend - start === 0 or operation.action === :equal
1046                     html << escaped_text
1047                 else
1048                     html << "<#{tag}>#{escaped_text}</#{tag}>"
1049                 end
1050             end
1051             html.join
1052         end
1053     end
1054
1055     class ContextLine < Line
1056         def initialize(context)
1057             super("@", "@", context)
1058         end
1059
1060         def classes
1061             super << "context"
1062         end
1063     end
1064 end