Changeset - 8f3469917832
[Not reviewed]
Merge default
0 3 0
Mads Kiilerich - 8 years ago 2018-02-14 01:54:12
mads@kiilerich.com
Merge stable
3 files changed with 38 insertions and 2 deletions:
0 comments (0 inline, 0 general)
kallithea/lib/vcs/backends/hg/repository.py
Show inline comments
 
# -*- coding: utf-8 -*-
 
"""
 
    vcs.backends.hg.repository
 
    ~~~~~~~~~~~~~~~~~~~~~~~~~~
 

	
 
    Mercurial repository implementation.
 

	
 
    :created_on: Apr 8, 2010
 
    :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
 
"""
 

	
 
import os
 
import time
 
import urllib
 
import urllib2
 
import logging
 
import datetime
 

	
 
from kallithea.lib.vcs.backends.base import BaseRepository, CollectionGenerator
 

	
 
from kallithea.lib.vcs.exceptions import (
 
    BranchDoesNotExistError, ChangesetDoesNotExistError, EmptyRepositoryError,
 
    RepositoryError, VCSError, TagAlreadyExistError, TagDoesNotExistError
 
)
 
from kallithea.lib.vcs.utils import (
 
    author_email, author_name, date_fromtimestamp, makedate, safe_unicode, safe_str,
 
)
 
from kallithea.lib.vcs.utils.lazy import LazyProperty
 
from kallithea.lib.vcs.utils.ordered_dict import OrderedDict
 
from kallithea.lib.vcs.utils.paths import abspath
 
from kallithea.lib.vcs.utils.hgcompat import (
 
    ui, nullid, match, patch, diffopts, clone, get_contact,
 
    localrepository, RepoLookupError, Abort, RepoError, hex, scmutil, hg_url,
 
    httpbasicauthhandler, httpdigestauthhandler, peer, httppeer, sshpeer, tag
 
)
 

	
 
from .changeset import MercurialChangeset
 
from .inmemory import MercurialInMemoryChangeset
 
from .workdir import MercurialWorkdir
 

	
 
log = logging.getLogger(__name__)
 

	
 

	
 
