← Back to team overview

launchpad-reviewers team mailing list archive

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

 

Review: Approve code



Diff comments:

> === modified file 'lib/lp/code/configure.zcml'
> --- lib/lp/code/configure.zcml	2015-02-26 17:17:42 +0000
> +++ lib/lp/code/configure.zcml	2015-02-26 17:17:43 +0000
> @@ -865,6 +865,24 @@
>    <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.GitTraverser"
> +      provides="lp.code.interfaces.gitlookup.IGitTraverser">
> +    <allow interface="lp.code.interfaces.gitlookup.IGitTraverser" />
> +  </securedutility>
> +  <adapter factory="lp.code.model.gitlookup.PersonGitTraversable" />
> +  <adapter factory="lp.code.model.gitlookup.ProjectGitTraversable" />
> +  <adapter factory="lp.code.model.gitlookup.DistributionGitTraversable" />
> +  <adapter factory="lp.code.model.gitlookup.DistributionSourcePackageGitTraversable" />
> +
>    <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-26 17:17:43 +0000
> @@ -0,0 +1,137 @@
> +# 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__ = [
> +    'IGitLookup',
> +    'IGitTraversable',
> +    'IGitTraverser',
> +    ]
> +
> +from zope.interface import Interface
> +
> +
> +class IGitTraversable(Interface):
> +    """A thing that can be traversed to find a thing with a Git repository."""
> +
> +    def traverse(owner, name, segments):
> +        """Return the object beneath this one that matches 'name'.
> +
> +        :param owner: The current `IPerson` context, or None.
> +        :param name: The name of the object being traversed to.
> +        :param segments: An iterator over remaining path segments.
> +        :return: A tuple of
> +            * an `IPerson`, or None;
> +            * an `IGitTraversable`;
> +            * an `IGitRepository`, or None; if this is non-None then
> +              traversing should stop.
> +        """
> +
> +
> +class IGitTraverser(Interface):
> +    """Utility for traversing to an object that can have a Git repository."""
> +
> +    def traverse(segments):
> +        """Traverse to the object referred to by a prefix of the 'segments'
> +        iterable.
> +
> +        :raises InvalidNamespace: If the path cannot be parsed as a
> +            repository namespace.
> +        :raises InvalidProductName: If the project component of the path is
> +            not a valid name.
> +        :raises NoSuchGitRepository: If there is a '+git' segment, but the
> +            following segment doesn't match an existing Git repository.
> +        :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: A tuple of::
> +            * an `IPerson`, or None;
> +            * an `IHasGitRepositories`;
> +            * an `IGitRepository`, or None.
> +        """
> +
> +    def traverse_path(path):
> +        """Traverse to the object referred to by 'path'.
> +
> +        All segments of 'path' must be consumed.

Why must the path be fully consumed, but the segments not?

> +
> +        :raises InvalidNamespace: If the path cannot be parsed as a
> +            repository namespace.
> +        :raises InvalidProductName: If the project component of the path is
> +            not a valid name.
> +        :raises NoSuchGitRepository: If there is a '+git' segment, but the
> +            following segment doesn't match an existing Git repository.
> +        :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: A tuple of::
> +            * an `IPerson`, or None;
> +            * an `IHasGitRepositories`;
> +            * an `IGitRepository`, or None.
> +        """
> +
> +
> +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.
> +        """

I'd drop the "Hosting", as I immediately thought that this directly converted a URI to a turnip path.

> +
> +    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
> +
> +        :return: An `IGitRepository`, or None.
> +        """
> 
> === modified file 'lib/lp/code/interfaces/gitnamespace.py'
> --- lib/lp/code/interfaces/gitnamespace.py	2015-02-13 18:34:45 +0000
> +++ lib/lp/code/interfaces/gitnamespace.py	2015-02-26 17:17:43 +0000
> @@ -148,83 +148,6 @@
>      def get(person, project=None, distribution=None, sourcepackagename=None):
>          """Return the appropriate `IGitNamespace` for the given objects."""
>  
> -    def interpret(person, project, distribution, sourcepackagename):
> -        """Like `get`, but takes names of objects.
> -
> -        :raise NoSuchPerson: If the person referred to cannot be found.
> -        :raise NoSuchProduct: If the project referred to cannot be found.
> -        :raise NoSuchDistribution: If the distribution referred to cannot be
> -            found.
> -        :raise NoSuchSourcePackageName: If the sourcepackagename referred to
> -            cannot be found.
> -        :return: An `IGitNamespace`.
> -        """
> -
> -    def parse(namespace_name):
> -        """Parse 'namespace_name' into its components.
> -
> -        The name of a namespace is actually a path containing many elements,
> -        each of which maps to a particular kind of object in Launchpad.
> -        Elements that can appear in a namespace name are: 'person',
> -        'project', 'distribution', and 'sourcepackagename'.
> -
> -        `parse` returns a dict which maps the names of these elements (e.g.
> -        'person', 'project') to the values of these elements (e.g. 'mark',
> -        'firefox').  If the given path doesn't include a particular kind of
> -        element, the dict maps that element name to None.
> -
> -        For example::
> -            parse('~foo/bar') => {
> -                'person': 'foo', 'project': 'bar', 'distribution': None,
> -                'sourcepackagename': None,
> -                }
> -
> -        If the given 'namespace_name' cannot be parsed, then we raise an
> -        `InvalidNamespace` error.
> -
> -        :raise InvalidNamespace: If the name is too long, too short, or
> -            malformed.
> -        :return: A dict with keys matching each component in
> -            'namespace_name'.
> -        """
> -
> -    def lookup(namespace_name):
> -        """Return the `IGitNamespace` for 'namespace_name'.
> -
> -        :raise InvalidNamespace: if namespace_name cannot be parsed.
> -        :raise NoSuchPerson: if the person referred to cannot be found.
> -        :raise NoSuchProduct: if the project referred to cannot be found.
> -        :raise NoSuchDistribution: if the distribution referred to cannot be
> -            found.
> -        :raise NoSuchSourcePackageName: if the sourcepackagename referred to
> -            cannot be found.
> -        :return: An `IGitNamespace`.
> -        """
> -
> -    def traverse(segments):
> -        """Look up the Git repository at the path given by 'segments'.
> -
> -        The iterable 'segments' will be consumed until a repository is
> -        found.  As soon as a repository is found, the repository will be
> -        returned and the consumption of segments will stop.  Thus, there
> -        will often be unconsumed segments that can be used for further
> -        traversal.
> -
> -        :param segments: An iterable of URL segments, a prefix of which
> -            identifies a Git repository.  The first segment is the username,
> -            *not* preceded by a '~`.
> -        :raise InvalidNamespace: if there are not enough segments to define a
> -            repository.
> -        :raise NoSuchPerson: if the person referred to cannot be found.
> -        :raise NoSuchProduct: if the product or distro referred to cannot be
> -            found.
> -        :raise NoSuchDistribution: if the distribution referred to cannot be
> -            found.
> -        :raise NoSuchSourcePackageName: if the sourcepackagename referred to
> -            cannot be found.
> -        :return: `IGitRepository`.
> -        """
> -
>  
>  def get_git_namespace(target, owner):
>      if IProduct.providedBy(target):
> 
> === modified file 'lib/lp/code/model/branchlookup.py'
> --- lib/lp/code/model/branchlookup.py	2015-01-29 13:09:37 +0000
> +++ lib/lp/code/model/branchlookup.py	2015-02-26 17:17:43 +0000
> @@ -72,13 +72,13 @@
>  from lp.services.webapp.authorization import check_permission
>  
>  
> -def adapt(provided, interface):
> +def adapt(obj, interface):
>      """Adapt 'obj' to 'interface', using multi-adapters if necessary."""
> -    required = interface(provided, None)
> +    required = interface(obj, None)
>      if required is not None:
>          return required
>      try:
> -        return queryMultiAdapter(provided, interface)
> +        return queryMultiAdapter(obj, interface)
>      except TypeError:
>          return None
>  
> 
> === 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-26 17:17:43 +0000
> @@ -0,0 +1,349 @@
> +# 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.errors import NameLookupFailed
> +from lp.app.validators.name import valid_name
> +from lp.code.errors import (
> +    InvalidNamespace,
> +    NoSuchGitRepository,
> +    )
> +from lp.code.interfaces.gitlookup import (
> +    IGitLookup,
> +    IGitTraversable,
> +    IGitTraverser,
> +    )
> +from lp.code.interfaces.gitnamespace import IGitNamespaceSet
> +from lp.code.interfaces.gitrepository import IGitRepositorySet
> +from lp.code.interfaces.hasgitrepositories import IHasGitRepositories
> +from lp.code.model.gitrepository import GitRepository
> +from lp.registry.errors import NoSuchSourcePackageName
> +from lp.registry.interfaces.distribution import IDistribution
> +from lp.registry.interfaces.distributionsourcepackage import (
> +    IDistributionSourcePackage,
> +    )
> +from lp.registry.interfaces.person import (
> +    IPerson,
> +    IPersonSet,
> +    NoSuchPerson,
> +    )
> +from lp.registry.interfaces.pillar import IPillarNameSet
> +from lp.registry.interfaces.product import (
> +    InvalidProductName,
> +    IProduct,
> +    NoSuchProduct,
> +    )
> +from lp.services.config import config
> +from lp.services.database.interfaces import IStore
> +
> +
> +def adapt(obj, interface):
> +    """Adapt 'obj' to 'interface', using multi-adapters if necessary."""
> +    required = interface(obj, None)
> +    if required is not None:
> +        return required
> +    try:
> +        return queryMultiAdapter(obj, interface)
> +    except TypeError:
> +        return None
> +
> +
> +class RootGitTraversable:
> +    """Root traversable for Git repository objects.
> +
> +    Corresponds to '/' in the path.  From here, you can traverse to a
> +    project or a distribution, optionally with a person context as well.
> +    """
> +
> +    implements(IGitTraversable)
> +
> +    # Marker for references to Git URL layouts: ##GITNAMESPACE##
> +    def traverse(self, owner, name, segments):
> +        """See `IGitTraversable`.
> +
> +        :raises InvalidProductName: If 'name' is not a valid name.
> +        :raises NoSuchPerson: If 'name' begins with a '~', but the remainder
> +            doesn't match an existing person.
> +        :raises NoSuchProduct: If 'name' doesn't match an existing pillar.
> +        :return: A tuple of (`IPerson`, `IPillar`, None).
> +        """
> +        assert owner is None
> +        if name.startswith("~"):
> +            owner_name = name[1:]
> +            owner = getUtility(IPersonSet).getByName(owner_name)
> +            if owner is None:
> +                raise NoSuchPerson(owner_name)
> +            return owner, owner, None
> +        else:
> +            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 owner, pillar, None
> +
> +
> +class _BaseGitTraversable:
> +    """Base class for traversable implementations."""
> +
> +    def __init__(self, context):
> +        self.context = context
> +
> +    # Marker for references to Git URL layouts: ##GITNAMESPACE##
> +    def traverse(self, owner, name, segments):
> +        """See `IGitTraversable`.
> +
> +        :raises InvalidNamespace: If 'name' is not '+git', or there is no
> +            owner, or there are no further segments.
> +        :raises NoSuchGitRepository: If the segment after '+git' doesn't
> +            match an existing Git repository.
> +        :return: A tuple of (`IPerson`, `IHasGitRepositories`,
> +            `IGitRepository`).
> +        """
> +        if owner is None or name != "+git":
> +            raise InvalidNamespace("/".join(segments.traversed))
> +        try:
> +            repository_name = next(segments)
> +        except StopIteration:
> +            raise InvalidNamespace("/".join(segments.traversed))
> +        repository = self.getNamespace(owner).getByName(repository_name)
> +        if repository is None:
> +            raise NoSuchGitRepository(repository_name)
> +        return owner, self.context, repository
> +
> +
> +class ProjectGitTraversable(_BaseGitTraversable):
> +    """Git repository traversable for projects.
> +
> +    From here, you can traverse to a named project repository.
> +    """
> +
> +    adapts(IProduct)
> +    implements(IGitTraversable)
> +
> +    def getNamespace(self, owner):
> +        return getUtility(IGitNamespaceSet).get(owner, project=self.context)
> +
> +
> +class DistributionGitTraversable(_BaseGitTraversable):
> +    """Git repository traversable for distributions.
> +
> +    From here, you can traverse to a distribution source package.
> +    """
> +
> +    adapts(IDistribution)
> +    implements(IGitTraversable)
> +
> +    # Marker for references to Git URL layouts: ##GITNAMESPACE##
> +    def traverse(self, owner, name, segments):
> +        """See `IGitTraversable`.
> +
> +        :raises InvalidNamespace: If 'name' is not '+source' or there are no
> +            further segments.
> +        :raises NoSuchSourcePackageName: If the segment after '+source'
> +            doesn't match an existing source package name.
> +        :return: A tuple of (`IPerson`, `IDistributionSourcePackage`, None).
> +        """
> +        # Distributions don't support named repositories themselves, so
> +        # ignore the base traverse method.
> +        if name != "+source":
> +            raise InvalidNamespace("/".join(segments.traversed))
> +        try:
> +            spn_name = next(segments)
> +        except StopIteration:
> +            raise InvalidNamespace("/".join(segments.traversed))
> +        distro_source_package = self.context.getSourcePackage(spn_name)
> +        if distro_source_package is None:
> +            raise NoSuchSourcePackageName(spn_name)
> +        return owner, distro_source_package, None
> +
> +
> +class DistributionSourcePackageGitTraversable(_BaseGitTraversable):
> +    """Git repository traversable for distribution source packages.
> +
> +    From here, you can traverse to a named package repository.
> +    """
> +
> +    adapts(IDistributionSourcePackage)
> +    implements(IGitTraversable)
> +
> +    def getNamespace(self, owner):
> +        return getUtility(IGitNamespaceSet).get(
> +            owner, distribution=self.context.distribution,
> +            sourcepackagename=self.context.sourcepackagename)
> +
> +
> +class PersonGitTraversable(_BaseGitTraversable):
> +    """Git repository traversable for people.
> +
> +    From here, you can traverse to a named personal repository, or to a
> +    project or a distribution with a person context.
> +    """
> +
> +    adapts(IPerson)
> +    implements(IGitTraversable)
> +
> +    def getNamespace(self, owner):
> +        return getUtility(IGitNamespaceSet).get(owner)
> +
> +    # Marker for references to Git URL layouts: ##GITNAMESPACE##
> +    def traverse(self, owner, name, segments):
> +        """See `IGitTraversable`.
> +
> +        :raises InvalidNamespace: If 'name' is '+git' and there are no
> +            further segments.
> +        :raises InvalidProductName: If 'name' is not '+git' and is not a
> +            valid name.
> +        :raises NoSuchGitRepository: If the segment after '+git' doesn't
> +            match an existing Git repository.
> +        :raises NoSuchProduct: If 'name' is not '+git' and doesn't match an
> +            existing pillar.
> +        :return: A tuple of (`IPerson`, `IHasGitRepositories`,
> +            `IGitRepository`).
> +        """
> +        if name == "+git":
> +            return super(PersonGitTraversable, self).traverse(
> +                owner, name, segments)
> +        else:
> +            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 owner, pillar, None
> +
> +
> +class SegmentIterator:
> +    """An iterator that remembers the elements it has traversed."""
> +
> +    def __init__(self, iterator):
> +        self._iterator = iterator
> +        self.traversed = []
> +
> +    def next(self):
> +        segment = next(self._iterator)
> +        if not isinstance(segment, unicode):
> +            segment = segment.decode("US-ASCII")
> +        self.traversed.append(segment)
> +        return segment
> +
> +
> +class GitTraverser:
> +    """Utility for traversing to objects that can have Git repositories."""
> +
> +    implements(IGitTraverser)
> +
> +    def traverse(self, segments):
> +        """See `IGitTraverser`."""
> +        owner = None
> +        target = None
> +        repository = None
> +        traversable = RootGitTraversable()
> +        segments_iter = SegmentIterator(segments)
> +        while True:

