Changeset - ceaa65df5add
[Not reviewed]
default
0 6 0
domruf - 8 years ago 2017-10-25 22:57:31
dominikruf@gmail.com
diff: use fontello icon-diff-* names in generated html

This way we need less style code ... but generate slightly bigger html.

Also move the style code to kallithea-diff.less and remove the now obsolete
comment.
6 files changed with 96 insertions and 98 deletions:
0 comments (0 inline, 0 general)
kallithea/lib/diffs.py
Show inline comments
 
# -*- coding: utf-8 -*-
 
# 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/>.
 
"""
 
kallithea.lib.diffs
 
~~~~~~~~~~~~~~~~~~~
 

	
 
Set of diffing helpers, previously part of vcs
 

	
 

	
 
This file was forked by the Kallithea project in July 2014.
 
Original author and date, and relevant copyright and licensing information is below:
 
:created_on: Dec 4, 2011
 
:author: marcink
 
:copyright: (c) 2013 RhodeCode GmbH, and others.
 
:license: GPLv3, see LICENSE.md for more details.
 
"""
 
import re
 
import difflib
 
import logging
 

	
 
from tg.i18n import ugettext as _
 

	
 
from kallithea.lib import helpers as h
 
from kallithea.lib.vcs.exceptions import VCSError
 
from kallithea.lib.vcs.nodes import FileNode, SubModuleNode
 
from kallithea.lib.vcs.backends.base import EmptyChangeset
 
from kallithea.lib.utils2 import safe_unicode
 

	
 
log = logging.getLogger(__name__)
 

	
 

	
 
def _safe_id(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', "_", idstring)
 
    # Remove everything that is not a hyphen or a member of \w
 
    idstring = re.sub(r'(?!-)\W', "", idstring).lower()
 
    return idstring
 

	
 

	
 
def as_html(table_class='code-difftable', line_class='line',
 
            old_lineno_class='lineno old', new_lineno_class='lineno new',
 
            no_lineno_class='lineno',
 
            code_class='code', enable_comments=False, parsed_lines=None):
 
    """
 
    Return given diff as html table with customized css classes
 
    """
 
    def _link_to_if(condition, label, url):
 
        """
 
        Generates a link if condition is meet or just the label if not.
 
        """
 

	
 
        if condition:
 
            return '''<a href="%(url)s">%(label)s</a>''' % {
 
                'url': url,
 
                'label': label
 
            }
 
        else:
 
            return label
 

	
 
    _html_empty = True
 
    _html = []
 
    _html.append('''<table class="%(table_class)s">\n''' % {
 
        'table_class': table_class
 
    })
 

	
 
    for diff in parsed_lines:
 
        for line in diff['chunks']:
 
            _html_empty = False
 
            for change in line:
 
                _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
 
                    'lc': line_class,
 
                    'action': change['action']
 
                })
 
                anchor_old_id = ''
 
                anchor_new_id = ''
 
                anchor_old = "%(filename)s_o%(oldline_no)s" % {
 
                    'filename': _safe_id(diff['filename']),
 
                    'oldline_no': change['old_lineno']
 
                }
 
                anchor_new = "%(filename)s_n%(oldline_no)s" % {
 
                    'filename': _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'])
 
                no_lineno = (change['old_lineno'] == '...' and
 
                             change['new_lineno'] == '...')
 
                if cond_old:
 
                    anchor_old_id = 'id="%s"' % anchor_old
 
                if cond_new:
 
                    anchor_new_id = 'id="%s"' % anchor_new
 
                ###########################################################
 
                # OLD LINE NUMBER
 
                ###########################################################
 
                _html.append('''\t<td %(a_id)s class="%(olc)s" %(colspan)s>''' % {
 
                    'a_id': anchor_old_id,
 
                    'olc': no_lineno_class if no_lineno else old_lineno_class,
 
                    'colspan': 'colspan="2"' if no_lineno else ''
 
                })
 

	
 
                _html.append('''%(link)s''' % {
 
                    'link': _link_to_if(not no_lineno, change['old_lineno'],
 
                                        '#%s' % anchor_old)
 
                })
 
                _html.append('''</td>\n''')
 
                ###########################################################
 
                # NEW LINE NUMBER
 
                ###########################################################
 

	
 
                if not no_lineno:
 
                    _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
 
                        'a_id': anchor_new_id,
 
                        'nlc': new_lineno_class
 
                    })
 

	
 
                    _html.append('''%(link)s''' % {
 
                        'link': _link_to_if(True, change['new_lineno'],
 
                                            '#%s' % anchor_new)
 
                    })
 
                    _html.append('''</td>\n''')
 
                ###########################################################
 
                # CODE
 
                ###########################################################
 
                comments = '' if enable_comments else 'no-comment'
 
                _html.append('''\t<td class="%(cc)s %(inc)s">''' % {
 
                    'cc': code_class,
 
                    'inc': comments
 
                })
 
                _html.append('''\n\t\t<div class="add-bubble"><div>&nbsp;</div></div><pre>%(code)s</pre>\n''' % {
 
                    'code': change['line']
 
                })
 

	
 
                _html.append('''\t</td>''')
 
                _html.append('''\n</tr>\n''')
 
    _html.append('''</table>''')
 
    if _html_empty:
 
        return None
 
    return ''.join(_html)
 

	
 

	
 
