1require 'cgi' 2require 'diff' 3require 'open3' 4require 'open-uri' 5require 'pp' 6require 'set' 7require 'tempfile' 8 9module PrettyPatch 10 11public 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 52private 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 178h1 { 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 188h1 :link, h1 :visited { 189 color: inherit; 190} 191 192h1 :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 465div: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> 506EOF 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 685diff --git a/#{filename} b/#{filename} 686new file mode 100644 687index 0000000000000000000000000000000000000000..#{git_index} 688GIT binary patch 689#{encoded_chunk.join("")}literal 0 690HcmV?d00001 691 692END 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 697diff --git a/#{from_filename} b/#{to_filename} 698copy from #{from_filename} 699+++ b/#{to_filename} 700index #{from_git_index}..#{to_git_index} 701GIT binary patch 702#{encoded_chunk.join("")}literal 0 703HcmV?d00001 704 705END 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? ? ' ' : @fromLineNumber, 967 @toLineNumber.nil? ? ' ' : @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 1005end 1006