Should this be "while traversable"? Otherwise I think eg. ~wgrant/launchpad-project will crash.

> +            try:
> +                name = next(segments_iter)
> +            except StopIteration:
> +                break
> +            owner, target, repository = traversable.traverse(
> +                owner, name, segments_iter)
> +            if repository is not None:
> +                break
> +            traversable = adapt(target, IGitTraversable)
> +        if target is None or not IHasGitRepositories.providedBy(target):
> +            raise InvalidNamespace("/".join(segments_iter.traversed))
> +        return owner, target, repository
> +
> +    def traverse_path(self, path):
> +        """See `IGitTraverser`."""
> +        segments = iter(path.split("/"))
> +        owner, target, repository = self.traverse(segments)
> +        if list(segments):
> +            raise InvalidNamespace(path)
> +        return owner, target, repository
> +
> +
> +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
> +        return self.getByPath(path)
> +
> +    def getByUniqueName(self, unique_name):
> +        """See `IGitLookup`."""
> +        try:
> +            if unique_name.startswith("~"):
> +                segments = iter(unique_name.split("/"))
> +                _, _, repository = getUtility(IGitTraverser).traverse(segments)
> +                if repository is None or list(segments):
> +                    raise InvalidNamespace(unique_name)
> +                return repository
> +        except (InvalidNamespace, NameLookupFailed):
> +            pass
> +        return None
> +
> +    def getByPath(self, path):
> +        """See `IGitLookup`."""
> +        traverser = getUtility(IGitTraverser)
> +        try:
> +            owner, target, repository = traverser.traverse_path(path)
> +        except (InvalidNamespace, InvalidProductName, NameLookupFailed):
> +            return None
> +        if repository is not None:
> +            return repository
> +        repository_set = getUtility(IGitRepositorySet)
> +        if owner is None:
> +            return repository_set.getDefaultRepository(target)
> +        else:
> +            return repository_set.getDefaultRepositoryForOwner(owner, target)
> 
> === modified file 'lib/lp/code/model/gitnamespace.py'
> --- lib/lp/code/model/gitnamespace.py	2015-02-13 18:34:45 +0000
> +++ lib/lp/code/model/gitnamespace.py	2015-02-26 17:17:43 +0000
> @@ -30,8 +30,6 @@
>      GitRepositoryCreatorNotMemberOfOwnerTeam,
>      GitRepositoryCreatorNotOwner,
>      GitRepositoryExists,
> -    InvalidNamespace,
> -    NoSuchGitRepository,
>      )
>  from lp.code.interfaces.gitnamespace import (
>      IGitNamespace,
> @@ -50,27 +48,9 @@
>      )
>  from lp.code.model.gitrepository import GitRepository
>  from lp.registry.enums import PersonVisibility
> -from lp.registry.errors import NoSuchSourcePackageName
> -from lp.registry.interfaces.distribution import (
> -    IDistribution,
> -    IDistributionSet,
> -    NoSuchDistribution,
> -    )
>  from lp.registry.interfaces.distributionsourcepackage import (
>      IDistributionSourcePackage,
>      )
> -from lp.registry.interfaces.person import (
> -    IPersonSet,
> -    NoSuchPerson,
> -    )
> -from lp.registry.interfaces.pillar import IPillarNameSet
> -from lp.registry.interfaces.product import (
> -    IProduct,
> -    IProductSet,
> -    NoSuchProduct,
> -    )
> -from lp.registry.interfaces.projectgroup import IProjectGroup
> -from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet
>  from lp.services.database.constants import DEFAULT
>  from lp.services.database.interfaces import IStore
>  from lp.services.propertycache import get_property_cache
> @@ -397,142 +377,3 @@
>                  person, distribution.getSourcePackage(sourcepackagename))
>          else:
>              return PersonalGitNamespace(person)
> -
> -    def _findOrRaise(self, error, name, finder, *args):
> -        if name is None:
> -            return None
> -        args = list(args)
> -        args.append(name)
> -        result = finder(*args)
> -        if result is None:
> -            raise error(name)
> -        return result
> -
> -    def _findPerson(self, person_name):
> -        return self._findOrRaise(
> -            NoSuchPerson, person_name, getUtility(IPersonSet).getByName)
> -
> -    # Marker for references to Git URL layouts: ##GITNAMESPACE##
> -    def _findPillar(self, pillar_name):
> -        """Find and return the pillar with the given name.
> -
> -        If the given name is '+git' (indicating a personal repository) or
> -        None, return None.
> -
> -        :raise NoSuchProduct if there's no pillar with the given name or it
> -            is a project group.
> -        """
> -        if pillar_name == "+git":
> -            return None
> -        pillar = self._findOrRaise(
> -            NoSuchProduct, pillar_name, getUtility(IPillarNameSet).getByName)
> -        if IProjectGroup.providedBy(pillar):
> -            raise NoSuchProduct(pillar_name)
> -        return pillar
> -
> -    def _findProject(self, project_name):
> -        return self._findOrRaise(
> -            NoSuchProduct, project_name, getUtility(IProductSet).getByName)
> -
> -    def _findDistribution(self, distribution_name):
> -        return self._findOrRaise(
> -            NoSuchDistribution, distribution_name,
> -            getUtility(IDistributionSet).getByName)
> -
> -    def _findSourcePackageName(self, sourcepackagename_name):
> -        return self._findOrRaise(
> -            NoSuchSourcePackageName, sourcepackagename_name,
> -            getUtility(ISourcePackageNameSet).queryByName)
> -
> -    def _realize(self, names):
> -        """Turn a dict of object names into a dict of objects.
> -
> -        Takes the results of `IGitNamespaceSet.parse` and turns them into a
> -        dict where the values are Launchpad objects.
> -        """
> -        data = {}
> -        data["person"] = self._findPerson(names["person"])
> -        data["project"] = self._findProject(names["project"])
> -        data["distribution"] = self._findDistribution(names["distribution"])
> -        data["sourcepackagename"] = self._findSourcePackageName(
> -            names["sourcepackagename"])
> -        return data
> -
> -    def interpret(self, person, project, distribution, sourcepackagename):
> -        names = dict(
> -            person=person, project=project, distribution=distribution,
> -            sourcepackagename=sourcepackagename)
> -        data = self._realize(names)
> -        return self.get(**data)
> -
> -    # Marker for references to Git URL layouts: ##GITNAMESPACE##
> -    def parse(self, namespace_name):
> -        """See `IGitNamespaceSet`."""
> -        data = dict(
> -            person=None, project=None, distribution=None,
> -            sourcepackagename=None)
> -        tokens = namespace_name.split("/")
> -        if len(tokens) == 1:
> -            data["person"] = tokens[0]
> -        elif len(tokens) == 2:
> -            data["person"] = tokens[0]
> -            data["project"] = tokens[1]
> -        elif len(tokens) == 4 and tokens[2] == "+source":
> -            data["person"] = tokens[0]
> -            data["distribution"] = tokens[1]
> -            data["sourcepackagename"] = tokens[3]
> -        else:
> -            raise InvalidNamespace(namespace_name)
> -        if not data["person"].startswith("~"):
> -            raise InvalidNamespace(namespace_name)
> -        data["person"] = data["person"][1:]
> -        return data
> -
> -    def lookup(self, namespace_name):
> -        """See `IGitNamespaceSet`."""
> -        names = self.parse(namespace_name)
> -        return self.interpret(**names)
> -
> -    # Marker for references to Git URL layouts: ##GITNAMESPACE##
> -    def traverse(self, segments):
> -        """See `IGitNamespaceSet`."""
> -        traversed_segments = []
> -
> -        def get_next_segment():
> -            try:
> -                result = segments.next()
> -            except StopIteration:
> -                raise InvalidNamespace("/".join(traversed_segments))
> -            if result is None:
> -                raise AssertionError("None segment passed to traverse()")
> -            if not isinstance(result, unicode):
> -                result = result.decode("US-ASCII")
> -            traversed_segments.append(result)
> -            return result
> -
> -        person_name = get_next_segment()
> -        person = self._findPerson(person_name)
> -        pillar_name = get_next_segment()
> -        pillar = self._findPillar(pillar_name)
> -        if pillar is None:
> -            namespace = self.get(person)
> -            git_literal = pillar_name
> -        elif IProduct.providedBy(pillar):
> -            namespace = self.get(person, project=pillar)
> -            git_literal = get_next_segment()
> -        else:
> -            source_literal = get_next_segment()
> -            if source_literal != "+source":
> -                raise InvalidNamespace("/".join(traversed_segments))
> -            sourcepackagename_name = get_next_segment()
> -            sourcepackagename = self._findSourcePackageName(
> -                sourcepackagename_name)
> -            namespace = self.get(
> -                person, distribution=IDistribution(pillar),
> -                sourcepackagename=sourcepackagename)
> -            git_literal = get_next_segment()
> -        if git_literal != "+git":
> -            raise InvalidNamespace("/".join(traversed_segments))
> -        repository_name = get_next_segment()
> -        return self._findOrRaise(
> -            NoSuchGitRepository, repository_name, namespace.getByName)
> 
> === modified file 'lib/lp/code/model/gitrepository.py'
> --- lib/lp/code/model/gitrepository.py	2015-02-26 17:17:42 +0000
> +++ lib/lp/code/model/gitrepository.py	2015-02-26 17:17:43 +0000
> @@ -39,6 +39,7 @@
>      GitDefaultConflict,
>      GitTargetError,
>      )
> +from lp.code.interfaces.gitlookup import IGitLookup
>  from lp.code.interfaces.gitnamespace import (
>      get_git_namespace,
>      IGitNamespacePolicy,
> @@ -357,8 +358,10 @@
>  
>      def getByPath(self, user, path):
>          """See `IGitRepositorySet`."""
> -        # XXX cjwatson 2015-02-06: Fill this in once IGitLookup is in place.
> -        raise NotImplementedError
> +        repository = getUtility(IGitLookup).getByPath(path)
> +        if repository is not None and repository.visibleByUser(user):
> +            return repository
> +        return None
>  
>      def getDefaultRepository(self, target):
>          """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-26 17:17:43 +0000
> @@ -0,0 +1,440 @@
> +# 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 (
> +    InvalidNamespace,
> +    NoSuchGitRepository,
> +    )
> +from lp.code.interfaces.gitlookup import (
> +    IGitLookup,
> +    IGitTraverser,
> +    )
> +from lp.code.interfaces.gitrepository import IGitRepositorySet
> +from lp.registry.errors import NoSuchSourcePackageName
> +from lp.registry.interfaces.person import NoSuchPerson
> +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):
> +            getUtility(IGitRepositorySet).setDefaultRepository(
> +                repository.target, 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):
> +            getUtility(IGitRepositorySet).setDefaultRepository(
> +                repository.target, 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_invalid_namespace(self):
> +        # If `getByPath` is given a path to something with no default Git
> +        # repository, such as a distribution, it returns None.
> +        distro = self.factory.makeDistribution()
> +        self.assertIsNone(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 returns None.
> +        project = self.factory.makeProduct()
> +        self.assertIsNone(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):
> +            getUtility(IGitRepositorySet).setDefaultRepository(
> +                repository.target, 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 TestGitTraverser(TestCaseWithFactory):
> +    """Tests for the repository traverser."""
> +
> +    layer = DatabaseFunctionalLayer
> +
> +    def setUp(self):
> +        super(TestGitTraverser, self).setUp()
> +        self.traverser = getUtility(IGitTraverser)
> +
> +    def assertTraverses(self, path, owner, target, repository=None):
> +        self.assertEqual(
> +            (owner, target, repository), self.traverser.traverse_path(path))
> +
> +    def test_nonexistent_project(self):
> +        # `traverse_path` raises `NoSuchProduct` when resolving a path of
> +        # 'project' if the project doesn't exist.
> +        self.assertRaises(NoSuchProduct, self.traverser.traverse_path, "bb")
> +
> +    def test_invalid_project(self):
> +        # `traverse_path` raises `InvalidProductName` when resolving a path
> +        # for a completely invalid default project repository.
> +        self.assertRaises(
> +            InvalidProductName, self.traverser.traverse_path, "b")
> +
> +    def test_project(self):
> +        # `traverse_path` resolves the name of a project to the project itself.
> +        project = self.factory.makeProduct()
> +        self.assertTraverses(project.name, None, project)
> +
> +    def test_project_no_named_repositories(self):
> +        # Projects do not have named repositories without an owner context,
> +        # so trying to traverse to them raises `InvalidNamespace`.
> +        project = self.factory.makeProduct()
> +        repository = self.factory.makeGitRepository(target=project)
> +        self.assertRaises(
> +            InvalidNamespace, self.traverser.traverse_path,
> +            "%s/+git/%s" % (project.name, repository.name))
> +
> +    def test_no_such_distribution(self):
> +        # `traverse_path` 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_path,
> +            "distro/+source/package")
> +
> +    def test_missing_sourcepackagename(self):
> +        # `traverse_path` raises `InvalidNamespace` if there are no segments
> +        # after '+source'.
> +        self.factory.makeDistribution(name="distro")
> +        self.assertRaises(
> +            InvalidNamespace, self.traverser.traverse_path, "distro/+source")
> +
> +    def test_no_such_sourcepackagename(self):
> +        # `traverse_path` raises `NoSuchSourcePackageName` if the package in
> +        # distro/+source/package doesn't exist.
> +        self.factory.makeDistribution(name="distro")
> +        self.assertRaises(
> +            NoSuchSourcePackageName, self.traverser.traverse_path,
> +            "distro/+source/nonexistent")
> +
> +    def test_package(self):
> +        # `traverse_path` 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, None, dsp)
> +
> +    def test_package_no_named_repositories(self):
> +        # Packages do not have named repositories without an owner context,
> +        # so trying to traverse to them raises `InvalidNamespace`.
> +        dsp = self.factory.makeDistributionSourcePackage()
> +        repository = self.factory.makeGitRepository(target=dsp)
> +        self.assertRaises(
> +            InvalidNamespace, self.traverser.traverse_path,
> +            "%s/+source/%s/+git/%s" % (
> +                dsp.distribution.name, dsp.sourcepackagename.name,
> +                repository.name))
> +
> +    def test_nonexistent_person(self):
> +        # `traverse_path` raises `NoSuchPerson` when resolving a path of
> +        # '~person/project' if the person doesn't exist.
> +        self.assertRaises(
> +            NoSuchPerson, self.traverser.traverse_path, "~person/bb")
> +
> +    def test_nonexistent_person_project(self):
> +        # `traverse_path` 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_path, "~person/bb")
> +
> +    def test_invalid_person_project(self):
> +        # `traverse_path` 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_path, "~person/b")
> +
> +    def test_person_missing_repository_name(self):
> +        # `traverse_path` raises `InvalidNamespace` if there are no segments
> +        # after '+git'.
> +        self.factory.makePerson(name="person")
> +        self.assertRaises(
> +            InvalidNamespace, self.traverser.traverse_path, "~person/+git")
> +
> +    def test_person_no_such_repository(self):
> +        # `traverse_path` raises `NoSuchGitRepository` if the repository in
> +        # project/+git/repository doesn't exist.
> +        self.factory.makePerson(name="person")
> +        self.assertRaises(
> +            NoSuchGitRepository, self.traverser.traverse_path,
> +            "~person/+git/repository")
> +
> +    def test_person_repository(self):
> +        # `traverse_path` resolves an existing project repository.
> +        person = self.factory.makePerson(name="person")
> +        repository = self.factory.makeGitRepository(
> +            owner=person, target=person, name=u"repository")
> +        self.assertTraverses(
> +            "~person/+git/repository", person, person, repository)
> +
> +    def test_person_project(self):
> +        # `traverse_path` resolves '~person/project' to the person and the
> +        # project.
> +        person = self.factory.makePerson()
> +        project = self.factory.makeProduct()
> +        self.assertTraverses(
> +            "~%s/%s" % (person.name, project.name), person, project)
> +
> +    def test_person_project_missing_repository_name(self):
> +        # `traverse_path` raises `InvalidNamespace` if there are no segments
> +        # after '+git'.
> +        person = self.factory.makePerson()
> +        project = self.factory.makeProduct()
> +        self.assertRaises(
> +            InvalidNamespace, self.traverser.traverse_path,
> +            "~%s/%s/+git" % (person.name, project.name))
> +
> +    def test_person_project_no_such_repository(self):
> +        # `traverse_path` raises `NoSuchGitRepository` if the repository in
> +        # ~person/project/+git/repository doesn't exist.
> +        person = self.factory.makePerson()
> +        project = self.factory.makeProduct()
> +        self.assertRaises(
> +            NoSuchGitRepository, self.traverser.traverse_path,
> +            "~%s/%s/+git/nonexistent" % (person.name, project.name))
> +
> +    def test_person_project_repository(self):
> +        # `traverse_path` resolves an existing person-project repository.
> +        person = self.factory.makePerson()
> +        project = self.factory.makeProduct()
> +        repository = self.factory.makeGitRepository(
> +            owner=person, target=project)
> +        self.assertTraverses(
> +            "~%s/%s/+git/%s" % (person.name, project.name, repository.name),
> +            person, project, repository)
> +
> +    def test_no_such_person_distribution(self):
> +        # `traverse_path` 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_path,
> +            "~person/distro/+source/package")
> +
> +    def test_missing_person_sourcepackagename(self):
> +        # `traverse_path` 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_path,
> +            "~person/distro/+source")
> +
> +    def test_no_such_person_sourcepackagename(self):
> +        # `traverse_path` 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_path,
> +            "~person/distro/+source/nonexistent")
> +
> +    def test_person_package(self):
> +        # `traverse_path` resolves '~person/distro/+source/package' to the
> +        # person and the DSP.
> +        person = self.factory.makePerson()
> +        dsp = self.factory.makeDistributionSourcePackage()
> +        path = "~%s/%s/+source/%s" % (
> +            person.name, dsp.distribution.name, dsp.sourcepackagename.name)
> +        self.assertTraverses(path, person, dsp)
> +
> +    def test_person_package_missing_repository_name(self):
> +        # `traverse_path` raises `InvalidNamespace` if there are no segments
> +        # after '+git'.
> +        person = self.factory.makePerson()
> +        dsp = self.factory.makeDistributionSourcePackage()
> +        self.assertRaises(
> +            InvalidNamespace, self.traverser.traverse_path,
> +            "~%s/%s/+source/%s/+git" % (
> +                person.name, dsp.distribution.name,
> +                dsp.sourcepackagename.name))
> +
> +    def test_person_package_no_such_repository(self):
> +        # `traverse_path` raises `NoSuchGitRepository` if the repository in
> +        # ~person/project/+git/repository doesn't exist.
> +        person = self.factory.makePerson()
> +        dsp = self.factory.makeDistributionSourcePackage()
> +        self.assertRaises(
> +            NoSuchGitRepository, self.traverser.traverse_path,
> +            "~%s/%s/+source/%s/+git/nonexistent" % (
> +                person.name, dsp.distribution.name,
> +                dsp.sourcepackagename.name))
> +
> +    def test_person_package_repository(self):
> +        # `traverse_path` resolves an existing person-package repository.
> +        person = self.factory.makePerson()
> +        dsp = self.factory.makeDistributionSourcePackage()
> +        repository = self.factory.makeGitRepository(owner=person, target=dsp)
> +        self.assertTraverses(
> +            "~%s/%s/+source/%s/+git/%s" % (
> +                person.name, dsp.distribution.name, dsp.sourcepackagename.name,
> +                repository.name),
> +            person, dsp, repository)
> 
> === modified file 'lib/lp/code/model/tests/test_gitrepository.py'
> --- lib/lp/code/model/tests/test_gitrepository.py	2015-02-26 17:17:42 +0000
> +++ lib/lp/code/model/tests/test_gitrepository.py	2015-02-26 17:17:43 +0000
> @@ -512,6 +512,19 @@
>          # GitRepositorySet instances provide IGitRepositorySet.
>          verifyObject(IGitRepositorySet, self.repository_set)
>  
> +    def test_getByPath(self):
> +        # getByPath returns a repository matching the path that it's given.
> +        a = self.factory.makeGitRepository()
> +        self.factory.makeGitRepository()
> +        repository = self.repository_set.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(self.repository_set.getByPath(person, "nonexistent"))
> +
>      def test_setDefaultRepository_refuses_person(self):
>          # setDefaultRepository refuses if the target is a person.
>          person = self.factory.makePerson()
> 


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


References