class MercurialRepository(BaseRepository):
 
    """
 
    Mercurial repository backend
 
    """
 
    DEFAULT_BRANCH_NAME = 'default'
 
    scm = 'hg'
 

	
 
    def __init__(self, repo_path, create=False, baseui=None, src_url=None,
 
                 update_after_clone=False):
 
        """
 
        Raises RepositoryError if repository could not be find at the given
 
        ``repo_path``.
 

	
 
        :param repo_path: local path of the repository
 
        :param create=False: if set to True, would try to create repository if
 
           it does not exist rather than raising exception
 
        :param baseui=None: user data
 
        :param src_url=None: would try to clone repository from given location
 
        :param update_after_clone=False: sets update of working copy after
 
          making a clone
 
        """
 

	
 
        if not isinstance(repo_path, str):
 
            raise VCSError('Mercurial backend requires repository path to '
 
                           'be instance of <str> got %s instead' %
 
                           type(repo_path))
 

	
 
        self.path = abspath(repo_path)
 
        self.baseui = baseui or ui.ui()
 
        # We've set path and ui, now we can set _repo itself
 
        self._repo = self._get_repo(create, src_url, update_after_clone)
 

	
 
    @property
 
    def _empty(self):
 
        """
 
        Checks if repository is empty ie. without any changesets
 
        """
 
        # TODO: Following raises errors when using InMemoryChangeset...
 
        # return len(self._repo.changelog) == 0
 
        return len(self.revisions) == 0
 

	
 
    @LazyProperty
 
    def revisions(self):
 
        """
 
        Returns list of revisions' ids, in ascending order.  Being lazy
 
        attribute allows external tools to inject shas from cache.
 
        """
 
        return self._get_all_revisions()
 

	
 
    @LazyProperty
 
    def name(self):
 
        return os.path.basename(self.path)
 

	
 
    @LazyProperty
 
    def branches(self):
 
        return self._get_branches()
 

	
 
    @LazyProperty
 
    def closed_branches(self):
 
        return self._get_branches(normal=False, closed=True)
 

	
 
    @LazyProperty
 
    def allbranches(self):
 
        """
 
        List all branches, including closed branches.
 
        """
 
        return self._get_branches(closed=True)
 

	
 
    def _get_branches(self, normal=True, closed=False):
 
        """
 
        Gets branches for this repository
 
        Returns only not closed branches by default
 

	
 
        :param closed: return also closed branches for mercurial
 
        :param normal: return also normal branches
 
        """
 

	
 
        if self._empty:
 
            return {}
 

	
 
        bt = OrderedDict()
 
        for bn, _heads, tip, isclosed in sorted(self._repo.branchmap().iterbranches()):
 
            if isclosed:
 
                if closed:
 
                    bt[safe_unicode(bn)] = hex(tip)
 
            else:
 
                if normal:
 
                    bt[safe_unicode(bn)] = hex(tip)
 

	
 
        return bt
 

	
 
    @LazyProperty
 
    def tags(self):
 
        """
 
        Gets tags for this repository
 
        """
 
        return self._get_tags()
 

	
 
    def _get_tags(self):
 
        if self._empty:
 
            return {}
 

	
 
        sortkey = lambda ctx: ctx[0]  # sort by name
 
        _tags = [(safe_unicode(n), hex(h),) for n, h in
 
                 self._repo.tags().items()]
 

	
 
        return OrderedDict(sorted(_tags, key=sortkey, reverse=True))
 

	
 
    def tag(self, name, user, revision=None, message=None, date=None,
 
            **kwargs):
 
        """
 
        Creates and returns a tag for the given ``revision``.
 

	
 
        :param name: name for new tag
 
        :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
 
        :param revision: changeset id for which new tag would be created
 
        :param message: message of the tag's commit
 
        :param date: date of tag's commit
 

	
 
        :raises TagAlreadyExistError: if tag with same name already exists
 
        """
 
        if name in self.tags:
 
            raise TagAlreadyExistError("Tag %s already exists" % name)
 
        changeset = self.get_changeset(revision)
 
        local = kwargs.setdefault('local', False)
 

	
 
        if message is None:
 
            message = "Added tag %s for changeset %s" % (name,
 
                changeset.short_id)
 

	
 
        if date is None:
 
            date = datetime.datetime.now().strftime('%a, %d %b %Y %H:%M:%S')
 

	
 
        try:
 
            tag(self._repo, name, changeset._ctx.node(), message, local, user, date)
 
        except Abort as e:
 
            raise RepositoryError(e.message)
 

	
 
        # Reinitialize tags
 
        self.tags = self._get_tags()
 
        tag_id = self.tags[name]
 

	
 
        return self.get_changeset(revision=tag_id)
 

	
 
    def remove_tag(self, name, user, message=None, date=None):
 
        """
 
        Removes tag with the given ``name``.
 

	
 
        :param name: name of the tag to be removed
 
        :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
 
        :param message: message of the tag's removal commit
 
        :param date: date of tag's removal commit
 

	
 
        :raises TagDoesNotExistError: if tag with given name does not exists
 
        """
 
        if name not in self.tags:
 
            raise TagDoesNotExistError("Tag %s does not exist" % name)
 
        if message is None:
 
            message = "Removed tag %s" % name
 
        if date is None:
 
            date = datetime.datetime.now().strftime('%a, %d %b %Y %H:%M:%S')
 
        local = False
 

	
 
        try:
 
            tag(self._repo, name, nullid, message, local, user, date)
 
            self.tags = self._get_tags()
 
        except Abort as e:
 
            raise RepositoryError(e.message)
 

	
 
    @LazyProperty
 
    def bookmarks(self):
 
        """
 
        Gets bookmarks for this repository
 
        """
 
        return self._get_bookmarks()
 

	
 
    def _get_bookmarks(self):
 
        if self._empty:
 
            return {}
 

	
 
        sortkey = lambda ctx: ctx[0]  # sort by name
 
        _bookmarks = [(safe_unicode(n), hex(h),) for n, h in
 
                 self._repo._bookmarks.items()]
 
        return OrderedDict(sorted(_bookmarks, key=sortkey, reverse=True))
 

	
 
    def _get_all_revisions(self):
 

	
 
        return [self._repo[x].hex() for x in self._repo.filtered('visible').changelog.revs()]
 

	
 
    def get_diff(self, rev1, rev2, path='', ignore_whitespace=False,
 
                  context=3):
 
        """
 
        Returns (git like) *diff*, as plain text. Shows changes introduced by
 
        ``rev2`` since ``rev1``.
 

	
 
        :param rev1: Entry point from which diff is shown. Can be
 
          ``self.EMPTY_CHANGESET`` - in this case, patch showing all
 
          the changes since empty state of the repository until ``rev2``
 
        :param rev2: Until which revision changes should be shown.
 
        :param ignore_whitespace: If set to ``True``, would not show whitespace
 
          changes. Defaults to ``False``.
 
        :param context: How many lines before/after changed lines should be
 
          shown. Defaults to ``3``. If negative value is passed-in, it will be
 
          set to ``0`` instead.
 
        """
 

	
 
        # Negative context values make no sense, and will result in
 
        # errors. Ensure this does not happen.
 
        if context < 0:
 
            context = 0
 

	
 
        if hasattr(rev1, 'raw_id'):
 
            rev1 = getattr(rev1, 'raw_id')
 

	
 
        if hasattr(rev2, 'raw_id'):
 
            rev2 = getattr(rev2, 'raw_id')
 

	
 
        # Check if given revisions are present at repository (may raise
 
        # ChangesetDoesNotExistError)
 
        if rev1 != self.EMPTY_CHANGESET:
 
            self.get_changeset(rev1)
 
        self.get_changeset(rev2)
 
        if path:
 
            file_filter = match(self.path, '', [path])
 
            file_filter = match(self.path, '', [path], exact=True)
 
        else:
 
            file_filter = None
 

	
 
        return ''.join(patch.diff(self._repo, rev1, rev2, match=file_filter,
 
                          opts=diffopts(git=True,
 
                                        showfunc=True,
 
                                        ignorews=ignore_whitespace,
 
                                        context=context)))
 

	
 
    @classmethod
 
    def _check_url(cls, url, repoui=None):
 
        """
 
        Function will check given url and try to verify if it's a valid
 
        link. Sometimes it may happened that mercurial will issue basic
 
        auth request that can cause whole API to hang when used from python
 
        or other external calls.
 

	
 
        On failures it'll raise urllib2.HTTPError, exception is also thrown
 
        when the return code is non 200
 
        """
 
        # check first if it's not an local url
 
        if os.path.isdir(url) or url.startswith('file:'):
 
            return True
 

	
 
        if url.startswith('ssh:'):
 
            # in case of invalid uri or authentication issues, sshpeer will
 
            # throw an exception.
 
            sshpeer(repoui or ui.ui(), url).lookup('tip')
 
            return True
 

	
 
        url_prefix = None
 
        if '+' in url[:url.find('://')]:
 
            url_prefix, url = url.split('+', 1)
 

	
 
        handlers = []
 
        url_obj = hg_url(url)
 
        test_uri, authinfo = url_obj.authinfo()
 
        url_obj.passwd = '*****'
 
        cleaned_uri = str(url_obj)
 

	
 
        if authinfo:
 
            # create a password manager
 
            passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
 
            passmgr.add_password(*authinfo)
 

	
 
            handlers.extend((httpbasicauthhandler(passmgr),
 
                             httpdigestauthhandler(passmgr)))
 

	
 
        o = urllib2.build_opener(*handlers)
 
        o.addheaders = [('Content-Type', 'application/mercurial-0.1'),
 
                        ('Accept', 'application/mercurial-0.1')]
 

	
 
        q = {"cmd": 'between'}
 
        q.update({'pairs': "%s-%s" % ('0' * 40, '0' * 40)})
 
        qs = '?%s' % urllib.urlencode(q)
 
        cu = "%s%s" % (test_uri, qs)
 
        req = urllib2.Request(cu, None, {})
 

	
 
        try:
 
            resp = o.open(req)
 
            if resp.code != 200:
 
                raise Exception('Return Code is not 200')
 
        except Exception as e:
 
            # means it cannot be cloned
 
            raise urllib2.URLError("[%s] org_exc: %s" % (cleaned_uri, e))
 

	
 
        if not url_prefix: # skip svn+http://... (and git+... too)
 
            # now check if it's a proper hg repo
 
            try:
 
                httppeer(repoui or ui.ui(), url).lookup('tip')
 
            except Exception as e:
 
                raise urllib2.URLError(
 
                    "url [%s] does not look like an hg repo org_exc: %s"
 
                    % (cleaned_uri, e))
 

	
 
        return True
 

	
 
    def _get_repo(self, create, src_url=None, update_after_clone=False):
 
        """
 
        Function will check for mercurial repository in given path and return
 
        a localrepo object. If there is no repository in that path it will
 
        raise an exception unless ``create`` parameter is set to True - in
 
        that case repository would be created and returned.
 
        If ``src_url`` is given, would try to clone repository from the
 
        location at given clone_point. Additionally it'll make update to
 
        working copy accordingly to ``update_after_clone`` flag
 
        """
 

	
 
        try:
 
            if src_url:
 
                url = safe_str(self._get_url(src_url))
 
                opts = {}
 
                if not update_after_clone:
 
                    opts.update({'noupdate': True})
 
                MercurialRepository._check_url(url, self.baseui)
 
                clone(self.baseui, url, self.path, **opts)
 

	
 
                # Don't try to create if we've already cloned repo
 
                create = False
 
            return localrepository(self.baseui, self.path, create=create)
 
        except (Abort, RepoError) as err:
 
            if create:
 
                msg = "Cannot create repository at %s. Original error was %s" \
 
                    % (self.path, err)
 
            else:
 
                msg = "Not valid repository at %s. Original error was %s" \
 
                    % (self.path, err)
 
            raise RepositoryError(msg)
 

	
 
    @LazyProperty
 
    def in_memory_changeset(self):
 
        return MercurialInMemoryChangeset(self)
 

	
 
    @LazyProperty
 
    def description(self):
 
        undefined_description = u'unknown'
 
        _desc = self._repo.ui.config('web', 'description', None, untrusted=True)
 
        return safe_unicode(_desc or undefined_description)
 

	
 
    @LazyProperty
 
    def contact(self):
 
        undefined_contact = u'Unknown'
 
        return safe_unicode(get_contact(self._repo.ui.config)
 
                            or undefined_contact)
 

	
 
    @LazyProperty
 
    def last_change(self):
 
        """
 
        Returns last change made on this repository as datetime object
 
        """
 
        return date_fromtimestamp(self._get_mtime(), makedate()[1])
 

	
 
    def _get_mtime(self):
 
        try:
 
            return time.mktime(self.get_changeset().date.timetuple())
 
        except RepositoryError:
 
            # fallback to filesystem
 
            cl_path = os.path.join(self.path, '.hg', "00changelog.i")
 
            st_path = os.path.join(self.path, '.hg', "store")
 
            if os.path.exists(cl_path):
 
                return os.stat(cl_path).st_mtime
 
            else:
 
                return os.stat(st_path).st_mtime
 

	
 
    def _get_revision(self, revision):
 
        """
 
        Gets an ID revision given as str. This will always return a fill
 
        40 char revision number
 

	
 
        :param revision: str or int or None
 
        """
 
        if isinstance(revision, unicode):
 
            revision = safe_str(revision)
 

	
 
        if self._empty:
 
            raise EmptyRepositoryError("There are no changesets yet")
 

	
 
        if revision in [-1, 'tip', None]:
 
            revision = 'tip'
 

	
 
        try:
 
            revision = hex(self._repo.lookup(revision))
 
        except (IndexError, ValueError, RepoLookupError, TypeError):
 
            msg = ("Revision %s does not exist for %s" % (revision, self))
 
            raise ChangesetDoesNotExistError(msg)
 
        except (LookupError, ):
 
            msg = ("Ambiguous identifier `%s` for %s" % (revision, self))
 
            raise ChangesetDoesNotExistError(msg)
 

	
 
        return revision
 

	
 
    def get_ref_revision(self, ref_type, ref_name):
 
        """
 
        Returns revision number for the given reference.
 
        """
 
        ref_name = safe_str(ref_name)
 
        if ref_type == 'rev' and not ref_name.strip('0'):
 
            return self.EMPTY_CHANGESET
 
        # lookup up the exact node id
 
        _revset_predicates = {
 
                'branch': 'branch',
 
                'book': 'bookmark',
 
                'tag': 'tag',
 
                'rev': 'id',
 
            }
 
        # avoid expensive branch(x) iteration over whole repo
 
        rev_spec = "%%s & %s(%%s)" % _revset_predicates[ref_type]
 
        try:
 
            revs = self._repo.revs(rev_spec, ref_name, ref_name)
 
        except LookupError:
 
            msg = ("Ambiguous identifier %s:%s for %s" % (ref_type, ref_name, self.name))
 
            raise ChangesetDoesNotExistError(msg)
 
        except RepoLookupError:
 
            msg = ("Revision %s:%s does not exist for %s" % (ref_type, ref_name, self.name))
 
            raise ChangesetDoesNotExistError(msg)
 
        if revs:
 
            try:
 
                revision = revs.last()
 
            except AttributeError:
 
                # removed in hg 3.2
 
                revision = revs[-1]
 
        else:
 
            # TODO: just report 'not found'?
 
            revision = ref_name
 

	
 
        return self._get_revision(revision)
 

	
 
    def _get_archives(self, archive_name='tip'):
 
        allowed = self.baseui.configlist("web", "allow_archive",
 
                                         untrusted=True)
 
        for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
 
            if i[0] in allowed or self._repo.ui.configbool("web",
 
                                                           "allow" + i[0],
 
                                                           untrusted=True):
 
                yield {"type": i[0], "extension": i[1], "node": archive_name}
 

	
 
    def _get_url(self, url):
 
        """
 
        Returns normalized url. If schema is not given, would fall
 
        to filesystem
 
        (``file:///``) schema.
 
        """
 
        url = safe_str(url)
 
        if url != 'default' and '://' not in url:
 
            url = "file:" + urllib.pathname2url(url)
 
        return url
 

	
 
    def get_hook_location(self):
 
        """
 
        returns absolute path to location where hooks are stored
 
        """
 
        return os.path.join(self.path, '.hg', '.hgrc')
 

	
 
    def get_changeset(self, revision=None):
 
        """
 
        Returns ``MercurialChangeset`` object representing repository's
 
        changeset at the given ``revision``.
 
        """
 
        revision = self._get_revision(revision)
 
        changeset = MercurialChangeset(repository=self, revision=revision)
 
        return changeset
 

	
 
    def get_changesets(self, start=None, end=None, start_date=None,
 
                       end_date=None, branch_name=None, reverse=False, max_revisions=None):
 
        """
 
        Returns iterator of ``MercurialChangeset`` objects from start to end
 
        (both are inclusive)
 

	
 
        :param start: None, str, int or mercurial lookup format
 
        :param end:  None, str, int or mercurial lookup format
 
        :param start_date:
 
        :param end_date:
 
        :param branch_name:
 
        :param reversed: return changesets in reversed order
 
        """
 

	
 
        start_raw_id = self._get_revision(start)
 
        start_pos = self.revisions.index(start_raw_id) if start else None
 
        end_raw_id = self._get_revision(end)
 
        end_pos = self.revisions.index(end_raw_id) if end else None
 

	
 
        if None not in [start, end] and start_pos > end_pos:
 
            raise RepositoryError("Start revision '%s' cannot be "
 
                                  "after end revision '%s'" % (start, end))
 

	
 
        if branch_name and branch_name not in self.allbranches.keys():
 
            msg = ("Branch %s not found in %s" % (branch_name, self))
 
            raise BranchDoesNotExistError(msg)
 
        if end_pos is not None:
 
            end_pos += 1
 
        # filter branches
 
        filter_ = []
 
        if branch_name:
 
            filter_.append('branch("%s")' % (branch_name))
 
        if start_date:
 
            filter_.append('date(">%s")' % start_date)
 
        if end_date:
 
            filter_.append('date("<%s")' % end_date)
 
        if filter_ or max_revisions:
 
            if filter_:
 
                revspec = ' and '.join(filter_)
 
            else:
 
                revspec = 'all()'
 
            if max_revisions:
 
                revspec = 'limit(%s, %s)' % (revspec, max_revisions)
 
            revisions = scmutil.revrange(self._repo, [revspec])
 
        else:
 
            revisions = self.revisions
 

	
 
        # this is very much a hack to turn this into a list; a better solution
 
        # would be to get rid of this function entirely and use revsets
 
        revs = list(revisions)[start_pos:end_pos]
 
        if reverse:
 
            revs = reversed(revs)
 

	
 
        return CollectionGenerator(self, revs)
 

	
 
    def pull(self, url):
 
        """
 
        Tries to pull changes from external location.
 
        """
 
        url = self._get_url(url)
 
        other = peer(self._repo, {}, url)
 
        try:
 
            # hg 3.2 moved push / pull to exchange module
 
            from mercurial import exchange
 
            exchange.pull(self._repo, other, heads=None, force=None)
 
        except ImportError:
 
            self._repo.pull(other, heads=None, force=None)
 
        except Abort as err:
 
            # Propagate error but with vcs's type
 
            raise RepositoryError(str(err))
 

	
 
    @LazyProperty
 
    def workdir(self):
 
        """
 
        Returns ``Workdir`` instance for this repository.
 
        """
 
        return MercurialWorkdir(self)
 

	
 
    def get_config_value(self, section, name=None, config_file=None):
 
        """
 
        Returns configuration value for a given [``section``] and ``name``.
 

	
 
        :param section: Section we want to retrieve value from
 
        :param name: Name of configuration we want to retrieve
 
        :param config_file: A path to file which should be used to retrieve
 
          configuration from (might also be a list of file paths)
 
        """
 
        if config_file is None:
 
            config_file = []
 
        elif isinstance(config_file, basestring):
 
            config_file = [config_file]
 

	
 
        config = self._repo.ui
 
        for path in config_file:
 
            config.readconfig(path)
 
        return config.config(section, name)
 

	
 
    def get_user_name(self, config_file=None):
 
        """
 
        Returns user's name from global configuration file.
 

	
 
        :param config_file: A path to file which should be used to retrieve
 
          configuration from (might also be a list of file paths)
 
        """
 
        username = self.get_config_value('ui', 'username')
 
        if username:
 
            return author_name(username)
 
        return None
 

	
 
    def get_user_email(self, config_file=None):
 
        """
 
        Returns user's email from global configuration file.
 

	
 
        :param config_file: A path to file which should be used to retrieve
 
          configuration from (might also be a list of file paths)
 
        """
 
        username = self.get_config_value('ui', 'username')
 
        if username:
 
            return author_email(username)
 
        return None
kallithea/public/less/style.less
Show inline comments
 
body {
 
  background: url("../images/background.png") repeat scroll 0 0 #B0B0B0;
 
}
 

	
 
/* pseude content that should not be selected or copied by the user */
 
[data-pseudo-content]:before {
 
  content: attr(data-pseudo-content);
 
}
 

	
 
/* class for texts where newlines should be preserved, for very light-weight ascii art markup (like pull request descriptions) */
 
.formatted-fixed {
 
  white-space: pre-wrap;
 
}
 

	
 
/* use monospace for changeset hashes */
 
.changeset_hash {
 
  font-family: @font-family-monospace;
 
}
 

	
 
/* Note: class 'icon-empty' or 'icon-gravatar' can be used to get icon-ish styling without an actual glyph */
 
i[class^='icon-empty'],
 
i[class^='icon-gravatar'] {
 
  background-repeat: no-repeat;
 
  background-position: center;
 
  display: inline-block;
 
  min-width: 16px;
 
  min-height: 16px;
 
  margin: -2px 0 -4px 0;
 
}
 

	
 
.inline-comments-general.show-general-status .hidden.general-only {
 
  display: block !important;
 
}
 
.truncate {
 
  white-space: nowrap;
 
  overflow: hidden;
 
  text-overflow: ellipsis;
 
  -o-text-overflow: ellipsis;
 
  -ms-text-overflow: ellipsis;
 
}
 
