# -*- coding: utf-8 -*-
"""
rhodecode.lib.diffs
~~~~~~~~~~~~~~~~~~~
Set of diffing helpers, previously part of vcs
:created_on: Dec 4, 2011
:author: marcink
:copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
:original copyright: 2007-2008 by Armin Ronacher
: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
import markupsafe
from itertools import tee, imap
from mercurial import patch
from mercurial.mdiff import diffopts
from mercurial.bundlerepo import bundlerepository
from pylons.i18n.translation import _
from rhodecode.lib.compat import BytesIO
from rhodecode.lib.vcs.utils.hgcompat import localrepo
from rhodecode.lib.vcs.exceptions import VCSError
from rhodecode.lib.vcs.nodes import FileNode, SubModuleNode
from rhodecode.lib.vcs.backends.base import EmptyChangeset
from rhodecode.lib.helpers import escape
from rhodecode.lib.utils import make_ui
from rhodecode.lib.utils2 import safe_unicode
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:
diff = wrap_to_table(_('Changeset was too big and was cut off, use '
'diff menu to display this diff'))
if not diff:
submodules = filter(lambda o: isinstance(o, SubModuleNode),
[filenode_new, filenode_old])
if submodules:
diff = wrap_to_table(escape('Submodule %r' % submodules[0]))
@@ -169,97 +170,97 @@ class DiffProcessor(object):
def escaper(self, string):
return markupsafe.escape(string)
def copy_iterator(self):
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
def _extract_rev(self, line1, line2):
Extract the operation (A/M/D), filename and revision hint from a line.
try:
if line1.startswith('--- ') and line2.startswith('+++ '):
l1 = line1[4:].split(None, 1)
old_filename = (l1[0].replace('a/', '', 1)
if len(l1) >= 1 else None)
old_rev = l1[1] if len(l1) == 2 else 'old'
l2 = line2[4:].split(None, 1)
new_filename = (l2[0].replace('b/', '', 1)
new_rev = l2[1] if len(l2) == 2 else 'new'
filename = (old_filename
if old_filename != '/dev/null' else new_filename)
operation = 'D' if new_filename == '/dev/null' else None
if not operation:
operation = 'M' if old_filename != '/dev/null' else 'A'
return operation, filename, new_rev, old_rev
except (ValueError, IndexError):
pass
return None, None, None, None
def _parse_gitdiff(self, diffiterator):
def line_decoder(l):
if l.startswith('+') and not l.startswith('+++'):
self.adds += 1
elif l.startswith('-') and not l.startswith('---'):
self.removes += 1
return l.decode('utf8', 'replace')
return safe_unicode(l)
output = list(diffiterator)
size = len(output)
if size == 2:
l = []
l.extend([output[0]])
l.extend(output[1].splitlines(1))
return map(line_decoder, l)
elif size == 1:
return map(line_decoder, output[0].splitlines(1))
elif size == 0:
return []
raise Exception('wrong size of diff %s' % size)
def _highlight_line_difflib(self, line, next_):
Highlight inline changes in both lines.
if line['action'] == 'del':
old, new = line, next_
old, new = next_, line
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)
def _highlight_line_udiff(self, line, next_):
# original copyright: 2007-2008 by Armin Ronacher
# licensed under the BSD license.
import logging
from difflib import unified_diff
from mercurial.match import match
from rhodecode.lib.vcs.nodes import FileNode, NodeError
from rhodecode.lib.vcs.utils import safe_unicode
def get_udiff(filenode_old, filenode_new, show_whitespace=True):
Returns unified diff between given ``filenode_old`` and ``filenode_new``.
filenode_old_date = filenode_old.changeset.date
except NodeError:
filenode_old_date = None
filenode_new_date = filenode_new.changeset.date
filenode_new_date = None
for filenode in (filenode_old, filenode_new):
if not isinstance(filenode, FileNode):
raise VCSError("Given object should be FileNode object, not %s"
% filenode.__class__)
if filenode_old_date and filenode_new_date:
if not filenode_old_date < filenode_new_date:
logging.debug("Generating udiff for filenodes with not increasing "
"dates")
vcs_udiff = unified_diff(filenode_old.content.splitlines(True),
filenode_new.content.splitlines(True),
filenode_old.name,
filenode_new.name,
filenode_old_date,
filenode_old_date)
return vcs_udiff
def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True):
Returns git style diff between given ``filenode_old`` and ``filenode_new``.
:param ignore_whitespace: ignore whitespaces in diff
old_raw_id = getattr(filenode_old.changeset, 'raw_id', '0' * 40)
@@ -104,97 +105,97 @@ class DiffProcessor(object):
# Select a differ.
if differ == 'difflib':
self.differ = self._highlight_line_difflib
self.differ = self._highlight_line_udiff
return string.replace('<', '<').replace('>', '>')
Extract the filename and revision hint from a line.
old_filename = l1[0].lstrip('a/') if len(l1) >= 1 else None
new_filename = l2[0].lstrip('b/') if len(l1) >= 1 else None
filename = old_filename if (old_filename !=
'dev/null') else new_filename
return filename, new_rev, old_rev
return None, None, None
def _highlight_line_difflib(self, line, next):
old, new = line, next
old, new = next, line
def _highlight_line_udiff(self, line, next):
Status change: