diff rhodecode/lib/vcs/backends/hg/repository.py @ 2007:324ac367a4da beta

Added VCS into rhodecode core for faster and easier deployments of new versions
author Marcin Kuzminski <marcin@python-works.com>
date Mon, 20 Feb 2012 23:00:54 +0200
parents
children ed8c2fc8dd3b
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rhodecode/lib/vcs/backends/hg/repository.py	Mon Feb 20 23:00:54 2012 +0200
@@ -0,0 +1,521 @@
+import os
+import time
+import datetime
+import urllib
+import urllib2
+
+from rhodecode.lib.vcs.backends.base import BaseRepository
+from .workdir import MercurialWorkdir
+from .changeset import MercurialChangeset
+from .inmemory import MercurialInMemoryChangeset
+
+from rhodecode.lib.vcs.exceptions import BranchDoesNotExistError, \
+    ChangesetDoesNotExistError, EmptyRepositoryError, RepositoryError, \
+    VCSError, TagAlreadyExistError, TagDoesNotExistError
+from rhodecode.lib.vcs.utils import author_email, author_name, date_fromtimestamp, \
+    makedate, safe_unicode
+from rhodecode.lib.vcs.utils.lazy import LazyProperty
+from rhodecode.lib.vcs.utils.ordered_dict import OrderedDict
+from rhodecode.lib.vcs.utils.paths import abspath
+
+from ...utils.hgcompat import ui, nullid, match, patch, diffopts, clone, \
+    get_contact, pull, localrepository, RepoLookupError, Abort, RepoError, hex
+
+
+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 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()
+
+    def _get_branches(self, closed=False):
+        """
+        Get's branches for this repository
+        Returns only not closed branches by default
+
+        :param closed: return also closed branches for mercurial
+        """
+
+        if self._empty:
+            return {}
+
+        def _branchtags(localrepo):
+            """
+            Patched version of mercurial branchtags to not return the closed
+            branches
+
+            :param localrepo: locarepository instance
+            """
+
+            bt = {}
+            bt_closed = {}
+            for bn, heads in localrepo.branchmap().iteritems():
+                tip = heads[-1]
+                if 'close' in localrepo.changelog.read(tip)[5]:
+                    bt_closed[bn] = tip
+                else:
+                    bt[bn] = tip
+
+            if closed:
+                bt.update(bt_closed)
+            return bt
+
+        sortkey = lambda ctx: ctx[0]  # sort by name
+        _branches = [(safe_unicode(n), hex(h),) for n, h in
+                     _branchtags(self._repo).items()]
+
+        return OrderedDict(sorted(_branches, key=sortkey, reverse=False))
+
+    @LazyProperty
+    def tags(self):
+        """
+        Get's 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().ctime()
+
+        try:
+            self._repo.tag(name, changeset._ctx.node(), message, local, user,
+                date)
+        except Abort, 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().ctime()
+        local = False
+
+        try:
+            self._repo.tag(name, nullid, message, local, user, date)
+            self.tags = self._get_tags()
+        except Abort, e:
+            raise RepositoryError(e.message)
+
+    @LazyProperty
+    def bookmarks(self):
+        """
+        Get's 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 map(lambda x: hex(x[7]), self._repo.changelog.index)[:-1]
+
+    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``.
+        """
+        # Check if given revisions are present at repository (may raise
+        # ChangesetDoesNotExistError)
+        if rev1 != self.EMPTY_CHANGESET:
+            self.get_changeset(rev1)
+        self.get_changeset(rev2)
+
+        file_filter = match(self.path, '', [path])
+        return ''.join(patch.diff(self._repo, rev1, rev2, match=file_filter,
+                          opts=diffopts(git=True,
+                                        ignorews=ignore_whitespace,
+                                        context=context)))
+
+    def _check_url(self, url):
+        """
+        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, return code 200 if url
+        is valid or True if it's a local path
+        """
+
+        from mercurial.util import url as Url
+
+        # those authnadlers are patched for python 2.6.5 bug an
+        # infinit looping when given invalid resources
+        from mercurial.url import httpbasicauthhandler, httpdigestauthhandler
+
+        # check first if it's not an local url
+        if os.path.isdir(url) or url.startswith('file:'):
+            return True
+
+        handlers = []
+        test_uri, authinfo = Url(url).authinfo()
+
+        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)
+            return resp.code == 200
+        except Exception, e:
+            # means it cannot be cloned
+            raise urllib2.URLError(e)
+
+    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 = str(self._get_url(src_url))
+                opts = {}
+                if not update_after_clone:
+                    opts.update({'noupdate': True})
+                try:
+                    self._check_url(url)
+                    clone(self.baseui, url, self.path, **opts)
+#                except urllib2.URLError:
+#                    raise Abort("Got HTTP 404 error")
+                except Exception:
+                    raise
+                # Don't try to create if we've already cloned repo
+                create = False
+            return localrepository(self.baseui, self.path, create=create)
+        except (Abort, RepoError), 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'
+        return safe_unicode(self._repo.ui.config('web', 'description',
+                                   undefined_description, untrusted=True))
+
+    @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_hidden(self):
+        return self._repo.ui.configbool("web", "hidden", untrusted=True)
+
+    def _get_revision(self, revision):
+        """
+        Get's an ID revision given as str. This will always return a fill
+        40 char revision number
+
+        :param revision: str or int or None
+        """
+
+        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):
+            raise ChangesetDoesNotExistError("Revision %r does not "
+                                    "exist for this repository %s" \
+                                    % (revision, self))
+        return 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 = str(url)
+        if url != 'default' and not '://' in url:
+            url = "file:" + urllib.pathname2url(url)
+        return url
+
+    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):
+        """
+        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.branches.keys():
+            raise BranchDoesNotExistError('Such branch %s does not exists for'
+                                  ' this repository' % branch_name)
+        if end_pos is not None:
+            end_pos += 1
+
+        slice_ = reversed(self.revisions[start_pos:end_pos]) if reverse else \
+            self.revisions[start_pos:end_pos]
+
+        for id_ in slice_:
+            cs = self.get_changeset(id_)
+            if branch_name and cs.branch != branch_name:
+                continue
+            if start_date and cs.date < start_date:
+                continue
+            if end_date and cs.date > end_date:
+                continue
+
+            yield cs
+
+    def pull(self, url):
+        """
+        Tries to pull changes from external location.
+        """
+        url = self._get_url(url)
+        try:
+            pull(self.baseui, self._repo, url)
+        except Abort, 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, 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