.truncate.autoexpand:hover {
 
  overflow: visible;
 
}
 

	
 
/* show comment anchors when hovering over panel-heading */
 
a.permalink {
 
  visibility: hidden;
 
}
 
.panel-heading:hover .permalink {
 
  visibility: visible;
 
}
 

	
 
.navbar-inverse {
 
  border: none;
 
}
 

	
 
/* logo */
 
nav.navbar .navbar-brand {
 
  font-size: 20px;
 
  padding-top: 12px;
 
}
 
nav.navbar.mainmenu .navbar-brand .branding {
 
  &:before {
 
    content: "";
 
    display: inline-block;
 
    margin-right: .2em;
 
    background-image: url(@kallithea-logo-url);
 
    width: @kallithea-logo-width;
 
    height: @kallithea-logo-height;
 
    margin-bottom: -@kallithea-logo-bottom;
 
    margin-top: -12px;
 
  }
 
}
 

	
 
/* code highlighting */
 
/* don't use bootstrap style for code blocks */
 
.code-highlighttable pre {
 
  background: inherit;
 
  border: 0;
 
}
 

	
 
/* every direct child of a panel, that is not .panel-heading, should auto
 
 * overflow to prevent overflowing of elements like text boxes and tables */
 
.panel > :not(.panel-heading){
 
  overflow-x: auto;
 
  min-height: 0.01%;
 
}
 

	
 
/* search highlighting */
 
div.search-code-body pre .match {
 
  background-color: @highlight-color;
 
}
 
div.search-code-body pre .break {
 
  background-color: @highlight-line-color;
 
  width: 100%;
 
  display: block;
 
}
 

	
 
/* use @alert-danger-text for form error messages and .alert-danger for the input element */
 
.form-group .error-message {
 
  color: @alert-danger-text;
 
  display: inline-block;
 
  padding-top: 5px;
 
  &:empty{
 
    display: none;
 
  }
 
}
 
input.error {
 
  .alert-danger;
 
}
 

	
 
/* datatable */
 
.dataTables_left {
 
  .pull-left;
 
}
 
.dataTables_right {
 
  .pull-right;
 
}
 

	
 
/* make all datatable paginations small */
 
.dataTables_paginate .pagination {
 
  .pagination-sm;
 
}
 

	
 
/* show column sort icons in our font ... and before column header */
 
