diffs.py
767 lines
| 27.8 KiB
| text/x-python
|
PythonLexer
r1753 | # -*- coding: utf-8 -*- | ||
""" | |||
rhodecode.lib.diffs | |||
~~~~~~~~~~~~~~~~~~~ | |||
Set of diffing helpers, previously part of vcs | |||
r1781 | |||
r1753 | :created_on: Dec 4, 2011 | ||
:author: marcink | |||
r1824 | :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com> | ||
r1781 | :original copyright: 2007-2008 by Armin Ronacher | ||
r1753 | :license: GPLv3, see COPYING for more details. | ||
""" | |||
# This program is free software: you can redistribute it and/or modify | |||
# it under the terms of the GNU General Public License as published by | |||
# the Free Software Foundation, either version 3 of the License, or | |||
# (at your option) any later version. | |||
# | |||
# This program is distributed in the hope that it will be useful, | |||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
# GNU General Public License for more details. | |||
# | |||
# You should have received a copy of the GNU General Public License | |||
# along with this program. If not, see <http://www.gnu.org/licenses/>. | |||
import re | |||
import difflib | |||
r2892 | import logging | ||
r2995 | import traceback | ||
r2355 | |||
r1789 | from itertools import tee, imap | ||
r1753 | |||
r2355 | from mercurial import patch | ||
from mercurial.mdiff import diffopts | |||
from mercurial.bundlerepo import bundlerepository | |||
r1789 | from pylons.i18n.translation import _ | ||
r1753 | |||
r2552 | from rhodecode.lib.compat import BytesIO | ||
r2684 | from rhodecode.lib.vcs.utils.hgcompat import localrepo | ||
r2007 | from rhodecode.lib.vcs.exceptions import VCSError | ||
r2233 | from rhodecode.lib.vcs.nodes import FileNode, SubModuleNode | ||
r2684 | from rhodecode.lib.vcs.backends.base import EmptyChangeset | ||
r2233 | from rhodecode.lib.helpers import escape | ||
r2684 | from rhodecode.lib.utils import make_ui | ||
r2843 | from rhodecode.lib.utils2 import safe_unicode | ||
r1789 | |||
r2892 | log = logging.getLogger(__name__) | ||
r1789 | |||
def wrap_to_table(str_): | |||
return '''<table class="code-difftable"> | |||
<tr class="line no-comment"> | |||
<td class="lineno new"></td> | |||
<td class="code no-comment"><pre>%s</pre></td> | |||
</tr> | |||
</table>''' % str_ | |||
def wrapped_diff(filenode_old, filenode_new, cut_off_limit=None, | |||
ignore_whitespace=True, line_context=3, | |||
enable_comments=False): | |||
""" | |||
returns a wrapped diff into a table, checks for cut_off_limit and presents | |||
proper message | |||
""" | |||
if filenode_old is None: | |||
filenode_old = FileNode(filenode_new.path, '', EmptyChangeset()) | |||
if filenode_old.is_binary or filenode_new.is_binary: | |||
diff = wrap_to_table(_('binary file')) | |||
stats = (0, 0) | |||
size = 0 | |||
elif cut_off_limit != -1 and (cut_off_limit is None or | |||
(filenode_old.size < cut_off_limit and filenode_new.size < cut_off_limit)): | |||
f_gitdiff = get_gitdiff(filenode_old, filenode_new, | |||
ignore_whitespace=ignore_whitespace, | |||
context=line_context) | |||
diff_processor = DiffProcessor(f_gitdiff, format='gitdiff') | |||
diff = diff_processor.as_html(enable_comments=enable_comments) | |||
stats = diff_processor.stat() | |||
size = len(diff or '') | |||
else: | |||
r2340 | diff = wrap_to_table(_('Changeset was too big and was cut off, use ' | ||
r1789 | 'diff menu to display this diff')) | ||
stats = (0, 0) | |||
size = 0 | |||
if not diff: | |||
r2233 | submodules = filter(lambda o: isinstance(o, SubModuleNode), | ||
[filenode_new, filenode_old]) | |||
if submodules: | |||
diff = wrap_to_table(escape('Submodule %r' % submodules[0])) | |||
else: | |||
diff = wrap_to_table(_('No changes detected')) | |||
r1789 | |||
r2084 | cs1 = filenode_old.changeset.raw_id | ||
cs2 = filenode_new.changeset.raw_id | |||
r1789 | |||
return size, cs1, cs2, diff, stats | |||
r1781 | |||
r1753 | |||
r1768 | def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3): | ||
r1753 | """ | ||
Returns git style diff between given ``filenode_old`` and ``filenode_new``. | |||
r1781 | |||
r1753 | :param ignore_whitespace: ignore whitespaces in diff | ||
""" | |||
r1894 | # make sure we pass in default context | ||
context = context or 3 | |||
r2233 | submodules = filter(lambda o: isinstance(o, SubModuleNode), | ||
[filenode_new, filenode_old]) | |||
if submodules: | |||
return '' | |||
r1753 | |||
for filenode in (filenode_old, filenode_new): | |||
if not isinstance(filenode, FileNode): | |||
raise VCSError("Given object should be FileNode object, not %s" | |||
% filenode.__class__) | |||
r1894 | repo = filenode_new.changeset.repository | ||
old_raw_id = getattr(filenode_old.changeset, 'raw_id', repo.EMPTY_CHANGESET) | |||
new_raw_id = getattr(filenode_new.changeset, 'raw_id', repo.EMPTY_CHANGESET) | |||
r1753 | |||
r1883 | vcs_gitdiff = repo.get_diff(old_raw_id, new_raw_id, filenode_new.path, | ||
r2995 | ignore_whitespace, context) | ||
r1753 | return vcs_gitdiff | ||
r2995 | NEW_FILENODE = 1 | ||
DEL_FILENODE = 2 | |||
MOD_FILENODE = 3 | |||
RENAMED_FILENODE = 4 | |||
CHMOD_FILENODE = 5 | |||
class DiffLimitExceeded(Exception): | |||
pass | |||
class LimitedDiffContainer(object): | |||
def __init__(self, diff_limit, cur_diff_size, diff): | |||
self.diff = diff | |||
self.diff_limit = diff_limit | |||
self.cur_diff_size = cur_diff_size | |||
def __iter__(self): | |||
for l in self.diff: | |||
yield l | |||
r1753 | |||
class DiffProcessor(object): | |||
""" | |||
r2995 | Give it a unified or git diff and it returns a list of the files that were | ||
r1753 | mentioned in the diff together with a dict of meta information that | ||
can be used to render it in a HTML template. | |||
""" | |||
r3022 | _chunk_re = re.compile(r'^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)') | ||
_newline_marker = re.compile(r'^\\ No newline at end of file') | |||
r2995 | _git_header_re = re.compile(r""" | ||
#^diff[ ]--git | |||
[ ]a/(?P<a_path>.+?)[ ]b/(?P<b_path>.+?)\n | |||
(?:^similarity[ ]index[ ](?P<similarity_index>\d+)%\n | |||
^rename[ ]from[ ](?P<rename_from>\S+)\n | |||
^rename[ ]to[ ](?P<rename_to>\S+)(?:\n|$))? | |||
(?:^old[ ]mode[ ](?P<old_mode>\d+)\n | |||
^new[ ]mode[ ](?P<new_mode>\d+)(?:\n|$))? | |||
(?:^new[ ]file[ ]mode[ ](?P<new_file_mode>.+)(?:\n|$))? | |||
(?:^deleted[ ]file[ ]mode[ ](?P<deleted_file_mode>.+)(?:\n|$))? | |||
(?:^index[ ](?P<a_blob_id>[0-9A-Fa-f]+) | |||
\.\.(?P<b_blob_id>[0-9A-Fa-f]+)[ ]?(?P<b_mode>.+)?(?:\n|$))? | |||
(?:^---[ ](a/(?P<a_file>.+)|/dev/null)(?:\n|$))? | |||
(?:^\+\+\+[ ](b/(?P<b_file>.+)|/dev/null)(?:\n|$))? | |||
""", re.VERBOSE | re.MULTILINE) | |||
_hg_header_re = re.compile(r""" | |||
#^diff[ ]--git | |||
[ ]a/(?P<a_path>.+?)[ ]b/(?P<b_path>.+?)\n | |||
(?:^similarity[ ]index[ ](?P<similarity_index>\d+)%(?:\n|$))? | |||
(?:^rename[ ]from[ ](?P<rename_from>\S+)\n | |||
^rename[ ]to[ ](?P<rename_to>\S+)(?:\n|$))? | |||
(?:^old[ ]mode[ ](?P<old_mode>\d+)\n | |||
^new[ ]mode[ ](?P<new_mode>\d+)(?:\n|$))? | |||
(?:^new[ ]file[ ]mode[ ](?P<new_file_mode>.+)(?:\n|$))? | |||
(?:^deleted[ ]file[ ]mode[ ](?P<deleted_file_mode>.+)(?:\n|$))? | |||
(?:^index[ ](?P<a_blob_id>[0-9A-Fa-f]+) | |||
\.\.(?P<b_blob_id>[0-9A-Fa-f]+)[ ]?(?P<b_mode>.+)?(?:\n|$))? | |||
(?:^---[ ](a/(?P<a_file>.+)|/dev/null)(?:\n|$))? | |||
(?:^\+\+\+[ ](b/(?P<b_file>.+)|/dev/null)(?:\n|$))? | |||
""", re.VERBOSE | re.MULTILINE) | |||
r1753 | |||
r2995 | def __init__(self, diff, vcs='hg', format='gitdiff', diff_limit=None): | ||
r1753 | """ | ||
r2995 | :param diff: a text in diff format | ||
:param vcs: type of version controll hg or git | |||
:param format: format of diff passed, `udiff` or `gitdiff` | |||
:param diff_limit: define the size of diff that is considered "big" | |||
based on that parameter cut off will be triggered, set to None | |||
to show full diff | |||
""" | |||
if not isinstance(diff, basestring): | |||
raise Exception('Diff must be a basestring got %s instead' % type(diff)) | |||
r1753 | |||
r2995 | self._diff = diff | ||
self._format = format | |||
r1753 | self.adds = 0 | ||
self.removes = 0 | |||
r2995 | # calculate diff size | ||
self.diff_size = len(diff) | |||
self.diff_limit = diff_limit | |||
self.cur_diff_size = 0 | |||
self.parsed = False | |||
self.parsed_diff = [] | |||
self.vcs = vcs | |||
r1753 | |||
r2995 | if format == 'gitdiff': | ||
r1753 | self.differ = self._highlight_line_difflib | ||
r2995 | self._parser = self._parse_gitdiff | ||
r1753 | else: | ||
self.differ = self._highlight_line_udiff | |||
r2995 | self._parser = self._parse_udiff | ||
r1753 | |||
r2995 | def _copy_iterator(self): | ||
r1753 | """ | ||
make a fresh copy of generator, we should not iterate thru | |||
an original as it's needed for repeating operations on | |||
this instance of DiffProcessor | |||
""" | |||
self.__udiff, iterator_copy = tee(self.__udiff) | |||
return iterator_copy | |||
r2995 | def _escaper(self, string): | ||
r1753 | """ | ||
r2995 | Escaper for diff escapes special chars and checks the diff limit | ||
:param string: | |||
:type string: | |||
r1753 | """ | ||
r2995 | self.cur_diff_size += len(string) | ||
r1753 | |||
r2995 | # escaper get's iterated on each .next() call and it checks if each | ||
# parsed line doesn't exceed the diff limit | |||
if self.diff_limit is not None and self.cur_diff_size > self.diff_limit: | |||
raise DiffLimitExceeded('Diff Limit Exceeded') | |||
r1753 | |||
r2995 | return safe_unicode(string).replace('&', '&')\ | ||
.replace('<', '<')\ | |||
.replace('>', '>') | |||
r1753 | |||
r2995 | def _line_counter(self, l): | ||
""" | |||
Checks each line and bumps total adds/removes for this diff | |||
r1753 | |||
r2995 | :param l: | ||
""" | |||
if l.startswith('+') and not l.startswith('+++'): | |||
self.adds += 1 | |||
elif l.startswith('-') and not l.startswith('---'): | |||
self.removes += 1 | |||
r2996 | return safe_unicode(l) | ||
r1753 | |||
r1781 | def _highlight_line_difflib(self, line, next_): | ||
r1753 | """ | ||
Highlight inline changes in both lines. | |||
""" | |||
if line['action'] == 'del': | |||
r1781 | old, new = line, next_ | ||
r1753 | else: | ||
r1781 | old, new = next_, line | ||
r1753 | |||
oldwords = re.split(r'(\W)', old['line']) | |||
newwords = re.split(r'(\W)', new['line']) | |||
sequence = difflib.SequenceMatcher(None, oldwords, newwords) | |||
oldfragments, newfragments = [], [] | |||
for tag, i1, i2, j1, j2 in sequence.get_opcodes(): | |||
oldfrag = ''.join(oldwords[i1:i2]) | |||
newfrag = ''.join(newwords[j1:j2]) | |||
if tag != 'equal': | |||
if oldfrag: | |||
oldfrag = '<del>%s</del>' % oldfrag | |||
if newfrag: | |||
newfrag = '<ins>%s</ins>' % newfrag | |||
oldfragments.append(oldfrag) | |||
newfragments.append(newfrag) | |||
old['line'] = "".join(oldfragments) | |||
new['line'] = "".join(newfragments) | |||
r1781 | def _highlight_line_udiff(self, line, next_): | ||
r1753 | """ | ||
Highlight inline changes in both lines. | |||
""" | |||
start = 0 | |||
r1781 | limit = min(len(line['line']), len(next_['line'])) | ||
while start < limit and line['line'][start] == next_['line'][start]: | |||
r1753 | start += 1 | ||
end = -1 | |||
limit -= start | |||
r1781 | while -end <= limit and line['line'][end] == next_['line'][end]: | ||
r1753 | end -= 1 | ||
end += 1 | |||
if start or end: | |||
def do(l): | |||
last = end + len(l['line']) | |||
if l['action'] == 'add': | |||
tag = 'ins' | |||
else: | |||
tag = 'del' | |||
l['line'] = '%s<%s>%s</%s>%s' % ( | |||
l['line'][:start], | |||
tag, | |||
l['line'][start:last], | |||
tag, | |||
l['line'][last:] | |||
) | |||
do(line) | |||
r1781 | do(next_) | ||
r1753 | |||
r2995 | def _get_header(self, diff_chunk): | ||
r1753 | """ | ||
r2995 | parses the diff header, and returns parts, and leftover diff | ||
parts consists of 14 elements:: | |||
r2967 | |||
r2995 | a_path, b_path, similarity_index, rename_from, rename_to, | ||
old_mode, new_mode, new_file_mode, deleted_file_mode, | |||
a_blob_id, b_blob_id, b_mode, a_file, b_file | |||
:param diff_chunk: | |||
:type diff_chunk: | |||
""" | |||
r1753 | |||
r2995 | if self.vcs == 'git': | ||
match = self._git_header_re.match(diff_chunk) | |||
diff = diff_chunk[match.end():] | |||
return match.groupdict(), imap(self._escaper, diff.splitlines(1)) | |||
elif self.vcs == 'hg': | |||
match = self._hg_header_re.match(diff_chunk) | |||
diff = diff_chunk[match.end():] | |||
return match.groupdict(), imap(self._escaper, diff.splitlines(1)) | |||
else: | |||
raise Exception('VCS type %s is not supported' % self.vcs) | |||
r2967 | |||
r3022 | def _clean_line(self, line, command): | ||
if command in ['+', '-', ' ']: | |||
#only modify the line if it's actually a diff thing | |||
line = line[1:] | |||
return line | |||
r2995 | def _parse_gitdiff(self, inline_diff=True): | ||
_files = [] | |||
diff_container = lambda arg: arg | |||
r1753 | |||
r2995 | ##split the diff in chunks of separate --git a/file b/file chunks | ||
for raw_diff in ('\n' + self._diff).split('\ndiff --git')[1:]: | |||
binary = False | |||
binary_msg = 'unknown binary' | |||
head, diff = self._get_header(raw_diff) | |||
r1753 | |||
r2995 | if not head['a_file'] and head['b_file']: | ||
op = 'A' | |||
elif head['a_file'] and head['b_file']: | |||
op = 'M' | |||
elif head['a_file'] and not head['b_file']: | |||
op = 'D' | |||
else: | |||
#probably we're dealing with a binary file 1 | |||
binary = True | |||
if head['deleted_file_mode']: | |||
op = 'D' | |||
stats = ['b', DEL_FILENODE] | |||
binary_msg = 'deleted binary file' | |||
elif head['new_file_mode']: | |||
op = 'A' | |||
stats = ['b', NEW_FILENODE] | |||
binary_msg = 'new binary file %s' % head['new_file_mode'] | |||
else: | |||
if head['new_mode'] and head['old_mode']: | |||
stats = ['b', CHMOD_FILENODE] | |||
op = 'M' | |||
binary_msg = ('modified binary file chmod %s => %s' | |||
% (head['old_mode'], head['new_mode'])) | |||
elif (head['rename_from'] and head['rename_to'] | |||
and head['rename_from'] != head['rename_to']): | |||
stats = ['b', RENAMED_FILENODE] | |||
op = 'M' | |||
binary_msg = ('file renamed from %s to %s' | |||
% (head['rename_from'], head['rename_to'])) | |||
else: | |||
stats = ['b', MOD_FILENODE] | |||
op = 'M' | |||
binary_msg = 'modified binary file' | |||
r1753 | |||
r2995 | if not binary: | ||
try: | |||
chunks, stats = self._parse_lines(diff) | |||
except DiffLimitExceeded: | |||
diff_container = lambda _diff: LimitedDiffContainer( | |||
self.diff_limit, | |||
self.cur_diff_size, | |||
_diff) | |||
break | |||
else: | |||
chunks = [] | |||
chunks.append([{ | |||
'old_lineno': '', | |||
'new_lineno': '', | |||
'action': 'binary', | |||
'line': binary_msg, | |||
}]) | |||
r1753 | |||
r2995 | _files.append({ | ||
'filename': head['b_path'], | |||
'old_revision': head['a_blob_id'], | |||
'new_revision': head['b_blob_id'], | |||
'chunks': chunks, | |||
'operation': op, | |||
'stats': stats, | |||
}) | |||
r1753 | |||
r2385 | sorter = lambda info: {'A': 0, 'M': 1, 'D': 2}.get(info['operation']) | ||
r2995 | |||
r2385 | if inline_diff is False: | ||
r2995 | return diff_container(sorted(_files, key=sorter)) | ||
r2385 | |||
r1753 | # highlight inline changes | ||
r2995 | for diff_data in _files: | ||
r2385 | for chunk in diff_data['chunks']: | ||
r1753 | lineiter = iter(chunk) | ||
try: | |||
while 1: | |||
line = lineiter.next() | |||
r2566 | if line['action'] not in ['unmod', 'context']: | ||
r1753 | nextline = lineiter.next() | ||
r2360 | if nextline['action'] in ['unmod', 'context'] or \ | ||
r1753 | nextline['action'] == line['action']: | ||
continue | |||
self.differ(line, nextline) | |||
except StopIteration: | |||
pass | |||
r2995 | return diff_container(sorted(_files, key=sorter)) | ||
r1753 | |||
r2995 | def _parse_udiff(self, inline_diff=True): | ||
raise NotImplementedError() | |||
def _parse_lines(self, diff): | |||
""" | |||
Parse the diff an return data for the template. | |||
r1753 | """ | ||
r2995 | |||
lineiter = iter(diff) | |||
stats = [0, 0] | |||
try: | |||
chunks = [] | |||
line = lineiter.next() | |||
while line: | |||
lines = [] | |||
chunks.append(lines) | |||
match = self._chunk_re.match(line) | |||
if not match: | |||
break | |||
gr = match.groups() | |||
(old_line, old_end, | |||
new_line, new_end) = [int(x or 1) for x in gr[:-1]] | |||
old_line -= 1 | |||
new_line -= 1 | |||
context = len(gr) == 5 | |||
old_end += old_line | |||
new_end += new_line | |||
if context: | |||
# skip context only if it's first line | |||
if int(gr[0]) > 1: | |||
lines.append({ | |||
'old_lineno': '...', | |||
'new_lineno': '...', | |||
'action': 'context', | |||
'line': line, | |||
}) | |||
line = lineiter.next() | |||
while old_line < old_end or new_line < new_end: | |||
r3022 | command = ' ' | ||
r2995 | if line: | ||
command = line[0] | |||
affects_old = affects_new = False | |||
# ignore those if we don't expect them | |||
if command in '#@': | |||
continue | |||
elif command == '+': | |||
affects_new = True | |||
action = 'add' | |||
stats[0] += 1 | |||
elif command == '-': | |||
affects_old = True | |||
action = 'del' | |||
stats[1] += 1 | |||
else: | |||
affects_old = affects_new = True | |||
action = 'unmod' | |||
r3022 | if not self._newline_marker.match(line): | ||
r2995 | old_line += affects_old | ||
new_line += affects_new | |||
lines.append({ | |||
'old_lineno': affects_old and old_line or '', | |||
'new_lineno': affects_new and new_line or '', | |||
'action': action, | |||
r3022 | 'line': self._clean_line(line, command) | ||
r2995 | }) | ||
line = lineiter.next() | |||
r3022 | if self._newline_marker.match(line): | ||
r2995 | # we need to append to lines, since this is not | ||
# counted in the line specs of diff | |||
lines.append({ | |||
'old_lineno': '...', | |||
'new_lineno': '...', | |||
'action': 'context', | |||
r3022 | 'line': self._clean_line(line, command) | ||
r2995 | }) | ||
except StopIteration: | |||
pass | |||
return chunks, stats | |||
r1753 | |||
def _safe_id(self, idstring): | |||
"""Make a string safe for including in an id attribute. | |||
The HTML spec says that id attributes 'must begin with | |||
a letter ([A-Za-z]) and may be followed by any number | |||
of letters, digits ([0-9]), hyphens ("-"), underscores | |||
("_"), colons (":"), and periods (".")'. These regexps | |||
are slightly over-zealous, in that they remove colons | |||
and periods unnecessarily. | |||
Whitespace is transformed into underscores, and then | |||
anything which is not a hyphen or a character that | |||
matches \w (alphanumerics and underscore) is removed. | |||
""" | |||
# Transform all whitespace to underscore | |||
idstring = re.sub(r'\s', "_", '%s' % idstring) | |||
# Remove everything that is not a hyphen or a member of \w | |||
idstring = re.sub(r'(?!-)\W', "", idstring).lower() | |||
return idstring | |||
r2995 | def prepare(self, inline_diff=True): | ||
""" | |||
Prepare the passed udiff for HTML rendering. It'l return a list | |||
of dicts with diff information | |||
""" | |||
parsed = self._parser(inline_diff=inline_diff) | |||
self.parsed = True | |||
self.parsed_diff = parsed | |||
return parsed | |||
def as_raw(self, diff_lines=None): | |||
r1753 | """ | ||
r2996 | Returns raw string diff | ||
r1753 | """ | ||
r2996 | return self._diff | ||
#return u''.join(imap(self._line_counter, self._diff.splitlines(1))) | |||
r1753 | |||
def as_html(self, table_class='code-difftable', line_class='line', | |||
new_lineno_class='lineno old', old_lineno_class='lineno new', | |||
r2995 | code_class='code', enable_comments=False, parsed_lines=None): | ||
r1753 | """ | ||
r2349 | Return given diff as html table with customized css classes | ||
r1753 | """ | ||
def _link_to_if(condition, label, url): | |||
""" | |||
Generates a link if condition is meet or just the label if not. | |||
""" | |||
if condition: | |||
r1789 | return '''<a href="%(url)s">%(label)s</a>''' % { | ||
'url': url, | |||
'label': label | |||
} | |||
r1753 | else: | ||
return label | |||
r2995 | if not self.parsed: | ||
self.prepare() | |||
diff_lines = self.parsed_diff | |||
if parsed_lines: | |||
diff_lines = parsed_lines | |||
r1753 | _html_empty = True | ||
_html = [] | |||
r1789 | _html.append('''<table class="%(table_class)s">\n''' % { | ||
'table_class': table_class | |||
}) | |||
r2995 | |||
r1753 | for diff in diff_lines: | ||
for line in diff['chunks']: | |||
_html_empty = False | |||
for change in line: | |||
r1789 | _html.append('''<tr class="%(lc)s %(action)s">\n''' % { | ||
'lc': line_class, | |||
'action': change['action'] | |||
}) | |||
r1753 | anchor_old_id = '' | ||
anchor_new_id = '' | |||
r1789 | anchor_old = "%(filename)s_o%(oldline_no)s" % { | ||
'filename': self._safe_id(diff['filename']), | |||
'oldline_no': change['old_lineno'] | |||
} | |||
anchor_new = "%(filename)s_n%(oldline_no)s" % { | |||
'filename': self._safe_id(diff['filename']), | |||
'oldline_no': change['new_lineno'] | |||
} | |||
cond_old = (change['old_lineno'] != '...' and | |||
change['old_lineno']) | |||
cond_new = (change['new_lineno'] != '...' and | |||
change['new_lineno']) | |||
r1753 | if cond_old: | ||
anchor_old_id = 'id="%s"' % anchor_old | |||
if cond_new: | |||
anchor_new_id = 'id="%s"' % anchor_new | |||
########################################################### | |||
# OLD LINE NUMBER | |||
########################################################### | |||
r1789 | _html.append('''\t<td %(a_id)s class="%(olc)s">''' % { | ||
'a_id': anchor_old_id, | |||
'olc': old_lineno_class | |||
}) | |||
r1753 | |||
r1789 | _html.append('''%(link)s''' % { | ||
'link': _link_to_if(True, change['old_lineno'], | |||
'#%s' % anchor_old) | |||
}) | |||
r1753 | _html.append('''</td>\n''') | ||
########################################################### | |||
# NEW LINE NUMBER | |||
########################################################### | |||
r1789 | _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % { | ||
'a_id': anchor_new_id, | |||
'nlc': new_lineno_class | |||
}) | |||
r1753 | |||
r1789 | _html.append('''%(link)s''' % { | ||
'link': _link_to_if(True, change['new_lineno'], | |||
'#%s' % anchor_new) | |||
}) | |||
r1753 | _html.append('''</td>\n''') | ||
########################################################### | |||
# CODE | |||
########################################################### | |||
r1787 | comments = '' if enable_comments else 'no-comment' | ||
r1789 | _html.append('''\t<td class="%(cc)s %(inc)s">''' % { | ||
'cc': code_class, | |||
'inc': comments | |||
}) | |||
_html.append('''\n\t\t<pre>%(code)s</pre>\n''' % { | |||
'code': change['line'] | |||
}) | |||
r2967 | |||
r1753 | _html.append('''\t</td>''') | ||
_html.append('''\n</tr>\n''') | |||
_html.append('''</table>''') | |||
if _html_empty: | |||
return None | |||
return ''.join(_html) | |||
def stat(self): | |||
""" | |||
Returns tuple of added, and removed lines for this instance | |||
""" | |||
return self.adds, self.removes | |||
r2337 | |||
r2362 | class InMemoryBundleRepo(bundlerepository): | ||
def __init__(self, ui, path, bundlestream): | |||
self._tempparent = None | |||
localrepo.localrepository.__init__(self, ui, path) | |||
self.ui.setconfig('phases', 'publish', False) | |||
self.bundle = bundlestream | |||
# dict with the mapping 'filename' -> position in the bundle | |||
self.bundlefilespos = {} | |||
r2892 | def differ(org_repo, org_ref, other_repo, other_ref, discovery_data=None, | ||
r3015 | remote_compare=False, context=3, ignore_whitespace=False): | ||
r2337 | """ | ||
r3015 | General differ between branches, bookmarks, revisions of two remote or | ||
local but related repositories | |||
r2337 | |||
:param org_repo: | |||
:param org_ref: | |||
:param other_repo: | |||
:type other_repo: | |||
:type other_ref: | |||
""" | |||
r2355 | |||
r3010 | org_repo_scm = org_repo.scm_instance | ||
r3015 | other_repo_scm = other_repo.scm_instance | ||
r3010 | org_repo = org_repo_scm._repo | ||
r3015 | other_repo = other_repo_scm._repo | ||
r2337 | org_ref = org_ref[1] | ||
other_ref = other_ref[1] | |||
r3010 | if org_repo == other_repo: | ||
log.debug('running diff between %s@%s and %s@%s' | |||
r3023 | % (org_repo.path, org_ref, other_repo.path, other_ref)) | ||
r3015 | _diff = org_repo_scm.get_diff(rev1=org_ref, rev2=other_ref, | ||
r3010 | ignore_whitespace=ignore_whitespace, context=context) | ||
return _diff | |||
r3015 | elif remote_compare: | ||
opts = diffopts(git=True, ignorews=ignore_whitespace, context=context) | |||
r2355 | common, incoming, rheads = discovery_data | ||
r3015 | org_repo_peer = localrepo.locallegacypeer(org_repo.local()) | ||
r2355 | # create a bundle (uncompressed if other repo is not local) | ||
r3015 | if org_repo_peer.capable('getbundle'): | ||
r2355 | # disable repo hooks here since it's just bundle ! | ||
# patch and reset hooks section of UI config to not run any | |||
# hooks on fetching archives with subrepos | |||
r3015 | for k, _ in org_repo.ui.configitems('hooks'): | ||
org_repo.ui.setconfig('hooks', k, None) | |||
r3023 | unbundle = org_repo.getbundle('incoming', common=None, | ||
r3015 | heads=None) | ||
r2355 | |||
r2552 | buf = BytesIO() | ||
r2355 | while True: | ||
r2364 | chunk = unbundle._stream.read(1024 * 4) | ||
r2355 | if not chunk: | ||
break | |||
buf.write(chunk) | |||
r2337 | |||
r2355 | buf.seek(0) | ||
r2364 | # replace chunked _stream with data that can do tell() and seek() | ||
r2355 | unbundle._stream = buf | ||
r2362 | ui = make_ui('db') | ||
bundlerepo = InMemoryBundleRepo(ui, path=org_repo.root, | |||
bundlestream=unbundle) | |||
r2431 | |||
r3015 | return ''.join(patch.diff(bundlerepo, | ||
node1=other_repo[other_ref].node(), | |||
node2=org_repo[org_ref].node(), | |||
opts=opts)) | |||
r3010 | |||
r3015 | return '' |