Commits

Jannis Leidel committed 9bcda40

Redid the registratio system for the version control support. Added schemes attributes to version control classes. Fixed some minor bugs. Added Bazaar version control class.

Comments (0)

Files changed (1)

 if os.environ.get('PIP_DEFAULT_VCS'):
     default_vcs = os.environ['PIP_DEFAULT_VCS']
 
-# Register more schemes with urlparse for git and hg
-pip_schemes = ['ssh', 'git', 'hg']
-urlparse.uses_netloc.extend(pip_schemes)
-urlparse.uses_fragment.extend(pip_schemes)
 
 try:
     pip_dist = pkg_resources.get_distribution('pip')
     # when running pip.py without installing
     version=None
 
+
+class VcsSupport(object):
+    _registry = {}
+    # Register more schemes with urlparse for git and hg
+    schemes = ['ssh', 'git', 'hg', 'bzr']
+
+    def __init__(self):
+        urlparse.uses_netloc.extend(self.schemes)
+        urlparse.uses_fragment.extend(self.schemes)
+        super(VcsSupport, self).__init__()
+
+    def __iter__(self):
+        return self._registry.__iter__()
+
+    def register(self, cls):
+        if not hasattr(cls, 'name'):
+            logger.warn('Cannot register VCS %s' % cls.__name__)
+            return
+        if cls.name not in self._registry:
+            self._registry[cls.name] = cls
+
+    def unregister(self, cls=None, name=None):
+        if not (cls or name):
+            logger.warn('Cannot unregister because no class or name given')
+            return
+        if name and name in self._registry:
+            del self._registry[name]
+        elif cls and cls in self._registry.value():
+            del self._registry[cls.name]
+
+    def get_backend(self, name):
+        if name in self._registry:
+            return self._registry[name]
+
+    def get_backend_from_location(self, location):
+        for vc_type in self._registry:
+            path = os.path.join(location, '.%s' % vc_type)
+            if os.path.exists(path):
+                return self.get_backend(vc_type)
+        return None
+
+    def backends(self):
+        return self._registry.values()
+
+vcs = VcsSupport()
+
 parser = optparse.OptionParser(
     usage='%prog COMMAND [OPTIONS]',
     version=version)
             return
         vc_type, url = self.url.split('+', 1)
         vc_type = vc_type.lower()
-        if vc_type == 'svn':
-            Subversion(self.url).checkout(self.source_dir)
-        elif vc_type == 'git':
-            Git(self.url).clone(self.source_dir)
-        elif vc_type == 'hg':
-            Mercurial(self.url).clone(self.source_dir)
+        version_control = vcs.get_backend(vc_type)
+        if version_control:
+            version_control(self.url).obtain(self.source_dir)
         else:
             assert 0, (
                 'Unexpected version control type (in %s): %s' 
                 logger.indent -= 2
 
     def unpack_url(self, link, location):
-        if link.scheme in ('svn', 'svn+ssh'):
-            Subversion(link).unpack(location)
-            return
-        if link.scheme in ('git', 'git+http', 'git+ssh'):
-            Git(link).unpack(location)
-            return
-        if link.scheme in ('hg', 'hg+http', 'hg+ssh'):
-            Mercurial(link).unpack(location)
-            return
+        for version_control in vcs.backends():
+            if link.scheme in version_control.schemes:
+                version_control(link).unpack(location)
+                return
         dir = tempfile.mkdtemp()
         if link.url.lower().startswith('file:'):
             source = url_to_filename(link.url)
         elif (content_type.startswith('text/html')
               and is_svn_page(file_contents(filename))):
             # We don't really care about this
-            self.svn_checkout(link.url, location)
+            Subversion(link.url).unpack(location)
         else:
             ## FIXME: handle?
             ## FIXME: magic signatures?
         finally:
             tar.close()
 
-    def _filter_svn(self, line):
-        return (Logger.INFO, line)
-
     def install(self, install_options):
         """Install everything in this set (after having downloaded and unpacked the packages)"""
         requirements = sorted(self.requirements.values(), key=lambda p: p.name.lower())
         url = urlparse.urlunsplit((scheme, netloc, path, query, ''))
         return url, rev
 
-_version_controls = {}
-def register_version_control(cls):
-    _version_controls[cls.name] = cls
+    def _filter(self, line):
+        return (Logger.INFO, line)
+
+    def obtain(self, dest):
+        raise NotImplementedError
+
+    def unpack(self, location):
+        raise NotImplementedError
+
+    def get_src_requirement(self, dist, location, find_tags=False):
+        raise NotImplementedError
 
 _svn_xml_url_re = re.compile('url="([^"]+)"')
 _svn_rev_re = re.compile('committed-rev="(\d+)"')
 
 class Subversion(VersionControl):
     name = 'svn'
+    schemes = ('svn', 'svn+ssh')
     guide = ('# This was an svn checkout; to make it a checkout again run:\n'
             'svn checkout --force -r %(rev)s %(url)s .\n')
 
                 os.rmdir(location)
             call_subprocess(
                 ['svn', 'checkout', url, location],
-                filter_stdout=self._filter_svn, show_stdout=False)
+                filter_stdout=self._filter, show_stdout=False)
         finally:
             logger.indent -= 2
 
-    def checkout(self, dest):
+    def obtain(self, dest):
         url, rev = self.get_url_rev()
         if rev:
             rev_options = ['-r', rev]
                 best_tag = tag
         return best_tag
 
-    def src_requirement(self, dist, location, find_tags=False):
+    def get_src_requirement(self, dist, location, find_tags=False):
         repo = self.get_url(location)
         if repo is None:
             return None
             logger.warn('svn URL does not fit normal structure (tags/branches/trunk): %s' % repo)
             rev = self.get_revision(location)
             return 'svn+%s@%s#egg=%s-dev' % (repo, rev, egg_project_name)
-register_version_control(Subversion)
+
+vcs.register(Subversion)
+
 
 class Git(VersionControl):
     name = 'git'
+    schemes = ('git', 'git+http', 'git+ssh')
     guide = ('# This was a Git repo; to make it a repo again run:\n'
         'git init\ngit remote add origin %(url)s -f\ngit checkout %(rev)s\n')
 
                 os.rmdir(location)
             call_subprocess(
                 [GIT_CMD, 'clone', url, location],
-                filter_stdout=self._filter_svn, show_stdout=False)
+                filter_stdout=self._filter, show_stdout=False)
         finally:
             logger.indent -= 2
 
-    def clone(self, dest):
+    def obtain(self, dest):
         url, rev = self.get_url_rev()
         if rev:
             rev_options = [rev]
         branch_revs = dict(branch_revs)
         return branch_revs
 
-    def src_requirement(self, dist, location, find_tags):
+    def get_src_requirement(self, dist, location, find_tags):
         repo = self.get_url(location)
         if not repo.lower().startswith('git:'):
             repo = 'git+' + repo
             # Don't know what it is
             logger.warn('Git URL does not fit normal structure: %s' % repo)
             return '%s@%s#egg=%s-dev' % (repo, current_rev, egg_project_name)
-register_version_control(Git)
+
+vcs.register(Git)
+
 
 class Mercurial(VersionControl):
     name = 'hg'
+    schemes = ('hg', 'hg+http', 'hg+ssh')
     guide = ('# This was a Mercurial repo; to make it a repo again run:\n'
             'hg init\nhg pull %(url)s\nhg update -r %(rev)s\n')
 
                 os.rmdir(location)
             call_subprocess(
                 ['hg', 'clone', url, location],
-                filter_stdout=self._filter_svn, show_stdout=False)
+                filter_stdout=self._filter, show_stdout=False)
         finally:
             logger.indent -= 2
 
-    def clone(self, dest):
+    def obtain(self, dest):
         url, rev = self.get_url_rev()
         if rev:
             rev_options = [rev]
             rev_display = ''
         clone = True
         if os.path.exists(os.path.join(dest, '.hg')):
-            existing_url = Mercurial().get_url(dest)
+            existing_url = self.get_url(dest)
             clone = False
             if existing_url == url:
                 logger.info('Clone in %s exists, and has correct URL (%s)'
 
     def get_url(self, location):
         url = call_subprocess(
-            ['hg', 'showconfig', 'paths.default'], show_stdout=False, cwd=location)
+            ['hg', 'showconfig', 'paths.default'],
+            show_stdout=False, cwd=location)
         return url.strip()
 
     def get_tip_revision(self, location):
                 return branch
         return self.get_tip_revision(location)
 
-    def src_requirement(self, dist, location, find_tags):
+    def get_src_requirement(self, dist, location, find_tags):
         repo = self.get_url(location)
         if not repo.lower().startswith('hg:'):
             repo = 'hg+' + repo
             # Don't know what it is
             logger.warn('Mercurial URL does not fit normal structure: %s' % repo)
             return '%s@%s#egg=%s-dev' % (repo, current_rev, egg_project_name)