table.dataTable {
 
  .sorting_asc:before {
 
    font-family: "kallithea";
 
    content: "\23f6";
 
    padding-right: 8px;
 
  }
 
  .sorting_desc:before {
 
    font-family: "kallithea";
 
    content: "\23f7";
 
    padding-right: 8px;
 
  }
 
  .sorting:before {
 
    font-family: "kallithea";
 
    content: "\2195";
 
    padding-right: 8px;
 
    opacity: 0.5;
 
  }
 
  .sorting_asc:after,
 
  .sorting_desc:after,
 
  .sorting:after {
 
    content: "" !important;
 
  }
 
}
 

	
 
/* language bars (summary page) */
 
#lang_stats {
 
  .progress-bar {
 
    min-width: 15px;
 
    border-top-right-radius: 8px;
 
    border-bottom-right-radius: 8px;
 
  }
 
  td {
 
    padding: 1px 0 !important;
 
  }
 
}
 

	
 
/* use pointer cursor for expand_commit */
 
.expand_commit .icon-align-left {
 
  cursor: pointer;
 
  color: #999;
 
}
 

	
 
/* don't break author, date and comment cells into multiple lines in changeset table */
 
table.changesets {
 
  .author,
 
  .date,
 
  .comments {
 
    white-space: nowrap;
 
  }
 
}
 

	
 
/* textareas should be at least 100px high and 400px wide */
 
textarea.form-control {
 
  min-height: 100px;
 
  min-width: 400px;
 
}
 

	
 
/* add some space between the code-browser icons and the file names */
 
.browser-dir > i[class^='icon-'],
 
.submodule-dir > i[class^='icon-'],
 
.browser-file > i[class^='icon-'] {
 
  padding-right: 0.3em;
 
}
 

	
 
.form-group > label {
 
  float: left;
 
}
 
.dt_repo_pending {
 
  opacity: 0.5;
 
}
 

	
 
div.panel-primary {
 
  border: none;
 
}
 

	
 
/* no extra vertical margin */
 
#content div.panel ul.pagination {
 
  margin: 0;
 
}
 

	
 
/* remove margin below footer */
 
.navbar.footer {
 
  margin-bottom: 0;
 
}
 

	
 
.user-menu {
 
  padding: 0 !important;
 
}
 
#quick_login {
 
  width: 360px;
 
  margin-top: 15px;
 
  min-height: 110px;
 
}
 
#quick_login input#username,
 
#quick_login input#password {
 
  display: block;
 
  margin: 5px 0 10px;
 
}
 
#quick_login .password_forgotten a,
 
#quick_login .register a {
 
  padding: 0 !important;
 
  line-height: 25px !important;
 
  float: left;
 
  clear: both;
 
}
 
#quick_login .submit {
 
  float: right;
 
}
 
#quick_login .submit input#sign_in {
 
  margin-top: 5px;
 
}
 
#quick_login > .pull-left {
 
  width: 170px;
 
}
 
#quick_login > .pull-right {
 
  width: 140px;
 
}
 
#quick_login .full_name {
 
  font-weight: bold;
 
  padding: 3px;
 
}
 
#quick_login .email {
 
  padding: 3px 3px 3px 0;
 
}
 
#quick_login :not(input) {
 
  color: @kallithea-theme-inverse-color;
 
  padding-bottom: 3px;
 
}
 
#register div.form div.form-group > label {
 
  width: 135px;
 
  float: left;
 
  text-align: right;
 
  margin: 2px 10px 0 0;
 
  padding: 5px 0 0 5px;
 
}
 
#register div.form div.form-group > div input {
 
  width: 300px;
 
}
 
#register div.form div.buttons {
 
  border-top: 1px solid #DDD;
 
  margin: 0;
 
  padding: 10px 0 0 145px;
 
}
 
#journal .journal_user {
 
  color: #747474;
 
  font-size: 14px;
 
  font-weight: bold;
 
  height: 30px;
 
}
 
#journal .journal_user.deleted {
 
  color: #747474;
 
  font-size: 14px;
 
  font-weight: normal;
 
  height: 30px;
 
  font-style: italic;
 
}
 
#journal .journal_icon {
 
  clear: both;
 
  float: left;
 
  padding-right: 4px;
 
  padding-top: 3px;
 
}
 
#journal .journal_action {
 
  padding-top: 4px;
 
  min-height: 2px;
 
  float: left;
 
}
 
#journal .journal_action_params {
 
  clear: left;
 
  padding-left: 22px;
 
}
 
#journal .date {
 
  clear: both;
 
  color: #777777;
 
  font-size: 11px;
 
  padding-left: 22px;
 
}
 
#journal .journal_repo_name {
 
  font-weight: bold;
 
  font-size: 1.1em;
 
}
 
#journal .compare_view {
 
  padding: 5px 0px 5px 0px;
 
  width: 95px;
 
}
 
.trending_language_tbl,
 
.trending_language_tbl td {
 
  border: 0 !important;
 
  margin: 0 !important;
 
  padding: 0 !important;
 
}
 
.trending_language_tbl,
 
.trending_language_tbl tr {
 
  border-spacing: 1px;
 
}
 
h3.files_location {
 
  font-size: 1.8em;
 
  font-weight: 700;
 
  border-bottom: none !important;
 
  margin: 10px 0 !important;
 
}
 
.file_history {
 
  padding-top: 10px;
 
  font-size: 16px;
 
}
 
.file_author {
 
  float: left;
 
}
 
.file_author .item {
 
  float: left;
 
  padding: 5px;
 
  color: #888;
 
}
 
#graph_nodes {
 
  position: absolute;
 
  width: 100px;
 
}
 
#graph_content,
 
#graph_content_pr,
 
#graph .container_header {
 
  margin-left: 100px;
 
}
 
#graph_content {
 
  position: relative;
 
}
 
table#updaterevs-table tr.mergerow,
 
table#updaterevs-table tr.out-of-range,
 
table#changesets tr.mergerow,
 
table#changesets tr.out-of-range {
 
  opacity: 0.6;
 
}
 
#updaterevs-graph {
 
  position: relative;
 
  width: 40px;
 
  height: 0;
 
}
 
#updaterevs-table {
 
  margin-left: 40px !important;
 
}
 
.issue-tracker-link {
 
  color: #3F6F9F;
 
  font-weight: bold !important;
 
}
 
/* changeset statuses (must be the same name as the status) */
 
.changeset-status-not_reviewed {
 
  color: #bababa;
 
}
 
.changeset-status-approved {
 
  color: #81ba51;
 
}
 
.changeset-status-rejected {
 
  color: #d06060;
 
}
 
.changeset-status-under_review {
 
  color: #ffc71e;
 
}
 

	
 
#repo_size {
 
  display: block;
 
  margin-top: 4px;
 
  color: #666;
 
  float: right;
 
}
 
