← Back to team overview

launchpad-reviewers team mailing list archive

Re: [Merge] lp:~cjwatson/launchpad/git-lookup into lp:launchpad

 

Review: Needs Fixing code



Diff comments:

> === 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."""

obj == provided? "provided" also usually indicates an interface, not an implementation.

> +    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)

I suspect this whole module might be cleaner if the traversers returned the person and target dimensions separately. All this duplicated code would disappear, as a person wouldn't be traversable -- they'd just be an element in the tuple returned from RootGitTraversable.

In that case this could probably also be used by GitNamespaceSet.traverse, rather than further duplicating most of that code here.

> +
> +
> +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

getByPath can raise a tonne of exceptions, while getByUrl and getByUniqueName just return None.

> 
> === 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

Any reason to not use visibleByUser directly?

> +        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"))
> 


-- 
https://code.launchpad.net/~cjwatson/launchpad/git-lookup/+merge/250628
Your team Launchpad code reviewers is subscribed to branch lp:launchpad.


References