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