.currently_following {
 
  padding-left: 10px;
 
  padding-bottom: 5px;
 
}
 
#switch_repos {
 
  position: absolute;
 
  height: 25px;
 
  z-index: 1;
 
}
 
#switch_repos select {
 
  min-width: 150px;
 
  max-height: 250px;
 
  z-index: 1;
 
}
 
.breadcrumbs {
 
  border: medium none;
 
  color: #FFF;
 
  font-weight: 700;
 
  font-size: 14px;
 
}
 
table#permissions_manage span.private_repo_msg {
 
  font-size: 0.8em;
 
  opacity: 0.6;
 
}
 
table#permissions_manage td.private_repo_msg {
 
  font-size: 0.8em;
 
}
 
table#permissions_manage tr#add_perm_input td {
 
  vertical-align: middle;
 
}
 
div.gravatar {
 
  float: left;
 
  background-color: #FFF;
 
  margin-right: 0.7em;
 
  padding: 1px 1px 1px 1px;
 
  line-height: 0;
 
  border-radius: 3px;
 
}
 
div.gravatar img {
 
  border-radius: 2px;
 
}
 
.panel-body.settings > ul.nav-stacked {
 
  float: left;
 
  width: 150px;
 
  padding-right: 35px;
 
  color: #393939;
 
  font-weight: 700;
 
}
 
.panel-body.settings .nav-pills > :not(.active) > a {
 
  color: inherit;
 
}
 
.panel-body.settings > div,
 
.panel-body.settings > form {
 
  float: left;
 
  width: 750px;
 
  margin: 0;
 
}
 
.panel-body.no-padding {
 
  padding: 0;
 
}
 
.panel-body ~ .panel-body {
 
  padding-top: 0;
 
}
 
.panel-body.no-padding ~ .panel-body {
 
  padding-top: 15px;
 
}
 
.panel-body > :last-child {
 
  margin-bottom: 0;
 
}
 
.panel-body.settings .text-muted {
 
  margin: 5px 0;
 
}
 
ins,
 
div.options a:hover {
 
  text-decoration: none;
 
}
 
img,
 
nav.navbar #quick li a:hover span.normal,
 
#clone_url,
 
#clone_url_id {
 
  border: none;
 
}
 
img.icon,
 
.right .merge img {
 
  vertical-align: bottom;
 
}
 
#content div.panel div.panel-heading ul.links,
 
#content div.panel div.message div.dismiss {
 
  float: right;
 
  margin: 0;
 
  padding: 0;
 
}
 
nav.navbar #home,
 
#content div.panel ul.left,
 
#content div.panel ol.left,
 
div#commit_history,
 
div#legend_data,
 
div#legend_container,
 
div#legend_choices {
 
  float: left;
 
}
 

	
 
/* set size for statistics charts */
 
#commit_history {
 
  width: 450px;
 
  height: 300px;
 
}
 
#overview {
 
  clear: both;
 
  width: 450px;
 
  height: 100px;
 
}
 

	
 
#content #left #menu ul.closed,
 
#content #left #menu li ul.collapsed,
 
.yui-tt-shadow {
 
  display: none;
 
}
 
#content #left #menu ul.opened,
 
#content #left #menu li ul.expanded {
 
  display: block !important;
 
}
 

	
 
#content div.panel ol.lower-roman,
 
#content div.panel ol.upper-roman,
 
#content div.panel ol.lower-alpha,
 
#content div.panel ol.upper-alpha,
 
#content div.panel ol.decimal {
 
  margin: 10px 24px 10px 44px;
 
}
 
#content div.panel div.form div.form-group > div {
 
  margin: 0 0 10px 200px;
 
}
 
div.form div.form-group div.button input,
 
#content div.panel div.form div.buttons input,
 
div.form div.buttons input,
 
#content div.panel div.action div.button input {
 
  font-size: 11px;
 
  font-weight: 700;
 
  margin: 0;
 
}
 
div.form div.form-group div.highlight,
 
#content div.panel div.form div.buttons div.highlight {
 
  display: inline;
 
}
 
#content div.panel div.form div.buttons,
 
div.form div.buttons {
 
  margin: 10px 10px 0 200px;
 
  padding: 0;
 
}
 
#content div.panel table td.user,
 
#content div.panel table td.address {
 
  width: 10%;
 
  text-align: center;
 
}
 
#content div.panel div.action div.button {
 
  text-align: right;
 
  margin: 6px 0 0;
 
  padding: 0;
 
}
 
#login,
 
#register {
 
  width: 520px;
 
  margin: 10% auto 0;
 
  padding: 0;
 
}
 
.ac .match {
 
  font-weight: 700;
 
  padding-top: 5px;
 
  padding-bottom: 5px;
 
}
 
.q_filter_box {
 
  border-radius: 4px;
 
  border: 0 none;
 
  margin-bottom: -4px;
 
  margin-top: -4px;
 
  padding-left: 3px;
 
}
 
#node_filter {
 
  border: 0px solid #545454;
 
  color: #AAAAAA;
 
  padding-left: 3px;
 
}
 
/** comment main **/
 
.comment .panel,
 
.comment-inline-form {
 
  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;
 
}
 
.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-inlines textarea {
 
  font-family: @font-family-monospace;
 
}
 
/** comment inline **/
 
.inline-comments {
 
  padding: 5px;
 
}
 
.inline-comments .comments-number {
 
  padding: 0px 0px 10px 0px;
 
}
 
input.status_change_checkbox,
 
input.status_change_radio {
 
  margin: 0 0 5px 15px;
 
}
 
@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
 
*****/
 
div.pr-details-title.closed {
 
  color: #555;
 
  background: #eee;
 
}
 
div.pr {
 
  margin: 0px 15px;
 
  padding: 4px 4px;
 
}
 
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: @kallithea-theme-main-color;
 
  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 > .pr-not-edit {
 
  min-height: 50px !important;
 
}
 
#pr-edit-btn {
 
  margin: 20px 0 0 !important;
 
  position: absolute;
 
}
 
/* make 'next iteration' changeset table smaller and scrollable */
 
#pr-summary #updaterevs {
 
  max-height: 200px;
 
  overflow-y: auto;
 
  overflow-x: hidden;
 
}
 

	
 
/****
 
  PERMS
 
*****/
 
.perm-gravatar-ac {
 
  vertical-align: middle;
 
  padding: 2px;
 
  width: 14px;
 
  height: 14px;
 
}
 

	
 
/* avoid gaps between the navbar and browser */
 
.navbar.mainmenu {
 
  border-top-left-radius: 0;
 
  border-top-right-radius: 0;
 
}
 
.navbar.footer {
 
  border-bottom-left-radius: 0;
 
  border-bottom-right-radius: 0;
 
}
 

	
 