-register_version_control(Mercurial)
+
+vcs.register(Mercurial)
+
+
+class Bazaar(VersionControl):
+    name = 'bzr'
+    schemes = ('bzr', 'bzr+http', 'bzr+ssh', 'bzr+sftp', 'bzr+https')
+    guide = ('# This was a Bazaar repo; to make it a repo again run:\n'
+             'bzr checkout -r %(rev)s %(url)s .\n')
+
+    def __init__(self, *args, **kwargs):
+        try:
+            from bzrlib.branch import Branch
+        except ImportError, e:
+            logger.fatal("bzrlib could not be imported")
+        super(Bazaar, self).__init__(*args, **kwargs)
+
+    def get_branch(self, location):
+        try:
+            from bzrlib.branch import Branch
+        except ImportError, e:
+            logger.fatal("bzrlib could not be imported")
+        return Branch.open(location)
+
+    def get_info(self, location):
+        """Returns (url, revision), where both are strings"""
+        assert not location.rstrip('/').endswith('.bzr'), 'Bad directory: %s' % location
+        return self.get_url(location), self.get_revision(location)
+
+    def parse_clone_text(self, text):
+        url = rev = None
+        for line in text.splitlines():
+            if not line.strip() or line.strip().startswith('#'):
+                continue
+            match = re.search(r'^bzr\s*checkout\s*-r\s*(\d*)', line)
+            if match:
+                rev = match.group(1).strip()
+            url = line[match.end():].strip().split(None, 1)[0]
+            if url and rev:
+                return url, rev
+        return None, None
+
+    def unpack(self, location):
+        """Check out the bzr repository at the url to the destination location"""
+        url, rev = self.get_url_rev()
+        logger.notify('Checking out bzr repository %s to %s' % (url, location))
+        logger.indent += 2
+        try:
+            if os.path.exists(location):
+                os.rmdir(location)
+            call_subprocess(
+                ['bzr', 'checkout', url, location],
+                filter_stdout=self._filter, show_stdout=False)
+        finally:
+            logger.indent -= 2
+
+    def obtain(self, dest):
+        url, rev = self.get_url_rev()
+        if rev:
+            rev_options = ['-r', rev]
+            rev_display = ' (to revision %s)' % rev
+        else:
+            rev_options = ['default']
+            rev_display = ''
+        checkout = True
+        if os.path.exists(os.path.join(dest, '.bzr')):
+            existing_url = self.get_url(dest)
+            checkout = False
+            if existing_url == url:
+                logger.info('Checkout in %s exists, and has correct URL (%s)'
+                            % (display_path(dest), url))
+                logger.notify('Updating checkout %s%s'
+                              % (display_path(dest), rev_display))
+                checkout = True
+            else:
+                logger.warn('Bazaar checkout in %s exists with URL %s'
+                            % (display_path(dest), existing_url))
+                logger.warn('The plan is to install the Bazaar repository %s'
+                            % url)
+                response = ask('What to do?  (s)witch, (i)gnore, (w)ipe, (b)ackup ', ('s', 'i', 'w', 'b'))
+                if response == 's':
+                    logger.notify('Switching checkout %s to %s%s'
+                                  % (display_path(dest), url, rev_display))
+                    call_subprocess(['bzr', 'switch', url], cwd=dest)
+                elif response == 'i':
+                    # do nothing
+                    pass
+                elif response == 'w':
+                    logger.warn('Deleting %s' % display_path(dest))
+                    shutil.rmtree(dest)
+                    checkout = True
+                elif response == 'b':
+                    dest_dir = backup_dir(dest)
+                    logger.warn('Backing up %s to %s' % (display_path(dest), dest_dir))
+                    shutil.move(dest, dest_dir)
+                    checkout = True
+        if checkout:
+            logger.notify('Checking out %s%s to %s'
+                          % (url, rev_display, display_path(dest)))
+            call_subprocess(
+                ['bzr', 'checkout', '-q'] + rev_options + [url, dest])
+
+    def get_url(self, location):
+        branch = self.get_branch(location)
+        if branch.get_parent():
+            # This is a branch
+            return branch.get_parent()
+        master_branch = branch.get_master_branch()
+        if master_branch:
+            # This is a checkout
+            return master_branch.get_parent()
+        return None
+
+    def get_revision(self, location):
+        branch = self.get_branch(location)
+        if branch.last_revision():
+            return branch.last_revision().strip()
+        return None
+
+    def get_newest_revision(self, location):
+        url = self.get_url(location)
+        match = re.search(r'([.\w-]+)\s*(.*)$', url)
+        if match:
+            return match.group(1).strip()
+        return None
+
+    def get_tag_revs(self, location):
+        tags = call_subprocess(
+            ['bzr', 'tags'], show_stdout=False, cwd=location)
+        tag_revs = []
+        for line in tags.splitlines():
+            tags_match = re.search(r'([.\w-]+)\s*(.*)$', line)
+            if tags_match:
+                tag = tags_match.group(1)
+                rev = tags_match.group(2)
+                tag_revs.append((rev.strip(), tag.strip()))
+        return dict(tag_revs)
+
+    def get_revision(self, location):
+        current_branch = call_subprocess(
+            ['bzr', 'revno'], show_stdout=False, cwd=location).strip()
+        branch_revs = self.get_branch_revs(location)
+        for branch in branch_revs:
+            if current_branch == branch_revs[branch]:
+                return branch
+        return self.get_tip_revision(location)
+
+    def get_src_requirement(self, dist, location, find_tags):
+        repo = self.get_url(location)
+        if not repo.lower().startswith('bzr:'):
+            repo = 'bzr+' + repo
+        egg_project_name = dist.egg_name().split('-', 1)[0]
+        if not repo:
+            return None
+        current_rev = self.get_revision(location)
+        tag_revs = self.get_tag_revs(location)
+        newest_rev = self.get_newest_revision(location)
+        if current_rev in tag_revs:
+            # It's a tag, perfect!
+            tag = tag_revs.get(current_rev, current_rev)
+            return '%s@%s#egg=%s-%s' % (repo, tag, egg_project_name, tag)
+        elif current_rev == newest_rev:
+            if find_tags:
+                if current_rev in tag_revs:
+                    tag = tag_revs.get(current_rev, current_rev)
+                    logger.notify('Revision %s seems to be equivalent to tag %s' % (current_rev, tag))
+                    return '%s@%s#egg=%s-%s' % (repo, tag, egg_project_name, tag)
+            return '%s@%s#egg=%s-dev' % (repo, newest_rev, dist.egg_name())
+        else:
+            # Don't know what it is
+            logger.warn('Bazaar URL does not fit normal structure: %s' % repo)
+            return '%s@%s#egg=%s-dev' % (repo, current_rev, egg_project_name)
+
+vcs.register(Bazaar)
+
 
 def get_src_requirement(dist, location, find_tags):