def wrap_to_table(html):
 
    """Given a string with html, return it wrapped in a table, similar to what
 
    DiffProcessor returns."""
 
    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>''' % html
 

	
 

	
 
def wrapped_diff(filenode_old, filenode_new, diff_limit=None,
 
                ignore_whitespace=True, line_context=3,
 
                enable_comments=False):
 
    """
 
    Returns a file diff wrapped into a table.
 
    Checks for diff_limit and presents a message if the diff is too big.
 
    """
 
    if filenode_old is None:
 
        filenode_old = FileNode(filenode_new.path, '', EmptyChangeset())
 

	
 
    op = None
 
    a_path = filenode_old.path # default, might be overriden by actual rename in diff
 
    if filenode_old.is_binary or filenode_new.is_binary:
 
        html_diff = wrap_to_table(_('Binary file'))
 
        stats = (0, 0)
 

	
 
    elif diff_limit != -1 and (
 
            diff_limit is None or
 
            (filenode_old.size < diff_limit and filenode_new.size < diff_limit)):
 

	
 
        raw_diff = get_gitdiff(filenode_old, filenode_new,
 
                                ignore_whitespace=ignore_whitespace,
 
                                context=line_context)
 
        diff_processor = DiffProcessor(raw_diff)
 
        if diff_processor.parsed: # there should be exactly one element, for the specified file
 
            f = diff_processor.parsed[0]
 
            op = f['operation']
 
            a_path = f['old_filename']
 

	
 
        html_diff = as_html(parsed_lines=diff_processor.parsed, enable_comments=enable_comments)
 
        stats = diff_processor.stat()
 

	
 
    else:
 
        html_diff = wrap_to_table(_('Changeset was too big and was cut off, use '
 
                               'diff menu to display this diff'))
 
        stats = (0, 0)
 

	
 
    if not html_diff:
 
        submodules = filter(lambda o: isinstance(o, SubModuleNode),
 
                            [filenode_new, filenode_old])
 
        if submodules:
 
            html_diff = wrap_to_table(h.escape('Submodule %r' % submodules[0]))
 
        else:
 
            html_diff = wrap_to_table(_('No changes detected'))
 

	
 
    cs1 = filenode_old.changeset.raw_id
 
    cs2 = filenode_new.changeset.raw_id
 

	
 
    return cs1, cs2, a_path, html_diff, stats, op
 

	
 

	
 
def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
 
    """
 
    Returns git style diff between given ``filenode_old`` and ``filenode_new``.
 
    """
 
    # make sure we pass in default context
 
    context = context or 3
 
    submodules = filter(lambda o: isinstance(o, SubModuleNode),
 
                        [filenode_new, filenode_old])
 
    if submodules:
 
        return ''
 

	
 
    for filenode in (filenode_old, filenode_new):
 
        if not isinstance(filenode, FileNode):
 
            raise VCSError("Given object should be FileNode object, not %s"
 
                % filenode.__class__)
 

	
 
    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)
 

	
 
    vcs_gitdiff = get_diff(repo, old_raw_id, new_raw_id, filenode_new.path,
 
                           ignore_whitespace, context)
 
    return vcs_gitdiff
 

	
 

	
 
def get_diff(scm_instance, rev1, rev2, path=None, ignore_whitespace=False, context=3):
 
    """
 
    A thin wrapper around vcs lib get_diff.
 
    """
 
    try:
 
        return scm_instance.get_diff(rev1, rev2, path=path,
 
                                     ignore_whitespace=ignore_whitespace, context=context)
 
    except MemoryError:
 
        h.flash('MemoryError: Diff is too big', category='error')
 
        return ''
 

	
 

	
 
NEW_FILENODE = 1
 
DEL_FILENODE = 2
 
MOD_FILENODE = 3
 
RENAMED_FILENODE = 4
 
COPIED_FILENODE = 5
 
CHMOD_FILENODE = 6
 
BIN_FILENODE = 7
 

	
 

	
 
class DiffProcessor(object):
 
    """
 
    Give it a unified or git diff and it returns a list of the files that were
 
    mentioned in the diff together with a dict of meta information that
 
    can be used to render it in a HTML template.
 
    """
 
    _diff_git_re = re.compile('^diff --git', re.MULTILINE)
 

	
 
    def __init__(self, diff, vcs='hg', diff_limit=None, inline_diff=True):
 
        """
 
        :param diff:   a text in diff format
 
        :param vcs: type of version control hg or git
 
        :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))
 

	
 
        self._diff = diff
 
        self.adds = 0
 
        self.removes = 0
 
        self.diff_limit = diff_limit
 
        self.limited_diff = False
 
        self.vcs = vcs
 
        self.parsed = self._parse_gitdiff(inline_diff=inline_diff)
 

	
 
    def _parse_gitdiff(self, inline_diff):
 
        """Parse self._diff and return a list of dicts with meta info and chunks for each file.
 
        Might set limited_diff.
 
        Optionally, do an extra pass and to extra markup of one-liner changes.
 
        """
 
        _files = [] # list of dicts with meta info and chunks
 

	
 
        starts = [m.start() for m in self._diff_git_re.finditer(self._diff)]
 
        starts.append(len(self._diff))
 

	
 
        for start, end in zip(starts, starts[1:]):
 
            if self.diff_limit and end > self.diff_limit:
 
                self.limited_diff = True
 
                continue
 

	
 
            head, diff_lines = _get_header(self.vcs, buffer(self._diff, start, end - start))
 

	
 
            op = None
 
            stats = {
 
                'added': 0,
 
                'deleted': 0,
 
                'binary': False,
 
                'ops': {},
 
            }
 

	
 
            if head['deleted_file_mode']:
 
                op = 'D'
 
                op = 'removed'
 
                stats['binary'] = True
 
                stats['ops'][DEL_FILENODE] = 'deleted file'
 

	
 
            elif head['new_file_mode']:
 
                op = 'A'
 
                op = 'added'
 
                stats['binary'] = True
 
                stats['ops'][NEW_FILENODE] = 'new file %s' % head['new_file_mode']
 
            else:  # modify operation, can be cp, rename, chmod
 
                # CHMOD
 
                if head['new_mode'] and head['old_mode']:
 
                    op = 'M'
 
                    op = 'modified'
 
                    stats['binary'] = True
 
                    stats['ops'][CHMOD_FILENODE] = ('modified file chmod %s => %s'
 
                                        % (head['old_mode'], head['new_mode']))
 
                # RENAME
 
                if (head['rename_from'] and head['rename_to']
 
                      and head['rename_from'] != head['rename_to']):
 
                    op = 'R'
 
                    op = 'renamed'
 
                    stats['binary'] = True
 
                    stats['ops'][RENAMED_FILENODE] = ('file renamed from %s to %s'
 
                                    % (head['rename_from'], head['rename_to']))
 
                # COPY
 
                if head.get('copy_from') and head.get('copy_to'):
 
                    op = 'M'
 
                    op = 'modified'
 
                    stats['binary'] = True
 
                    stats['ops'][COPIED_FILENODE] = ('file copied from %s to %s'
 
                                        % (head['copy_from'], head['copy_to']))
 
                # FALL BACK: detect missed old style add or remove
 
                if op is None:
 
                    if not head['a_file'] and head['b_file']:
 
                        op = 'A'
 
                        op = 'added'
 
                        stats['binary'] = True
 
                        stats['ops'][NEW_FILENODE] = 'new file'
 

	
 
                    elif head['a_file'] and not head['b_file']:
 
                        op = 'D'
 
                        op = 'removed'
 
                        stats['binary'] = True
 
                        stats['ops'][DEL_FILENODE] = 'deleted file'
 

	
 
                # it's not ADD not DELETE
 
                if op is None:
 
                    op = 'M'
 
                    op = 'modified'
 
                    stats['binary'] = True
 
                    stats['ops'][MOD_FILENODE] = 'modified file'
 

	
 
            # a real non-binary diff
 
            if head['a_file'] or head['b_file']:
 
                chunks, added, deleted = _parse_lines(diff_lines)
 
                stats['binary'] = False
 
                stats['added'] = added
 
                stats['deleted'] = deleted
 
                # explicit mark that it's a modified file
 
                if op == 'M':
 
                if op == 'modified':
 
                    stats['ops'][MOD_FILENODE] = 'modified file'
 
            else:  # Git binary patch (or empty diff)
 
                # Git binary patch
 
                if head['bin_patch']:
 
                    stats['ops'][BIN_FILENODE] = 'binary diff not shown'
 
                chunks = []
 

	
 
            if op == 'D' and chunks:
 
            if op == 'removed' and chunks:
 
                # a way of seeing deleted content could perhaps be nice - but
 
                # not with the current UI
 
                chunks = []
 

	
 
            chunks.insert(0, [{
 
                'old_lineno': '',
 
                'new_lineno': '',
 
                'action':     'context',
 
                'line':       msg,
 
                } for _op, msg in stats['ops'].iteritems()
 
                  if _op not in [MOD_FILENODE]])
 

	
 
            _files.append({
 
                'old_filename':     head['a_path'],
 
                'filename':         head['b_path'],
 
                'old_revision':     head['a_blob_id'],
 
                'new_revision':     head['b_blob_id'],
 
                'chunks':           chunks,
 
                'operation':        op,
 
                'stats':            stats,
 
            })
 

	
 
        if not inline_diff:
 
            return _files
 

	
 
        # highlight inline changes when one del is followed by one add
 
        for diff_data in _files:
 
            for chunk in diff_data['chunks']:
 
                lineiter = iter(chunk)
 
                try:
 
                    peekline = lineiter.next()
 
                    while True:
 
                        # find a first del line
 
                        while peekline['action'] != 'del':
 
                            peekline = lineiter.next()
 
                        delline = peekline
 
                        peekline = lineiter.next()
 
                        # if not followed by add, eat all following del lines
 
                        if peekline['action'] != 'add':
 
                            while peekline['action'] == 'del':
 
                                peekline = lineiter.next()
 
                            continue
 
                        # found an add - make sure it is the only one
 
                        addline = peekline
 
                        try:
 
                            peekline = lineiter.next()
 
                        except StopIteration:
 
                            # add was last line - ok
 
                            _highlight_inline_diff(delline, addline)
 
                            raise
 
                        if peekline['action'] != 'add':
 
                            # there was only one add line - ok
 
                            _highlight_inline_diff(delline, addline)
 
                except StopIteration:
 
                    pass
 

	
 
        return _files
 

	
 
    def stat(self):
 
        """
 
        Returns tuple of added, and removed lines for this instance
 
        """
 
        return self.adds, self.removes
 

	
 

	
 
_escape_re = re.compile(r'(&)|(<)|(>)|(\t)|(\r)|(?<=.)( \n| $)')
 

	
 

	
 
def _escaper(string):
 
    """
 
    Do HTML escaping/markup
 
    """
 

	
 
    def substitute(m):
 
        groups = m.groups()
 
        if groups[0]:
 
            return '&amp;'
 
        if groups[1]:
 
            return '&lt;'
 
        if groups[2]:
 
            return '&gt;'
 
        if groups[3]:
 
            return '<u>\t</u>'
 
        if groups[4]:
 
            return '<u class="cr"></u>'
 
        if groups[5]:
 
            return ' <i></i>'
 
        assert False
 

	
 
    return _escape_re.sub(substitute, safe_unicode(string))
 

	
 

	
 
_git_header_re = re.compile(r"""
 
    ^diff[ ]--git[ ]a/(?P<a_path>.+?)[ ]b/(?P<b_path>.+?)\n
 
    (?:^old[ ]mode[ ](?P<old_mode>\d+)\n
 
       ^new[ ]mode[ ](?P<new_mode>\d+)(?:\n|$))?
 
    (?:^similarity[ ]index[ ](?P<similarity_index>\d+)%\n
 
       ^rename[ ]from[ ](?P<rename_from>.+)\n
 
       ^rename[ ]to[ ](?P<rename_to>.+)(?:\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|$))?
 
    (?:^(?P<bin_patch>GIT[ ]binary[ ]patch)(?:\n|$))?
 
    (?:^---[ ](a/(?P<a_file>.+?)|/dev/null)\t?(?:\n|$))?
 
    (?:^\+\+\+[ ](b/(?P<b_file>.+?)|/dev/null)\t?(?:\n|$))?
 
""", re.VERBOSE | re.MULTILINE)
 

	
 

	
 
_hg_header_re = re.compile(r"""
 
    ^diff[ ]--git[ ]a/(?P<a_path>.+?)[ ]b/(?P<b_path>.+?)\n
 
    (?:^old[ ]mode[ ](?P<old_mode>\d+)\n
 
       ^new[ ]mode[ ](?P<new_mode>\d+)(?:\n|$))?
 
    (?:^similarity[ ]index[ ](?P<similarity_index>\d+)%(?:\n|$))?
 
    (?:^rename[ ]from[ ](?P<rename_from>.+)\n
 
       ^rename[ ]to[ ](?P<rename_to>.+)(?:\n|$))?
 
    (?:^copy[ ]from[ ](?P<copy_from>.+)\n
 
       ^copy[ ]to[ ](?P<copy_to>.+)(?:\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|$))?
 
    (?:^(?P<bin_patch>GIT[ ]binary[ ]patch)(?:\n|$))?
 
    (?:^---[ ](a/(?P<a_file>.+?)|/dev/null)\t?(?:\n|$))?
 
    (?:^\+\+\+[ ](b/(?P<b_file>.+?)|/dev/null)\t?(?:\n|$))?
 
""", re.VERBOSE | re.MULTILINE)
 

	
 

	
 
def _get_header(vcs, diff_chunk):
 
    """
 
    Parses a Git diff for a single file (header and chunks) and returns a tuple with:
 

	
 
    1. A dict with meta info:
 

	
 
        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
 

	
 
    2. An iterator yielding lines with simple HTML markup.
 
    """
 
    match = None
 
    if vcs == 'git':
 
        match = _git_header_re.match(diff_chunk)
 
    elif vcs == 'hg':
 
        match = _hg_header_re.match(diff_chunk)
 
    if match is None:
 
        raise Exception('diff not recognized as valid %s diff' % vcs)
 
    meta_info = match.groupdict()
 
    rest = diff_chunk[match.end():]
 
    if rest and not rest.startswith('@') and not rest.startswith('literal ') and not rest.startswith('delta '):
 
        raise Exception('cannot parse %s diff header: %r followed by %r' % (vcs, diff_chunk[:match.end()], rest[:1000]))
 
    diff_lines = (_escaper(m.group(0)) for m in re.finditer(r'.*\n|.+$', rest)) # don't split on \r as str.splitlines do
 
    return meta_info, diff_lines
 

	
 

	
 
_chunk_re = re.compile(r'^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
 
_newline_marker = re.compile(r'^\\ No newline at end of file')
 

	
 

	
 
def _parse_lines(diff_lines):
 
    """
 
    Given an iterator of diff body lines, parse them and return a dict per
 
    line and added/removed totals.
 
    """
 
    added = deleted = 0
 
    old_line = old_end = new_line = new_end = None
 

	
 
    try:
 
        chunks = []
 
        line = diff_lines.next()
 

	
 
        while True:
 
            lines = []
 
            chunks.append(lines)
 

	
 
            match = _chunk_re.match(line)
 

	
 
            if not match:
 
                raise Exception('error parsing diff @@ line %r' % line)
 

	
 
            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 = diff_lines.next()
 

	
 
            while old_line < old_end or new_line < new_end:
 
                if not line:
 
                    raise Exception('error parsing diff - empty line at -%s+%s' % (old_line, new_line))
 

	
 
                affects_old = affects_new = False
 

	
 
                command = line[0]
 
                if command == '+':
 
                    affects_new = True
 
                    action = 'add'
 
                    added += 1
 
                elif command == '-':
 
                    affects_old = True
 
                    action = 'del'
 
                    deleted += 1
 
                elif command == ' ':
 
                    affects_old = affects_new = True
 
                    action = 'unmod'
 
                else:
 
                    raise Exception('error parsing diff - unknown command in line %r at -%s+%s' % (line, old_line, new_line))
 

	
 
                if not _newline_marker.match(line):
 
                    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,
 
                        'line':         line[1:],
 
                    })
 

	
 
                line = diff_lines.next()
 

	
 
                if _newline_marker.match(line):
 
                    # 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',
 
                        'line':         line,
 
                    })
 
                    line = diff_lines.next()
 
            if old_line > old_end:
 
                raise Exception('error parsing diff - more than %s "-" lines at -%s+%s' % (old_end, old_line, new_line))
 
            if new_line > new_end:
 
                raise Exception('error parsing diff - more than %s "+" lines at -%s+%s' % (new_end, old_line, new_line))
 
    except StopIteration:
 
        pass
 
    if old_line != old_end or new_line != new_end:
 
        raise Exception('diff processing broken when old %s<>%s or new %s<>%s line %r' % (old_line, old_end, new_line, new_end, line))
 

	
 
    return chunks, added, deleted
 

	
 
# Used for inline highlighter word split, must match the substitutions in _escaper
 
_token_re = re.compile(r'()(&amp;|&lt;|&gt;|<u>\t</u>|<u class="cr"></u>| <i></i>|\W+?)')
 

	
 

	
 
def _highlight_inline_diff(old, new):
 
    """
 
    Highlight simple add/remove in two lines given as info dicts. They are
 
    modified in place and given markup with <del>/<ins>.
 
    """
 
    assert old['action'] == 'del'
 
    assert new['action'] == 'add'
 

	
 
    oldwords = _token_re.split(old['line'])
 
    newwords = _token_re.split(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)
kallithea/public/css/contextbar.css
Show inline comments
 
/**
 
 * Stylesheets for the context bar
 
 */
 
i[class^='icon-'] {
 
  background-repeat: no-repeat;
 
  background-position: center;
 
  display: inline-block;
 
  min-width: 16px;
 
  min-height: 16px;
 
  margin: -2px 0 -4px 0;
 
  /* background-color: red; /* for debugging */
 
}
 
/* Note: class 'icon-empty' or 'icon-gravatar' can be used to get icon styling without an actual glyph */
 
/* css classes for diff file status ... it'd be nice if css had a way to
 
   inherit from another class but alas, we must make sure this content is the
 
   same from the icon font file */
 
.icon-diff-M:before {
 
  font-family: 'kallithea';
 
  content: '\22a1';
 
  color: #d0b44c;
 
}
 
.icon-diff-D:before {
 
  font-family: 'kallithea';
 
  content: '\229f';
 
  color: #bd2c00;
 
}
 
.icon-diff-A:before {
 
  font-family: 'kallithea';
 
  content: '\229e';
 
  color: #6cc644;
 
}
 
.icon-diff-R:before {
 
  font-family: 'kallithea';
 
  content: '\e81f';
 
  color: #677a85;
 
}
 
nav.navbar #quick a,
 
#content #context-bar,
 
#content #context-bar a {
 
  color: #FFFFFF;
 
}
 
nav.navbar #quick a:hover,
 
#content #context-bar a:hover {
 
  text-decoration: none;
 
  background: inherit;
 
}
 
.navbar-text {
 
  margin-top: 0;
 
  margin-bottom: 0;
 
}
 
ul#quick ul.dropdown-menu > li {
 
  border-bottom: 1px solid rgba(0,0,0,0.1);
 
  border-top: 1px solid rgba(255,255,255,0.1);
 
}
 
ul#quick ul.dropdown-menu > li:first-child {
 
  border-top: none;
 
}
 
ul#quick ul.dropdown-menu > li:last-child {
 
  border-bottom: none;
 
}
 
nav.navbar #quick ul,
 
#context-pages ul {
 
  background: #577632;
 
}
 
.repo-switcher-dropdown.select2-drop.select2-drop-active .select2-results .select2-highlighted,
 
ul.dropdown-menu li a:focus,
 
nav.navbar #quick li:hover,
 
#quick_login > .pull-right .list-group-item:hover,
 
#context-pages li:hover,
 
nav.navbar #quick li.active,
 
nav.navbar #quick li a.menu_link:focus,
 
#context-pages li.active {
 
  background: linear-gradient(to bottom, rgba(255,255,255,0.2) 0%, rgba(255,255,255,0.1) 100%); /* W3C */
 
}
 
#quick_login > .pull-right .list-group-item {
 
  background-color: #577632;
 
  border: 0;
 
}
 
#content #context-pages .follow .show-following,
 
#content #context-pages .following .show-follow {
 
  display: none;
 
}
 
nav.navbar #quick li,
 
nav.navbar #quick_login .pull-right,
 
#content #context-pages li {
 
  border-right: 1px solid rgba(0,0,0,0.1);
 
  border-left: 1px solid rgba(255,255,255,0.1);
 
  padding: 0;
 
}
 
.nav .open > a {
 
  background: inherit !important;
 
}
 
nav.navbar #quick li:last-child,
 
#content #context-pages li:last-child {
 
  border-right: none;
 
}
 
nav.navbar #quick > li:first-child {
 
  border-left: none;
 
}
 
nav.navbar #quick > li:first-child > a {
 
  border-radius: 4px 0 0 4px;
 
}
 
nav.navbar #quick > li > a,
 
nav.navbar #quick .select2-container .select2-choice .select2-chosen,
 
#context-pages > ul > li > a,
 
#context-pages .select2-container .select2-choice .select2-chosen {
 
  padding: 0px 15px 1px 15px;
 
  line-height: 43px;
 
}
 
nav.navbar #quick a#quick_login_link {
 
  padding-left: 0px;
 
}
 
nav.navbar #quick a {
 
  overflow: hidden;
 
}
 
#context-pages a.dropdown-toggle:after {
 
  position: absolute;
 
  float: right;
 
  padding-left: 5px;
 
  padding-right: 5px;
 
}
kallithea/public/css/style.css
Show inline comments
 
@@ -1608,670 +1608,680 @@ div#legend_choices table td {
 
  float: left;
 
}
 
.emails_wrap .email_entry .email_action {
 
  float: left;
 
}
 
.ips_wrap .ip_entry {
 
  height: 30px;
 
  padding: 0px 0px 0px 10px;
 
}
 
.ips_wrap .ip_entry .ip {
 
  float: left;
 
}
 
.ips_wrap .ip_entry .ip_action {
 
  float: left;
 
}
 
/*README STYLE*/
 
div.readme h2 {
 
  font-weight: normal;
 
}
 
div.readme {
 
  clear: both;
 
  overflow: hidden;
 
}
 
div.readme h1,
 
div.readme h2,
 
div.readme h3,
 
div.readme h4,
 
div.readme h5,
 
div.readme h6 {
 
  border-bottom: 0 !important;
 
  margin: 0 !important;
 
  padding: 0 !important;
 
  line-height: 1.5em !important;
 
}
 
div.readme h2,
 
div.readme h3 {
 
  margin: 1em 0 !important;
 
}
 
div.readme h2 {
 
  margin-top: 1.5em !important;
 
  border-top: 4px solid #e0e0e0 !important;
 
  padding-top: .5em !important;
 
}
 
div.readme p {
 
  color: black !important;
 
  margin: 1em 0 !important;
 
  line-height: 1.5em !important;
 
}
 
div.readme ul {
 
  list-style: disc !important;
 
  margin: 1em 0 1em 2em !important;
 
}
 
div.readme ol {
 
  list-style: decimal;
 
  margin: 1em 0 1em 2em !important;
 
}
 
div.readme code {
 
  font-size: 12px !important;
 
  background-color: ghostWhite !important;
 
  color: #444 !important;
 
  padding: 0 .2em !important;
 
  border: 1px solid #dedede !important;
 
}
 
div.readme pre code {
 
  padding: 0 !important;
 
  font-size: 12px !important;
 
  background-color: #eee !important;
 
  border: none !important;
 
}
 
div.readme pre {
 
  margin: 1em 0;
 
  font-size: 12px;
 
  background-color: #eee;
 
  border: 1px solid #ddd;
 
  padding: 5px;
 
  color: #444;
 
  overflow: auto;
 
  border-radius: 3px;
 
}
 
div.readme table {
 
  display: table;
 
  border-collapse: separate;
 
  border-spacing: 2px;
 
  border-color: gray;
 
  width: auto !important;
 
}
 
/** comment main **/
 
.comment .panel {
 
  max-width: 978px;
 
}
 
.comment .panel-body {
 
  background-color: #FAFAFA;
 
}
 
.comments-number {
 
  padding: 10px 0;
 
  color: #666;
 
}
 
.automatic-comment {
 
  font-style: italic;
 
}
 
/** comment form **/
 
.status-block {
 
  margin: 5px;
 
  clear: both;
 
}
 
.comment-form textarea {
 
  width: 100%;
 
  height: 100px;
 
  font-family: Lucida Console, Consolas, Monaco, Inconsolata, Liberation Mono, monospace;
 
}
 
form.comment-form {
 
  margin-top: 10px;
 
  margin-left: 10px;
 
}
 
.comment-inline-form .comment-block-ta,
 
.comment-form .comment-block-ta {
 
  border: 1px solid #ccc;
 
  border-radius: 3px;
 
  box-sizing: border-box;
 
}
 
.comment-form-submit {
 
  margin-top: 5px;
 
  margin-left: 525px;
 
}
 
.file-comments {
 
  display: none;
 
}
 
.comment-form .comment {
 
  margin-left: 10px;
 
}
 
.comment-form .comment-help {
 
  padding: 5px 5px 5px 5px;
 
  color: #666;
 
}
 
.comment-form .comment-button {
 
  padding-top: 5px;
 
}
 
.add-another-button {
 
  margin-left: 10px;
 
  margin-top: 10px;
 
  margin-bottom: 10px;
 
}
 
.panel-heading .pull-left input[type=checkbox],
 
.panel-heading .pull-right input[type=checkbox] {
 
  position: relative;
 
  top: 4px;
 
  margin: -10px 2px 0;
 
}
 
/** comment inline form **/
 
.comment-inline-form {
 
  margin: 4px;
 
  max-width: 978px;
 
}
 
#comments-general-comments .comment-inline-form {
 
  margin: 0;
 
}
 
.comment-inline-form .submitting-overlay {
 
  height: 0;
 
  text-align: center;
 
  font-size: 16px;
 
  opacity: 0.5;
 
}
 
.comment-inline-form .clearfix,
 
.comment-form .clearfix {
 
  background: #EEE;
 
  border-radius: 4px;
 
  padding: 5px;
 
  margin: 0px;
 
}
 
div.comment-inline-form {
 
  padding: 4px 0px 6px 0px;
 
}
 
.comment-inline-form textarea {
 
  width: 100%;
 
  height: 100px;
 
  font-family: Lucida Console, Consolas, Monaco, Inconsolata, Liberation Mono, monospace;
 
}
 
form.comment-inline-form {
 
  margin-top: 10px;
 
  margin-left: 10px;
 
}
 
.comment-inline-form-submit {
 
  margin-top: 5px;
 
  margin-left: 525px;
 
}
 
.file-comments {
 
  display: none;
 
}
 
.comment-inline-form .comment {
 
  margin-left: 10px;
 
}
 
.comment-inline-form .comment-help {
 
  padding: 5px 5px 5px 5px;
 
  color: #666;
 
}
 
.comment-inline-form .comment-button {
 
  padding-top: 5px;
 
}
 
/** comment inline **/
 
.inline-comments .comment {
 
  margin: 3px 3px 5px 5px;
 
}
 
.inline-comments #comments-general-comments .comment {
 
  margin-left: 0;
 
}
 
.inline-comments .add-button-row {
 
  padding: 2px 4px 8px 5px;
 
}
 
.inline-comments .comments-number {
 
  padding: 0px 0px 10px 0px;
 
  color: #666;
 
}
 
input.status_change_checkbox,
 
input.status_change_radio {
 
  margin: 0 0 5px 15px;
 
}
 
.badge {
 
  padding: 4px 4px !important;
 
  text-align: center;
 
  color: #888 !important;
 
  background-color: #DEDEDE !important;
 
  border-radius: 4px !important;
 
}
 
@keyframes animated-comment-background {
 
  0% {
 
    background-position: 0 0;
 
  }
 
  100% {
 
    background-position: 20px 0;
 
  }
 
}
 
.comment-preview.failed .user,
 
.comment-preview.failed .panel-body {
 
  color: #666;
 
}
 
.comment-preview .comment-submission-status {
 
  float: right;
 
}
 
.comment-preview .comment-submission-status .btn-group {
 
  margin-left: 10px;
 
}
 
.comment-preview.submitting .panel-body {
 
  background-image: linear-gradient(-45deg, #FAFAFA, #FAFAFA 25%, #FFF 25%, #FFF 50%, #FAFAFA 50%, #FAFAFA 75%, #FFF 75%, #FFF 100%);
 
  background-size: 20px 20px;
 
  animation: animated-comment-background 0.4s linear infinite;
 
}
 
/****
 
PULL REQUESTS
 
*****/
 
.pullrequests_section_head {
 
  padding: 10px 10px 10px 0px;
 
  margin: 0 15px;
 
  font-size: 16px;
 
  font-weight: bold;
 
}
 
div.pr-details-title.closed {
 
  color: #555;
 
  background: #eee;
 
}
 
div.pr {
 
  margin: 0px 15px;
 
  padding: 4px 4px;
 
}
 
div.pr-desc {
 
  margin: 0px 15px;
 
}
 
tr.pr-closed td {
 
  background-color: #eee !important;
 
  color: #555 !important;
 
}
 
span.pr-closed-tag {
 
  margin-bottom: 1px;
 
  margin-right: 1px;
 
  padding: 1px 3px;
 
  font-size: 10px;
 
  color: #577632;
 
  white-space: nowrap;
 
  border-radius: 4px;
 
  border: 1px solid #d9e8f8;
 
  line-height: 1.5em;
 
}
 
.panel-body .pr-box {
 
  max-width: 978px;
 
  margin-right: 15px;
 
}
 
#s2id_org_ref,
 
#s2id_other_ref,
 
#s2id_org_repo,
 
#s2id_other_repo {
 
  min-width: 150px;
 
  margin: 5px;
 
}
 
#pr-summary .msg-div {
 
  margin: 5px 0;
 
}
 
#pr-summary > .pr-not-edit {
 
  min-height: 50px !important;
 
}
 
#pr-edit-btn {
 
  margin: 20px 0 0 !important;
 
  position: absolute;
 
}
 
/****
 
  PERMS
 
*****/
 
#perms .perms_section_head {
 
  padding: 10px 10px 10px 0px;
 
  font-size: 16px;
 
  font-weight: bold;
 
  text-transform: capitalize;
 
}
 
#perms .perms_section_head label {
 
  margin-left: 10px;
 
}
 
input.perm_filter {
 
  position: relative;
 
  top: 2px;
 
}
 
.perm-gravatar {
 
  vertical-align: middle;
 
  padding: 2px;
 
}
 
.perm-gravatar-ac {
 
  vertical-align: middle;
 
  padding: 2px;
 
  width: 14px;
 
  height: 14px;
 
}
 
.cs_files .progress {
 
  margin-bottom: 0;
 
}
 
.cs_files .changes {
 
  float: right;
 
  color: #577632;
 
}
 
.cs_files .changes .added {
 
  color: inherit;
 
  background-color: #BBFFBB;
 
  float: left;
 
  text-align: center;
 
  font-size: 9px;
 
  padding: 2px 0px 2px 0px;
 
}
 
.cs_files .changes .deleted {
 
  background-color: #FF8888;
 
  float: left;
 
  text-align: center;
 
  font-size: 9px;
 
  padding: 2px 0px 2px 0px;
 
}
 
/*new binary
 
NEW_FILENODE = 1
 
DEL_FILENODE = 2
 
MOD_FILENODE = 3
 
RENAMED_FILENODE = 4
 
CHMOD_FILENODE = 5
 
BIN_FILENODE = 6
 
*/
 
.cs_files .changes .bin {
 
  background-color: #BBFFBB;
 
  float: left;
 
  text-align: center;
 
  font-size: 9px;
 
  padding: 2px 0px 2px 0px;
 
}
 
.cs_files .changes .bin.bin1 {
 
  background-color: #BBFFBB;
 
}
 
/*deleted binary*/
 
.cs_files .changes .bin.bin2 {
 
  background-color: #FF8888;
 
}
 
/*mod binary*/
 
.cs_files .changes .bin.bin3 {
 
  background-color: #DDDDDD;
 
}
 
/*rename file*/
 
.cs_files .changes .bin.bin4 {
 
  background-color: #6D99FF;
 
}
 
/*chmod file*/
 
.cs_files .changes .bin.bin5 {
 
  background-color: #6D99FF;
 
}
 
.cs_files .cs_added,
 
.cs_files .cs_A {
 
.cs_files .cs_added {
 
  height: 16px;
 
  margin-top: 7px;
 
  text-align: left;
 
}
 
.cs_files .cs_changed,
 
.cs_files .cs_M {
 
.cs_files .cs_modified {
 
  height: 16px;
 
  margin-top: 7px;
 
  text-align: left;
 
}
 
.cs_files .cs_removed,
 
.cs_files .cs_D {
 
.cs_files .cs_removed {
 
  height: 16px;
 
  margin-top: 7px;
 
  text-align: left;
 
}
 
.cs_files .cs_renamed,
 
.cs_files .cs_R {
 
.cs_files .cs_renamed {
 
  height: 16px;
 
  margin-top: 7px;
 
  text-align: left;
 
}
 
.diff-collapse {
 
  text-align: center;
 
  margin-bottom: 15px;
 
}
 
table.code-difftable {
 
  border-collapse: collapse;
 
  border-radius: 0px !important;
 
  width: 100%;
 
}
 
table.code-difftable td {
 
  padding: 0 !important;
 
  background: none !important;
 
  border: 0 !important;
 
  vertical-align: baseline !important;
 
}
 
table.code-difftable .context {
 
  background: none repeat scroll 0 0 #DDE7EF;
 
  color: #999;
 
}
 
table.code-difftable .add {
 
  background: none repeat scroll 0 0 #DDFFDD;
 
}
 
table.code-difftable .add ins {
 
  background: none repeat scroll 0 0 #AAFFAA;
 
  text-decoration: none;
 
}
 
table.code-difftable .del {
 
  background: none repeat scroll 0 0 #FFDDDD;
 
}
 
table.code-difftable .del del {
 
  background: none repeat scroll 0 0 #FFAAAA;
 
  text-decoration: none;
 
}
 
table.code-difftable td.code pre u:before {
 
  content: "\21a6";
 
  display: inline-block;
 
  width: 0;
 
}
 
table.code-difftable td.code pre u.cr:before {
 
  content: "\21a4";
 
  display: inline-block;
 
  color: rgba(0, 0, 0, 0.5);
 
}
 
table.code-difftable td.code pre u {
 
  color: rgba(0, 0, 0, 0.15);
 
}
 
table.code-difftable td.code pre i {
 
  border-style: solid;
 
  border-width: 0 0 0 1px;
 
  color: rgba(0, 0, 0, 0.5);
 
}
 
/** LINE NUMBERS **/
 
table.code-difftable .lineno {
 
  padding-left: 2px;
 
  padding-right: 2px !important;
 
  width: 30px;
 
  -moz-user-select: none;
 
  -webkit-user-select: none;
 
  border-right: 1px solid #CCC !important;
 
  border-left: 0px solid #CCC !important;
 
  border-top: 0px solid #CCC !important;
 
  border-bottom: none !important;
 
  vertical-align: middle !important;
 
  text-align: center;
 
}
 
table.code-difftable .lineno.new {
 
  text-align: right;
 
}
 
table.code-difftable .lineno.old {
 
  text-align: right;
 
}
 
table.code-difftable .lineno a {
 
  color: #aaa !important;
 
  font: 11px Lucida Console, Consolas, Monaco, Inconsolata, Liberation Mono, monospace !important;
 
  padding-left: 6px;
 
  padding-right: 6px;
 
  display: block;
 
}
 
table.code-difftable .line:hover .lineno a {
 
  color: #333 !important;
 
}
 
table.code-difftable .lineno-inline {
 
  background: none repeat scroll 0 0 #FFF !important;
 
  padding-left: 2px;
 
  padding-right: 2px;
 
  text-align: right;
 
  width: 30px;
 
  -moz-user-select: none;
 
  -webkit-user-select: none;
 
}
 
/** CODE **/
 
table.code-difftable .code {
 
  display: block;
 
  width: 100%;
 
}
 
table.code-difftable .code pre {
 
  margin: 0 0 0 12px !important;
 
  padding: 0;
 
  min-height: 17px;
 
  line-height: 17px;
 
  white-space: pre-wrap;
 
  word-break: break-all;
 
}
 
table.code-difftable .del .code pre:before {
 
  content: "-";
 
  color: #800;
 
  float: left;
 
  left: -1em;
 
  position: relative;
 
  width: 0;
 
}
 
table.code-difftable .add .code pre:before {
 
  content: "+";
 
  color: #080;
 
  float: left;
 
  left: -1em;
 
  position: relative;
 
  width: 0;
 
}
 
table.code-difftable .unmod .code pre:before {
 
  content: " ";
 
  float: left;
 
  left: -1em;
 
  position: relative;
 
  width: 0;
 
}
 
.add-bubble {
 
  position: relative;
 
  display: none;
 
  float: left;
 
  width: 0px;
 
  height: 0px;
 
  left: -8px;
 
  box-sizing: border-box;
 
}
 
/* comment bubble, only visible when in a commentable diff */
 
.commentable-diff tr.line.add:hover td .add-bubble,
 
.commentable-diff tr.line.del:hover td .add-bubble,
 
.commentable-diff tr.line.unmod:hover td .add-bubble {
 
  display: block;
 
  z-index: 1;
 
}
 
.add-bubble div {
 
  background: #577632;
 
  width: 16px;
 
  height: 16px;
 
  cursor: pointer;
 
  padding: 0 2px 2px 0.5px;
 
  border: 1px solid #577632;
 
  border-radius: 3px;
 
  box-sizing: border-box;
 
}
 
.add-bubble div:before {
 
  font-size: 14px;
 
  color: #ffffff;
 
  font-family: "kallithea";
 
  content: '\1f5ea';
 
}
 
.add-bubble div:hover {
 
  transform: scale(1.2, 1.2);
 
}
 
/* file diff icons */
 
.icon-diff-modified:before {
 
  color: #d0b44c;
 
}
 
.icon-diff-removed:before {
 
  color: #bd2c00;
 
}
 
.icon-diff-added:before {
 
  color: #6cc644;
 
}
 
.icon-diff-renamed:before {
 
  color: #677a85;
 
}
 
/* show some context of link targets - but only works when the link target
 
   can be extended with any visual difference */
 
div.comment:target:before {
 
  display: block;
 
  height: 100px;
 
  margin: -100px 0 0;
 
  content: "";
 
}
 
div.comment:target > .panel {
 
  border: solid 2px #ee0 !important;
 
}
 
.lineno:target a {
 
  border: solid 2px #ee0 !important;
 
  margin: -2px;
 
}
 
.btn-image-diff-show,
 
.btn-image-diff-swap {
 
  margin: 5px;
 
}
 
.img-diff {
 
  max-width: 45%;
 
  height: auto;
 
  margin: 5px;
 
  /* http://lea.verou.me/demos/css3-patterns.html */
 
  background-image: linear-gradient(45deg, #888 25%, transparent 25%, transparent), linear-gradient(-45deg, #888 25%, transparent 25%, transparent), linear-gradient(45deg, transparent 75%, #888 75%), linear-gradient(-45deg, transparent 75%, #888 75%);
 
  background-size: 10px 10px;
 
  background-color: #999;
 
}
 
.img-preview {
 
  max-width: 100%;
 
  height: auto;
 
  margin: 5px;
 
}
 
div.comment-prev-next-links div.prev-comment,
 
div.comment-prev-next-links div.next-comment {
 
  display: inline-block;
 
  min-width: 150px;
 
  margin: 3px 6px;
 
}
 
#comments-general-comments div.comment-prev-next-links div.prev-comment,
 
#comments-general-comments div.comment-prev-next-links div.next-comment {
 
  margin-left: 0;
 
}
 
body table.dataTable thead .sorting {
 
  background-image: none;
 
}
 
body table.dataTable thead .sorting_asc {
 
  background-image: none;
 
}
 
body table.dataTable thead .sorting_desc {
 
  background-image: none;
 
}
 
body table.dataTable thead .sorting_asc_disabled {
 
  background-image: none;
 
}
 
body table.dataTable thead .sorting_desc_disabled {
 
  background-image: none;
 
}
 
body table.dataTable thead .sorting_asc::after {
 
  font-family: "kallithea";
 
  content: "\23f6";
 
}
 
body table.dataTable thead .sorting_desc::after {
 
  font-family: "kallithea";
 
  content: "\23f7";
 
}
 
.dataTables_wrapper .dataTables_left {
 
  float: left !important;
 
}
 
.dataTables_wrapper .dataTables_right {
 
  float: right;
 
}
 
.dataTables_wrapper .dataTables_right > div {
 
  padding-left: 30px;
 
}
 
.dataTables_wrapper .dataTables_info {
 
  clear: none;
 
  padding-top: 3px;
 
}
 
.dataTables_wrapper .dataTables_paginate {
 
  padding-top: 0;
 
}
 
.dataTables_wrapper .dataTables_paginate .paginate_button {
 
  padding: 3px 10px;
 
}
 
.dataTables_wrapper .dataTables_paginate > a.paginate_button {
 
  padding-top: 1px;
 
  border: 0 !important;
 
}
 
.dataTables_wrapper label {
 
  margin-bottom: 0;
 
  font-weight: inherit;
 
}
 
#content div.panel .changelog-panel > .changelog-heading,
 
#content div.panel .changelog-panel > ul.pagination {
 
  margin-left: 100px;
 
}
 
textarea.commit-message {
 
  width: 100% !important;
 
  box-sizing: border-box;
 
}
kallithea/public/less/kallithea-diff.less
Show inline comments
 
.cs_files .progress {
 
  margin-bottom: 0;
 
}
 
.cs_files .changes {
 
  float: right;
 
  color: #577632;
 
}
 
.cs_files .changes .added {
 
  color: inherit;
 
  background-color: #BBFFBB;
 
  float: left;
 
  text-align: center;
 
  font-size: 9px;
 
  padding: 2px 0px 2px 0px;
 
}
 
.cs_files .changes .deleted {
 
  background-color: #FF8888;
 
  float: left;
 
  text-align: center;
 
  font-size: 9px;
 
  padding: 2px 0px 2px 0px;
 
}
 
/*new binary
 
NEW_FILENODE = 1
 
DEL_FILENODE = 2
 
MOD_FILENODE = 3
 
RENAMED_FILENODE = 4
 
CHMOD_FILENODE = 5
 
BIN_FILENODE = 6
 
*/
 
.cs_files .changes .bin {
 
  background-color: #BBFFBB;
 
  float: left;
 
  text-align: center;
 
  font-size: 9px;
 
  padding: 2px 0px 2px 0px;
 
}
 
.cs_files .changes .bin.bin1 {
 
  background-color: #BBFFBB;
 
}
 
/*deleted binary*/
 
.cs_files .changes .bin.bin2 {
 
  background-color: #FF8888;
 
}
 
/*mod binary*/
 
.cs_files .changes .bin.bin3 {
 
  background-color: #DDDDDD;
 
}
 
/*rename file*/
 
.cs_files .changes .bin.bin4 {
 
  background-color: #6D99FF;
 
}
 
/*chmod file*/
 
.cs_files .changes .bin.bin5 {
 
  background-color: #6D99FF;
 
}
 
.cs_files .cs_added,
 
.cs_files .cs_A {
 
.cs_files .cs_added {
 
  height: 16px;
 
  margin-top: 7px;
 
  text-align: left;
 
}
 
.cs_files .cs_changed,
 
.cs_files .cs_M {
 
.cs_files .cs_modified {
 
  height: 16px;
 
  margin-top: 7px;
 
  text-align: left;
 
}
 
.cs_files .cs_removed,
 
.cs_files .cs_D {
 
.cs_files .cs_removed {
 
  height: 16px;
 
  margin-top: 7px;
 
  text-align: left;
 
}
 
.cs_files .cs_renamed,
 
.cs_files .cs_R {
 
.cs_files .cs_renamed {
 
  height: 16px;
 
  margin-top: 7px;
 
  text-align: left;
 
}
 
.diff-collapse {
 
  text-align: center;
 
  margin-bottom: 15px;
 
}
 
table.code-difftable {
 
  border-collapse: collapse;
 
  border-radius: 0px !important;
 
  width: 100%;
 
}
 
table.code-difftable td {
 
  padding: 0 !important;
 
  background: none !important;
 
  border: 0 !important;
 
  vertical-align: baseline !important;
 
}
 
table.code-difftable .context {
 
  background: none repeat scroll 0 0 #DDE7EF;
 
  color: #999;
 
}
 
table.code-difftable .add {
 
  background: none repeat scroll 0 0 #DDFFDD;
 
}
 
table.code-difftable .add ins {
 
  background: none repeat scroll 0 0 #AAFFAA;
 
  text-decoration: none;
 
}
 
table.code-difftable .del {
 
  background: none repeat scroll 0 0 #FFDDDD;
 
}
 
table.code-difftable .del del {
 
  background: none repeat scroll 0 0 #FFAAAA;
 
  text-decoration: none;
 
}
 
table.code-difftable td.code pre u:before {
 
  content: "\21a6";
 
  display: inline-block;
 
  width: 0;
 
}
 
table.code-difftable td.code pre u.cr:before {
 
  content: "\21a4";
 
  display: inline-block;
 
  color: rgba(0, 0, 0, 0.5);
 
}
 
table.code-difftable td.code pre u {
 
  color: rgba(0, 0, 0, 0.15);
 
}
 
table.code-difftable td.code pre i {
 
  border-style: solid;
 
  border-width: 0 0 0 1px;
 
  color: rgba(0, 0, 0, 0.5);
 
}
 
/** LINE NUMBERS **/
 
table.code-difftable .lineno {
 
  padding-left: 2px;
 
  padding-right: 2px !important;
 
  width: 30px;
 
  -moz-user-select: none;
 
  -webkit-user-select: none;
 
  border-right: 1px solid #CCC !important;
 
  border-left: 0px solid #CCC !important;
 
  border-top: 0px solid #CCC !important;
 
  border-bottom: none !important;
 
  vertical-align: middle !important;
 
  text-align: center;
 
}
 
table.code-difftable .lineno.new {
 
  text-align: right;
 
}
 
table.code-difftable .lineno.old {
 
  text-align: right;
 
}
 
table.code-difftable .lineno a {
 
  color: #aaa !important;
 
  font: 11px Lucida Console, Consolas, Monaco, Inconsolata, Liberation Mono, monospace !important;
 
  padding-left: 6px;
 
  padding-right: 6px;
 
  display: block;
 
}
 
table.code-difftable .line:hover .lineno a {
 
  color: #333 !important;
 
}
 
table.code-difftable .lineno-inline {
 
  background: none repeat scroll 0 0 #FFF !important;
 
  padding-left: 2px;
 
  padding-right: 2px;
 
  text-align: right;
 
  width: 30px;
 
  -moz-user-select: none;
 
  -webkit-user-select: none;
 
}
 
/** CODE **/
 
table.code-difftable .code {
 
  display: block;
 
  width: 100%;
 
}
 
table.code-difftable .code pre {
 
  margin: 0 0 0 12px !important;
 
  padding: 0;
 
  min-height: 17px;
 
  line-height: 17px;
 
  white-space: pre-wrap;
 
  word-break: break-all;
 
}
 
table.code-difftable .del .code pre:before {
 
  content: "-";
 
  color: #800;
 
  float: left;
 
  left: -1em;
 
  position: relative;
 
  width: 0;
 
}
 
table.code-difftable .add .code pre:before {
 
  content: "+";
 
  color: #080;
 
  float: left;
 
  left: -1em;
 
  position: relative;
 
  width: 0;
 
}
 
table.code-difftable .unmod .code pre:before {
 
  content: " ";
 
  float: left;
 
  left: -1em;
 
  position: relative;
 
  width: 0;
 
}
 
.add-bubble {
 
  position: relative;
 
  display: none;
 
  float: left;
 
  width: 0px;
 
  height: 0px;
 
  left: -8px;
 
  box-sizing: border-box;
 
}
 
/* comment bubble, only visible when in a commentable diff */
 
.commentable-diff tr.line.add:hover td .add-bubble,
 
.commentable-diff tr.line.del:hover td .add-bubble,
 
.commentable-diff tr.line.unmod:hover td .add-bubble {
 
  display: block;
 
  z-index: 1;
 
}
 
.add-bubble div {
 
  background: #577632;
 
  width: 16px;
 
  height: 16px;
 
  cursor: pointer;
 
  padding: 0 2px 2px 0.5px;
 
  border: 1px solid #577632;
 
  border-radius: 3px;
 
  box-sizing: border-box;
 
}
 
.add-bubble div:before {
 
  font-size: 14px;
 
  color: #ffffff;
 
  font-family: "kallithea";
 
  content: '\1f5ea';
 
}
 
.add-bubble div:hover {
 
  transform: scale(1.2, 1.2);
 
}
 

	
 
/* file diff icons */
 
.icon-diff-modified:before {
 
  color: #d0b44c;
 
}
 
.icon-diff-removed:before {
 
  color: #bd2c00;
 
}
 
.icon-diff-added:before {
 
  color: #6cc644;
 
}
 
.icon-diff-renamed:before {
 
  color: #677a85;
 
}
kallithea/tests/functional/test_compare_local.py
Show inline comments
 
# -*- coding: utf-8 -*-
 
from kallithea.tests.base import *
 

	
 
class TestCompareController(TestController):
 

	
 
    def test_compare_tag_hg(self):
 
        self.log_user()
 
        tag1 = 'v0.1.2'
 
        tag2 = 'v0.1.3'
 
        response = self.app.get(url('compare_url',
 
                                    repo_name=HG_REPO,
 
                                    org_ref_type="tag",
 
                                    org_ref_name=tag1,
 
                                    other_ref_type="tag",
 
                                    other_ref_name=tag2,
 
                                    ), status=200)
 
        response.mustcontain('%s@%s' % (HG_REPO, tag1))
 
        response.mustcontain('%s@%s' % (HG_REPO, tag2))
 

	
 
        ## outgoing changesets between tags
 
        response.mustcontain('''<a class="changeset_hash" href="/%s/changeset/c5ddebc06eaaba3010c2d66ea6ec9d074eb0f678">r112:c5ddebc06eaa</a>''' % HG_REPO)
 
        response.mustcontain('''<a class="changeset_hash" href="/%s/changeset/70d4cef8a37657ee4cf5aabb3bd9f68879769816">r115:70d4cef8a376</a>''' % HG_REPO)
 
        response.mustcontain('''<a class="changeset_hash" href="/%s/changeset/9749bfbfc0d2eba208d7947de266303b67c87cda">r116:9749bfbfc0d2</a>''' % HG_REPO)
 
        response.mustcontain('''<a class="changeset_hash" href="/%s/changeset/41fda979f02fda216374bf8edac4e83f69e7581c">r117:41fda979f02f</a>''' % HG_REPO)
 
        response.mustcontain('''<a class="changeset_hash" href="/%s/changeset/bb1a3ab98cc45cb934a77dcabf87a5a598b59e97">r118:bb1a3ab98cc4</a>''' % HG_REPO)
 
        response.mustcontain('''<a class="changeset_hash" href="/%s/changeset/36e0fc9d2808c5022a24f49d6658330383ed8666">r119:36e0fc9d2808</a>''' % HG_REPO)
 
        response.mustcontain('''<a class="changeset_hash" href="/%s/changeset/17544fbfcd33ffb439e2b728b5d526b1ef30bfcf">r120:17544fbfcd33</a>''' % HG_REPO)
 

	
 
        response.mustcontain('11 files changed with 94 insertions and 64 deletions')
 

	
 
        ## files diff
 
        response.mustcontain(
 
                   '''<span class="node">
 
                          <i class="icon-diff-A"></i>
 
                          <i class="icon-diff-added"></i>
 
                          <a href="#C--1c5cf9e91c12">docs/api/utils/index.rst</a>
 
                      </span>''')
 
        response.mustcontain(
 
                   '''<span class="node">
 
                          <i class="icon-diff-A"></i>
 
                          <i class="icon-diff-added"></i>
 
                          <a href="#C--e3305437df55">test_and_report.sh</a>''')
 
        response.mustcontain(
 
                   '''<span class="node">
 
                          <i class="icon-diff-M"></i>
 
                          <i class="icon-diff-modified"></i>
 
                          <a href="#C--c8e92ef85cd1">.hgignore</a>''')
 
        response.mustcontain(
 
                   '''<span class="node">
 
                          <i class="icon-diff-M"></i>
 
                          <i class="icon-diff-modified"></i>
 
                          <a href="#C--6e08b694d687">.hgtags</a>''')
 
        response.mustcontain(
 
                   '''<span class="node">
 
                          <i class="icon-diff-M"></i>
 
                          <i class="icon-diff-modified"></i>
 
                          <a href="#C--2c14b00f3393">docs/api/index.rst</a>''')
 
        response.mustcontain(
 
                   '''<span class="node">
 
                          <i class="icon-diff-M"></i>
 
                          <i class="icon-diff-modified"></i>
 
                          <a href="#C--430ccbc82bdf">vcs/__init__.py</a>''')
 
        response.mustcontain(
 
                   '''<span class="node">
 
                          <i class="icon-diff-M"></i>
 
                          <i class="icon-diff-modified"></i>
 
                          <a href="#C--9c390eb52cd6">vcs/backends/hg.py</a>''')
 
        response.mustcontain(
 
                   '''<span class="node">
 
                          <i class="icon-diff-M"></i>
 
                          <i class="icon-diff-modified"></i>
 
                          <a href="#C--ebb592c595c0">vcs/utils/__init__.py</a>''')
 
        response.mustcontain(
 
                   '''<span class="node">
 
                          <i class="icon-diff-M"></i>
 
                          <i class="icon-diff-modified"></i>
 
                          <a href="#C--7abc741b5052">vcs/utils/annotate.py</a>''')
 
        response.mustcontain(
 
                   '''<span class="node">
 
                          <i class="icon-diff-M"></i>
 
                          <i class="icon-diff-modified"></i>
 
                          <a href="#C--2ef0ef106c56">vcs/utils/diffs.py</a>''')
 
        response.mustcontain(
 
                   '''<span class="node">
 
                          <i class="icon-diff-M"></i>
 
                          <i class="icon-diff-modified"></i>
 
                          <a href="#C--3150cb87d4b7">vcs/utils/lazy.py</a>''')
 

	
 
    def test_compare_tag_git(self):
 
        self.log_user()
 
        tag1 = 'v0.1.2'
 
        tag2 = 'v0.1.3'
 
        response = self.app.get(url('compare_url',
 
                                    repo_name=GIT_REPO,
 
                                    org_ref_type="tag",
 
                                    org_ref_name=tag1,
 
                                    other_ref_type="tag",
 
                                    other_ref_name=tag2,
 
                                    ), status=200)
 
        response.mustcontain('%s@%s' % (GIT_REPO, tag1))
 
        response.mustcontain('%s@%s' % (GIT_REPO, tag2))
 

	
 
        ## outgoing changesets between tags
 
        response.mustcontain('''<a class="changeset_hash" href="/%s/changeset/794bbdd31545c199f74912709ea350dedcd189a2">r113:794bbdd31545</a>''' % GIT_REPO)
 
        response.mustcontain('''<a class="changeset_hash" href="/%s/changeset/e36d8c5025329bdd4212bd53d4ed8a70ff44985f">r115:e36d8c502532</a>''' % GIT_REPO)
 
        response.mustcontain('''<a class="changeset_hash" href="/%s/changeset/5c9ff4f6d7508db0e72b1d2991c357d0d8e07af2">r116:5c9ff4f6d750</a>''' % GIT_REPO)
 
        response.mustcontain('''<a class="changeset_hash" href="/%s/changeset/b7187fa2b8c1d773ec35e9dee12f01f74808c879">r117:b7187fa2b8c1</a>''' % GIT_REPO)
 
        response.mustcontain('''<a class="changeset_hash" href="/%s/changeset/5f3b74262014a8de2dc7dade1152de9fd0c8efef">r118:5f3b74262014</a>''' % GIT_REPO)
 
        response.mustcontain('''<a class="changeset_hash" href="/%s/changeset/17438a11f72b93f56d0e08e7d1fa79a378578a82">r119:17438a11f72b</a>''' % GIT_REPO)
 
        response.mustcontain('''<a class="changeset_hash" href="/%s/changeset/5a3a8fb005554692b16e21dee62bf02667d8dc3e">r120:5a3a8fb00555</a>''' % GIT_REPO)
 

	
 
        response.mustcontain('11 files changed with 94 insertions and 64 deletions')
 

	
 
        # files
 
        response.mustcontain('''<a href="#C--1c5cf9e91c12">docs/api/utils/index.rst</a>''')
 
        response.mustcontain('''<a href="#C--e3305437df55">test_and_report.sh</a>''')
 
        response.mustcontain('''<a href="#C--c8e92ef85cd1">.hgignore</a>''')
 
        response.mustcontain('''<a href="#C--6e08b694d687">.hgtags</a>''')
 
        response.mustcontain('''<a href="#C--2c14b00f3393">docs/api/index.rst</a>''')
 
        response.mustcontain('''<a href="#C--430ccbc82bdf">vcs/__init__.py</a>''')
 
        response.mustcontain('''<a href="#C--9c390eb52cd6">vcs/backends/hg.py</a>''')
 
        response.mustcontain('''<a href="#C--ebb592c595c0">vcs/utils/__init__.py</a>''')
 
        response.mustcontain('''<a href="#C--7abc741b5052">vcs/utils/annotate.py</a>''')
 
        response.mustcontain('''<a href="#C--2ef0ef106c56">vcs/utils/diffs.py</a>''')
 
        response.mustcontain('''<a href="#C--3150cb87d4b7">vcs/utils/lazy.py</a>''')
 

	
 
    def test_index_branch_hg(self):
 
        self.log_user()
 
        response = self.app.get(url('compare_url',
 
                                    repo_name=HG_REPO,
 
                                    org_ref_type="branch",
 
                                    org_ref_name='default',
 
                                    other_ref_type="branch",
 
                                    other_ref_name='default',
 
                                    ))
 

	
 
        response.mustcontain('%s@default' % (HG_REPO))
 
        response.mustcontain('%s@default' % (HG_REPO))
 
        # branch are equal
 
        response.mustcontain('<span class="empty_data">No files</span>')
 
        response.mustcontain('<span class="empty_data">No changesets</span>')
 

	
 
    def test_index_branch_git(self):
 
        self.log_user()
 
        response = self.app.get(url('compare_url',
 
                                    repo_name=GIT_REPO,
 
                                    org_ref_type="branch",
 
                                    org_ref_name='master',
 
                                    other_ref_type="branch",
 
                                    other_ref_name='master',
 
                                    ))
 

	
 
        response.mustcontain('%s@master' % (GIT_REPO))
 
        response.mustcontain('%s@master' % (GIT_REPO))
 
        # branch are equal
 
        response.mustcontain('<span class="empty_data">No files</span>')
 
        response.mustcontain('<span class="empty_data">No changesets</span>')
 

	
 
    def test_compare_revisions_hg(self):
 
        self.log_user()
 
        rev1 = 'b986218ba1c9'
 
        rev2 = '3d8f361e72ab'
 

	
 
        response = self.app.get(url('compare_url',
 
                                    repo_name=HG_REPO,
 
                                    org_ref_type="rev",
 
                                    org_ref_name=rev1,
 
                                    other_ref_type="rev",
 
                                    other_ref_name=rev2,
 
                                    ))
 
        response.mustcontain('%s@%s' % (HG_REPO, rev1))
 
        response.mustcontain('%s@%s' % (HG_REPO, rev2))
 

	
 
        ## outgoing changesets between those revisions
 
        response.mustcontain("""<a class="changeset_hash" href="/%s/changeset/3d8f361e72ab303da48d799ff1ac40d5ac37c67e">r1:%s</a>""" % (HG_REPO, rev2))
 

	
 
        response.mustcontain('1 file changed with 7 insertions and 0 deletions')
 
        ## files
 
        response.mustcontain("""<a href="#C--c8e92ef85cd1">.hgignore</a>""")
 

	
 
    def test_compare_revisions_git(self):
 
        self.log_user()
 
        rev1 = 'c1214f7e79e02fc37156ff215cd71275450cffc3'
 
        rev2 = '38b5fe81f109cb111f549bfe9bb6b267e10bc557'
 

	
 
        response = self.app.get(url('compare_url',
 
                                    repo_name=GIT_REPO,
 
                                    org_ref_type="rev",
 
                                    org_ref_name=rev1,
 
                                    other_ref_type="rev",
 
                                    other_ref_name=rev2,
 
                                    ))
 
        response.mustcontain('%s@%s' % (GIT_REPO, rev1))
 
        response.mustcontain('%s@%s' % (GIT_REPO, rev2))
 

	
 
        ## outgoing changesets between those revisions
 
        response.mustcontain("""<a class="changeset_hash" href="/%s/changeset/38b5fe81f109cb111f549bfe9bb6b267e10bc557">r1:%s</a>""" % (GIT_REPO, rev2[:12]))
 
        response.mustcontain('1 file changed with 7 insertions and 0 deletions')
 

	
 
        ## files
 
        response.mustcontain("""<a href="#C--c8e92ef85cd1">.hgignore</a>""")
 

	
 
    def test_compare_revisions_hg_is_ajax_preview(self):
 
        self.log_user()
 
        rev1 = 'b986218ba1c9'
 
        rev2 = '3d8f361e72ab'
 

	
 
        response = self.app.get(url('compare_url',
 
                                    repo_name=HG_REPO,
 
                                    org_ref_type="rev",
 
                                    org_ref_name=rev1,
 
                                    other_ref_type="rev",
 
                                    other_ref_name=rev2,
 
                                    is_ajax_preview=True,
 
                                    ),
 
                                extra_environ={'HTTP_X_PARTIAL_XHR': '1'},)
 

	
 
        ## outgoing changesets between those revisions
 
        response.mustcontain("""<a class="changeset_hash" href="/%s/changeset/3d8f361e72ab303da48d799ff1ac40d5ac37c67e">r1:%s</a>""" % (HG_REPO, rev2))
 

	
 
        response.mustcontain('Merge Ancestor')
 
        response.mustcontain("""<a class="changeset_hash" href="/%s/changeset/b986218ba1c9b0d6a259fac9b050b1724ed8e545">%s</a>""" % (HG_REPO, rev1))
 

	
 
    def test_compare_revisions_git_is_ajax_preview(self):
 
        self.log_user()
 
        rev1 = 'c1214f7e79e02fc37156ff215cd71275450cffc3'
 
        rev2 = '38b5fe81f109cb111f549bfe9bb6b267e10bc557'
 

	
 
        response = self.app.get(url('compare_url',
 
                                    repo_name=GIT_REPO,
 
                                    org_ref_type="rev",
 
                                    org_ref_name=rev1,
 
                                    other_ref_type="rev",
 
                                    other_ref_name=rev2,
 
                                    is_ajax_preview=True,
 
                                    ),
 
                                extra_environ={'HTTP_X_PARTIAL_XHR': '1'},)
 
        ## outgoing changesets between those revisions
 
        response.mustcontain("""<a class="changeset_hash" href="/%s/changeset/38b5fe81f109cb111f549bfe9bb6b267e10bc557">r1:%s</a>""" % (GIT_REPO, rev2[:12]))
 

	
 
        response.mustcontain('Merge Ancestor')
 
        response.mustcontain("""<a class="changeset_hash" href="/%s/changeset/c1214f7e79e02fc37156ff215cd71275450cffc3">%s</a>""" % (GIT_REPO, rev1[:12]))
kallithea/tests/models/test_diff_parsers.py
Show inline comments
 
from kallithea.tests.base import *
 
from kallithea.lib.diffs import DiffProcessor, NEW_FILENODE, DEL_FILENODE, \
 
    MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE, COPIED_FILENODE
 
from kallithea.tests.fixture import Fixture
 

	
 
fixture = Fixture()
 

	
 

	
 
DIFF_FIXTURES = {
 
    'hg_diff_add_single_binary_file.diff': [
 
        ('US Warszawa.jpg', 'A',
 
        ('US Warszawa.jpg', 'added',
 
         {'added': 0,
 
          'deleted': 0,
 
          'binary': True,
 
          'ops': {NEW_FILENODE: 'new file 100755',
 
                  BIN_FILENODE: 'binary diff not shown'}}),
 
    ],
 
    'hg_diff_mod_single_binary_file.diff': [
 
        ('US Warszawa.jpg', 'M',
 
        ('US Warszawa.jpg', 'modified',
 
         {'added': 0,
 
          'deleted': 0,
 
          'binary': True,
 
          'ops': {MOD_FILENODE: 'modified file',
 
                  BIN_FILENODE: 'binary diff not shown'}}),
 
    ],
 

	
 
    'hg_diff_mod_single_file_and_rename_and_chmod.diff': [
 
        ('README', 'R',
 
        ('README', 'renamed',
 
         {'added': 3,
 
          'deleted': 0,
 
          'binary': False,
 
          'ops': {RENAMED_FILENODE: 'file renamed from README.rst to README',
 
                  CHMOD_FILENODE: 'modified file chmod 100755 => 100644'}}),
 
    ],
 
    'hg_diff_mod_file_and_rename.diff': [
 
        ('README.rst', 'R',
 
        ('README.rst', 'renamed',
 
         {'added': 3,
 
          'deleted': 0,
 
          'binary': False,
 
          'ops': {RENAMED_FILENODE: 'file renamed from README to README.rst'}}),
 
    ],
 
    'hg_diff_del_single_binary_file.diff': [
 
        ('US Warszawa.jpg', 'D',
 
        ('US Warszawa.jpg', 'removed',
 
         {'added': 0,
 
          'deleted': 0,
 
          'binary': True,
 
          'ops': {DEL_FILENODE: 'deleted file',
 
                  BIN_FILENODE: 'binary diff not shown'}}),
 
    ],
 
    'hg_diff_chmod_and_mod_single_binary_file.diff': [
 
        ('gravatar.png', 'M',
 
        ('gravatar.png', 'modified',
 
         {'added': 0,
 
          'deleted': 0,
 
          'binary': True,
 
          'ops': {CHMOD_FILENODE: 'modified file chmod 100644 => 100755',
 
                  BIN_FILENODE: 'binary diff not shown'}}),
 
    ],
 
    'hg_diff_chmod.diff': [
 
        ('file', 'M',
 
        ('file', 'modified',
 
         {'added': 0,
 
          'deleted': 0,
 
          'binary': True,
 
          'ops': {CHMOD_FILENODE: 'modified file chmod 100755 => 100644'}}),
 
    ],
 
    'hg_diff_rename_file.diff': [
 
        ('file_renamed', 'R',
 
        ('file_renamed', 'renamed',
 
         {'added': 0,
 
          'deleted': 0,
 
          'binary': True,
 
          'ops': {RENAMED_FILENODE: 'file renamed from file to file_renamed'}}),
 
    ],
 
    'hg_diff_rename_and_chmod_file.diff': [
 
        ('README', 'R',
 
        ('README', 'renamed',
 
         {'added': 0,
 
          'deleted': 0,
 
          'binary': True,
 
          'ops': {CHMOD_FILENODE: 'modified file chmod 100644 => 100755',
 
                  RENAMED_FILENODE: 'file renamed from README.rst to README'}}),
 
    ],
 
    'hg_diff_binary_and_normal.diff': [
 
        ('img/baseline-10px.png', 'A',
 
        ('img/baseline-10px.png', 'added',
 
         {'added': 0,
 
          'deleted': 0,
 
          'binary': True,
 
          'ops': {NEW_FILENODE: 'new file 100644',
 
                  BIN_FILENODE: 'binary diff not shown'}}),
 
        ('img/baseline-20px.png', 'D',
 
        ('img/baseline-20px.png', 'removed',
 
         {'added': 0,
 
          'deleted': 0,
 
          'binary': True,
 
          'ops': {DEL_FILENODE: 'deleted file',
 
                  BIN_FILENODE: 'binary diff not shown'}}),
 
        ('index.html', 'M',
 
        ('index.html', 'modified',
 
         {'added': 3,
 
          'deleted': 2,
 
          'binary': False,
 
          'ops': {MOD_FILENODE: 'modified file'}}),
 
        ('js/global.js', 'D',
 
        ('js/global.js', 'removed',
 
         {'added': 0,
 
          'deleted': 75,
 
          'binary': False,
 
          'ops': {DEL_FILENODE: 'deleted file'}}),
 
        ('js/jquery/hashgrid.js', 'A',
 
        ('js/jquery/hashgrid.js', 'added',
 
         {'added': 340,
 
          'deleted': 0,
 
          'binary': False,
 
          'ops': {NEW_FILENODE: 'new file 100755'}}),
 
        ('less/docs.less', 'M',
 
        ('less/docs.less', 'modified',
 
         {'added': 34,
 
          'deleted': 0,
 
          'binary': False,
 
          'ops': {MOD_FILENODE: 'modified file'}}),
 
        ('less/scaffolding.less', 'M',
 
        ('less/scaffolding.less', 'modified',
 
         {'added': 1,
 
          'deleted': 3,
 
          'binary': False,
 
          'ops': {MOD_FILENODE: 'modified file'}}),
 
        ('readme.markdown', 'M',
 
        ('readme.markdown', 'modified',
 
         {'added': 1,
 
          'deleted': 10,
 
          'binary': False,
 
          'ops': {MOD_FILENODE: 'modified file'}}),
 
    ],
 
    'git_diff_chmod.diff': [
 
        ('work-horus.xls', 'M',
 
        ('work-horus.xls', 'modified',
 
         {'added': 0,
 
          'deleted': 0,
 
          'binary': True,
 
          'ops': {CHMOD_FILENODE: 'modified file chmod 100644 => 100755'}})
 
    ],
 
    'git_diff_rename_file.diff': [
 
        ('file.xls', 'R',
 
        ('file.xls', 'renamed',
 
         {'added': 0,
 
          'deleted': 0,
 
          'binary': True,
 
          'ops': {RENAMED_FILENODE: 'file renamed from work-horus.xls to file.xls'}}),
 
        ('files/var/www/favicon.ico/DEFAULT',
 
         'R',
 
         'renamed',
 
         {'added': 0,
 
          'binary': True,
 
          'deleted': 0,
 
          'ops': {4: 'file renamed from files/var/www/favicon.ico to files/var/www/favicon.ico/DEFAULT',
 
                  6: 'modified file chmod 100644 => 100755'}})
 
    ],
 
    'git_diff_mod_single_binary_file.diff': [
 
        ('US Warszawa.jpg', 'M',
 
        ('US Warszawa.jpg', 'modified',
 
         {'added': 0,
 
          'deleted': 0,
 
          'binary': True,
 
          'ops': {MOD_FILENODE: 'modified file',
 
                  BIN_FILENODE: 'binary diff not shown'}})
 
    ],
 
    'git_diff_binary_and_normal.diff': [
 
        ('img/baseline-10px.png', 'A',
 
        ('img/baseline-10px.png', 'added',
 
         {'added': 0,
 
          'deleted': 0,
 
          'binary': True,
 
          'ops': {NEW_FILENODE: 'new file 100644',
 
                  BIN_FILENODE: 'binary diff not shown'}}),
 
        ('img/baseline-20px.png', 'D',
 
        ('img/baseline-20px.png', 'removed',
 
         {'added': 0,
 
          'deleted': 0,
 
          'binary': True,
 
          'ops': {DEL_FILENODE: 'deleted file',
 
                  BIN_FILENODE: 'binary diff not shown'}}),
 
        ('index.html', 'M',
 
        ('index.html', 'modified',
 
         {'added': 3,
 
          'deleted': 2,
 
          'binary': False,
 
          'ops': {MOD_FILENODE: 'modified file'}}),
 
        ('js/global.js', 'D',
 
        ('js/global.js', 'removed',
 
         {'added': 0,
 
          'deleted': 75,
 
          'binary': False,
 
          'ops': {DEL_FILENODE: 'deleted file'}}),
 
        ('js/jquery/hashgrid.js', 'A',
 
        ('js/jquery/hashgrid.js', 'added',
 
         {'added': 340,
 
          'deleted': 0,
 
          'binary': False,
 
          'ops': {NEW_FILENODE: 'new file 100755'}}),
 
        ('less/docs.less', 'M',
 
        ('less/docs.less', 'modified',
 
         {'added': 34,
 
          'deleted': 0,
 
          'binary': False,
 
          'ops': {MOD_FILENODE: 'modified file'}}),
 
        ('less/scaffolding.less', 'M',
 
        ('less/scaffolding.less', 'modified',
 
         {'added': 1,
 
          'deleted': 3,
 
          'binary': False,
 
          'ops': {MOD_FILENODE: 'modified file'}}),
 
        ('readme.markdown', 'M',
 
        ('readme.markdown', 'modified',
 
         {'added': 1,
 
          'deleted': 10,
 
          'binary': False,
 
          'ops': {MOD_FILENODE: 'modified file'}}),
 
    ],
 
    'diff_with_diff_data.diff': [
 
        ('vcs/backends/base.py', 'M',
 
        ('vcs/backends/base.py', 'modified',
 
         {'added': 18,
 
          'deleted': 2,
 
          'binary': False,
 
          'ops': {MOD_FILENODE: 'modified file'}}),
 
        ('vcs/backends/git/repository.py', 'M',
 
        ('vcs/backends/git/repository.py', 'modified',
 
         {'added': 46,
 
          'deleted': 15,
 
          'binary': False,
 
          'ops': {MOD_FILENODE: 'modified file'}}),
 
        ('vcs/backends/hg.py', 'M',
 
        ('vcs/backends/hg.py', 'modified',
 
         {'added': 22,
 
          'deleted': 3,
 
          'binary': False,
 
          'ops': {MOD_FILENODE: 'modified file'}}),
 
        ('vcs/tests/test_git.py', 'M',
 
        ('vcs/tests/test_git.py', 'modified',
 
         {'added': 5,
 
          'deleted': 5,
 
          'binary': False,
 
          'ops': {MOD_FILENODE: 'modified file'}}),
 
        ('vcs/tests/test_repository.py', 'M',
 
        ('vcs/tests/test_repository.py', 'modified',
 
         {'added': 174,
 
          'deleted': 2,
 
          'binary': False,
 
          'ops': {MOD_FILENODE: 'modified file'}}),
 
    ],
 
    'git_diff_modify_binary_file.diff': [
 
        ('file.name', 'M',
 
        ('file.name', 'modified',
 
         {'added': 0,
 
          'deleted': 0,
 
          'binary': True,
 
          'ops': {MOD_FILENODE: 'modified file',
 
                  BIN_FILENODE: 'binary diff not shown'}})
 
    ],
 
    'hg_diff_copy_file.diff': [
 
        ('file2', 'M',
 
        ('file2', 'modified',
 
         {'added': 0,
 
          'deleted': 0,
 
          'binary': True,
 
          'ops': {COPIED_FILENODE: 'file copied from file1 to file2'}}),
 
    ],
 
    'hg_diff_copy_and_modify_file.diff': [
 
        ('file3', 'M',
 
        ('file3', 'modified',
 
         {'added': 1,
 
          'deleted': 0,
 
          'binary': False,
 
          'ops': {COPIED_FILENODE: 'file copied from file2 to file3',
 
                  MOD_FILENODE: 'modified file'}}),
 
    ],
 
    'hg_diff_copy_and_chmod_file.diff': [
 
        ('file4', 'M',
 
        ('file4', 'modified',
 
         {'added': 0,
 
          'deleted': 0,
 
          'binary': True,
 
          'ops': {COPIED_FILENODE: 'file copied from file3 to file4',
 
                  CHMOD_FILENODE: 'modified file chmod 100644 => 100755'}}),
 
    ],
 
    'hg_diff_copy_chmod_and_edit_file.diff': [
 
        ('file5', 'M',
 
        ('file5', 'modified',
 
         {'added': 2,
 
          'deleted': 1,
 
          'binary': False,
 
          'ops': {COPIED_FILENODE: 'file copied from file4 to file5',
 
                  CHMOD_FILENODE: 'modified file chmod 100755 => 100644',
 
                  MOD_FILENODE: 'modified file'}}),
 
    ],
 
    'hg_diff_rename_space_cr.diff': [
 
        ('oh yes', 'R',
 
        ('oh yes', 'renamed',
 
         {'added': 3,
 
          'deleted': 2,
 
          'binary': False,
 
          'ops': {RENAMED_FILENODE: 'file renamed from oh no to oh yes'}}),
 
    ],
 
}
 

	
 

	
 
class TestDiffLib(TestController):
 

	
 
    @parametrize('diff_fixture', DIFF_FIXTURES)
 
    def test_diff(self, diff_fixture):
 
        raw_diff = fixture.load_resource(diff_fixture, strip=False)
 
        vcs = 'hg'
 
        if diff_fixture.startswith('git_'):
 
            vcs = 'git'
 
        diff_processor = DiffProcessor(raw_diff, vcs=vcs)
 
        data = [(x['filename'], x['operation'], x['stats']) for x in diff_processor.parsed]
 
        expected_data = DIFF_FIXTURES[diff_fixture]
 
        assert expected_data == data
 

	
 
    def test_diff_markup(self):
 
        raw_diff = fixture.load_resource('markuptest.diff', strip=False)
 
        diff_processor = DiffProcessor(raw_diff)
 
        chunks = diff_processor.parsed[0]['chunks']
 
        assert not chunks[0]
 
        #from pprint import pprint; pprint(chunks[1])
 
        l = ['\n']
 
        for d in chunks[1]:
 
            l.append('%(action)-7s %(new_lineno)3s %(old_lineno)3s %(line)r\n' % d)
 
        s = ''.join(l)
 
        print s
 
        assert s == r'''
 
context ... ... u'@@ -51,6 +51,13 @@\n'
 
unmod    51  51 u'<u>\t</u>begin();\n'
 
unmod    52  52 u'<u>\t</u>\n'
 
add      53     u'<u>\t</u>int foo;<u class="cr"></u>\n'
 
add      54     u'<u>\t</u>int bar; <u class="cr"></u>\n'
 
add      55     u'<u>\t</u>int baz;<u>\t</u><u class="cr"></u>\n'
 
add      56     u'<u>\t</u>int space; <i></i>'
 
add      57     u'<u>\t</u>int tab;<u>\t</u>\n'
 
add      58     u'<u>\t</u>\n'
 
unmod    59  53 u' <i></i>'
 
del          54 u'<u>\t</u>#define MAX_STEPS (48)\n'
 
add      60     u'<u>\t</u><u class="cr"></u>\n'
 
add      61     u'<u>\t</u>#define MAX_STEPS (64)<u class="cr"></u>\n'
 
unmod    62  55 u'\n'
 
del          56 u'<u>\t</u>#define MIN_STEPS (<del>48</del>)\n'
 
add      63     u'<u>\t</u>#define MIN_STEPS (<ins>42</ins>)\n'
 
'''
0 comments (0 inline, 0 general)