/* 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;
 
}
 

	
 
#content div.panel .changelog-panel > .changelog-heading,
 
#content div.panel .changelog-panel > ul.pagination {
 
  margin-left: 100px;
 
}
 

	
 
/* repo table icons */
 
#repos_list_wrap_wrapper {
 
  /* make icon-folder and repotag the same width */
 
  .icon-folder:before {
 
    margin: 0; // default margin would otherwise add to the total width 
 
    margin: 0; // default margin would otherwise add to the total width
 
    width: 24px;
 
    text-align: left;
 
  }
 
  .label-repo {
 
    display: inline-block;
 
    width: 24px;
 
  }
 
}
 

	
 
/* changelog table columns */
 
.table#changesets {
 
  table-layout: fixed;
 
  td {
 
    overflow: hidden;
 
    text-overflow: ellipsis;
 
    white-space: nowrap;
 
  }
 
  .checkbox-column {
 
    width: 24px;
 
    /* the optional second checkbox will be inline-block but should wrap to a new line */
 
    white-space: normal;
 
  }
 
  .changeset-logical-index {
 
    color: @gray-light;
 
    font-style: italic;
 
    font-size: 85%;
 
    text-align: right;
 
    overflow: visible;
 
  }
 
  .changeset-logical-index,
 
  .expand_commit,
 
  .status {
 
    width: 28px;
 
  }
 
  .author {
 
    width: 200px;
 
  }
 
  .hash {
 
    .small;
 
    width: 110px;
 
  }
 
  .date {
 
    .small;
 
    width: 100px;
 
  }
 
  .mid > .log-container {
 
    position: relative;
 
    > .extra-container {
 
      position: absolute;
 
      top: 0;
 
      right: 0;
 
    }
 
  }
 
}
 

	
 
/* undo Bootstrap chrome/webkit blue outline on focus in navbar */
 
.navbar-inverse .navbar-nav > li > a:focus {
 
  outline: 0;
 
}
 

	
 
/* use same badge coloring in navbar inverse as in panel-heading */
 
.navbar-inverse {
 
  .badge {
 
    color: @navbar-inverse-bg;
 
    background-color: @navbar-inverse-color;
 
  }
 
}
 

	
 
/* pygments style */
 
div.search-code-body pre .match {
 
  background-color: @highlight-color;
 
}
 
div.search-code-body pre .break {
 
  background-color: @highlight-line-color;
 
  width: 100%;
 
  color: #747474;
 
  display: block;
 
}
 
div.annotatediv {
 
  margin-left: 2px;
 
  margin-right: 4px;
 
}
 
.code-highlight {
 
  border-left: 1px solid #ccc;
 
}
 
.code-highlight pre,
 
.linenodiv pre {
 
  padding: 5px 2px 0px 5px;
 
  margin: 0;
 
}
 
.code-highlight pre div:target {
 
  background-color: #FFFFBE !important;
 
}
 
.linenos a { text-decoration: none; }
 

	
 
/* Stylesheets for the context bar */
 
#quick_login > .pull-right .list-group-item {
 
  background-color: @kallithea-theme-main-color;
 
  border: 0;
 
}
 
#content #context-pages .follow .show-following,
 
#content #context-pages .following .show-follow {
 
  display: none;
 
}
 

	
 
nav.navbar #quick > li > a,
 
#context-pages > ul > li > a {
 
  height: @navbar-height;
 
}
kallithea/tests/vcs/test_repository.py
Show inline comments
 
import datetime
 

	
 
import pytest
 

	
 
from kallithea.lib.vcs.nodes import FileNode
 
from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError
 

	
 
from kallithea.tests.vcs.base import _BackendTestMixin
 
from kallithea.tests.vcs.conf import SCM_TESTS
 
from kallithea.tests.vcs import TEST_USER_CONFIG_FILE
 

	
 

	
 
class RepositoryBaseTest(_BackendTestMixin):
 
    recreate_repo_per_test = False
 

	
 
    @classmethod
 
    def _get_commits(cls):
 
        return super(RepositoryBaseTest, cls)._get_commits()[:1]
 

	
 
    def test_get_config_value(self):
 
        assert self.repo.get_config_value('universal', 'foo', TEST_USER_CONFIG_FILE) == 'bar'
 

	
 
    def test_get_config_value_defaults_to_None(self):
 
        assert self.repo.get_config_value('universal', 'nonexist', TEST_USER_CONFIG_FILE) == None
 

	
 
    def test_get_user_name(self):
 
        assert self.repo.get_user_name(TEST_USER_CONFIG_FILE) == 'Foo Bar'
 

	
 
    def test_get_user_email(self):
 
        assert self.repo.get_user_email(TEST_USER_CONFIG_FILE) == 'foo.bar@example.com'
 

	
 
    def test_repo_equality(self):
 
        assert self.repo == self.repo
 

	
 
    def test_repo_equality_broken_object(self):
 
        import copy
 
        _repo = copy.copy(self.repo)
 
        delattr(_repo, 'path')
 
        assert self.repo != _repo
 

	
 
    def test_repo_equality_other_object(self):
 
        class dummy(object):
 
            path = self.repo.path
 
        assert self.repo != dummy()
 

	
 

	
 
class RepositoryGetDiffTest(_BackendTestMixin):
 

	
 
    @classmethod
 
    def _get_commits(cls):
 
        commits = [
 
            {
 
                'message': 'Initial commit',
 
                'author': 'Joe Doe <joe.doe@example.com>',
 
                'date': datetime.datetime(2010, 1, 1, 20),
 
                'added': [
 
                    FileNode('foobar', content='foobar'),
 
                    FileNode('foobar2', content='foobar2'),
 
                ],
 
            },
 
            {
 
                'message': 'Changed foobar, added foobar3',
 
                'author': 'Jane Doe <jane.doe@example.com>',
 
                'date': datetime.datetime(2010, 1, 1, 21),
 
                'added': [
 
                    FileNode('foobar3', content='foobar3'),
 
                ],
 
                'changed': [
 
                    FileNode('foobar', 'FOOBAR'),
 
                ],
 
            },
 
            {
 
                'message': 'Removed foobar, changed foobar3',
 
                'author': 'Jane Doe <jane.doe@example.com>',
 
                'date': datetime.datetime(2010, 1, 1, 22),
 
                'changed': [
 
                    FileNode('foobar3', content='FOOBAR\nFOOBAR\nFOOBAR\n'),
 
                ],
 
                'removed': [FileNode('foobar')],
 
            },
 
            {
 
                'message': u'Commit that contains glob pattern in filename',
 
                'author': 'Jane Doe <jane.doe@example.com>',
 
                'date': datetime.datetime(2010, 1, 1, 22),
 
                'added': [
 
                    FileNode('README{', content='Strangely-named README file'),
 
                ],
 
            },
 
        ]
 
        return commits
 

	
 
    def test_raise_for_wrong(self):
 
        with pytest.raises(ChangesetDoesNotExistError):
 
            self.repo.get_diff('a' * 40, 'b' * 40)
 

	
 
    def test_glob_patterns_in_filename_do_not_raise_exception(self):
 
        revs = self.repo.revisions
 

	
 
        diff = self.repo.get_diff(revs[2], revs[3], path='README{') # should not raise
 

	
 

	
 
