14 def self.prettify(string)
15 fileDiffs = FileDiff.parse(string)
18 str += fileDiffs.collect{ |diff| diff.to_html }.join
21 def self.filename_from_diff_header(line)
22 DIFF_HEADER_FORMATS.each do |format|
23 match = format.match(line)
24 return match[1] unless match.nil?
29 def self.diff_header?(line)
30 RELAXED_DIFF_HEADER_FORMATS.any? { |format| line =~ format }
34 DIFF_HEADER_FORMATS = [
36 /^diff --git "?a\/.+"? "?b\/(.+)"?\r?$/,
37 /^\+\+\+ ([^\t]+)(\t.*)?\r?$/
40 RELAXED_DIFF_HEADER_FORMATS = [
45 BINARY_FILE_MARKER_FORMAT = /^Cannot display: file marked as a binary type.$/
47 IMAGE_FILE_MARKER_FORMAT = /^svn:mime-type = image\/png$/
49 GIT_INDEX_MARKER_FORMAT = /^index ([0-9a-f]{40})\.\.([0-9a-f]{40})/
51 GIT_BINARY_FILE_MARKER_FORMAT = /^GIT binary patch$/
53 GIT_LITERAL_FORMAT = /^literal \d+$/
55 START_OF_BINARY_DATA_FORMAT = /^[0-9a-zA-Z\+\/=]{20,}/ # Assume 20 chars without a space is base64 binary data.
57 START_OF_SECTION_FORMAT = /^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@\s*(.*)/
59 START_OF_EXTENT_STRING = "%c" % 0
60 END_OF_EXTENT_STRING = "%c" % 1
62 SMALLEST_EQUAL_OPERATION = 3
64 OPENSOURCE_TRAC_URL = "http://trac.webkit.org/"
66 OPENSOURCE_DIRS = Set.new %w[
79 def self.find_url_and_path(file_path)
80 # Search file_path from the bottom up, at each level checking whether
81 # we've found a directory we know exists in the source tree.
83 dirname, basename = File.split(file_path)
84 dirname.split(/\//).reverse.inject(basename) do |path, directory|
85 path = directory + "/" + path
87 return [OPENSOURCE_TRAC_URL, path] if OPENSOURCE_DIRS.include?(directory)
95 def self.linkifyFilename(filename)
96 url, pathBeneathTrunk = find_url_and_path(filename)
98 url.nil? ? filename : "<a href='#{url}browser/trunk/#{pathBeneathTrunk}'>#{filename}</a>"
105 text-decoration: none;
106 border-bottom: 1px dotted;
114 background-color: #f8f8f8;
115 border: 1px solid #ddd;
116 font-family: monospace;
122 font-family: sans-serif;
127 h1 :link, h1 :visited {
133 background-color: #eee;
137 background-color: white;
139 border-width: 1px 0px;
142 .lineNumber, .expansionLineNumber {
143 border-bottom: 1px solid #998;
144 border-right: 1px solid #ddd;
146 display: inline-block;
147 padding: 1px 5px 0px 0px;
149 vertical-align: bottom;
154 background-color: #eed;
157 .expansionLineNumber {
158 background-color: #eee;
164 white-space: pre-wrap;
168 border: 2px solid black;
171 .context, .context .lineNumber {
173 background-color: #fef;
177 background-color: #dfd;
181 background-color: #9e9;
182 text-decoration: none;
186 background-color: #fdd;
190 background-color: #e99;
191 text-decoration: none;
194 /* Support for inline comments */
208 .overallComments textarea {
212 .comment textarea, .overallComments textarea {
217 .overallComments .open {
218 -webkit-transition: height .2s;
222 #statusBubbleContainer.wrap {
231 display: -webkit-box;
238 border-top: 1px solid #ddd;
239 background-color: #eee;
240 font-family: sans-serif;
254 background-color: black;
276 .commentContext .lineNumber {
277 background-color: yellow;
280 .selected .lineNumber {
281 background-color: #69F;
282 border-bottom-color: #69F;
283 border-right-color: #69F;
294 .ExpandLinkContainer a {
298 .ExpandLinkContainer a:after {
303 .ExpandLinkContainer a:last-of-type:after {
314 font-family: sans-serif;
321 .comment, .previousComment, .frozenComment {
322 background-color: #ffd;
331 .previousComment, .frozenComment {
334 white-space: pre-wrap;
342 border: 1px solid blue;
346 /* The width/height get set to the bubble contents via postMessage on browsers that support it. */
351 vertical-align: middle;
354 <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script>
355 <script src="code-review.js?version=17"></script>
358 def self.revisionOrDescription(string)
360 when /\(revision \d+\)/
361 /\(revision (\d+)\)/.match(string)[1]
363 /\((.*)\)/.match(string)[1]
367 def self.has_image_suffix(filename)
368 filename =~ /\.(png|jpg|gif)$/
372 def initialize(lines)
373 @filename = PrettyPatch.filename_from_diff_header(lines[0].chomp)
375 for i in 0...lines.length
378 @from = PrettyPatch.revisionOrDescription(lines[i])
380 @filename = PrettyPatch.filename_from_diff_header(lines[i].chomp) if @filename.nil?
381 @to = PrettyPatch.revisionOrDescription(lines[i])
382 startOfSections = i + 1
384 when BINARY_FILE_MARKER_FORMAT
386 if (IMAGE_FILE_MARKER_FORMAT.match(lines[i + 1]) or PrettyPatch.has_image_suffix(@filename)) then
388 startOfSections = i + 2
389 for x in startOfSections...lines.length
390 # Binary diffs often have property changes listed before the actual binary data. Skip them.
391 if START_OF_BINARY_DATA_FORMAT.match(lines[x]) then
398 when GIT_INDEX_MARKER_FORMAT
399 @git_indexes = [$1, $2]
400 when GIT_BINARY_FILE_MARKER_FORMAT
402 if (GIT_LITERAL_FORMAT.match(lines[i + 1]) and PrettyPatch.has_image_suffix(@filename)) then
404 startOfSections = i + 1
409 lines_with_contents = lines[startOfSections...lines.length]
410 @sections = DiffSection.parse(lines_with_contents) unless @binary
412 @image_url = "data:image/png;base64," + lines_with_contents.join
415 raise "index line is missing" unless @git_indexes
418 for i in 0...lines_with_contents.length
419 if lines_with_contents[i] =~ /^$/
420 chunks = [lines_with_contents[i + 1 .. -1], lines_with_contents[0 .. i]]
425 raise "no binary chunks" unless chunks
427 @image_urls = chunks.zip(@git_indexes).collect do |chunk, git_index|
428 FileDiff.extract_contents_from_git_binary_chunk(chunk, git_index)
431 @image_error = "Exception raised during decoding git binary patch:<pre>#{CGI.escapeHTML($!.to_s + "\n" + $!.backtrace.join("\n"))}</pre>"
438 str = "<div class='FileDiff'>\n"
439 str += "<h1>#{PrettyPatch.linkifyFilename(@filename)}</h1>\n"
441 str += "<img class='image' src='" + @image_url + "' />"
442 elsif @git_image then
447 image_url = @image_urls[i]
448 style = ["remove", "add"][i]
449 str += "<p class=\"#{style}\">"
451 str += "<img class='image' src='" + image_url + "' />"
453 str += ["Added", "Removed"][i]
458 str += "<span class='text'>Binary file, nothing to see here</span>"
460 str += @sections.collect{ |section| section.to_html }.join("<br>\n") unless @sections.nil?
465 def self.parse(string)
466 haveSeenDiffHeader = false
468 string.each_line do |line|
469 if (PrettyPatch.diff_header?(line))
471 haveSeenDiffHeader = true
472 elsif (!haveSeenDiffHeader && line =~ /^--- /)
474 haveSeenDiffHeader = false
476 linesForDiffs.last << line unless linesForDiffs.last.nil?
479 linesForDiffs.collect { |lines| FileDiff.new(lines) }
482 def self.git_new_file_binary_patch(filename, encoded_chunk, git_index)
484 diff --git a/#{filename} b/#{filename}
486 index 0000000000000000000000000000000000000000..#{git_index}
488 #{encoded_chunk.join("")}literal 0
494 def self.extract_contents_from_git_binary_chunk(encoded_chunk, git_index)
495 # We use Tempfile we need a unique file among processes.
496 tempfile = Tempfile.new("PrettyPatch")
497 # We need a filename which doesn't exist to apply a patch
498 # which creates a new file. Append a suffix so filename
500 filepath = tempfile.path + '.bin'
501 filename = File.basename(filepath)
503 patch = FileDiff.git_new_file_binary_patch(filename, encoded_chunk, git_index)
505 # Apply the git binary patch using git-apply.
506 cmd = GIT_PATH + " apply --directory=" + File.dirname(filepath)
507 stdin, stdout, stderr = *Open3.popen3(cmd)
513 raise error if error != ""
515 contents = File.read(filepath)
517 stdin.close unless stdin.closed?
520 File.unlink(filename) if File.exists?(filename)
523 return nil if contents.empty?
524 return "data:image/png;base64," + [contents].pack("m")
529 def initialize(lines)
530 lines.length >= 1 or raise "DiffSection.parse only received %d lines" % lines.length
532 matches = START_OF_SECTION_FORMAT.match(lines[0])
533 from, to = [matches[1].to_i, matches[2].to_i] unless matches.nil?
535 @lines = lines[1...lines.length].collect do |line|
536 startOfLine = line =~ /^[-\+ ]/ ? 1 : 0
537 text = line[startOfLine...line.length].chomp
540 result = CodeLine.new(from, nil, text)
541 from += 1 unless from.nil?
544 result = CodeLine.new(nil, to, text)
545 to += 1 unless to.nil?
548 result = CodeLine.new(from, to, text)
549 from += 1 unless from.nil?
550 to += 1 unless to.nil?
555 @lines.unshift(ContextLine.new(matches[3])) unless matches.nil? || matches[3].empty?
557 changes = [ [ [], [] ] ]
559 if (!line.fromLineNumber.nil? and !line.toLineNumber.nil?) then
560 changes << [ [], [] ]
563 changes.last.first << line if line.toLineNumber.nil?
564 changes.last.last << line if line.fromLineNumber.nil?
567 for change in changes
568 next unless change.first.length == change.last.length
569 for i in (0...change.first.length)
570 raw_operations = HTMLDiff::DiffBuilder.new(change.first[i].text, change.last[i].text).operations
573 raw_operations.each_with_index do |operation, j|
574 if operation.action == :equal and j < raw_operations.length - 1
575 length = operation.end_in_new - operation.start_in_new
576 if length < SMALLEST_EQUAL_OPERATION
581 operation.start_in_old -= back
582 operation.start_in_new -= back
584 operations << operation
586 change.first[i].operations = operations
587 change.last[i].operations = operations
593 str = "<div class='DiffSection'>\n"
594 str += @lines.collect{ |line| line.to_html }.join
598 def self.parse(lines)
599 linesForSections = lines.inject([[]]) do |sections, line|
600 sections << [] if line =~ /^@@/
601 sections.last << line
605 linesForSections.delete_if { |lines| lines.nil? or lines.empty? }
606 linesForSections.collect { |lines| DiffSection.new(lines) }
611 attr_reader :fromLineNumber
612 attr_reader :toLineNumber
615 def initialize(from, to, text)
616 @fromLineNumber = from
626 lineClasses = ["Line"]
627 lineClasses << ["add"] unless @toLineNumber.nil? or !@fromLineNumber.nil?
628 lineClasses << ["remove"] unless @fromLineNumber.nil? or !@toLineNumber.nil?
633 markedUpText = self.text_as_html
634 str = "<div class='%s'>\n" % self.classes.join(' ')
635 str += "<span class='from lineNumber'>%s</span><span class='to lineNumber'>%s</span>\n" %
636 [@fromLineNumber.nil? ? ' ' : @fromLineNumber,
637 @toLineNumber.nil? ? ' ' : @toLineNumber] unless @fromLineNumber.nil? and @toLineNumber.nil?
638 str += "<span class='text'>%s</span>\n" % markedUpText
643 class CodeLine < Line
644 attr :operations, true
648 tag = @fromLineNumber.nil? ? "ins" : "del"
649 if @operations.nil? or @operations.empty?
650 return CGI.escapeHTML(@text)
652 @operations.each do |operation|
653 start = @fromLineNumber.nil? ? operation.start_in_new : operation.start_in_old
654 eend = @fromLineNumber.nil? ? operation.end_in_new : operation.end_in_old
655 escaped_text = CGI.escapeHTML(@text[start...eend])
656 if eend - start === 0 or operation.action === :equal
659 html << "<#{tag}>#{escaped_text}</#{tag}>"
666 class ContextLine < Line
667 def initialize(context)
668 super("@", "@", context)