← Back to team overview

launchpad-reviewers team mailing list archive

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

 


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.

(As mentioned on IRC, but for the record:) This was because it's convenient that way for the callers; the ones that pass an iterable of segments do so because they're expecting to have more to traverse afterwards.  (For example my uncommitted browser Person traversal code uses this.)  Also with a path it's inconvenient to indicate what's left, whereas with an iterable of segments we can just consume the used ones.

> +
> +        :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.
> +        """
> +
> +    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:

That doesn't crash because there are no remaining segments after the thing that fails to adapt, but ~wgrant/launchpad-project/+git/foo would indeed raise AttributeError.  Will fix, thanks.

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