class TestGitRepositoryGetDiff(RepositoryGetDiffTest):
 
    backend_alias = 'git'
 

	
 
    def test_initial_commit_diff(self):
 
        initial_rev = self.repo.revisions[0]
 
        assert self.repo.get_diff(self.repo.EMPTY_CHANGESET, initial_rev) == '''diff --git a/foobar b/foobar
 
new file mode 100644
 
index 0000000000000000000000000000000000000000..f6ea0495187600e7b2288c8ac19c5886383a4632
 
--- /dev/null
 
+++ b/foobar
 
@@ -0,0 +1 @@
 
+foobar
 
\ No newline at end of file
 
diff --git a/foobar2 b/foobar2
 
new file mode 100644
 
index 0000000000000000000000000000000000000000..e8c9d6b98e3dce993a464935e1a53f50b56a3783
 
--- /dev/null
 
+++ b/foobar2
 
@@ -0,0 +1 @@
 
+foobar2
 
\ No newline at end of file
 
'''
 

	
 
    def test_second_changeset_diff(self):
 
        revs = self.repo.revisions
 
        assert self.repo.get_diff(revs[0], revs[1]) == '''diff --git a/foobar b/foobar
 
index f6ea0495187600e7b2288c8ac19c5886383a4632..389865bb681b358c9b102d79abd8d5f941e96551 100644
 
--- a/foobar
 
+++ b/foobar
 
@@ -1 +1 @@
 
-foobar
 
\ No newline at end of file
 
+FOOBAR
 
\ No newline at end of file
 
diff --git a/foobar3 b/foobar3
 
new file mode 100644
 
index 0000000000000000000000000000000000000000..c11c37d41d33fb47741cff93fa5f9d798c1535b0
 
--- /dev/null
 
+++ b/foobar3
 
@@ -0,0 +1 @@
 
+foobar3
 
\ No newline at end of file
 
'''
 

	
 
    def test_third_changeset_diff(self):
 
        revs = self.repo.revisions
 
        assert self.repo.get_diff(revs[1], revs[2]) == '''diff --git a/foobar b/foobar
 
deleted file mode 100644
 
index 389865bb681b358c9b102d79abd8d5f941e96551..0000000000000000000000000000000000000000
 
--- a/foobar
 
+++ /dev/null
 
@@ -1 +0,0 @@
 
-FOOBAR
 
\ No newline at end of file
 
diff --git a/foobar3 b/foobar3
 
index c11c37d41d33fb47741cff93fa5f9d798c1535b0..f9324477362684ff692aaf5b9a81e01b9e9a671c 100644
 
--- a/foobar3
 
+++ b/foobar3
 
@@ -1 +1,3 @@
 
-foobar3
 
\ No newline at end of file
 
+FOOBAR
 
+FOOBAR
 
+FOOBAR
 
'''
 

	
 
    def test_fourth_changeset_diff(self):
 
        revs = self.repo.revisions
 
        assert self.repo.get_diff(revs[2], revs[3]) == '''diff --git a/README{ b/README{
 
new file mode 100644
 
index 0000000000000000000000000000000000000000..cdc0c1b5d234feedb37bbac19cd1b6442061102d
 
--- /dev/null
 
+++ b/README{
 
@@ -0,0 +1 @@
 
+Strangely-named README file
 
\ No newline at end of file
 
'''
 

	
 

	
 
class TestHgRepositoryGetDiff(RepositoryGetDiffTest):
 
    backend_alias = 'hg'
 

	
 
    def test_initial_commit_diff(self):
 
        initial_rev = self.repo.revisions[0]
 
        assert self.repo.get_diff(self.repo.EMPTY_CHANGESET, initial_rev) == '''diff --git a/foobar b/foobar
 
new file mode 100644
 
--- /dev/null
 
+++ b/foobar
 
@@ -0,0 +1,1 @@
 
+foobar
 
\ No newline at end of file
 
diff --git a/foobar2 b/foobar2
 
new file mode 100644
 
--- /dev/null
 
+++ b/foobar2
 
@@ -0,0 +1,1 @@
 
+foobar2
 
\ No newline at end of file
 
'''
 

	
 
    def test_second_changeset_diff(self):
 
        revs = self.repo.revisions
 
        assert self.repo.get_diff(revs[0], revs[1]) == '''diff --git a/foobar b/foobar
 
--- a/foobar
 
+++ b/foobar
 
@@ -1,1 +1,1 @@
 
-foobar
 
\ No newline at end of file
 
+FOOBAR
 
\ No newline at end of file
 
diff --git a/foobar3 b/foobar3
 
new file mode 100644
 
--- /dev/null
 
+++ b/foobar3
 
@@ -0,0 +1,1 @@
 
+foobar3
 
\ No newline at end of file
 
'''
 

	
 
    def test_third_changeset_diff(self):
 
        revs = self.repo.revisions
 
        assert self.repo.get_diff(revs[1], revs[2]) == '''diff --git a/foobar b/foobar
 
deleted file mode 100644
 
--- a/foobar
 
+++ /dev/null
 
@@ -1,1 +0,0 @@
 
-FOOBAR
 
\ No newline at end of file
 
diff --git a/foobar3 b/foobar3
 
--- a/foobar3
 
+++ b/foobar3
 
@@ -1,1 +1,3 @@
 
-foobar3
 
\ No newline at end of file
 
+FOOBAR
 
+FOOBAR
 
+FOOBAR
 
'''
 

	
 
    def test_fourth_changeset_diff(self):
 
        revs = self.repo.revisions
 
        assert self.repo.get_diff(revs[2], revs[3]) == '''diff --git a/README{ b/README{
 
new file mode 100644
 
--- /dev/null
 
+++ b/README{
 
@@ -0,0 +1,1 @@
 
+Strangely-named README file
 
\ No newline at end of file
 
'''
 

	
 

	
 
# For each backend create test case class
 
for alias in SCM_TESTS:
 
    attrs = {
 
        'backend_alias': alias,
 
    }
 
    cls_name = 'Test' + alias.capitalize() + RepositoryBaseTest.__name__
 
    globals()[cls_name] = type(cls_name, (RepositoryBaseTest,), attrs)
0 comments (0 inline, 0 general)