710 lines
24 KiB
Python
710 lines
24 KiB
Python
# Copyright 2008 Google Inc.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
"""Diff rendering in HTML for Gerrit."""
|
|
|
|
# Python imports
|
|
import re
|
|
import cgi
|
|
import difflib
|
|
import logging
|
|
import urlparse
|
|
|
|
# AppEngine imports
|
|
from google.appengine.api import urlfetch
|
|
from google.appengine.api import users
|
|
from google.appengine.ext import db
|
|
|
|
# Django imports
|
|
from django.template import loader
|
|
|
|
# Local imports
|
|
import library
|
|
import models
|
|
import patching
|
|
import intra_region_diff
|
|
|
|
|
|
# NOTE: this function is duplicated in upload.py, keep them in sync.
|
|
def SplitPatch(data):
|
|
"""Splits a patch into separate pieces for each file.
|
|
|
|
Args:
|
|
data: A string containing the output of svn diff.
|
|
|
|
Returns:
|
|
A list of 2-tuple (filename, text) where text is the svn diff output
|
|
pertaining to filename.
|
|
"""
|
|
patches = []
|
|
filename = None
|
|
diff = []
|
|
for line in data.splitlines(True):
|
|
new_filename = None
|
|
if line.startswith('Index:'):
|
|
unused, new_filename = line.split(':', 1)
|
|
new_filename = new_filename.strip()
|
|
elif line.startswith('Property changes on:'):
|
|
unused, temp_filename = line.split(':', 1)
|
|
# When a file is modified, paths use '/' between directories, however
|
|
# when a property is modified '\' is used on Windows. Make them the same
|
|
# otherwise the file shows up twice.
|
|
temp_filename = temp_filename.strip().replace('\\', '/')
|
|
if temp_filename != filename:
|
|
# File has property changes but no modifications, create a new diff.
|
|
new_filename = temp_filename
|
|
if new_filename:
|
|
if filename and diff:
|
|
patches.append((filename, ''.join(diff)))
|
|
filename = new_filename
|
|
diff = [line]
|
|
continue
|
|
if diff is not None:
|
|
diff.append(line)
|
|
if filename and diff:
|
|
patches.append((filename, ''.join(diff)))
|
|
return patches
|
|
|
|
|
|
|
|
def RenderDiffTableRows(request, old_lines, chunks, patch,
|
|
colwidth=80,
|
|
debug=False,
|
|
context=models.DEFAULT_CONTEXT):
|
|
"""Render the HTML table rows for a side-by-side diff for a patch.
|
|
|
|
Args:
|
|
request: Django Request object.
|
|
old_lines: List of lines representing the original file.
|
|
chunks: List of chunks as returned by patching.ParsePatchToChunks().
|
|
patch: A models.Patch instance.
|
|
colwidth: Optional column width (default 80).
|
|
debug: Optional debugging flag (default False).
|
|
context: Maximum number of rows surrounding a change (default CONTEXT).
|
|
|
|
Yields:
|
|
Strings, each of which represents the text rendering one complete
|
|
pair of lines of the side-by-side diff, possibly including comments.
|
|
Each yielded string may consist of several <tr> elements.
|
|
"""
|
|
rows = _RenderDiffTableRows(request, old_lines, chunks, patch,
|
|
colwidth, debug)
|
|
return _CleanupTableRowsGenerator(rows, context)
|
|
|
|
|
|
def RenderDiff2TableRows(request, old_lines, old_patch, new_lines, new_patch,
|
|
colwidth=80,
|
|
debug=False,
|
|
context=models.DEFAULT_CONTEXT):
|
|
"""Render the HTML table rows for a side-by-side diff between two patches.
|
|
|
|
Args:
|
|
request: Django Request object.
|
|
old_lines: List of lines representing the patched file on the left.
|
|
old_patch: The models.Patch instance corresponding to old_lines.
|
|
new_lines: List of lines representing the patched file on the right.
|
|
new_patch: The models.Patch instance corresponding to new_lines.
|
|
colwidth: Optional column width (default 80).
|
|
debug: Optional debugging flag (default False).
|
|
context: Maximum number of visible context lines (default models.DEFAULT_CONTEXT).
|
|
|
|
Yields:
|
|
Strings, each of which represents the text rendering one complete
|
|
pair of lines of the side-by-side diff, possibly including comments.
|
|
Each yielded string may consist of several <tr> elements.
|
|
"""
|
|
rows = _RenderDiff2TableRows(request, old_lines, old_patch,
|
|
new_lines, new_patch, colwidth, debug)
|
|
return _CleanupTableRowsGenerator(rows, context)
|
|
|
|
|
|
def _CleanupTableRowsGenerator(rows, context):
|
|
"""Cleanup rows returned by _TableRowGenerator for output.
|
|
|
|
Args:
|
|
rows: List of tuples (tag, text)
|
|
context: Maximum number of visible context lines.
|
|
|
|
Yields:
|
|
Rows marked as 'equal' are possibly contracted using _ShortenBuffer().
|
|
Stops on rows marked as 'error'.
|
|
"""
|
|
buffer = []
|
|
for tag, text in rows:
|
|
if tag == 'equal':
|
|
buffer.append(text)
|
|
continue
|
|
else:
|
|
for t in _ShortenBuffer(buffer, context):
|
|
yield t
|
|
buffer = []
|
|
yield text
|
|
if tag == 'error':
|
|
yield None
|
|
break
|
|
if buffer:
|
|
for t in _ShortenBuffer(buffer, context):
|
|
yield t
|
|
|
|
|
|
def _ShortenBuffer(buffer, context):
|
|
"""Render a possibly contracted series of HTML table rows.
|
|
|
|
Args:
|
|
buffer: a list of strings representing HTML table rows.
|
|
context: Maximum number of visible context lines.
|
|
|
|
Yields:
|
|
If the buffer has fewer than 3 times context items, yield all
|
|
the items. Otherwise, yield the first context items, a single
|
|
table row representing the contraction, and the last context
|
|
items.
|
|
"""
|
|
if len(buffer) < 3*context:
|
|
for t in buffer:
|
|
yield t
|
|
else:
|
|
last_id = None
|
|
for t in buffer[:context]:
|
|
m = re.match('^<tr( name="hook")? id="pair-(?P<rowcount>\d+)">', t)
|
|
if m:
|
|
last_id = int(m.groupdict().get("rowcount"))
|
|
yield t
|
|
skip = len(buffer) - 2*context
|
|
if skip <= 10:
|
|
expand_link = ('<a href="javascript:M_expandSkipped(%(before)d, '
|
|
'%(after)d, \'b\', %(skip)d)">Show</a>')
|
|
else:
|
|
expand_link = ('<a href="javascript:M_expandSkipped(%(before)d, '
|
|
'%(after)d, \'t\', %(skip)d)">Show 10 above</a> '
|
|
'<a href="javascript:M_expandSkipped(%(before)d, '
|
|
'%(after)d, \'b\', %(skip)d)">Show 10 below</a> ')
|
|
expand_link = expand_link % {'before': last_id+1,
|
|
'after': last_id+skip,
|
|
'skip': last_id}
|
|
yield ('<tr id="skip-%d"><td colspan="2" align="center" '
|
|
'style="background:lightblue">'
|
|
'(...skipping <span id="skipcount-%d">%d</span> matching lines...) '
|
|
'<span id="skiplinks-%d">%s</span>'
|
|
'</td></tr>\n' % (last_id, last_id, skip,
|
|
last_id, expand_link))
|
|
for t in buffer[-context:]:
|
|
yield t
|
|
|
|
|
|
def _RenderDiff2TableRows(request, old_lines, old_patch, new_lines, new_patch,
|
|
colwidth=80, debug=False):
|
|
"""Internal version of RenderDiff2TableRows().
|
|
|
|
Args:
|
|
The same as for RenderDiff2TableRows.
|
|
|
|
Yields:
|
|
Tuples (tag, row) where tag is an indication of the row type.
|
|
"""
|
|
old_dict = {}
|
|
new_dict = {}
|
|
for patch, dct in [(old_patch, old_dict), (new_patch, new_dict)]:
|
|
# XXX GQL doesn't support OR yet... Otherwise we'd be using that.
|
|
for comment in models.Comment.gql(
|
|
'WHERE patch = :1 AND left = FALSE ORDER BY date', patch):
|
|
if comment.draft and comment.author != request.user:
|
|
continue # Only show your own drafts
|
|
comment.complete(patch)
|
|
lst = dct.setdefault(comment.lineno, [])
|
|
lst.append(comment)
|
|
library.prefetch_names([comment.author])
|
|
return _TableRowGenerator(old_patch, old_dict, len(old_lines)+1, 'new',
|
|
new_patch, new_dict, len(new_lines)+1, 'new',
|
|
_GenerateTriples(old_lines, new_lines),
|
|
colwidth, debug)
|
|
|
|
|
|
def _GenerateTriples(old_lines, new_lines):
|
|
"""Helper for _RenderDiff2TableRows yielding input for _TableRowGenerator.
|
|
|
|
Args:
|
|
old_lines: List of lines representing the patched file on the left.
|
|
new_lines: List of lines representing the patched file on the right.
|
|
|
|
Yields:
|
|
Tuples (tag, old_slice, new_slice) where tag is a tag as returned by
|
|
difflib.SequenceMatchser.get_opcodes(), and old_slice and new_slice
|
|
are lists of lines taken from old_lines and new_lines.
|
|
"""
|
|
sm = difflib.SequenceMatcher(None, old_lines, new_lines)
|
|
for tag, i1, i2, j1, j2 in sm.get_opcodes():
|
|
yield tag, old_lines[i1:i2], new_lines[j1:j2]
|
|
|
|
|
|
def _GetComments(request):
|
|
"""Helper that returns comments for a patch.
|
|
|
|
Args:
|
|
request: Django Request object.
|
|
|
|
Returns:
|
|
A 2-tuple of (old, new) where old/new are dictionaries that holds comments
|
|
for that file, mapping from line number to a Comment entity.
|
|
"""
|
|
old_dict = {}
|
|
new_dict = {}
|
|
# XXX GQL doesn't support OR yet... Otherwise we'd be using
|
|
# .gql('WHERE patch = :1 AND (draft = FALSE OR author = :2) ORDER BY data',
|
|
# patch, request.user)
|
|
for comment in models.Comment.gql('WHERE patch = :1 ORDER BY date',
|
|
request.patch):
|
|
if comment.draft and comment.author != request.user:
|
|
continue # Only show your own drafts
|
|
comment.complete(request.patch)
|
|
if comment.left:
|
|
dct = old_dict
|
|
else:
|
|
dct = new_dict
|
|
dct.setdefault(comment.lineno, []).append(comment)
|
|
library.prefetch_names([comment.author])
|
|
return old_dict, new_dict
|
|
|
|
|
|
def _RenderDiffTableRows(request, old_lines, chunks, patch,
|
|
colwidth=80, debug=False):
|
|
"""Internal version of RenderDiffTableRows().
|
|
|
|
Args:
|
|
The same as for RenderDiffTableRows.
|
|
|
|
Yields:
|
|
Tuples (tag, row) where tag is an indication of the row type.
|
|
"""
|
|
old_dict = {}
|
|
new_dict = {}
|
|
if patch:
|
|
old_dict, new_dict = _GetComments(request)
|
|
old_max, new_max = _ComputeLineCounts(old_lines, chunks)
|
|
return _TableRowGenerator(patch, old_dict, old_max, 'old',
|
|
patch, new_dict, new_max, 'new',
|
|
patching.PatchChunks(old_lines, chunks),
|
|
colwidth, debug)
|
|
|
|
|
|
def _TableRowGenerator(old_patch, old_dict, old_max, old_snapshot,
|
|
new_patch, new_dict, new_max, new_snapshot,
|
|
triple_iterator, colwidth=80, debug=False):
|
|
"""Helper function to render side-by-side table rows.
|
|
|
|
Args:
|
|
old_patch: First models.Patch instance.
|
|
old_dict: Dictionary with line numbers as keys and comments as values (left)
|
|
old_max: Line count of the patch on the left.
|
|
old_snapshot: A tag used in the comments form.
|
|
new_patch: Second models.Patch instance.
|
|
new_dict: Same as old_dict, but for the right side.
|
|
new_max: Line count of the patch on the right.
|
|
new_snapshot: A tag used in the comments form.
|
|
triple_iterator: Iterator that yields (tag, old, new) triples.
|
|
colwidth: Optional column width (default 80).
|
|
debug: Optional debugging flag (default False).
|
|
|
|
Yields:
|
|
Tuples (tag, row) where tag is an indication of the row type and
|
|
row is an HTML fragment representing one or more <td> elements.
|
|
"""
|
|
diff_params = intra_region_diff.GetDiffParams(dbg=debug)
|
|
ndigits = 1 + max(len(str(old_max)), len(str(new_max)))
|
|
indent = 1 + ndigits
|
|
old_offset = new_offset = 0
|
|
row_count = 0
|
|
|
|
# Render a row with a message if a side is empty or both sides are equal.
|
|
if old_patch == new_patch and (old_max == 0 or new_max == 0):
|
|
if old_max == 0:
|
|
msg_old = '(Empty)'
|
|
else:
|
|
msg_old = ''
|
|
if new_max == 0:
|
|
msg_new = '(Empty)'
|
|
else:
|
|
msg_new = ''
|
|
yield '', ('<tr><td class="info">%s</td>'
|
|
'<td class="info">%s</td></tr>' % (msg_old, msg_new))
|
|
# TODO(sop)
|
|
#elif old_patch == new_patch:
|
|
# old_patch.patch_hash == new_patch.patch_hash
|
|
# yield '', ('<tr><td class="info" colspan="2">'
|
|
# '(Both sides are equal)</td></tr>')
|
|
|
|
for tag, old, new in triple_iterator:
|
|
if tag.startswith('error'):
|
|
yield 'error', '<tr><td><h3>%s</h3></td></tr>\n' % cgi.escape(tag)
|
|
return
|
|
old1 = old_offset
|
|
old_offset = old2 = old1 + len(old)
|
|
new1 = new_offset
|
|
new_offset = new2 = new1 + len(new)
|
|
old_buff = []
|
|
new_buff = []
|
|
frag_list = []
|
|
do_ir_diff = tag == 'replace' and intra_region_diff.CanDoIRDiff(old, new)
|
|
|
|
for i in xrange(max(len(old), len(new))):
|
|
row_count += 1
|
|
old_lineno = old1 + i + 1
|
|
new_lineno = new1 + i + 1
|
|
old_valid = old1+i < old2
|
|
new_valid = new1+i < new2
|
|
|
|
# Start rendering the first row
|
|
frags = []
|
|
if i == 0 and tag != 'equal':
|
|
# Mark the first row of each non-equal chunk as a 'hook'.
|
|
frags.append('<tr name="hook"')
|
|
else:
|
|
frags.append('<tr')
|
|
frags.append(' id="pair-%d">' % row_count)
|
|
|
|
old_intra_diff = ''
|
|
new_intra_diff = ''
|
|
if old_valid:
|
|
old_intra_diff = old[i]
|
|
if new_valid:
|
|
new_intra_diff = new[i]
|
|
|
|
frag_list.append(frags)
|
|
if do_ir_diff:
|
|
# Don't render yet. Keep saving state necessary to render the whole
|
|
# region until we have encountered all the lines in the region.
|
|
old_buff.append([old_valid, old_lineno, old_intra_diff])
|
|
new_buff.append([new_valid, new_lineno, new_intra_diff])
|
|
else:
|
|
# We render line by line as usual if do_ir_diff is false
|
|
old_intra_diff = intra_region_diff.Fold(
|
|
old_intra_diff, colwidth + indent, indent, indent)
|
|
new_intra_diff = intra_region_diff.Fold(
|
|
new_intra_diff, colwidth + indent, indent, indent)
|
|
old_buff_out = [[old_valid, old_lineno,
|
|
(old_intra_diff, True, None)]]
|
|
new_buff_out = [[new_valid, new_lineno,
|
|
(new_intra_diff, True, None)]]
|
|
for tg, frag in _RenderDiffInternal(old_buff_out, new_buff_out,
|
|
ndigits, tag, frag_list,
|
|
do_ir_diff,
|
|
old_dict, new_dict,
|
|
old_patch, new_patch,
|
|
old_snapshot, new_snapshot,
|
|
colwidth, debug):
|
|
yield tg, frag
|
|
frag_list = []
|
|
|
|
if do_ir_diff:
|
|
# So this was a replace block which means that the whole region still
|
|
# needs to be rendered.
|
|
old_lines = [b[2] for b in old_buff]
|
|
new_lines = [b[2] for b in new_buff]
|
|
ret = intra_region_diff.IntraRegionDiff(old_lines, new_lines,
|
|
diff_params)
|
|
old_chunks, new_chunks, ratio = ret
|
|
old_tag = 'old'
|
|
new_tag = 'new'
|
|
|
|
old_diff_out = intra_region_diff.RenderIntraRegionDiff(
|
|
old_lines, old_chunks, old_tag, ratio,
|
|
limit=colwidth, indent=indent,
|
|
dbg=debug)
|
|
new_diff_out = intra_region_diff.RenderIntraRegionDiff(
|
|
new_lines, new_chunks, new_tag, ratio,
|
|
limit=colwidth, indent=indent,
|
|
dbg=debug)
|
|
for (i, b) in enumerate(old_buff):
|
|
b[2] = old_diff_out[i]
|
|
for (i, b) in enumerate(new_buff):
|
|
b[2] = new_diff_out[i]
|
|
|
|
for tg, frag in _RenderDiffInternal(old_buff, new_buff,
|
|
ndigits, tag, frag_list,
|
|
do_ir_diff,
|
|
old_dict, new_dict,
|
|
old_patch, new_patch,
|
|
old_snapshot, new_snapshot,
|
|
colwidth, debug):
|
|
yield tg, frag
|
|
old_buff = []
|
|
new_buff = []
|
|
|
|
|
|
def _CleanupTableRows(rows):
|
|
"""Cleanup rows returned by _TableRowGenerator.
|
|
|
|
Args:
|
|
rows: Sequence of (tag, text) tuples.
|
|
|
|
Yields:
|
|
Rows marked as 'equal' are possibly contracted using _ShortenBuffer().
|
|
Stops on rows marked as 'error'.
|
|
"""
|
|
buffer = []
|
|
for tag, text in rows:
|
|
if tag == 'equal':
|
|
buffer.append(text)
|
|
continue
|
|
else:
|
|
for t in _ShortenBuffer(buffer):
|
|
yield t
|
|
buffer = []
|
|
yield text
|
|
if tag == 'error':
|
|
yield None
|
|
break
|
|
if buffer:
|
|
for t in _ShortenBuffer(buffer):
|
|
yield t
|
|
|
|
|
|
def _RenderDiffInternal(old_buff, new_buff, ndigits, tag, frag_list,
|
|
do_ir_diff, old_dict, new_dict,
|
|
old_patch, new_patch,
|
|
old_snapshot, new_snapshot,
|
|
colwidth, debug):
|
|
"""Helper for _TableRowGenerator()."""
|
|
obegin = (intra_region_diff.BEGIN_TAG %
|
|
intra_region_diff.COLOR_SCHEME['old']['match'])
|
|
nbegin = (intra_region_diff.BEGIN_TAG %
|
|
intra_region_diff.COLOR_SCHEME['new']['match'])
|
|
oend = intra_region_diff.END_TAG
|
|
nend = oend
|
|
user = users.get_current_user()
|
|
|
|
for i in xrange(len(old_buff)):
|
|
tg = tag
|
|
old_valid, old_lineno, old_out = old_buff[i]
|
|
new_valid, new_lineno, new_out = new_buff[i]
|
|
old_intra_diff, old_has_newline, old_debug_info = old_out
|
|
new_intra_diff, new_has_newline, new_debug_info = new_out
|
|
|
|
frags = frag_list[i]
|
|
# Render left text column
|
|
frags.append(_RenderDiffColumn(old_patch, old_valid, tag, ndigits,
|
|
old_lineno, obegin, oend, old_intra_diff,
|
|
do_ir_diff, old_has_newline, 'old'))
|
|
|
|
# Render right text column
|
|
frags.append(_RenderDiffColumn(new_patch, new_valid, tag, ndigits,
|
|
new_lineno, nbegin, nend, new_intra_diff,
|
|
do_ir_diff, new_has_newline, 'new'))
|
|
|
|
# End rendering the first row
|
|
frags.append('</tr>\n')
|
|
|
|
if debug:
|
|
frags.append('<tr>')
|
|
if old_debug_info:
|
|
frags.append('<td class="debug-info">%s</td>' %
|
|
old_debug_info.replace('\n', '<br>'))
|
|
else:
|
|
frags.append('<td></td>')
|
|
if new_debug_info:
|
|
frags.append('<td class="debug-info">%s</td>' %
|
|
new_debug_info.replace('\n', '<br>'))
|
|
else:
|
|
frags.append('<td></td>')
|
|
frags.append('</tr>\n')
|
|
|
|
if old_patch or new_patch:
|
|
# Start rendering the second row
|
|
if ((old_valid and old_lineno in old_dict) or
|
|
(new_valid and new_lineno in new_dict)):
|
|
tg += '_comment'
|
|
frags.append('<tr class="inline-comments" name="hook">')
|
|
else:
|
|
frags.append('<tr class="inline-comments">')
|
|
|
|
# Render left inline comments
|
|
frags.append(_RenderInlineComments(old_valid, old_lineno, old_dict,
|
|
user, old_patch, old_snapshot, 'old'))
|
|
|
|
# Render right inline comments
|
|
frags.append(_RenderInlineComments(new_valid, new_lineno, new_dict,
|
|
user, new_patch, new_snapshot, 'new'))
|
|
|
|
# End rendering the second row
|
|
frags.append('</tr>\n')
|
|
|
|
# Yield the combined fragments
|
|
yield tg, ''.join(frags)
|
|
|
|
|
|
def _RenderDiffColumn(patch, line_valid, tag, ndigits, lineno, begin, end,
|
|
intra_diff, do_ir_diff, has_newline, prefix):
|
|
"""Helper function for _RenderDiffInternal().
|
|
|
|
Returns:
|
|
A rendered column.
|
|
"""
|
|
if line_valid:
|
|
cls_attr = '%s%s' % (prefix, tag)
|
|
if tag == 'equal':
|
|
lno = '%*d' % (ndigits, lineno)
|
|
else:
|
|
lno = _MarkupNumber(ndigits, lineno, 'u')
|
|
if tag == 'replace':
|
|
col_content = ('%s%s %s%s' % (begin, lno, end, intra_diff))
|
|
# If IR diff has been turned off or there is no matching new line at
|
|
# the end then switch to dark background CSS style.
|
|
if not do_ir_diff or not has_newline:
|
|
cls_attr = cls_attr + '1'
|
|
else:
|
|
col_content = '%s %s' % (lno, intra_diff)
|
|
return '<td class="%s" id="%scode%d">%s</td>' % (cls_attr, prefix,
|
|
lineno, col_content)
|
|
else:
|
|
return '<td class="%sblank"></td>' % prefix
|
|
|
|
|
|
def _RenderInlineComments(line_valid, lineno, data, user,
|
|
patch, snapshot, prefix):
|
|
"""Helper function for _RenderDiffInternal().
|
|
|
|
Returns:
|
|
Rendered comments.
|
|
"""
|
|
comments = []
|
|
if line_valid:
|
|
comments.append('<td id="%s-line-%s">' % (prefix, lineno))
|
|
if lineno in data:
|
|
comments.append(
|
|
_ExpandTemplate('inline_comment.html',
|
|
inline_draft_url='/inline_draft',
|
|
user=user,
|
|
patch=patch,
|
|
patchset=patch.patchset,
|
|
change=patch.patchset.change,
|
|
snapshot=snapshot,
|
|
side='a' if prefix == 'old' else 'b',
|
|
comments=data[lineno],
|
|
lineno=lineno,
|
|
))
|
|
comments.append('</td>')
|
|
else:
|
|
comments.append('<td></td>')
|
|
return ''.join(comments)
|
|
|
|
|
|
def RenderUnifiedTableRows(request, parsed_lines):
|
|
"""Render the HTML table rows for a unified diff for a patch.
|
|
|
|
Args:
|
|
request: Django Request object.
|
|
parsed_lines: List of tuples for each line that contain the line number,
|
|
if they exist, for the old and new file.
|
|
|
|
Returns:
|
|
A list of html table rows.
|
|
"""
|
|
old_dict, new_dict = _GetComments(request)
|
|
|
|
rows = []
|
|
for old_line_no, new_line_no, line_text in parsed_lines:
|
|
row1_id = row2_id = ''
|
|
# When a line is unchanged (i.e. both old_line_no and new_line_no aren't 0)
|
|
# pick the old column line numbers when adding a comment.
|
|
if old_line_no:
|
|
row1_id = 'id="oldcode%d"' % old_line_no
|
|
row2_id = 'id="old-line-%d"' % old_line_no
|
|
elif new_line_no:
|
|
row1_id = 'id="newcode%d"' % new_line_no
|
|
row2_id = 'id="new-line-%d"' % new_line_no
|
|
rows.append('<tr><td class="udiff" %s>%s</td></tr>' %
|
|
(row1_id, cgi.escape(line_text)))
|
|
|
|
frags = []
|
|
if old_line_no in old_dict or new_line_no in new_dict:
|
|
frags.append('<tr class="inline-comments" name="hook">')
|
|
if old_line_no in old_dict:
|
|
dct = old_dict
|
|
line_no = old_line_no
|
|
snapshot = 'old'
|
|
else:
|
|
dct = new_dict
|
|
line_no = new_line_no
|
|
snapshot = 'new'
|
|
frags.append(_RenderInlineComments(True, line_no, dct, request.user,
|
|
request.patch, snapshot, snapshot))
|
|
else:
|
|
frags.append('<tr class="inline-comments">')
|
|
frags.append('<td ' + row2_id +'></td>')
|
|
frags.append('</tr>')
|
|
rows.append(''.join(frags))
|
|
return rows
|
|
|
|
|
|
def _ComputeLineCounts(old_lines, chunks):
|
|
"""Compute the length of the old and new sides of a diff.
|
|
|
|
Args:
|
|
old_lines: List of lines representing the original file.
|
|
chunks: List of chunks as returned by patching.ParsePatchToChunks().
|
|
|
|
Returns:
|
|
A tuple (old_len, new_len) representing len(old_lines) and
|
|
len(new_lines), where new_lines is the list representing the
|
|
result of applying the patch chunks to old_lines, however, without
|
|
actually computing new_lines.
|
|
"""
|
|
old_len = len(old_lines)
|
|
new_len = old_len
|
|
if chunks:
|
|
(old_a, old_b), (new_a, new_b), old_lines, new_lines = chunks[-1]
|
|
new_len += new_b - old_b
|
|
return old_len, new_len
|
|
|
|
|
|
def _MarkupNumber(ndigits, number, tag):
|
|
"""Format a number in HTML in a given width with extra markup.
|
|
|
|
Args:
|
|
ndigits: the total width available for formatting
|
|
number: the number to be formatted
|
|
tag: HTML tag name, e.g. 'u'
|
|
|
|
Returns:
|
|
An HTML string that displays as ndigits wide, with the
|
|
number right-aligned and surrounded by an HTML tag; for example,
|
|
_MarkupNumber(42, 4, 'u') returns ' <u>42</u>'.
|
|
"""
|
|
formatted_number = str(number)
|
|
space_prefix = ' ' * (ndigits - len(formatted_number))
|
|
return '%s<%s>%s</%s>' % (space_prefix, tag, formatted_number, tag)
|
|
|
|
|
|
def _ExpandTemplate(name, **params):
|
|
"""Wrapper around django.template.loader.render_to_string().
|
|
|
|
For convenience, this takes keyword arguments instead of a dict.
|
|
"""
|
|
return loader.render_to_string(name, params)
|
|
|
|
|
|
def ToText(text):
|
|
"""Helper to turn a string into a db.Text instance.
|
|
|
|
Args:
|
|
text: a string.
|
|
|
|
Returns:
|
|
A db.Text instance.
|
|
"""
|
|
try:
|
|
return db.Text(text, encoding='utf-8')
|
|
except UnicodeDecodeError:
|
|
return db.Text(text, encoding='latin-1')
|