| require 'cgi' |
| require 'diff' |
| require 'open3' |
| require 'open-uri' |
| require 'pp' |
| require 'set' |
| require 'tempfile' |
| |
| module PrettyPatch |
| |
| public |
| |
| GIT_PATH = "git" |
| |
| def self.prettify(string) |
| $last_prettify_file_count = -1 |
| $last_prettify_part_count = { "remove" => 0, "add" => 0, "shared" => 0, "binary" => 0, "extract-error" => 0 } |
| string = normalize_line_ending(string) |
| str = "#{HEADER}<body>\n" |
| |
| fileDiffs = FileDiff.parse(string) |
| |
| # Newly added images get two diffs with svn 1.7; toss the first one. |
| deleteIndices = [] |
| for i in 1...fileDiffs.length |
| prev = i - 1 |
| if fileDiffs[prev].image and not fileDiffs[prev].image_url and fileDiffs[i].image and fileDiffs[i].image_url and fileDiffs[prev].filename == fileDiffs[i].filename |
| deleteIndices.unshift(prev) |
| end |
| end |
| deleteIndices.each{ |i| fileDiffs.delete_at(i) } |
| |
| $last_prettify_file_count = fileDiffs.length |
| str << fileDiffs.collect{ |diff| diff.to_html }.join |
| str << "</body></html>" |
| end |
| |
| def self.filename_from_diff_header(line) |
| DIFF_HEADER_FORMATS.each do |format| |
| match = format.match(line) |
| return match[1] unless match.nil? |
| end |
| nil |
| end |
| |
| def self.diff_header?(line) |
| RELAXED_DIFF_HEADER_FORMATS.any? { |format| line =~ format } |
| end |
| |
| def self.message_header?(line) |
| MESSAGE_HEADER_FORMATS.any? { |format| line =~ format } |
| end |
| |
| def self.message_footer?(line) |
| MESSAGE_FOOTER_FORMATS.any? { |format| line =~ format } |
| end |
| |
| private |
| DIFF_HEADER_FORMATS = [ |
| /^Index: (.*)\r?$/, |
| /^diff --git "?a\/.+"? "?b\/(.+)"?\r?$/, |
| /^\+\+\+ ([^\t]+)(\t.*)?\r?$/ |
| ] |
| |
| RELAXED_DIFF_HEADER_FORMATS = [ |
| /^Index:/, |
| /^diff/ |
| ] |
| |
| MESSAGE_HEADER_FORMATS = [ |
| /^Subject: \[PATCH ?(\d+\/\d+)?\] (.+)/ |
| ] |
| |
| BUG_URL_RE = / (Need the bug URL \(OOPS!\).)|(\S+:\/\/\S+)/ |
| |
| MESSAGE_FOOTER_FORMATS = [ |
| /^---/ |
| ] |
| |
| RENAME_FROM = /^rename from (.*)/ |
| |
| SVN_BINARY_FILE_MARKER_FORMAT = /^Cannot display: file marked as a binary type.$/ |
| |
| SVN_IMAGE_FILE_MARKER_FORMAT = /^svn:mime-type = image\/png$/ |
| |
| SVN_PROPERTY_CHANGES_FORMAT = /^Property changes on: (.*)/ |
| |
| GIT_INDEX_MARKER_FORMAT = /^index ([0-9a-f]{40})\.\.([0-9a-f]{40})/ |
| |
| GIT_BINARY_FILE_MARKER_FORMAT = /^GIT binary patch$/ |
| |
| GIT_BINARY_PATCH_FORMAT = /^(literal|delta) \d+$/ |
| |
| GIT_LITERAL_FORMAT = /^literal \d+$/ |
| |
| GIT_DELTA_FORMAT = /^delta \d+$/ |
| |
| SVN_START_OF_BINARY_DATA_FORMAT = /^[0-9a-zA-Z\+\/=]{20,}/ # Assume 20 chars without a space is base64 binary data. |
| |
| START_OF_SECTION_FORMAT = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@\s*(.*)/ |
| |
| START_OF_EXTENT_STRING = "%c" % 0 |
| END_OF_EXTENT_STRING = "%c" % 1 |
| |
| # We won't search for intra-line diffs in lines longer than this length, to avoid hangs. See <http://webkit.org/b/56109>. |
| MAXIMUM_INTRALINE_DIFF_LINE_LENGTH = 10000 |
| |
| SMALLEST_EQUAL_OPERATION = 3 |
| |
| OPENSOURCE_GITHUB_URL = "https://github.com/WebKit/WebKit/blob/" |
| |
| OPENSOURCE_DIRS = Set.new %w[ |
| Examples |
| LayoutTests |
| PerformanceTests |
| Source |
| Tools |
| WebKitLibraries |
| Websites |
| ] |
| |
| 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." |
| |
| def self.normalize_line_ending(s) |
| if RUBY_VERSION >= "1.9" |
| # Transliteration table from http://stackoverflow.com/a/6609998 |
| transliteration_table = { '\xc2\x82' => ',', # High code comma |
| '\xc2\x84' => ',,', # High code double comma |
| '\xc2\x85' => '...', # Tripple dot |
| '\xc2\x88' => '^', # High carat |
| '\xc2\x91' => '\x27', # Forward single quote |
| '\xc2\x92' => '\x27', # Reverse single quote |
| '\xc2\x93' => '\x22', # Forward double quote |
| '\xc2\x94' => '\x22', # Reverse double quote |
| '\xc2\x95' => ' ', |
| '\xc2\x96' => '-', # High hyphen |
| '\xc2\x97' => '--', # Double hyphen |
| '\xc2\x99' => ' ', |
| '\xc2\xa0' => ' ', |
| '\xc2\xa6' => '|', # Split vertical bar |
| '\xc2\xab' => '<<', # Double less than |
| '\xc2\xbb' => '>>', # Double greater than |
| '\xc2\xbc' => '1/4', # one quarter |
| '\xc2\xbd' => '1/2', # one half |
| '\xc2\xbe' => '3/4', # three quarters |
| '\xca\xbf' => '\x27', # c-single quote |
| '\xcc\xa8' => '', # modifier - under curve |
| '\xcc\xb1' => '' # modifier - under line |
| } |
| encoded_string = s.force_encoding('UTF-8').encode('UTF-16', :invalid => :replace, :replace => '', :fallback => transliteration_table).encode('UTF-8') |
| encoded_string.gsub /\r\n?/, "\n" |
| else |
| s.gsub /\r\n?/, "\n" |
| end |
| end |
| |
| def self.linkifyFilename(filename) |
| "<a href='#{OPENSOURCE_GITHUB_URL}#{filename}'>#{filename}</a>" |
| end |
| |
| |
| HEADER =<<EOF |
| <html> |
| <head> |
| <meta charset='utf-8'> |
| <style> |
| :root { |
| color-scheme: light dark; |
| --link-color: #039; |
| --border-color: #ddd; |
| --grouped-bg-color: #eee; |
| --page-bg-color: white; |
| } |
| |
| :link, :visited { |
| text-decoration: none; |
| border-bottom: 1px dotted; |
| } |
| |
| :link { |
| color: var(--link-color); |
| } |
| |
| @media (prefers-color-scheme: dark) { |
| :root { |
| --link-color: #09f; |
| --border-color: #222; |
| --grouped-bg-color: #111; |
| --page-bg-color: black; |
| } |
| |
| :visited { |
| color: #882bce; |
| } |
| |
| body { |
| background-color: var(--page-bg-color); |
| color: #eee; |
| } |
| } |
| |
| .FileDiff { |
| background-color: #f8f8f8; |
| border: 1px solid var(--border-color); |
| font-family: monospace; |
| margin: 1em 0; |
| position: relative; |
| } |
| |
| @media (prefers-color-scheme: dark) { |
| .FileDiff { |
| background-color: #212121; |
| } |
| } |
| |
| h1 { |
| color: #333; |
| font-family: sans-serif; |
| font-size: 1em; |
| margin-left: 0.5em; |
| display: inline; |
| width: 100%; |
| padding: 0.5em; |
| } |
| |
| @media (prefers-color-scheme: dark) { |
| h1 { |
| color: #ccc; |
| } |
| } |
| |
| h1 :link, h1 :visited { |
| color: inherit; |
| } |
| |
| h1 :hover { |
| color: #555; |
| background-color: var(--grouped-bg-color); |
| } |
| |
| @media (prefers-color-scheme: dark) { |
| h1 :hover { |
| color: #aaa; |
| } |
| } |
| |
| .DiffLinks { |
| float: right; |
| } |
| |
| .FileDiffLinkContainer { |
| opacity: 0; |
| display: table-cell; |
| padding-right: 0.5em; |
| white-space: nowrap; |
| } |
| |
| .DiffSection { |
| background-color: var(--page-bg-color); |
| border: solid var(--border-color); |
| border-width: 1px 0px; |
| } |
| |
| .ExpansionLine, .LineContainer { |
| white-space: nowrap; |
| } |
| |
| .sidebyside .DiffBlockPart.add:first-child { |
| float: right; |
| } |
| |
| .LineSide, |
| .sidebyside .DiffBlockPart.remove, |
| .sidebyside .DiffBlockPart.add { |
| display:inline-block; |
| width: 50%; |
| vertical-align: top; |
| } |
| |
| .sidebyside .resizeHandle { |
| width: 5px; |
| height: 100%; |
| cursor: move; |
| position: absolute; |
| top: 0; |
| left: 50%; |
| } |
| |
| .sidebyside .resizeHandle:hover { |
| background-color: grey; |
| opacity: 0.5; |
| } |
| |
| .sidebyside .DiffBlockPart.remove .to, |
| .sidebyside .DiffBlockPart.add .from { |
| display: none; |
| } |
| |
| .lineNumber, .expansionLineNumber { |
| --border-bottom-color: #998; |
| border-bottom: 1px solid var(--border-bottom-color); |
| border-right: 1px solid var(--border-color); |
| color: #444; |
| display: inline-block; |
| padding: 1px 5px 0px 0px; |
| text-align: right; |
| vertical-align: bottom; |
| width: 3em; |
| } |
| |
| @media (prefers-color-scheme: dark) { |
| .lineNumber, .expansionLineNumber { |
| --border-bottom-color: #424242; |
| color: #bbb; |
| } |
| } |
| |
| .lineNumber { |
| background-color: #eed; |
| } |
| |
| @media (prefers-color-scheme: dark) { |
| .lineNumber { |
| background-color: #121212; |
| } |
| } |
| |
| .expansionLineNumber { |
| background-color: var(--grouped-bg-color); |
| } |
| |
| pre, .text { |
| padding-left: 5px; |
| white-space: pre-wrap; |
| word-wrap: break-word; |
| } |
| |
| .image { |
| border: 2px solid text; |
| } |
| |
| .context, .context .lineNumber { |
| color: #849; |
| background-color: #fef; |
| } |
| |
| @media (prefers-color-scheme: dark) { |
| .context, .context .lineNumber { |
| color: #a24bb7; |
| background-color: #1f0f24; |
| } |
| } |
| |
| .Line.add, .FileDiff .add { |
| background-color: #dfd; |
| } |
| |
| .Line.add ins { |
| background-color: #9e9; |
| text-decoration: none; |
| } |
| |
| @media (prefers-color-scheme: dark) { |
| .Line.add, .FileDiff .add { |
| background-color: #242; |
| } |
| |
| .Line.add ins { |
| background-color: #186e0c; |
| } |
| } |
| |
| .Line.remove, .FileDiff .remove { |
| background-color: #fdd; |
| } |
| |
| .Line.remove del { |
| background-color: #e99; |
| text-decoration: none; |
| } |
| |
| @media (prefers-color-scheme: dark) { |
| .Line.remove, .FileDiff .remove { |
| background-color: #410000; |
| } |
| |
| .Line.remove del { |
| background-color: #8d1e0b; |
| } |
| } |
| |
| /* Support for inline comments */ |
| |
| .author { |
| font-style: italic; |
| } |
| |
| .comment { |
| position: relative; |
| } |
| |
| .comment textarea { |
| height: 6em; |
| } |
| |
| .overallComments textarea { |
| height: 2em; |
| max-width: 100%; |
| min-width: 200px; |
| } |
| |
| .comment textarea, .overallComments textarea { |
| display: block; |
| width: 100%; |
| } |
| |
| .overallComments .open { |
| -webkit-transition: height .2s; |
| height: 4em; |
| } |
| |
| .statusBubble.wrap { |
| display: block; |
| } |
| |
| #toolbar { |
| display: -webkit-flex; |
| display: -moz-flex; |
| padding: 3px; |
| left: 0; |
| right: 0; |
| border: 1px solid var(--border-color); |
| background-color: var(--grouped-bg-color); |
| font-family: sans-serif; |
| position: fixed; |
| bottom: 0; |
| } |
| |
| #toolbar .actions { |
| float: right; |
| } |
| |
| .winter { |
| position: fixed; |
| z-index: 5; |
| left: 0; |
| right: 0; |
| top: 0; |
| bottom: 0; |
| background-color: black; |
| opacity: 0.8; |
| } |
| |
| .inactive { |
| display: none; |
| } |
| |
| .lightbox { |
| position: fixed; |
| z-index: 6; |
| left: 10%; |
| right: 10%; |
| top: 10%; |
| bottom: 10%; |
| background: var(--page-bg-color); |
| } |
| |
| .lightbox iframe { |
| width: 100%; |
| height: 100%; |
| } |
| |
| .commentContext .lineNumber { |
| background-color: yellow; |
| } |
| |
| @media (prefers-color-scheme: dark) { |
| .commentContext .lineNumber { |
| background-color: #770; |
| color: white; |
| } |
| } |
| |
| .selected .lineNumber { |
| --selection-color: #69f; |
| background-color: var(--selection-color); |
| border-bottom-color: var(--selection-color); |
| border-right-color: var(--selection-color); |
| } |
| |
| @media (prefers-color-scheme: dark) { |
| .selected .lineNumber { |
| color: white; |
| } |
| } |
| |
| .ExpandLinkContainer { |
| opacity: 0; |
| border-top: 1px solid var(--border-color); |
| border-bottom: 1px solid var(--border-color); |
| } |
| |
| .ExpandArea { |
| margin: 0; |
| } |
| |
| .ExpandText { |
| margin-left: 0.67em; |
| } |
| |
| .LinkContainer { |
| font-family: sans-serif; |
| font-size: small; |
| font-style: normal; |
| -webkit-transition: opacity 0.5s; |
| } |
| |
| .LinkContainer a { |
| border: 0; |
| } |
| |
| .LinkContainer label:after, |
| .LinkContainer a:after { |
| content: " | "; |
| color: text; |
| } |
| |
| .LinkContainer a:last-of-type:after { |
| content: ""; |
| } |
| |
| .LinkContainer label { |
| color: var(--link-color); |
| } |
| |
| .help { |
| color: gray; |
| font-style: italic; |
| } |
| |
| #message { |
| font-size: small; |
| font-family: sans-serif; |
| } |
| |
| .commentStatus { |
| font-style: italic; |
| } |
| |
| .comment, .previousComment, .frozenComment { |
| background-color: #ffd; |
| } |
| |
| @media (prefers-color-scheme: dark) { |
| .comment, .previousComment, .frozenComment { |
| background-color: #373700; |
| } |
| } |
| |
| .overallComments { |
| -webkit-flex: 1; |
| -moz-flex: 1; |
| margin-right: 3px; |
| } |
| |
| .previousComment, .frozenComment { |
| border: inset 1px; |
| padding: 5px; |
| white-space: pre-wrap; |
| } |
| |
| .comment button { |
| width: 6em; |
| } |
| |
| div:focus { |
| outline: 1px solid blue; |
| outline-offset: -1px; |
| } |
| |
| .statusBubble > iframe { |
| /* The width/height get set to the bubble contents via postMessage on browsers that support it. */ |
| width: 460px; |
| height: 20px; |
| margin: 2px 2px 0 0; |
| border: none; |
| vertical-align: middle; |
| } |
| |
| .revision { |
| display: none; |
| } |
| |
| .autosave-state { |
| position: absolute; |
| right: 0; |
| top: -1.3em; |
| padding: 0 3px; |
| outline: 1px solid var(--border-color); |
| color: #8FDF5F; |
| font-size: small; |
| background-color: var(--grouped-bg-color); |
| } |
| |
| .autosave-state:empty { |
| outline: 0px; |
| } |
| |
| .autosave-state.saving { |
| color: #E98080; |
| } |
| |
| .clear_float { |
| clear: both; |
| } |
| </style> |
| <script src="https://webkit.org/ajax/libs/jquery/jquery-1.4.2.min.js"></script> |
| <script src="js/status-bubble.js"></script> |
| <script src="code-review.js?version=48"></script> |
| </head> |
| EOF |
| |
| def self.revisionOrDescription(string) |
| case string |
| when /\(revision \d+\)/ |
| /\(revision (\d+)\)/.match(string)[1] |
| when /\(.*\)/ |
| /\((.*)\)/.match(string)[1] |
| end |
| end |
| |
| def self.has_image_suffix(filename) |
| filename =~ /\.(png|jpg|gif)$/ |
| end |
| |
| class FileDiff |
| attr_reader :filename |
| attr_reader :image |
| attr_reader :image_url |
| |
| def initialize(lines) |
| @filename = PrettyPatch.filename_from_diff_header(lines[0].chomp) |
| startOfSections = 1 |
| for i in 0...lines.length |
| case lines[i] |
| when /^--- / |
| @from = PrettyPatch.revisionOrDescription(lines[i]) |
| when /^\+\+\+ / |
| @filename = PrettyPatch.filename_from_diff_header(lines[i].chomp) if @filename.nil? |
| @to = PrettyPatch.revisionOrDescription(lines[i]) |
| startOfSections = i + 1 |
| |
| # Check for 'property' patch, then image data, since svn 1.7 creates a fake patch for property changes. |
| if /^$/.match(lines[startOfSections]) and SVN_PROPERTY_CHANGES_FORMAT.match(lines[startOfSections + 1]) then |
| startOfSections += 2 |
| for x in startOfSections...lines.length |
| next if not /^$/.match(lines[x]) |
| if SVN_START_OF_BINARY_DATA_FORMAT.match(lines[x + 1]) then |
| startOfSections = x + 1 |
| @binary = true |
| @image = true |
| break |
| end |
| end |
| end |
| break |
| when SVN_BINARY_FILE_MARKER_FORMAT |
| @binary = true |
| if (SVN_IMAGE_FILE_MARKER_FORMAT.match(lines[i + 1]) or PrettyPatch.has_image_suffix(@filename)) then |
| @image = true |
| startOfSections = i + 2 |
| for x in startOfSections...lines.length |
| # Binary diffs often have property changes listed before the actual binary data. Skip them. |
| if SVN_START_OF_BINARY_DATA_FORMAT.match(lines[x]) then |
| startOfSections = x |
| break |
| end |
| end |
| end |
| break |
| when GIT_INDEX_MARKER_FORMAT |
| @git_indexes = [$1, $2] |
| when GIT_BINARY_FILE_MARKER_FORMAT |
| @binary = true |
| if (GIT_BINARY_PATCH_FORMAT.match(lines[i + 1]) and PrettyPatch.has_image_suffix(@filename)) then |
| @git_image = true |
| startOfSections = i + 1 |
| end |
| break |
| when RENAME_FROM |
| @renameFrom = RENAME_FROM.match(lines[i])[1] |
| end |
| end |
| lines_with_contents = lines[startOfSections...lines.length] |
| @sections = DiffSection.parse(lines_with_contents) unless @binary |
| if @image and not lines_with_contents.empty? |
| @image_url = "data:image/png;base64," + lines_with_contents.join |
| @image_checksum = FileDiff.read_checksum_from_png(lines_with_contents.join.unpack("m").join) |
| elsif @git_image |
| begin |
| raise "index line is missing" unless @git_indexes |
| |
| chunks = nil |
| for i in 0...lines_with_contents.length |
| if lines_with_contents[i] =~ /^$/ |
| chunks = [lines_with_contents[i + 1 .. -1], lines_with_contents[0 .. i]] |
| break |
| end |
| end |
| |
| raise "no binary chunks" unless chunks |
| |
| from_filepath = FileDiff.extract_contents_from_remote(@filename, chunks[0], @git_indexes[0]) |
| to_filepath = FileDiff.extract_contents_of_to_revision(@filename, chunks[1], @git_indexes[1], from_filepath, @git_indexes[0]) |
| filepaths = from_filepath, to_filepath |
| |
| binary_contents = filepaths.collect { |filepath| File.exists?(filepath) ? File.read(filepath) : nil } |
| @image_urls = binary_contents.collect { |content| (content and not content.empty?) ? "data:image/png;base64," + [content].pack("m") : nil } |
| @image_checksums = binary_contents.collect { |content| FileDiff.read_checksum_from_png(content) } |
| rescue |
| $last_prettify_part_count["extract-error"] += 1 |
| @image_error = "Exception raised during decoding git binary patch:<pre>#{CGI.escapeHTML($!.to_s + "\n" + $!.backtrace.join("\n"))}</pre>" |
| ensure |
| File.unlink(from_filepath) if (from_filepath and File.exists?(from_filepath)) |
| File.unlink(to_filepath) if (to_filepath and File.exists?(to_filepath)) |
| end |
| end |
| nil |
| end |
| |
| def image_to_html |
| if not @image_url then |
| return "<span class='text'>Image file removed</span>" |
| end |
| |
| image_checksum = "" |
| if @image_checksum |
| image_checksum = @image_checksum |
| elsif @filename.include? "-expected.png" and @image_url |
| image_checksum = IMAGE_CHECKSUM_ERROR |
| end |
| |
| return "<p>" + image_checksum + "</p><img class='image' src='" + @image_url + "' />" |
| end |
| |
| def to_html |
| str = "<div class='FileDiff'>\n" |
| if @renameFrom |
| str += "<h1>#{@filename}</h1>" |
| str += "was renamed from" |
| str += "<h1>#{PrettyPatch.linkifyFilename(@renameFrom.to_s)}</h1>" |
| else |
| str += "<h1>#{PrettyPatch.linkifyFilename(@filename)}</h1>\n" |
| end |
| if @image then |
| str += self.image_to_html |
| elsif @git_image then |
| if @image_error |
| str += @image_error |
| else |
| for i in (0...2) |
| image_url = @image_urls[i] |
| image_checksum = @image_checksums[i] |
| |
| style = ["remove", "add"][i] |
| str += "<p class=\"#{style}\">" |
| |
| if image_checksum |
| str += image_checksum |
| elsif @filename.include? "-expected.png" and image_url |
| str += IMAGE_CHECKSUM_ERROR |
| end |
| |
| str += "<br>" |
| |
| if image_url |
| str += "<img class='image' src='" + image_url + "' />" |
| else |
| str += ["</p>Added", "</p>Removed"][i] |
| end |
| end |
| end |
| elsif @binary then |
| $last_prettify_part_count["binary"] += 1 |
| str += "<span class='text'>Binary file, nothing to see here</span>" |
| else |
| str += @sections.collect{ |section| section.to_html }.join("<br>\n") unless @sections.nil? |
| end |
| |
| if @from then |
| str += "<span class='revision'>" + @from + "</span>" |
| end |
| |
| str += "</div>\n" |
| end |
| |
| def self.parse(string) |
| commitMessageLength = 0 |
| haveSeenDiffHeader = false |
| haveCommitMessage = false |
| subject = '' |
| linesForDiffs = [] |
| line_array = string.lines.to_a |
| line_array.each_with_index do |line, index| |
| if (PrettyPatch.diff_header?(line)) |
| linesForDiffs << [] |
| haveSeenDiffHeader = true |
| elsif (!haveSeenDiffHeader && line =~ /^--- / && line_array[index + 1] =~ /^\+\+\+ /) |
| linesForDiffs << [] |
| haveSeenDiffHeader = false |
| elsif (PrettyPatch.message_header?(line)) |
| haveSeenDiffHeader = false |
| haveCommitMessage = true |
| commitMessageLength = 1 |
| linesForDiffs << [] |
| linesForDiffs.last << '+++ COMMIT_MESSAGE' |
| if line[MESSAGE_HEADER_FORMATS[0], 1] |
| linesForDiffs.last[-1] += ' (' + line[MESSAGE_HEADER_FORMATS[0], 1] + ')' |
| end |
| linesForDiffs.last << '@@ -0,0 +1,1 @@' |
| subject = line[MESSAGE_HEADER_FORMATS[0], 2] |
| elsif (!subject.empty? && line.strip.empty?) |
| subject.split(BUG_URL_RE).each { |mtch| |
| if !mtch.strip.empty? |
| commitMessageLength += 1 |
| linesForDiffs.last << '+' + mtch |
| end |
| } |
| subject = '' |
| elsif (!subject.empty?) |
| subject += line |
| elsif (commitMessageLength != 0 && PrettyPatch.message_footer?(line)) |
| linesForDiffs.last[1] = '@@ -0,0 +1,' + commitMessageLength.to_s + ' @@' |
| commitMessageLength = 0 |
| end |
| |
| if (subject.empty? && commitMessageLength != 0) |
| commitMessageLength += 1 |
| linesForDiffs.last << '+' + line unless linesForDiffs.last.nil? |
| elsif (subject.empty? && (!haveCommitMessage || haveSeenDiffHeader)) |
| linesForDiffs.last << line unless linesForDiffs.last.nil? |
| end |
| end |
| |
| linesForDiffs.collect { |lines| FileDiff.new(lines) } |
| end |
| |
| def self.read_checksum_from_png(png_bytes) |
| # Ruby 1.9 added the concept of string encodings, so to avoid treating binary data as UTF-8, |
| # we can force the encoding to binary at this point. |
| if RUBY_VERSION >= "1.9" |
| png_bytes.force_encoding('binary') |
| end |
| match = png_bytes && png_bytes.match(/tEXtchecksum\0([a-fA-F0-9]{32})/) |
| match ? match[1] : nil |
| end |
| |
| def self.git_new_file_binary_patch(filename, encoded_chunk, git_index) |
| return <<END |
| diff --git a/#{filename} b/#{filename} |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..#{git_index} |
| GIT binary patch |
| #{encoded_chunk.join("")}literal 0 |
| HcmV?d00001 |
| |
| END |
| end |
| |
| def self.git_changed_file_binary_patch(to_filename, from_filename, encoded_chunk, to_git_index, from_git_index) |
| return <<END |
| diff --git a/#{from_filename} b/#{to_filename} |
| copy from #{from_filename} |
| +++ b/#{to_filename} |
| index #{from_git_index}..#{to_git_index} |
| GIT binary patch |
| #{encoded_chunk.join("")}literal 0 |
| HcmV?d00001 |
| |
| END |
| end |
| |
| def self.get_github_uri(repository_path) |
| "https://raw.githubusercontent.com/WebKit/WebKit/main/" + (repository_path) |
| end |
| |
| def self.get_new_temp_filepath_and_name |
| tempfile = Tempfile.new("PrettyPatch") |
| filepath = tempfile.path + '.bin' |
| filename = File.basename(filepath) |
| return filepath, filename |
| end |
| |
| def self.download_from_revision_from_github(repository_path) |
| filepath, filename = get_new_temp_filepath_and_name |
| github_uri = get_github_uri(repository_path) |
| open(filepath, 'wb') do |to_file| |
| to_file << open(github_uri) { |from_file| from_file.read } |
| end |
| return filepath |
| end |
| |
| def self.run_git_apply_on_patch(output_filepath, patch) |
| # Apply the git binary patch using git-apply. |
| cmd = GIT_PATH + " apply" |
| # Check if we need to pass --unsafe-paths (git >= 2.3.3) |
| helpcmd = GIT_PATH + " help apply" |
| stdin, stdout, stderr = *Open3.popen3(helpcmd) |
| begin |
| if stdout.read().include? "--unsafe-paths" |
| cmd += " --unsafe-paths" |
| end |
| end |
| cmd += " --directory=" + File.dirname(output_filepath) |
| stdin, stdout, stderr = *Open3.popen3(cmd) |
| begin |
| stdin.puts(patch) |
| stdin.close |
| |
| error = stderr.read |
| if error != "" |
| error = "Error running " + cmd + "\n" + "with patch:\n" + patch[0..500] + "...\n" + error |
| end |
| raise error if error != "" |
| ensure |
| stdin.close unless stdin.closed? |
| stdout.close |
| stderr.close |
| end |
| end |
| |
| def self.extract_contents_from_git_binary_literal_chunk(encoded_chunk, git_index) |
| filepath, filename = get_new_temp_filepath_and_name |
| patch = FileDiff.git_new_file_binary_patch(filename, encoded_chunk, git_index) |
| run_git_apply_on_patch(filepath, patch) |
| return filepath |
| end |
| |
| def self.extract_contents_from_git_binary_delta_chunk(from_filepath, from_git_index, encoded_chunk, to_git_index) |
| to_filepath, to_filename = get_new_temp_filepath_and_name |
| from_filename = File.basename(from_filepath) |
| patch = FileDiff.git_changed_file_binary_patch(to_filename, from_filename, encoded_chunk, to_git_index, from_git_index) |
| run_git_apply_on_patch(to_filepath, patch) |
| return to_filepath |
| end |
| |
| def self.extract_contents_from_remote(repository_path, encoded_chunk, git_index) |
| # For literal encoded, simply reconstruct. |
| if GIT_LITERAL_FORMAT.match(encoded_chunk[0]) |
| return extract_contents_from_git_binary_literal_chunk(encoded_chunk, git_index) |
| end |
| # For delta encoded, download from svn. |
| if GIT_DELTA_FORMAT.match(encoded_chunk[0]) |
| return download_from_revision_from_github(repository_path) |
| end |
| raise "Error: unknown git patch encoding" |
| end |
| |
| def self.extract_contents_of_to_revision(repository_path, encoded_chunk, git_index, from_filepath, from_git_index) |
| # For literal encoded, simply reconstruct. |
| if GIT_LITERAL_FORMAT.match(encoded_chunk[0]) |
| return extract_contents_from_git_binary_literal_chunk(encoded_chunk, git_index) |
| end |
| # For delta encoded, reconstruct using delta and previously constructed 'from' revision. |
| if GIT_DELTA_FORMAT.match(encoded_chunk[0]) |
| return extract_contents_from_git_binary_delta_chunk(from_filepath, from_git_index, encoded_chunk, git_index) |
| end |
| raise "Error: unknown git patch encoding" |
| end |
| end |
| |
| class DiffBlock |
| attr_accessor :parts |
| |
| def initialize(container) |
| @parts = [] |
| container << self |
| end |
| |
| def to_html |
| str = "<div class='DiffBlock'>\n" |
| str += @parts.collect{ |part| part.to_html }.join |
| str += "<div class='clear_float'></div></div>\n" |
| end |
| end |
| |
| class DiffBlockPart |
| attr_reader :className |
| attr :lines |
| |
| def initialize(className, container) |
| $last_prettify_part_count[className] += 1 |
| @className = className |
| @lines = [] |
| container.parts << self |
| end |
| |
| def to_html |
| str = "<div class='DiffBlockPart %s'>\n" % @className |
| str += @lines.collect{ |line| line.to_html }.join |
| # Don't put white-space after this so adjacent inline-block DiffBlockParts will not wrap. |
| str += "</div>" |
| end |
| end |
| |
| class DiffSection |
| def initialize(lines) |
| lines.length >= 1 or raise "DiffSection.parse only received %d lines" % lines.length |
| |
| matches = START_OF_SECTION_FORMAT.match(lines[0]) |
| |
| if matches |
| from, to = [matches[1].to_i, matches[3].to_i] |
| if matches[2] and matches[4] |
| from_end = from + matches[2].to_i |
| to_end = to + matches[4].to_i |
| end |
| end |
| |
| @blocks = [] |
| diff_block = nil |
| diff_block_part = nil |
| |
| for line in lines[1...lines.length] |
| startOfLine = line =~ /^[-\+ ]/ ? 1 : 0 |
| text = line[startOfLine...line.length].chomp |
| case line[0] |
| when ?- |
| if (diff_block_part.nil? or diff_block_part.className != 'remove') |
| diff_block = DiffBlock.new(@blocks) |
| diff_block_part = DiffBlockPart.new('remove', diff_block) |
| end |
| |
| diff_block_part.lines << CodeLine.new(from, nil, text) |
| from += 1 unless from.nil? |
| when ?+ |
| if (diff_block_part.nil? or diff_block_part.className != 'add') |
| # Put add lines that immediately follow remove lines into the same DiffBlock. |
| if (diff_block.nil? or diff_block_part.className != 'remove') |
| diff_block = DiffBlock.new(@blocks) |
| end |
| |
| diff_block_part = DiffBlockPart.new('add', diff_block) |
| end |
| |
| diff_block_part.lines << CodeLine.new(nil, to, text) |
| to += 1 unless to.nil? |
| else |
| if (diff_block_part.nil? or diff_block_part.className != 'shared') |
| diff_block = DiffBlock.new(@blocks) |
| diff_block_part = DiffBlockPart.new('shared', diff_block) |
| end |
| |
| diff_block_part.lines << CodeLine.new(from, to, text) |
| from += 1 unless from.nil? |
| to += 1 unless to.nil? |
| end |
| |
| break if from_end and to_end and from == from_end and to == to_end |
| end |
| |
| changes = [ [ [], [] ] ] |
| for block in @blocks |
| for block_part in block.parts |
| for line in block_part.lines |
| if (!line.fromLineNumber.nil? and !line.toLineNumber.nil?) then |
| changes << [ [], [] ] |
| next |
| end |
| changes.last.first << line if line.toLineNumber.nil? |
| changes.last.last << line if line.fromLineNumber.nil? |
| end |
| end |
| end |
| |
| for change in changes |
| next unless change.first.length == change.last.length |
| for i in (0...change.first.length) |
| from_text = change.first[i].text |
| to_text = change.last[i].text |
| next if from_text.length > MAXIMUM_INTRALINE_DIFF_LINE_LENGTH or to_text.length > MAXIMUM_INTRALINE_DIFF_LINE_LENGTH |
| raw_operations = HTMLDiff::DiffBuilder.new(from_text, to_text).operations |
| operations = [] |
| back = 0 |
| raw_operations.each_with_index do |operation, j| |
| if operation.action == :equal and j < raw_operations.length - 1 |
| length = operation.end_in_new - operation.start_in_new |
| if length < SMALLEST_EQUAL_OPERATION |
| back = length |
| next |
| end |
| end |
| operation.start_in_old -= back |
| operation.start_in_new -= back |
| back = 0 |
| operations << operation |
| end |
| change.first[i].operations = operations |
| change.last[i].operations = operations |
| end |
| end |
| |
| @blocks.unshift(ContextLine.new(matches[5])) unless matches.nil? || matches[5].empty? |
| end |
| |
| def to_html |
| str = "<div class='DiffSection'>\n" |
| str += @blocks.collect{ |block| block.to_html }.join |
| str += "</div>\n" |
| end |
| |
| def self.parse(lines) |
| linesForSections = lines.inject([[]]) do |sections, line| |
| sections << [] if line =~ /^@@/ |
| sections.last << line |
| sections |
| end |
| |
| linesForSections.delete_if { |lines| lines.nil? or lines.empty? } |
| linesForSections.collect { |lines| DiffSection.new(lines) } |
| end |
| end |
| |
| class Line |
| attr_reader :fromLineNumber |
| attr_reader :toLineNumber |
| attr_reader :text |
| |
| def initialize(from, to, text) |
| @fromLineNumber = from |
| @toLineNumber = to |
| @text = text |
| end |
| |
| def text_as_html |
| CGI.escapeHTML(text) |
| end |
| |
| def classes |
| lineClasses = ["Line", "LineContainer"] |
| lineClasses << ["add"] unless @toLineNumber.nil? or !@fromLineNumber.nil? |
| lineClasses << ["remove"] unless @fromLineNumber.nil? or !@toLineNumber.nil? |
| lineClasses |
| end |
| |
| def to_html |
| markedUpText = self.text_as_html |
| str = "<div class='%s'>\n" % self.classes.join(' ') |
| str += "<span class='from lineNumber'>%s</span><span class='to lineNumber'>%s</span>" % |
| [@fromLineNumber.nil? ? ' ' : @fromLineNumber, |
| @toLineNumber.nil? ? ' ' : @toLineNumber] unless @fromLineNumber.nil? and @toLineNumber.nil? |
| str += "<span class='text'>%s</span>\n" % markedUpText |
| str += "</div>\n" |
| end |
| end |
| |
| class CodeLine < Line |
| attr :operations, true |
| |
| def text_as_html |
| html = [] |
| tag = @fromLineNumber.nil? ? "ins" : "del" |
| if @operations.nil? or @operations.empty? |
| return CGI.escapeHTML(@text) |
| end |
| @operations.each do |operation| |
| start = @fromLineNumber.nil? ? operation.start_in_new : operation.start_in_old |
| eend = @fromLineNumber.nil? ? operation.end_in_new : operation.end_in_old |
| escaped_text = CGI.escapeHTML(@text[start...eend]) |
| if eend - start === 0 or operation.action === :equal |
| html << escaped_text |
| else |
| html << "<#{tag}>#{escaped_text}</#{tag}>" |
| end |
| end |
| html.join |
| end |
| end |
| |
| class ContextLine < Line |
| def initialize(context) |
| super("@", "@", context) |
| end |
| |
| def classes |
| super << "context" |
| end |
| end |
| end |