launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #17935
[Merge] lp:~cjwatson/launchpad/git-lookup into lp:launchpad
Colin Watson has proposed merging lp:~cjwatson/launchpad/git-lookup into lp:launchpad with lp:~cjwatson/launchpad/git-defaults as a prerequisite.
Commit message:
Add support for looking up Git repositories by path.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
Bug #1032731 in Launchpad itself: "Support for Launchpad-hosted Git repositories"
https://bugs.launchpad.net/launchpad/+bug/1032731
For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/git-lookup/+merge/250628
Add support for looking up Git repositories by path.
--
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/git-lookup into lp:launchpad.
=== modified file 'lib/lp/code/configure.zcml'
--- lib/lp/code/configure.zcml 2015-02-23 14:47:40 +0000
+++ lib/lp/code/configure.zcml 2015-02-23 14:47:40 +0000
@@ -865,6 +865,22 @@
<adapter factory="lp.code.model.defaultgit.OwnerProjectDefaultGitRepository" />
<adapter factory="lp.code.model.defaultgit.OwnerPackageDefaultGitRepository" />
+ <class class="lp.code.model.gitlookup.GitLookup">
+ <allow interface="lp.code.interfaces.gitlookup.IGitLookup" />
+ </class>
+ <securedutility
+ class="lp.code.model.gitlookup.GitLookup"
+ provides="lp.code.interfaces.gitlookup.IGitLookup">
+ <allow interface="lp.code.interfaces.gitlookup.IGitLookup" />
+ </securedutility>
+ <securedutility
+ class="lp.code.model.gitlookup.DefaultGitTraverser"
+ provides="lp.code.interfaces.gitlookup.IDefaultGitTraverser">
+ <allow interface="lp.code.interfaces.gitlookup.IDefaultGitTraverser" />
+ </securedutility>
+ <adapter factory="lp.code.model.gitlookup.DistributionGitTraversable" />
+ <adapter factory="lp.code.model.gitlookup.PersonGitTraversable" />
+
<lp:help-folder folder="help" name="+help-code" />
<!-- Diffs -->
=== added file 'lib/lp/code/interfaces/gitlookup.py'
--- lib/lp/code/interfaces/gitlookup.py 1970-01-01 00:00:00 +0000
+++ lib/lp/code/interfaces/gitlookup.py 2015-02-23 14:47:40 +0000
@@ -0,0 +1,131 @@
+# Copyright 2015 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Utility for looking up Git repositories by name."""
+
+__metaclass__ = type
+__all__ = [
+ 'IDefaultGitTraversable',
+ 'IDefaultGitTraverser',
+ 'IGitLookup',
+ ]
+
+from zope.interface import Interface
+
+
+class IDefaultGitTraversable(Interface):
+ """A thing that can be traversed to find a thing with a default Git
+ repository."""
+
+ def traverse(name, segments):
+ """Return the object beneath this one that matches 'name'.
+
+ :param name: The name of the object being traversed to.
+ :param segments: Remaining path segments.
+ :return: An `IDefaultGitTraversable` object if traversing should
+ continue; an `ICanHasDefaultGitRepository` object otherwise.
+ """
+
+
+class IDefaultGitTraverser(Interface):
+ """Utility for traversing to an object that can have a default Git
+ repository."""
+
+ def traverse(path):
+ """Traverse to the object referred to by 'path'.
+
+ :raises InvalidNamespace: If the path cannot be parsed as a
+ namespace.
+ :raises InvalidProductName: If the project component of the path is
+ not a valid name.
+ :raises NoSuchPerson: If the first segment of the path begins with a
+ '~', but we can't find a person matching the remainder.
+ :raises NoSuchProduct: If we can't find a project that matches the
+ project component of the path.
+ :raises NoSuchSourcePackageName: If the source package referred to
+ does not exist.
+
+ :return: One of
+ * `IProduct`
+ * `IDistributionSourcePackage`
+ * `IPersonProduct`
+ * `IPersonDistributionSourcePackage`
+ """
+
+
+class IGitLookup(Interface):
+ """Utility for looking up a Git repository by name."""
+
+ def get(repository_id, default=None):
+ """Return the repository with the given id.
+
+ Return the default value if there is no such repository.
+ """
+
+ def getByUniqueName(unique_name):
+ """Find a repository by its unique name.
+
+ Unique names have one of the following forms:
+ ~OWNER/PROJECT/+git/NAME
+ ~OWNER/DISTRO/+source/SOURCE/+git/NAME
+ ~OWNER/+git/NAME
+
+ :return: An `IGitRepository`, or None.
+ """
+
+ def uriToHostingPath(uri):
+ """Return the path for the URI, if the URI is on codehosting.
+
+ This does not ensure that the path is valid.
+
+ :param uri: An instance of lazr.uri.URI
+ :return: The path if possible; None if the URI is not a valid
+ codehosting URI.
+ """
+
+ def getByUrl(url):
+ """Find a repository by URL.
+
+ Either from the URL on git.launchpad.net (various schemes) or the
+ lp: URL (which relies on client-side configuration).
+ """
+
+ def getByPath(path):
+ """Find a repository by its path.
+
+ Any of these forms may be used, with or without a leading slash:
+ Unique names:
+ ~OWNER/PROJECT/+git/NAME
+ ~OWNER/DISTRO/+source/SOURCE/+git/NAME
+ ~OWNER/+git/NAME
+ Owner-target default aliases:
+ ~OWNER/PROJECT
+ ~OWNER/DISTRO/+source/SOURCE
+ Official aliases:
+ PROJECT
+ DISTRO/+source/SOURCE
+
+ :raises InvalidNamespace: If the path looks like a unique repository
+ name but doesn't have enough segments to be a unique name.
+ :raises InvalidProductName: If the given project in a project
+ shortcut is an invalid name for a project.
+
+ :raises NoSuchGitRepository: If we can't find a repository that
+ matches the repository component of the path.
+ :raises NoSuchPerson: If we can't find a person who matches the
+ person component of the path.
+ :raises NoSuchProduct: If we can't find a project that matches the
+ project component of the path.
+ :raises NoSuchSourcePackageName: If the source package referred to
+ does not exist.
+
+ :raises NoDefaultGitRepository: If the path refers to an existing
+ thing that's not a Git repository and has no default repository
+ associated with it. For example, a product without a default
+ repository.
+ :raises CannotHaveDefaultGitRepository: If the path refers to an
+ existing thing that cannot have a default Git repository
+ associated with it. For example, a distribution.
+
+ :return: An `IGitRepository`.
+ """
=== added file 'lib/lp/code/model/gitlookup.py'
--- lib/lp/code/model/gitlookup.py 1970-01-01 00:00:00 +0000
+++ lib/lp/code/model/gitlookup.py 2015-02-23 14:47:40 +0000
@@ -0,0 +1,275 @@
+# Copyright 2015 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Database implementation of the Git repository lookup utility."""
+
+__metaclass__ = type
+# This module doesn't export anything. If you want to look up Git
+# repositories by name, then get the IGitLookup utility.
+__all__ = []
+
+from lazr.uri import (
+ InvalidURIError,
+ URI,
+ )
+from zope.component import (
+ adapts,
+ getUtility,
+ queryMultiAdapter,
+ )
+from zope.interface import implements
+
+from lp.app.validators.name import valid_name
+from lp.code.errors import (
+ CannotHaveDefaultGitRepository,
+ InvalidNamespace,
+ NoDefaultGitRepository,
+ NoSuchGitRepository,
+ )
+from lp.code.interfaces.defaultgit import get_default_git_repository
+from lp.code.interfaces.gitlookup import (
+ IDefaultGitTraversable,
+ IDefaultGitTraverser,
+ IGitLookup,
+ )
+from lp.code.interfaces.gitnamespace import IGitNamespaceSet
+from lp.code.model.gitrepository import GitRepository
+from lp.registry.errors import NoSuchSourcePackageName
+from lp.registry.interfaces.distribution import IDistribution
+from lp.registry.interfaces.person import (
+ IPerson,
+ IPersonSet,
+ NoSuchPerson,
+ )
+from lp.registry.interfaces.persondistributionsourcepackage import (
+ IPersonDistributionSourcePackageFactory,
+ )
+from lp.registry.interfaces.personproduct import IPersonProductFactory
+from lp.registry.interfaces.pillar import IPillarNameSet
+from lp.registry.interfaces.product import (
+ InvalidProductName,
+ NoSuchProduct,
+ )
+from lp.services.config import config
+from lp.services.database.interfaces import IStore
+
+
+def adapt(provided, interface):
+ """Adapt 'obj' to 'interface', using multi-adapters if necessary."""
+ required = interface(provided, None)
+ if required is not None:
+ return required
+ try:
+ return queryMultiAdapter(provided, interface)
+ except TypeError:
+ return None
+
+
+class RootGitTraversable:
+ """Root traversable for default Git repository objects.
+
+ Corresponds to '/' in the path. From here, you can traverse to a
+ person, a distribution, or a project.
+ """
+
+ implements(IDefaultGitTraversable)
+
+ def traverse(self, name, segments):
+ """See `IDefaultGitTraversable`.
+
+ :raise InvalidProductName: If 'name' is not a valid name.
+ :raise NoSuchPerson: If 'name' begins with a '~', but the remainder
+ doesn't match an existing person.
+ :raise NoSuchProduct: If 'name' doesn't match an existing pillar.
+ :return: `IPerson` or `IPillar`.
+ """
+ if name.startswith("~"):
+ person_name = name[1:]
+ person = getUtility(IPersonSet).getByName(person_name)
+ if person is None:
+ raise NoSuchPerson(person_name)
+ return person
+ if not valid_name(name):
+ raise InvalidProductName(name)
+ pillar = getUtility(IPillarNameSet).getByName(name)
+ if pillar is None:
+ # Actually, the pillar is no such *anything*.
+ raise NoSuchProduct(name)
+ return pillar
+
+
+class _BaseGitTraversable:
+ """Base class for traversable implementations.
+
+ This just defines a very simple constructor.
+ """
+
+ def __init__(self, context):
+ self.context = context
+
+
+class DistributionGitTraversable(_BaseGitTraversable):
+ """Default Git repository traversable for distributions.
+
+ From here, you can traverse to a distribution source package.
+ """
+
+ adapts(IDistribution)
+ implements(IDefaultGitTraversable)
+
+ def traverse(self, name, segments):
+ """See `IDefaultGitTraversable`.
+
+ :raise InvalidNamespace: If 'name' is not '+source' or there are no
+ further segments.
+ :raise NoSuchSourcePackageName: If the segment after '+source'
+ doesn't match an existing source package name.
+ :return: `IDistributionSourcePackage`.
+ """
+ if name != "+source" or not segments:
+ raise InvalidNamespace(name)
+ spn_name = segments.pop(0)
+ distro_source_package = self.context.getSourcePackage(spn_name)
+ if distro_source_package is None:
+ raise NoSuchSourcePackageName(spn_name)
+ return distro_source_package
+
+
+class PersonGitTraversable(_BaseGitTraversable):
+ """Default Git repository traversable for people.
+
+ From here, you can traverse to a person-distribution-source-package or a
+ person-project.
+ """
+
+ adapts(IPerson)
+ implements(IDefaultGitTraversable)
+
+ def traverse(self, name, segments):
+ """See `IDefaultGitTraversable`.
+
+ :raise InvalidNamespace: If 'name' matches an existing distribution,
+ and the next segment is not '+source' or there are no further
+ segments.
+ :raise InvalidProductName: If 'name' is not a valid name.
+ :raise NoSuchProduct: If 'name' doesn't match an existing pillar.
+ :raise NoSuchSourcePackageName: If 'name' matches an existing
+ distribution, and the segment after '+source' doesn't match an
+ existing source package name.
+ :return: `IPersonProduct` or `IPersonDistributionSourcePackage`.
+ """
+ if not valid_name(name):
+ raise InvalidProductName(name)
+ pillar = getUtility(IPillarNameSet).getByName(name)
+ if pillar is None:
+ # Actually, the pillar is no such *anything*.
+ raise NoSuchProduct(name)
+ # XXX cjwatson 2015-02-23: This would be neater if
+ # IPersonDistribution existed.
+ if IDistribution.providedBy(pillar):
+ if len(segments) < 2:
+ raise InvalidNamespace(name)
+ segments.pop(0)
+ spn_name = segments.pop(0)
+ distro_source_package = pillar.getSourcePackage(spn_name)
+ if distro_source_package is None:
+ raise NoSuchSourcePackageName(spn_name)
+ return getUtility(IPersonDistributionSourcePackageFactory).create(
+ self.context, distro_source_package)
+ else:
+ return getUtility(IPersonProductFactory).create(
+ self.context, pillar)
+
+
+class DefaultGitTraverser:
+ """Utility for traversing to objects that can have default repositories."""
+
+ implements(IDefaultGitTraverser)
+
+ def traverse(self, path):
+ """See `IDefaultGitTraverser`."""
+ segments = path.split("/")
+ traversable = RootGitTraversable()
+ while segments:
+ name = segments.pop(0)
+ context = traversable.traverse(name, segments)
+ traversable = adapt(context, IDefaultGitTraversable)
+ if traversable is None:
+ break
+ if segments:
+ raise InvalidNamespace(path)
+ return context
+
+
+class GitLookup:
+ """Utility for looking up Git repositories."""
+
+ implements(IGitLookup)
+
+ def get(self, repository_id, default=None):
+ """See `IGitLookup`."""
+ repository = IStore(GitRepository).get(GitRepository, repository_id)
+ if repository is None:
+ return default
+ return repository
+
+ @staticmethod
+ def uriToHostingPath(uri):
+ """See `IGitLookup`."""
+ schemes = ('git', 'git+ssh', 'https', 'ssh')
+ codehosting_host = URI(config.codehosting.git_anon_root).host
+ if ((uri.scheme in schemes and uri.host == codehosting_host) or
+ (uri.scheme == "lp" and uri.host is None)):
+ return uri.path.lstrip("/")
+ else:
+ return None
+
+ def getByUrl(self, url):
+ """See `IGitLookup`."""
+ if url is None:
+ return None
+ url = url.rstrip("/")
+ try:
+ uri = URI(url)
+ except InvalidURIError:
+ return None
+
+ path = self.uriToHostingPath(uri)
+ if path is None:
+ return None
+ try:
+ return self.getByPath(path)
+ except (
+ CannotHaveDefaultGitRepository, InvalidNamespace,
+ InvalidProductName, NoDefaultGitRepository, NoSuchGitRepository,
+ NoSuchPerson, NoSuchProduct, NoSuchSourcePackageName):
+ return None
+
+ def getByUniqueName(self, unique_name):
+ """See `IGitLookup`."""
+ try:
+ if unique_name.startswith("~"):
+ path = unique_name.lstrip("~")
+ namespace_set = getUtility(IGitNamespaceSet)
+ segments = iter(path.split("/"))
+ repository = namespace_set.traverse(segments)
+ if list(segments):
+ raise InvalidNamespace(path)
+ return repository
+ except InvalidNamespace:
+ pass
+ return None
+
+ def getByPath(self, path):
+ """See `IGitLookup`."""
+ # Try parsing as a unique name.
+ repository = self.getByUniqueName(path)
+ if repository is not None:
+ return repository
+
+ # Try parsing as a shortcut.
+ object_with_git_repository_default = getUtility(
+ IDefaultGitTraverser).traverse(path)
+ default = get_default_git_repository(
+ object_with_git_repository_default)
+ return default.repository
=== modified file 'lib/lp/code/model/gitrepository.py'
--- lib/lp/code/model/gitrepository.py 2015-02-19 23:57:34 +0000
+++ lib/lp/code/model/gitrepository.py 2015-02-23 14:47:40 +0000
@@ -24,21 +24,30 @@
Reference,
Unicode,
)
-from zope.component import getUtility
+from zope.component import (
+ getAdapter,
+ getUtility,
+ )
from zope.interface import implements
+from zope.security.proxy import removeSecurityProxy
from lp.app.enums import (
InformationType,
PRIVATE_INFORMATION_TYPES,
PUBLIC_INFORMATION_TYPES,
)
+from lp.app.errors import NotFoundError
from lp.app.interfaces.informationtype import IInformationType
from lp.app.interfaces.launchpad import IPrivacy
+from lp.app.interfaces.security import IAuthorization
from lp.app.interfaces.services import IService
from lp.code.errors import (
GitDefaultConflict,
GitTargetError,
+ InvalidGitRepositoryException,
+ InvalidNamespace,
)
+from lp.code.interfaces.gitlookup import IGitLookup
from lp.code.interfaces.gitnamespace import (
get_git_namespace,
IGitNamespacePolicy,
@@ -59,8 +68,14 @@
IDistributionSourcePackage,
)
from lp.registry.interfaces.person import IPerson
-from lp.registry.interfaces.product import IProduct
-from lp.registry.interfaces.role import IHasOwner
+from lp.registry.interfaces.product import (
+ InvalidProductName,
+ IProduct,
+ )
+from lp.registry.interfaces.role import (
+ IHasOwner,
+ IPersonRoles,
+ )
from lp.registry.interfaces.sharingjob import (
IRemoveArtifactSubscriptionsJobSource,
)
@@ -355,8 +370,18 @@
def getByPath(self, user, path):
"""See `IGitRepositorySet`."""
- # XXX cjwatson 2015-02-06: Fill this in once IGitLookup is in place.
- raise NotImplementedError
+ try:
+ repository = getUtility(IGitLookup).getByPath(path)
+ except (InvalidGitRepositoryException, InvalidNamespace,
+ InvalidProductName, NotFoundError):
+ return None
+ authz = getAdapter(
+ removeSecurityProxy(repository), IAuthorization, 'launchpad.View')
+ if ((user is None and authz.checkUnauthenticated()) or
+ (user is not None and authz.checkAuthenticated(
+ IPersonRoles(user)))):
+ return repository
+ return None
def getDefaultRepository(self, target, owner=None):
"""See `IGitRepositorySet`."""
=== added file 'lib/lp/code/model/tests/test_gitlookup.py'
--- lib/lp/code/model/tests/test_gitlookup.py 1970-01-01 00:00:00 +0000
+++ lib/lp/code/model/tests/test_gitlookup.py 2015-02-23 14:47:40 +0000
@@ -0,0 +1,338 @@
+# Copyright 2015 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests for the IGitLookup implementation."""
+
+__metaclass__ = type
+
+from lazr.uri import URI
+from zope.component import getUtility
+
+from lp.code.errors import (
+ CannotHaveDefaultGitRepository,
+ InvalidNamespace,
+ NoDefaultGitRepository,
+ )
+from lp.code.interfaces.gitlookup import (
+ IDefaultGitTraverser,
+ IGitLookup,
+ )
+from lp.registry.errors import NoSuchSourcePackageName
+from lp.registry.interfaces.person import NoSuchPerson
+from lp.registry.interfaces.persondistributionsourcepackage import (
+ IPersonDistributionSourcePackageFactory,
+ )
+from lp.registry.interfaces.personproduct import IPersonProductFactory
+from lp.registry.interfaces.product import (
+ InvalidProductName,
+ NoSuchProduct,
+ )
+from lp.services.config import config
+from lp.testing import (
+ person_logged_in,
+ TestCaseWithFactory,
+ )
+from lp.testing.layers import DatabaseFunctionalLayer
+
+
+class TestGetByUniqueName(TestCaseWithFactory):
+ """Tests for `IGitLookup.getByUniqueName`."""
+
+ layer = DatabaseFunctionalLayer
+
+ def setUp(self):
+ super(TestGetByUniqueName, self).setUp()
+ self.lookup = getUtility(IGitLookup)
+
+ def test_not_found(self):
+ unused_name = self.factory.getUniqueString()
+ self.assertIsNone(self.lookup.getByUniqueName(unused_name))
+
+ def test_project(self):
+ repository = self.factory.makeGitRepository()
+ self.assertEqual(
+ repository, self.lookup.getByUniqueName(repository.unique_name))
+
+ def test_package(self):
+ dsp = self.factory.makeDistributionSourcePackage()
+ repository = self.factory.makeGitRepository(target=dsp)
+ self.assertEqual(
+ repository, self.lookup.getByUniqueName(repository.unique_name))
+
+ def test_personal(self):
+ owner = self.factory.makePerson()
+ repository = self.factory.makeGitRepository(owner=owner, target=owner)
+ self.assertEqual(
+ repository, self.lookup.getByUniqueName(repository.unique_name))
+
+
+class TestGetByPath(TestCaseWithFactory):
+ """Test `IGitLookup.getByPath`."""
+
+ layer = DatabaseFunctionalLayer
+
+ def setUp(self):
+ super(TestGetByPath, self).setUp()
+ self.lookup = getUtility(IGitLookup)
+
+ def test_project(self):
+ repository = self.factory.makeGitRepository()
+ self.assertEqual(
+ repository, self.lookup.getByPath(repository.unique_name))
+
+ def test_project_default(self):
+ repository = self.factory.makeGitRepository()
+ with person_logged_in(repository.target.owner):
+ repository.target.setDefaultGitRepository(repository)
+ self.assertEqual(
+ repository, self.lookup.getByPath(repository.shortened_path))
+
+ def test_package(self):
+ dsp = self.factory.makeDistributionSourcePackage()
+ repository = self.factory.makeGitRepository(target=dsp)
+ self.assertEqual(
+ repository, self.lookup.getByPath(repository.unique_name))
+
+ def test_package_default(self):
+ dsp = self.factory.makeDistributionSourcePackage()
+ repository = self.factory.makeGitRepository(target=dsp)
+ with person_logged_in(repository.target.distribution.owner):
+ repository.target.setDefaultGitRepository(repository)
+ self.assertEqual(
+ repository, self.lookup.getByPath(repository.shortened_path))
+
+ def test_personal(self):
+ owner = self.factory.makePerson()
+ repository = self.factory.makeGitRepository(owner=owner, target=owner)
+ self.assertEqual(
+ repository, self.lookup.getByPath(repository.unique_name))
+
+ def test_cannot_have_default_git_repository(self):
+ # If `getByPath` is given a path to something with no default Git
+ # repository, such as a distribution, it raises
+ # CannotHaveDefaultGitRepository.
+ distro = self.factory.makeDistribution()
+ self.assertRaises(
+ CannotHaveDefaultGitRepository, self.lookup.getByPath, distro.name)
+
+ def test_no_default_git_repository(self):
+ # If `getByPath` is given a path to something that could have a Git
+ # repository but doesn't, it raises NoDefaultGitRepository.
+ project = self.factory.makeProduct()
+ self.assertRaises(
+ NoDefaultGitRepository, self.lookup.getByPath, project.name)
+
+
+class TestGetByUrl(TestCaseWithFactory):
+ """Test `IGitLookup.getByUrl`."""
+
+ layer = DatabaseFunctionalLayer
+
+ def setUp(self):
+ super(TestGetByUrl, self).setUp()
+ self.lookup = getUtility(IGitLookup)
+
+ def makeProjectRepository(self):
+ owner = self.factory.makePerson(name="aa")
+ project = self.factory.makeProduct(name="bb")
+ return self.factory.makeGitRepository(
+ owner=owner, target=project, name=u"cc")
+
+ def test_getByUrl_with_none(self):
+ # getByUrl returns None if given None.
+ self.assertIsNone(self.lookup.getByUrl(None))
+
+ def assertUrlMatches(self, url, repository):
+ self.assertEqual(repository, self.lookup.getByUrl(url))
+
+ def test_getByUrl_with_trailing_slash(self):
+ # Trailing slashes are stripped from the URL prior to searching.
+ repository = self.makeProjectRepository()
+ self.assertUrlMatches(
+ "git://git.launchpad.dev/~aa/bb/+git/cc/", repository)
+
+ def test_getByUrl_with_git(self):
+ # getByUrl recognises LP repositories for git URLs.
+ repository = self.makeProjectRepository()
+ self.assertUrlMatches(
+ "git://git.launchpad.dev/~aa/bb/+git/cc", repository)
+
+ def test_getByUrl_with_git_ssh(self):
+ # getByUrl recognises LP repositories for git+ssh URLs.
+ repository = self.makeProjectRepository()
+ self.assertUrlMatches(
+ "git+ssh://git.launchpad.dev/~aa/bb/+git/cc", repository)
+
+ def test_getByUrl_with_https(self):
+ # getByUrl recognises LP repositories for https URLs.
+ repository = self.makeProjectRepository()
+ self.assertUrlMatches(
+ "https://git.launchpad.dev/~aa/bb/+git/cc", repository)
+
+ def test_getByUrl_with_ssh(self):
+ # getByUrl recognises LP repositories for ssh URLs.
+ repository = self.makeProjectRepository()
+ self.assertUrlMatches(
+ "ssh://git.launchpad.dev/~aa/bb/+git/cc", repository)
+
+ def test_getByUrl_with_ftp(self):
+ # getByUrl does not recognise LP repositories for ftp URLs.
+ self.makeProjectRepository()
+ self.assertIsNone(
+ self.lookup.getByUrl("ftp://git.launchpad.dev/~aa/bb/+git/cc"))
+
+ def test_getByUrl_with_lp(self):
+ # getByUrl supports lp: URLs.
+ url = "lp:~aa/bb/+git/cc"
+ self.assertIsNone(self.lookup.getByUrl(url))
+ repository = self.makeProjectRepository()
+ self.assertUrlMatches(url, repository)
+
+ def test_getByUrl_with_default(self):
+ # getByUrl honours default repositories when looking up URLs.
+ repository = self.makeProjectRepository()
+ with person_logged_in(repository.target.owner):
+ repository.target.setDefaultGitRepository(repository)
+ self.assertUrlMatches("lp:bb", repository)
+
+ def test_uriToHostingPath(self):
+ # uriToHostingPath only supports our own URLs with certain schemes.
+ uri = URI(config.codehosting.git_anon_root)
+ uri.path = "/~foo/bar/baz"
+ # Test valid schemes.
+ for scheme in ("git", "git+ssh", "https", "ssh"):
+ uri.scheme = scheme
+ self.assertEqual("~foo/bar/baz", self.lookup.uriToHostingPath(uri))
+ # Test an invalid scheme.
+ uri.scheme = "ftp"
+ self.assertIsNone(self.lookup.uriToHostingPath(uri))
+ # Test valid scheme but invalid domain.
+ uri.scheme = 'sftp'
+ uri.host = 'example.com'
+ self.assertIsNone(self.lookup.uriToHostingPath(uri))
+
+
+class TestDefaultGitTraverser(TestCaseWithFactory):
+ """Tests for the default repository traverser."""
+
+ layer = DatabaseFunctionalLayer
+
+ def setUp(self):
+ super(TestDefaultGitTraverser, self).setUp()
+ self.traverser = getUtility(IDefaultGitTraverser)
+
+ def assertTraverses(self, path, result):
+ self.assertEqual(result, self.traverser.traverse(path))
+
+ def test_nonexistent_project(self):
+ # `traverse` raises `NoSuchProduct` when resolving a path of
+ # 'project' if the project doesn't exist.
+ self.assertRaises(NoSuchProduct, self.traverser.traverse, "bb")
+
+ def test_invalid_project(self):
+ # `traverse` raises `InvalidProductName` when resolving a path for a
+ # completely invalid default project repository.
+ self.assertRaises(InvalidProductName, self.traverser.traverse, "b")
+
+ def test_project(self):
+ # `traverse` resolves the name of a project to the project itself.
+ project = self.factory.makeProduct()
+ self.assertTraverses(project.name, project)
+
+ def test_no_such_distribution(self):
+ # `traverse` raises `NoSuchProduct` if the distribution doesn't
+ # exist. That's because it can't tell the difference between the
+ # name of a project that doesn't exist and the name of a
+ # distribution that doesn't exist.
+ self.assertRaises(
+ NoSuchProduct, self.traverser.traverse, "distro/+source/package")
+
+ def test_missing_sourcepackagename(self):
+ # `traverse` raises `InvalidNamespace` if there are no segments
+ # after '+source'.
+ self.factory.makeDistribution(name="distro")
+ self.assertRaises(
+ InvalidNamespace, self.traverser.traverse, "distro/+source")
+
+ def test_no_such_sourcepackagename(self):
+ # `traverse` raises `NoSuchSourcePackageName` if the package in
+ # distro/+source/package doesn't exist.
+ self.factory.makeDistribution(name="distro")
+ self.assertRaises(
+ NoSuchSourcePackageName, self.traverser.traverse,
+ "distro/+source/nonexistent")
+
+ def test_package(self):
+ # `traverse` resolves 'distro/+source/package' to the distribution
+ # source package.
+ dsp = self.factory.makeDistributionSourcePackage()
+ path = "%s/+source/%s" % (
+ dsp.distribution.name, dsp.sourcepackagename.name)
+ self.assertTraverses(path, dsp)
+
+ def test_nonexistent_person(self):
+ # `traverse` raises `NoSuchPerson` when resolving a path of
+ # '~person/project' if the person doesn't exist.
+ self.assertRaises(NoSuchPerson, self.traverser.traverse, "~person/bb")
+
+ def test_nonexistent_person_project(self):
+ # `traverse` raises `NoSuchProduct` when resolving a path of
+ # '~person/project' if the project doesn't exist.
+ self.factory.makePerson(name="person")
+ self.assertRaises(NoSuchProduct, self.traverser.traverse, "~person/bb")
+
+ def test_invalid_person_project(self):
+ # `traverse` raises `InvalidProductName` when resolving a path for a
+ # person and a completely invalid default project repository.
+ self.factory.makePerson(name="person")
+ self.assertRaises(
+ InvalidProductName, self.traverser.traverse, "~person/b")
+
+ def test_person_project(self):
+ # `traverse` resolves '~person/project' to a PersonProduct.
+ person = self.factory.makePerson()
+ project = self.factory.makeProduct()
+ person_project = getUtility(IPersonProductFactory).create(
+ person, project)
+ self.assertTraverses(
+ "~%s/%s" % (person.name, project.name), person_project)
+
+ def test_no_such_person_distribution(self):
+ # `traverse` raises `NoSuchProduct` when resolving a path of
+ # '~person/distro' if the distribution doesn't exist. That's
+ # because it can't tell the difference between the name of a project
+ # that doesn't exist and the name of a distribution that doesn't
+ # exist.
+ self.factory.makePerson(name="person")
+ self.assertRaises(
+ NoSuchProduct, self.traverser.traverse,
+ "~person/distro/+source/package")
+
+ def test_missing_person_sourcepackagename(self):
+ # `traverse` raises `InvalidNamespace` if there are no segments
+ # after '+source' in a person-DSP path.
+ self.factory.makePerson(name="person")
+ self.factory.makeDistribution(name="distro")
+ self.assertRaises(
+ InvalidNamespace, self.traverser.traverse,
+ "~person/distro/+source")
+
+ def test_no_such_person_sourcepackagename(self):
+ # `traverse` raises `NoSuchSourcePackageName` if the package in
+ # ~person/distro/+source/package doesn't exist.
+ self.factory.makePerson(name="person")
+ self.factory.makeDistribution(name="distro")
+ self.assertRaises(
+ NoSuchSourcePackageName, self.traverser.traverse,
+ "~person/distro/+source/nonexistent")
+
+ def test_person_package(self):
+ # `traverse` resolves '~person/distro/+source/package' to a
+ # PersonDistributionSourcePackage.
+ person = self.factory.makePerson()
+ dsp = self.factory.makeDistributionSourcePackage()
+ person_dsp = getUtility(
+ IPersonDistributionSourcePackageFactory).create(person, dsp)
+ path = "~%s/%s/+source/%s" % (
+ person.name, dsp.distribution.name, dsp.sourcepackagename.name)
+ self.assertTraverses(path, person_dsp)
=== modified file 'lib/lp/code/model/tests/test_gitrepository.py'
--- lib/lp/code/model/tests/test_gitrepository.py 2015-02-23 14:47:40 +0000
+++ lib/lp/code/model/tests/test_gitrepository.py 2015-02-23 14:47:40 +0000
@@ -29,7 +29,10 @@
IGitNamespacePolicy,
IGitNamespaceSet,
)
-from lp.code.interfaces.gitrepository import IGitRepository
+from lp.code.interfaces.gitrepository import (
+ IGitRepository,
+ IGitRepositorySet,
+ )
from lp.registry.enums import BranchSharingPolicy
from lp.registry.interfaces.persondistributionsourcepackage import (
IPersonDistributionSourcePackageFactory,
@@ -486,3 +489,27 @@
self.assertRaises(
GitTargetError, repository.setTarget,
target=commercial_project, user=owner)
+
+
+class TestGitRepositorySet(TestCaseWithFactory):
+
+ layer = DatabaseFunctionalLayer
+
+ def test_provides_IGitRepositorySet(self):
+ # GitRepositorySet instances provide IGitRepositorySet.
+ verifyObject(IGitRepositorySet, getUtility(IGitRepositorySet))
+
+ def test_getByPath(self):
+ # getByPath returns a repository matching the path that it's given.
+ a = self.factory.makeGitRepository()
+ self.factory.makeGitRepository()
+ repository = getUtility(IGitRepositorySet).getByPath(
+ a.owner, a.shortened_path)
+ self.assertEqual(a, repository)
+
+ def test_getByPath_not_found(self):
+ # If a repository cannot be found for a path, then getByPath returns
+ # None.
+ person = self.factory.makePerson()
+ self.assertIsNone(
+ getUtility(IGitRepositorySet).getByPath(person, "nonexistent"))
Follow ups