launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #17965
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