EWS bubbles are being hidden due to lack of space.
[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 .statusBubble.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 > iframe {
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="js/status-bubble.js"></script>
528 <script src="code-review.js?version=48"></script>
529 </head>
530 EOF
531
532     def self.revisionOrDescription(string)
533         case string
534         when /\(revision \d+\)/
535             /\(revision (\d+)\)/.match(string)[1]
536         when /\(.*\)/
537             /\((.*)\)/.match(string)[1]
538         end
539     end
540
541     def self.has_image_suffix(filename)
542         filename =~ /\.(png|jpg|gif)$/
543     end
544
545     class FileDiff
546         attr_reader :filename
547         attr_reader :image
548         attr_reader :image_url
549
550         def initialize(lines)
551             @filename = PrettyPatch.filename_from_diff_header(lines[0].chomp)
552             startOfSections = 1
553             for i in 0...lines.length
554                 case lines[i]
555                 when /^--- /
556                     @from = PrettyPatch.revisionOrDescription(lines[i])
557                 when /^\+\+\+ /
558                     @filename = PrettyPatch.filename_from_diff_header(lines[i].chomp) if @filename.nil?
559                     @to = PrettyPatch.revisionOrDescription(lines[i])
560                     startOfSections = i + 1
561
562                     # Check for 'property' patch, then image data, since svn 1.7 creates a fake patch for property changes.
563                     if /^$/.match(lines[startOfSections]) and SVN_PROPERTY_CHANGES_FORMAT.match(lines[startOfSections + 1]) then
564                         startOfSections += 2
565                         for x in startOfSections...lines.length
566                             next if not /^$/.match(lines[x])
567                             if SVN_START_OF_BINARY_DATA_FORMAT.match(lines[x + 1]) then
568                                 startOfSections = x + 1
569                                 @binary = true
570                                 @image = true
571                                 break
572                             end
573                         end
574                     end
575                     break
576                 when SVN_BINARY_FILE_MARKER_FORMAT
577                     @binary = true
578                     if (SVN_IMAGE_FILE_MARKER_FORMAT.match(lines[i + 1]) or PrettyPatch.has_image_suffix(@filename)) then
579                         @image = true
580                         startOfSections = i + 2
581                         for x in startOfSections...lines.length
582                             # Binary diffs often have property changes listed before the actual binary data.  Skip them.
583                             if SVN_START_OF_BINARY_DATA_FORMAT.match(lines[x]) then
584                                 startOfSections = x
585                                 break
586                             end
587                         end
588                     end
589                     break
590                 when GIT_INDEX_MARKER_FORMAT
591                     @git_indexes = [$1, $2]
592                 when GIT_BINARY_FILE_MARKER_FORMAT
593                     @binary = true
594                     if (GIT_BINARY_PATCH_FORMAT.match(lines[i + 1]) and PrettyPatch.has_image_suffix(@filename)) then
595                         @git_image = true
596                         startOfSections = i + 1
597                     end
598                     break
599                 when RENAME_FROM
600                     @renameFrom = RENAME_FROM.match(lines[i])[1]
601                 end
602             end
603             lines_with_contents = lines[startOfSections...lines.length]
604             @sections = DiffSection.parse(lines_with_contents) unless @binary
605             if @image and not lines_with_contents.empty?
606                 @image_url = "data:image/png;base64," + lines_with_contents.join
607                 @image_checksum = FileDiff.read_checksum_from_png(lines_with_contents.join.unpack("m").join)
608             elsif @git_image
609                 begin
610                     raise "index line is missing" unless @git_indexes
611
612                     chunks = nil
613                     for i in 0...lines_with_contents.length
614                         if lines_with_contents[i] =~ /^$/
615                             chunks = [lines_with_contents[i + 1 .. -1], lines_with_contents[0 .. i]]
616                             break
617                         end
618                     end
619
620                     raise "no binary chunks" unless chunks
621
622                     from_filepath = FileDiff.extract_contents_of_from_revision(@filename, chunks[0], @git_indexes[0])
623                     to_filepath = FileDiff.extract_contents_of_to_revision(@filename, chunks[1], @git_indexes[1], from_filepath, @git_indexes[0])
624                     filepaths = from_filepath, to_filepath
625
626                     binary_contents = filepaths.collect { |filepath| File.exists?(filepath) ? File.read(filepath) : nil }
627                     @image_urls = binary_contents.collect { |content| (content and not content.empty?) ? "data:image/png;base64," + [content].pack("m") : nil }
628                     @image_checksums = binary_contents.collect { |content| FileDiff.read_checksum_from_png(content) }
629                 rescue
630                     $last_prettify_part_count["extract-error"] += 1
631                     @image_error = "Exception raised during decoding git binary patch:<pre>#{CGI.escapeHTML($!.to_s + "\n" + $!.backtrace.join("\n"))}</pre>"
632                 ensure
633                     File.unlink(from_filepath) if (from_filepath and File.exists?(from_filepath))
634                     File.unlink(to_filepath) if (to_filepath and File.exists?(to_filepath))
635                 end
636             end
637             nil
638         end
639
640         def image_to_html
641             if not @image_url then
642                 return "<span class='text'>Image file removed</span>"
643             end
644
645             image_checksum = ""
646             if @image_checksum
647                 image_checksum = @image_checksum
648             elsif @filename.include? "-expected.png" and @image_url
649                 image_checksum = IMAGE_CHECKSUM_ERROR
650             end
651
652             return "<p>" + image_checksum + "</p><img class='image' src='" + @image_url + "' />"
653         end
654
655         def to_html
656             str = "<div class='FileDiff'>\n"
657             if @renameFrom
658                 str += "<h1>#{@filename}</h1>"
659                 str += "was renamed from"
660                 str += "<h1>#{PrettyPatch.linkifyFilename(@renameFrom.to_s, true)}</h1>"
661             else
662                 str += "<h1>#{PrettyPatch.linkifyFilename(@filename, false)}</h1>\n"
663             end
664             if @image then
665                 str += self.image_to_html
666             elsif @git_image then
667                 if @image_error
668                     str += @image_error
669                 else
670                     for i in (0...2)
671                         image_url = @image_urls[i]
672                         image_checksum = @image_checksums[i]
673
674                         style = ["remove", "add"][i]
675                         str += "<p class=\"#{style}\">"
676
677                         if image_checksum
678                             str += image_checksum
679                         elsif @filename.include? "-expected.png" and image_url
680                             str += IMAGE_CHECKSUM_ERROR
681                         end
682
683                         str += "<br>"
684
685                         if image_url
686                             str += "<img class='image' src='" + image_url + "' />"
687                         else
688                             str += ["</p>Added", "</p>Removed"][i]
689                         end
690                     end
691                 end
692             elsif @binary then
693                 $last_prettify_part_count["binary"] += 1
694                 str += "<span class='text'>Binary file, nothing to see here</span>"
695             else
696                 str += @sections.collect{ |section| section.to_html }.join("<br>\n") unless @sections.nil?
697             end
698
699             if @from then
700                 str += "<span class='revision'>" + @from + "</span>"
701             end
702
703             str += "</div>\n"
704         end
705
706         def self.parse(string)
707             haveSeenDiffHeader = false
708             linesForDiffs = []
709             line_array = string.lines.to_a
710             line_array.each_with_index do |line, index|
711                 if (PrettyPatch.diff_header?(line))
712                     linesForDiffs << []
713                     haveSeenDiffHeader = true
714                 elsif (!haveSeenDiffHeader && line =~ /^--- / && line_array[index + 1] =~ /^\+\+\+ /)
715                     linesForDiffs << []
716                     haveSeenDiffHeader = false
717                 end
718                 linesForDiffs.last << line unless linesForDiffs.last.nil?
719             end
720
721             linesForDiffs.collect { |lines| FileDiff.new(lines) }
722         end
723
724         def self.read_checksum_from_png(png_bytes)
725             # Ruby 1.9 added the concept of string encodings, so to avoid treating binary data as UTF-8,
726             # we can force the encoding to binary at this point.
727             if RUBY_VERSION >= "1.9"
728                 png_bytes.force_encoding('binary')
729             end
730             match = png_bytes && png_bytes.match(/tEXtchecksum\0([a-fA-F0-9]{32})/)
731             match ? match[1] : nil
732         end
733
734         def self.git_new_file_binary_patch(filename, encoded_chunk, git_index)
735             return <<END
736 diff --git a/#{filename} b/#{filename}
737 new file mode 100644
738 index 0000000000000000000000000000000000000000..#{git_index}
739 GIT binary patch
740 #{encoded_chunk.join("")}literal 0
741 HcmV?d00001
742
743 END
744         end
745
746         def self.git_changed_file_binary_patch(to_filename, from_filename, encoded_chunk, to_git_index, from_git_index)
747             return <<END
748 diff --git a/#{from_filename} b/#{to_filename}
749 copy from #{from_filename}
750 +++ b/#{to_filename}
751 index #{from_git_index}..#{to_git_index}
752 GIT binary patch
753 #{encoded_chunk.join("")}literal 0
754 HcmV?d00001
755
756 END
757         end
758
759         def self.get_svn_uri(repository_path)
760             "http://svn.webkit.org/repository/webkit/!svn/bc/" + $svn_revision.to_s + "/trunk/" + (repository_path)
761         end
762
763         def self.get_new_temp_filepath_and_name
764             tempfile = Tempfile.new("PrettyPatch")
765             filepath = tempfile.path + '.bin'
766             filename = File.basename(filepath)
767             return filepath, filename
768         end
769
770         def self.download_from_revision_from_svn(repository_path)
771             filepath, filename = get_new_temp_filepath_and_name
772             svn_uri = get_svn_uri(repository_path)
773             open(filepath, 'wb') do |to_file|
774                 to_file << open(svn_uri) { |from_file| from_file.read }
775             end
776             return filepath
777         end
778
779         def self.run_git_apply_on_patch(output_filepath, patch)
780             # Apply the git binary patch using git-apply.
781             cmd = GIT_PATH + " apply"
782             # Check if we need to pass --unsafe-paths (git >= 2.3.3)
783             helpcmd = GIT_PATH + " help apply"
784             stdin, stdout, stderr = *Open3.popen3(helpcmd)
785             begin
786                 if stdout.read().include? "--unsafe-paths"
787                     cmd += " --unsafe-paths"
788                 end
789             end
790             cmd += " --directory=" + File.dirname(output_filepath)
791             stdin, stdout, stderr = *Open3.popen3(cmd)
792             begin
793                 stdin.puts(patch)
794                 stdin.close
795
796                 error = stderr.read
797                 if error != ""
798                     error = "Error running " + cmd + "\n" + "with patch:\n" + patch[0..500] + "...\n" + error
799                 end
800                 raise error if error != ""
801             ensure
802                 stdin.close unless stdin.closed?
803                 stdout.close
804                 stderr.close
805             end
806         end
807
808         def self.extract_contents_from_git_binary_literal_chunk(encoded_chunk, git_index)
809             filepath, filename = get_new_temp_filepath_and_name
810             patch = FileDiff.git_new_file_binary_patch(filename, encoded_chunk, git_index)
811             run_git_apply_on_patch(filepath, patch)
812             return filepath
813         end
814
815         def self.extract_contents_from_git_binary_delta_chunk(from_filepath, from_git_index, encoded_chunk, to_git_index)
816             to_filepath, to_filename = get_new_temp_filepath_and_name
817             from_filename = File.basename(from_filepath)
818             patch = FileDiff.git_changed_file_binary_patch(to_filename, from_filename, encoded_chunk, to_git_index, from_git_index)
819             run_git_apply_on_patch(to_filepath, patch)
820             return to_filepath
821         end
822
823         def self.extract_contents_of_from_revision(repository_path, encoded_chunk, git_index)
824             # For literal encoded, simply reconstruct.
825             if GIT_LITERAL_FORMAT.match(encoded_chunk[0])
826                 return extract_contents_from_git_binary_literal_chunk(encoded_chunk, git_index)
827             end
828             #  For delta encoded, download from svn.
829             if GIT_DELTA_FORMAT.match(encoded_chunk[0])
830                 return download_from_revision_from_svn(repository_path)
831             end
832             raise "Error: unknown git patch encoding"
833         end
834
835         def self.extract_contents_of_to_revision(repository_path, encoded_chunk, git_index, from_filepath, from_git_index)
836             # For literal encoded, simply reconstruct.
837             if GIT_LITERAL_FORMAT.match(encoded_chunk[0])
838                 return extract_contents_from_git_binary_literal_chunk(encoded_chunk, git_index)
839             end
840             # For delta encoded, reconstruct using delta and previously constructed 'from' revision.
841             if GIT_DELTA_FORMAT.match(encoded_chunk[0])
842                 return extract_contents_from_git_binary_delta_chunk(from_filepath, from_git_index, encoded_chunk, git_index)
843             end
844             raise "Error: unknown git patch encoding"
845         end
846     end
847
848     class DiffBlock
849         attr_accessor :parts
850
851         def initialize(container)
852             @parts = []
853             container << self
854         end
855
856         def to_html
857             str = "<div class='DiffBlock'>\n"
858             str += @parts.collect{ |part| part.to_html }.join
859             str += "<div class='clear_float'></div></div>\n"
860         end
861     end
862
863     class DiffBlockPart
864         attr_reader :className
865         attr :lines
866
867         def initialize(className, container)
868             $last_prettify_part_count[className] += 1
869             @className = className
870             @lines = []
871             container.parts << self
872         end
873
874         def to_html
875             str = "<div class='DiffBlockPart %s'>\n" % @className
876             str += @lines.collect{ |line| line.to_html }.join
877             # Don't put white-space after this so adjacent inline-block DiffBlockParts will not wrap.
878             str += "</div>"
879         end
880     end
881
882     class DiffSection
883         def initialize(lines)
884             lines.length >= 1 or raise "DiffSection.parse only received %d lines" % lines.length
885
886             matches = START_OF_SECTION_FORMAT.match(lines[0])
887
888             if matches
889                 from, to = [matches[1].to_i, matches[3].to_i]
890                 if matches[2] and matches[4]
891                     from_end = from + matches[2].to_i
892                     to_end = to + matches[4].to_i
893                 end
894             end
895
896             @blocks = []
897             diff_block = nil
898             diff_block_part = nil
899
900             for line in lines[1...lines.length]
901                 startOfLine = line =~ /^[-\+ ]/ ? 1 : 0
902                 text = line[startOfLine...line.length].chomp
903                 case line[0]
904                 when ?-
905                     if (diff_block_part.nil? or diff_block_part.className != 'remove')
906                         diff_block = DiffBlock.new(@blocks)
907                         diff_block_part = DiffBlockPart.new('remove', diff_block)
908                     end
909
910                     diff_block_part.lines << CodeLine.new(from, nil, text)
911                     from += 1 unless from.nil?
912                 when ?+
913                     if (diff_block_part.nil? or diff_block_part.className != 'add')
914                         # Put add lines that immediately follow remove lines into the same DiffBlock.
915                         if (diff_block.nil? or diff_block_part.className != 'remove')
916                             diff_block = DiffBlock.new(@blocks)
917                         end
918
919                         diff_block_part = DiffBlockPart.new('add', diff_block)
920                     end
921
922                     diff_block_part.lines << CodeLine.new(nil, to, text)
923                     to += 1 unless to.nil?
924                 else
925                     if (diff_block_part.nil? or diff_block_part.className != 'shared')
926                         diff_block = DiffBlock.new(@blocks)
927                         diff_block_part = DiffBlockPart.new('shared', diff_block)
928                     end
929
930                     diff_block_part.lines << CodeLine.new(from, to, text)
931                     from += 1 unless from.nil?
932                     to += 1 unless to.nil?
933                 end
934
935                 break if from_end and to_end and from == from_end and to == to_end
936             end
937
938             changes = [ [ [], [] ] ]
939             for block in @blocks
940                 for block_part in block.parts
941                     for line in block_part.lines
942                         if (!line.fromLineNumber.nil? and !line.toLineNumber.nil?) then
943                             changes << [ [], [] ]
944                             next
945                         end
946                         changes.last.first << line if line.toLineNumber.nil?
947                         changes.last.last << line if line.fromLineNumber.nil?
948                     end
949                 end
950             end
951
952             for change in changes
953                 next unless change.first.length == change.last.length
954                 for i in (0...change.first.length)
955                     from_text = change.first[i].text
956                     to_text = change.last[i].text
957                     next if from_text.length > MAXIMUM_INTRALINE_DIFF_LINE_LENGTH or to_text.length > MAXIMUM_INTRALINE_DIFF_LINE_LENGTH
958                     raw_operations = HTMLDiff::DiffBuilder.new(from_text, to_text).operations
959                     operations = []
960                     back = 0
961                     raw_operations.each_with_index do |operation, j|
962                         if operation.action == :equal and j < raw_operations.length - 1
963                            length = operation.end_in_new - operation.start_in_new
964                            if length < SMALLEST_EQUAL_OPERATION
965                                back = length
966                                next
967                            end
968                         end
969                         operation.start_in_old -= back
970                         operation.start_in_new -= back
971                         back = 0
972                         operations << operation
973                     end
974                     change.first[i].operations = operations
975                     change.last[i].operations = operations
976                 end
977             end
978
979             @blocks.unshift(ContextLine.new(matches[5])) unless matches.nil? || matches[5].empty?
980         end
981
982         def to_html
983             str = "<div class='DiffSection'>\n"
984             str += @blocks.collect{ |block| block.to_html }.join
985             str += "</div>\n"
986         end
987
988         def self.parse(lines)
989             linesForSections = lines.inject([[]]) do |sections, line|
990                 sections << [] if line =~ /^@@/
991                 sections.last << line
992                 sections
993             end
994
995             linesForSections.delete_if { |lines| lines.nil? or lines.empty? }
996             linesForSections.collect { |lines| DiffSection.new(lines) }
997         end
998     end
999
1000     class Line
1001         attr_reader :fromLineNumber
1002         attr_reader :toLineNumber
1003         attr_reader :text
1004
1005         def initialize(from, to, text)
1006             @fromLineNumber = from
1007             @toLineNumber = to
1008             @text = text
1009         end
1010
1011         def text_as_html
1012             CGI.escapeHTML(text)
1013         end
1014
1015         def classes
1016             lineClasses = ["Line", "LineContainer"]
1017             lineClasses << ["add"] unless @toLineNumber.nil? or !@fromLineNumber.nil?
1018             lineClasses << ["remove"] unless @fromLineNumber.nil? or !@toLineNumber.nil?
1019             lineClasses
1020         end
1021
1022         def to_html
1023             markedUpText = self.text_as_html
1024             str = "<div class='%s'>\n" % self.classes.join(' ')
1025             str += "<span class='from lineNumber'>%s</span><span class='to lineNumber'>%s</span>" %
1026                    [@fromLineNumber.nil? ? '&nbsp;' : @fromLineNumber,
1027                     @toLineNumber.nil? ? '&nbsp;' : @toLineNumber] unless @fromLineNumber.nil? and @toLineNumber.nil?
1028             str += "<span class='text'>%s</span>\n" % markedUpText
1029             str += "</div>\n"
1030         end
1031     end
1032
1033     class CodeLine < Line
1034         attr :operations, true
1035
1036         def text_as_html
1037             html = []
1038             tag = @fromLineNumber.nil? ? "ins" : "del"
1039             if @operations.nil? or @operations.empty?
1040                 return CGI.escapeHTML(@text)
1041             end
1042             @operations.each do |operation|
1043                 start = @fromLineNumber.nil? ? operation.start_in_new : operation.start_in_old
1044                 eend = @fromLineNumber.nil? ? operation.end_in_new : operation.end_in_old
1045                 escaped_text = CGI.escapeHTML(@text[start...eend])
1046                 if eend - start === 0 or operation.action === :equal
1047                     html << escaped_text
1048                 else
1049                     html << "<#{tag}>#{escaped_text}</#{tag}>"
1050                 end
1051             end
1052             html.join
1053         end
1054     end
1055
1056     class ContextLine < Line
1057         def initialize(context)
1058             super("@", "@", context)
1059         end
1060
1061         def classes
1062             super << "context"
1063         end
1064     end
1065 end