14 def self.prettify(string)
15 string = normalize_line_ending(string)
16 fileDiffs = FileDiff.parse(string)
20 # Just look at the first line to see if it is an SVN revision number as added
21 # by webkit-patch for git checkouts.
22 string.each_line do |line|
23 match = /^Subversion\ Revision: (\d*)$/.match(line)
25 str += "<span class='revision'>" + match[1] + "</span>\n"
30 str += fileDiffs.collect{ |diff| diff.to_html }.join
33 def self.filename_from_diff_header(line)
34 DIFF_HEADER_FORMATS.each do |format|
35 match = format.match(line)
36 return match[1] unless match.nil?
41 def self.diff_header?(line)
42 RELAXED_DIFF_HEADER_FORMATS.any? { |format| line =~ format }
46 DIFF_HEADER_FORMATS = [
48 /^diff --git "?a\/.+"? "?b\/(.+)"?\r?$/,
49 /^\+\+\+ ([^\t]+)(\t.*)?\r?$/
52 RELAXED_DIFF_HEADER_FORMATS = [
57 BINARY_FILE_MARKER_FORMAT = /^Cannot display: file marked as a binary type.$/
59 IMAGE_FILE_MARKER_FORMAT = /^svn:mime-type = image\/png$/
61 GIT_INDEX_MARKER_FORMAT = /^index ([0-9a-f]{40})\.\.([0-9a-f]{40})/
63 GIT_BINARY_FILE_MARKER_FORMAT = /^GIT binary patch$/
65 GIT_LITERAL_FORMAT = /^literal \d+$/
67 START_OF_BINARY_DATA_FORMAT = /^[0-9a-zA-Z\+\/=]{20,}/ # Assume 20 chars without a space is base64 binary data.
69 START_OF_SECTION_FORMAT = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@\s*(.*)/
71 START_OF_EXTENT_STRING = "%c" % 0
72 END_OF_EXTENT_STRING = "%c" % 1
74 # We won't search for intra-line diffs in lines longer than this length, to avoid hangs. See <http://webkit.org/b/56109>.
75 MAXIMUM_INTRALINE_DIFF_LINE_LENGTH = 10000
77 SMALLEST_EQUAL_OPERATION = 3
79 OPENSOURCE_TRAC_URL = "http://trac.webkit.org/"
81 OPENSOURCE_DIRS = Set.new %w[
91 def self.normalize_line_ending(s)
95 def self.find_url_and_path(file_path)
96 # Search file_path from the bottom up, at each level checking whether
97 # we've found a directory we know exists in the source tree.
99 dirname, basename = File.split(file_path)
100 dirname.split(/\//).reverse.inject(basename) do |path, directory|
101 path = directory + "/" + path
103 return [OPENSOURCE_TRAC_URL, path] if OPENSOURCE_DIRS.include?(directory)
111 def self.linkifyFilename(filename)
112 url, pathBeneathTrunk = find_url_and_path(filename)
114 url.nil? ? filename : "<a href='#{url}browser/trunk/#{pathBeneathTrunk}'>#{filename}</a>"
121 text-decoration: none;
122 border-bottom: 1px dotted;
130 background-color: #f8f8f8;
131 border: 1px solid #ddd;
132 font-family: monospace;
138 font-family: sans-serif;
146 h1 :link, h1 :visited {
152 background-color: #eee;
159 .FileDiffLinkContainer {
162 padding-right: 0.5em;
167 background-color: white;
169 border-width: 1px 0px;
172 .ExpansionLine, .LineContainer {
176 .sidebyside .DiffBlockPart.add:first-child {
180 .LineSide:last-child {
185 .sidebyside .DiffBlockPart.remove,
186 .sidebyside .DiffBlockPart.add {
187 display:inline-block;
192 .sidebyside .DiffBlockPart.remove .to,
193 .sidebyside .DiffBlockPart.add .from {
197 .lineNumber, .expansionLineNumber {
198 border-bottom: 1px solid #998;
199 border-right: 1px solid #ddd;
201 display: inline-block;
202 padding: 1px 5px 0px 0px;
204 vertical-align: bottom;
209 background-color: #eed;
212 .expansionLineNumber {
213 background-color: #eee;
218 white-space: pre-wrap;
219 word-wrap: break-word;
223 border: 2px solid black;
226 .context, .context .lineNumber {
228 background-color: #fef;
232 background-color: #dfd;
236 background-color: #9e9;
237 text-decoration: none;
241 background-color: #fdd;
245 background-color: #e99;
246 text-decoration: none;
249 /* Support for inline comments */
263 .overallComments textarea {
267 .comment textarea, .overallComments textarea {
272 .overallComments .open {
273 -webkit-transition: height .2s;
277 #statusBubbleContainer.wrap {
286 display: -webkit-box;
291 border: 1px solid #ddd;
292 background-color: #eee;
293 font-family: sans-serif;
312 background-color: black;
335 .commentContext .lineNumber {
336 background-color: yellow;
339 .selected .lineNumber {
340 background-color: #69F;
341 border-bottom-color: #69F;
342 border-right-color: #69F;
345 .ExpandLinkContainer {
347 border-top: 1px solid #ddd;
348 border-bottom: 1px solid #ddd;
360 font-family: sans-serif;
363 -webkit-transition: opacity 0.5s;
370 .LinkContainer a:after {
375 .LinkContainer a:last-of-type:after {
386 font-family: sans-serif;
393 .comment, .previousComment, .frozenComment {
394 background-color: #ffd;
403 .previousComment, .frozenComment {
406 white-space: pre-wrap;
414 outline: 1px solid blue;
415 outline-offset: -1px;
419 /* The width/height get set to the bubble contents via postMessage on browsers that support it. */
424 vertical-align: middle;
427 .pseudo_resize_event_iframe {
443 outline: 1px solid #DDD;
446 background-color: #EEE;
449 .autosave-state:empty {
452 .autosave-state.saving {
460 <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script>
461 <script src="code-review.js?version=40"></script>
464 def self.revisionOrDescription(string)
466 when /\(revision \d+\)/
467 /\(revision (\d+)\)/.match(string)[1]
469 /\((.*)\)/.match(string)[1]
473 def self.has_image_suffix(filename)
474 filename =~ /\.(png|jpg|gif)$/
478 def initialize(lines)
479 @filename = PrettyPatch.filename_from_diff_header(lines[0].chomp)
481 for i in 0...lines.length
484 @from = PrettyPatch.revisionOrDescription(lines[i])
486 @filename = PrettyPatch.filename_from_diff_header(lines[i].chomp) if @filename.nil?
487 @to = PrettyPatch.revisionOrDescription(lines[i])
488 startOfSections = i + 1
490 when BINARY_FILE_MARKER_FORMAT
492 if (IMAGE_FILE_MARKER_FORMAT.match(lines[i + 1]) or PrettyPatch.has_image_suffix(@filename)) then
494 startOfSections = i + 2
495 for x in startOfSections...lines.length
496 # Binary diffs often have property changes listed before the actual binary data. Skip them.
497 if START_OF_BINARY_DATA_FORMAT.match(lines[x]) then
504 when GIT_INDEX_MARKER_FORMAT
505 @git_indexes = [$1, $2]
506 when GIT_BINARY_FILE_MARKER_FORMAT
508 if (GIT_LITERAL_FORMAT.match(lines[i + 1]) and PrettyPatch.has_image_suffix(@filename)) then
510 startOfSections = i + 1
515 lines_with_contents = lines[startOfSections...lines.length]
516 @sections = DiffSection.parse(lines_with_contents) unless @binary
518 @image_url = "data:image/png;base64," + lines_with_contents.join
521 raise "index line is missing" unless @git_indexes
524 for i in 0...lines_with_contents.length
525 if lines_with_contents[i] =~ /^$/
526 chunks = [lines_with_contents[i + 1 .. -1], lines_with_contents[0 .. i]]
531 raise "no binary chunks" unless chunks
533 @image_urls = chunks.zip(@git_indexes).collect do |chunk, git_index|
534 FileDiff.extract_contents_from_git_binary_chunk(chunk, git_index)
537 @image_error = "Exception raised during decoding git binary patch:<pre>#{CGI.escapeHTML($!.to_s + "\n" + $!.backtrace.join("\n"))}</pre>"
544 str = "<div class='FileDiff'>\n"
545 str += "<h1>#{PrettyPatch.linkifyFilename(@filename)}</h1>\n"
547 str += "<img class='image' src='" + @image_url + "' />"
548 elsif @git_image then
553 image_url = @image_urls[i]
554 style = ["remove", "add"][i]
555 str += "<p class=\"#{style}\">"
557 str += "<img class='image' src='" + image_url + "' />"
559 str += ["Added", "Removed"][i]
564 str += "<span class='text'>Binary file, nothing to see here</span>"
566 str += @sections.collect{ |section| section.to_html }.join("<br>\n") unless @sections.nil?
570 str += "<span class='revision'>" + @from + "</span>"
576 def self.parse(string)
577 haveSeenDiffHeader = false
579 string.each_line do |line|
580 if (PrettyPatch.diff_header?(line))
582 haveSeenDiffHeader = true
583 elsif (!haveSeenDiffHeader && line =~ /^--- /)
585 haveSeenDiffHeader = false
587 linesForDiffs.last << line unless linesForDiffs.last.nil?
590 linesForDiffs.collect { |lines| FileDiff.new(lines) }
593 def self.git_new_file_binary_patch(filename, encoded_chunk, git_index)
595 diff --git a/#{filename} b/#{filename}
597 index 0000000000000000000000000000000000000000..#{git_index}
599 #{encoded_chunk.join("")}literal 0
605 def self.extract_contents_from_git_binary_chunk(encoded_chunk, git_index)
606 # We use Tempfile we need a unique file among processes.
607 tempfile = Tempfile.new("PrettyPatch")
608 # We need a filename which doesn't exist to apply a patch
609 # which creates a new file. Append a suffix so filename
611 filepath = tempfile.path + '.bin'
612 filename = File.basename(filepath)
614 patch = FileDiff.git_new_file_binary_patch(filename, encoded_chunk, git_index)
616 # Apply the git binary patch using git-apply.
617 cmd = GIT_PATH + " apply --directory=" + File.dirname(filepath)
618 stdin, stdout, stderr = *Open3.popen3(cmd)
624 raise error if error != ""
626 contents = File.read(filepath)
628 stdin.close unless stdin.closed?
631 File.unlink(filename) if File.exists?(filename)
634 return nil if contents.empty?
635 return "data:image/png;base64," + [contents].pack("m")
642 def initialize(container)
648 str = "<div class='DiffBlock'>\n"
649 str += @parts.collect{ |part| part.to_html }.join
650 str += "<div class='clear_float'></div></div>\n"
655 attr_reader :className
658 def initialize(className, container)
659 @className = className
661 container.parts << self
665 str = "<div class='DiffBlockPart %s'>\n" % @className
666 str += @lines.collect{ |line| line.to_html }.join
667 # Don't put white-space after this so adjacent inline-block DiffBlockParts will not wrap.
673 def initialize(lines)
674 lines.length >= 1 or raise "DiffSection.parse only received %d lines" % lines.length
676 matches = START_OF_SECTION_FORMAT.match(lines[0])
679 from, to = [matches[1].to_i, matches[3].to_i]
680 if matches[2] and matches[4]
681 from_end = from + matches[2].to_i
682 to_end = to + matches[4].to_i
688 diff_block_part = nil
690 for line in lines[1...lines.length]
691 startOfLine = line =~ /^[-\+ ]/ ? 1 : 0
692 text = line[startOfLine...line.length].chomp
695 if (diff_block_part.nil? or diff_block_part.className != 'remove')
696 diff_block = DiffBlock.new(@blocks)
697 diff_block_part = DiffBlockPart.new('remove', diff_block)
700 diff_block_part.lines << CodeLine.new(from, nil, text)
701 from += 1 unless from.nil?
703 if (diff_block_part.nil? or diff_block_part.className != 'add')
704 # Put add lines that immediately follow remove lines into the same DiffBlock.
705 if (diff_block.nil? or diff_block_part.className != 'remove')
706 diff_block = DiffBlock.new(@blocks)
709 diff_block_part = DiffBlockPart.new('add', diff_block)
712 diff_block_part.lines << CodeLine.new(nil, to, text)
713 to += 1 unless to.nil?
715 if (diff_block_part.nil? or diff_block_part.className != 'shared')
716 diff_block = DiffBlock.new(@blocks)
717 diff_block_part = DiffBlockPart.new('shared', diff_block)
720 diff_block_part.lines << CodeLine.new(from, to, text)
721 from += 1 unless from.nil?
722 to += 1 unless to.nil?
725 break if from_end and to_end and from == from_end and to == to_end
728 changes = [ [ [], [] ] ]
730 for block_part in block.parts
731 for line in block_part.lines
732 if (!line.fromLineNumber.nil? and !line.toLineNumber.nil?) then
733 changes << [ [], [] ]
736 changes.last.first << line if line.toLineNumber.nil?
737 changes.last.last << line if line.fromLineNumber.nil?
742 for change in changes
743 next unless change.first.length == change.last.length
744 for i in (0...change.first.length)
745 from_text = change.first[i].text
746 to_text = change.last[i].text
747 next if from_text.length > MAXIMUM_INTRALINE_DIFF_LINE_LENGTH or to_text.length > MAXIMUM_INTRALINE_DIFF_LINE_LENGTH
748 raw_operations = HTMLDiff::DiffBuilder.new(from_text, to_text).operations
751 raw_operations.each_with_index do |operation, j|
752 if operation.action == :equal and j < raw_operations.length - 1
753 length = operation.end_in_new - operation.start_in_new
754 if length < SMALLEST_EQUAL_OPERATION
759 operation.start_in_old -= back
760 operation.start_in_new -= back
762 operations << operation
764 change.first[i].operations = operations
765 change.last[i].operations = operations
769 @blocks.unshift(ContextLine.new(matches[5])) unless matches.nil? || matches[5].empty?
773 str = "<div class='DiffSection'>\n"
774 str += @blocks.collect{ |block| block.to_html }.join
778 def self.parse(lines)
779 linesForSections = lines.inject([[]]) do |sections, line|
780 sections << [] if line =~ /^@@/
781 sections.last << line
785 linesForSections.delete_if { |lines| lines.nil? or lines.empty? }
786 linesForSections.collect { |lines| DiffSection.new(lines) }
791 attr_reader :fromLineNumber
792 attr_reader :toLineNumber
795 def initialize(from, to, text)
796 @fromLineNumber = from
806 lineClasses = ["Line", "LineContainer"]
807 lineClasses << ["add"] unless @toLineNumber.nil? or !@fromLineNumber.nil?
808 lineClasses << ["remove"] unless @fromLineNumber.nil? or !@toLineNumber.nil?
813 markedUpText = self.text_as_html
814 str = "<div class='%s'>\n" % self.classes.join(' ')
815 str += "<span class='from lineNumber'>%s</span><span class='to lineNumber'>%s</span>" %
816 [@fromLineNumber.nil? ? ' ' : @fromLineNumber,
817 @toLineNumber.nil? ? ' ' : @toLineNumber] unless @fromLineNumber.nil? and @toLineNumber.nil?
818 str += "<span class='text'>%s</span>\n" % markedUpText
823 class CodeLine < Line
824 attr :operations, true
828 tag = @fromLineNumber.nil? ? "ins" : "del"
829 if @operations.nil? or @operations.empty?
830 return CGI.escapeHTML(@text)
832 @operations.each do |operation|
833 start = @fromLineNumber.nil? ? operation.start_in_new : operation.start_in_old
834 eend = @fromLineNumber.nil? ? operation.end_in_new : operation.end_in_old
835 escaped_text = CGI.escapeHTML(@text[start...eend])
836 if eend - start === 0 or operation.action === :equal
839 html << "<#{tag}>#{escaped_text}</#{tag}>"
846 class ContextLine < Line
847 def initialize(context)
848 super("@", "@", context)