-    for vc_type in _version_controls:
-        path = os.path.join(location, '.%s' % vc_type)
-        if os.path.exists(path):
-            vc_type = _version_controls[vc_type]()
-            return vc_type.src_requirement(dist, location, find_tags)
-    logger.warn('cannot determine version of editable source in %s (is not SVN checkout, Git clone or Mercurial clone)' % location)
+    version_control = vcs.get_backend_from_location(location)
+    if version_control:
+        return version_control().get_src_requirement(dist, location, find_tags)
+    logger.warn('cannot determine version of editable source in %s (is not SVN checkout, Git clone, Mercurial clone or Bazaar checkout)' % location)
     return dist.as_requirement()
 
 ############################################################
         url = filename_to_url(url)
     if url.lower().startswith('file:'):
         return None, url
-    for vc_type in _version_controls:
-        if url.lower().startswith('%s:' % vc_type):
-            url = '%s+' % vc_type + url
+    for version_control in vcs:
+        if url.lower().startswith('%s:' % version_control):
+            url = '%s+%s' % (version_control, url)
     if '+' not in url:
         if default_vcs:
             url = default_vcs + '+' + url
         else:
             raise InstallationError(
-                '--editable=%s should be formatted with svn+URL, git+URL or hg+URL' % editable_req)
+                '--editable=%s should be formatted with svn+URL, git+URL, hg+URL or bzr+URL' % editable_req)
     vc_type = url.split('+', 1)[0].lower()
-    if vc_type not in _version_controls:
+    if not vcs.get_backend(vc_type):
         raise InstallationError(
-            'For --editable=%s only svn (svn+URL), Git (git+URL) and Mercurial (hg+URL) is currently supported' % editable_req)
+            'For --editable=%s only svn (svn+URL), Git (git+URL), Mercurial (hg+URL) and Bazaar (bzr+URL) is currently supported' % editable_req)
     match = re.search(r'(?:#|#.*?&)egg=([^&]*)', editable_req)
-    if (not match or not match.group(1)) and vc_type in _version_controls:
+    if (not match or not match.group(1)) and vcs.get_backend(vc_type):
         parts = [p for p in editable_req.split('#', 1)[0].split('/') if p]
         if parts[-2] in ('tags', 'branches', 'tag', 'branch'):
             req = parts[-3]