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