← Back to team overview

launchpad-reviewers team mailing list archive

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

 

Review: Needs Fixing code



Diff comments:

> === modified file 'lib/lp/code/configure.zcml'
> --- lib/lp/code/configure.zcml	2015-02-07 10:28:57 +0000
> +++ lib/lp/code/configure.zcml	2015-02-07 10:28:57 +0000
> @@ -838,6 +838,26 @@
>      <allow interface="lp.code.interfaces.gitrepository.IGitRepositorySet" />
>    </securedutility>
>  
> +  <!-- GitNamespace -->
> +
> +  <class class="lp.code.model.gitnamespace.PackageGitNamespace">
> +    <allow interface="lp.code.interfaces.gitnamespace.IGitNamespace" />
> +    <allow interface="lp.code.interfaces.gitnamespace.IGitNamespacePolicy" />
> +  </class>
> +  <class class="lp.code.model.gitnamespace.PersonalGitNamespace">
> +    <allow interface="lp.code.interfaces.gitnamespace.IGitNamespace" />
> +    <allow interface="lp.code.interfaces.gitnamespace.IGitNamespacePolicy" />
> +  </class>
> +  <class class="lp.code.model.gitnamespace.ProjectGitNamespace">
> +    <allow interface="lp.code.interfaces.gitnamespace.IGitNamespace" />
> +    <allow interface="lp.code.interfaces.gitnamespace.IGitNamespacePolicy" />
> +  </class>
> +  <securedutility
> +      class="lp.code.model.gitnamespace.GitNamespaceSet"
> +      provides="lp.code.interfaces.gitnamespace.IGitNamespaceSet">
> +    <allow interface="lp.code.interfaces.gitnamespace.IGitNamespaceSet" />
> +  </securedutility>
> +
>    <lp:help-folder folder="help" name="+help-code" />
>  
>    <!-- Diffs -->
> 
> === modified file 'lib/lp/code/errors.py'
> --- lib/lp/code/errors.py	2013-12-20 05:38:18 +0000
> +++ lib/lp/code/errors.py	2015-02-07 10:28:57 +0000
> @@ -28,11 +28,18 @@
>      'CodeImportNotInReviewedState',
>      'ClaimReviewFailed',
>      'DiffNotFound',
> +    'GitRepositoryCreationException',
> +    'GitRepositoryCreationFault',
> +    'GitRepositoryCreationForbidden',
> +    'GitRepositoryCreatorNotMemberOfOwnerTeam',
> +    'GitRepositoryCreatorNotOwner',
> +    'GitRepositoryExists',
>      'InvalidBranchMergeProposal',
>      'InvalidMergeQueueConfig',
>      'InvalidNamespace',
>      'NoLinkedBranch',
>      'NoSuchBranch',
> +    'NoSuchGitRepository',
>      'PrivateBranchRecipe',
>      'ReviewNotPending',
>      'StaleLastMirrored',
> @@ -312,6 +319,62 @@
>      """Raised when the user specifies an unrecognized branch type."""
>  
>  
> +class GitRepositoryCreationException(Exception):
> +    """Base class for Git repository creation exceptions."""
> +
> +
> +@error_status(httplib.CONFLICT)
> +class GitRepositoryExists(GitRepositoryCreationException):
> +    """Raised when creating a Git repository that already exists."""
> +
> +    def __init__(self, existing_repository):
> +        params = {
> +            "name": existing_repository.name,
> +            "context": existing_repository.namespace.name,
> +            }
> +        message = (
> +            'A Git repository with the name "%(name)s" already exists for '
> +            '%(context)s.' % params)
> +        self.existing_repository = existing_repository
> +        GitRepositoryCreationException.__init__(self, message)
> +
> +
> +class GitRepositoryCreationForbidden(GitRepositoryCreationException):
> +    """A visibility policy forbids Git repository creation.
> +
> +    The exception is raised if the policy for the project does not allow the
> +    creator of the repository to create a repository for that project.
> +    """
> +
> +
> +@error_status(httplib.BAD_REQUEST)
> +class GitRepositoryCreatorNotMemberOfOwnerTeam(GitRepositoryCreationException):
> +    """Git repository creator is not a member of the owner team.
> +
> +    Raised when a user is attempting to create a repository and set the
> +    owner of the repository to a team that they are not a member of.
> +    """
> +
> +
> +@error_status(httplib.BAD_REQUEST)
> +class GitRepositoryCreatorNotOwner(GitRepositoryCreationException):
> +    """A user cannot create a Git repository belonging to another user.
> +
> +    Raised when a user is attempting to create a repository and set the
> +    owner of the repository to another user.
> +    """
> +
> +
> +class GitRepositoryCreationFault(GitRepositoryCreationException):
> +    """Raised when there is a hosting fault creating a Git repository."""
> +
> +
> +class NoSuchGitRepository(NameLookupFailed):
> +    """Raised when we try to load a Git repository that does not exist."""
> +
> +    _message_prefix = "No such Git repository"
> +
> +
>  @error_status(httplib.BAD_REQUEST)
>  class CodeImportNotInReviewedState(Exception):
>      """Raised when the user requests an import of a non-automatic import."""
> 
> === added file 'lib/lp/code/interfaces/gitnamespace.py'
> --- lib/lp/code/interfaces/gitnamespace.py	1970-01-01 00:00:00 +0000
> +++ lib/lp/code/interfaces/gitnamespace.py	2015-02-07 10:28:57 +0000
> @@ -0,0 +1,260 @@
> +# Copyright 2015 Canonical Ltd.  This software is licensed under the
> +# GNU Affero General Public License version 3 (see the file LICENSE).
> +
> +"""Interface for a Git repository namespace."""
> +
> +__metaclass__ = type
> +__all__ = [
> +    'get_git_namespace',
> +    'get_git_namespace_for_target',
> +    'IGitNamespace',
> +    'IGitNamespacePolicy',
> +    'IGitNamespaceSet',
> +    'lookup_git_namespace',
> +    'split_git_unique_name',
> +    ]
> +
> +from zope.component import getUtility
> +from zope.interface import (
> +    Attribute,
> +    Interface,
> +    )
> +
> +from lp.code.errors import InvalidNamespace
> +from lp.registry.interfaces.distributionsourcepackage import (
> +    IDistributionSourcePackage,
> +    )
> +from lp.registry.interfaces.product import IProduct
> +
> +
> +class IGitNamespace(Interface):
> +    """A namespace that a Git repository lives in."""
> +
> +    name = Attribute(
> +        "The name of the namespace. This is prepended to the repository name.")
> +
> +    target = Attribute("The `IHasGitRepositories` for this namespace.")
> +
> +    def createRepository(registrant, name, information_type=None,
> +                         date_created=None, with_hosting=True):
> +        """Create and return an `IGitRepository` in this namespace."""
> +
> +    def isNameUsed(name):
> +        """Is 'name' already used in this namespace?"""
> +
> +    def findUnusedName(prefix):
> +        """Find an unused repository name starting with 'prefix'.
> +
> +        Note that there is no guarantee that the name returned by this method
> +        will remain unused for very long.
> +        """
> +
> +    def moveRepository(repository, mover, new_name=None,
> +                       rename_if_necessary=False):
> +        """Move the repository into this namespace.
> +
> +        :param repository: The `IGitRepository` to move.
> +        :param mover: The `IPerson` doing the moving.
> +        :param new_name: A new name for the repository.
> +        :param rename_if_necessary: Rename the repository if the repository
> +            name already exists in this namespace.
> +        :raises GitRepositoryCreatorNotMemberOfOwnerTeam: if the namespace
> +            owner is a team and 'mover' is not in that team.
> +        :raises GitRepositoryCreatorNotOwner: if the namespace owner is an
> +            individual and 'mover' is not the owner.
> +        :raises GitRepositoryCreationForbidden: if 'mover' is not allowed to
> +            create a repository in this namespace due to privacy rules.
> +        :raises GitRepositoryExists: if a repository with the new name
> +            already exists in the namespace, and 'rename_if_necessary' is
> +            False.
> +        """
> +
> +    def getRepositories():
> +        """Return the repositories in this namespace."""
> +
> +    def getByName(repository_name, default=None):
> +        """Find the repository in this namespace called 'repository_name'.
> +
> +        :return: `IGitRepository` if found, otherwise 'default'.
> +        """
> +
> +    def __eq__(other):
> +        """Is this namespace the same as another namespace?"""
> +
> +    def __ne__(other):
> +        """Is this namespace not the same as another namespace?"""
> +
> +
> +class IGitNamespacePolicy(Interface):
> +    """Methods relating to Git repository creation and validation."""
> +
> +    def getAllowedInformationTypes(who):
> +        """Get the information types that a repository in this namespace can
> +        have.
> +
> +        :param who: The user making the request.
> +        :return: A sequence of `InformationType`s.
> +        """
> +
> +    def getDefaultInformationType(who):
> +        """Get the default information type for repositories in this namespace.
> +
> +        :param who: The user to return the information type for.
> +        :return: An `InformationType`.
> +        """
> +
> +    def validateRegistrant(registrant):
> +        """Check that the registrant can create a repository in this namespace.
> +
> +        :param registrant: An `IPerson`.
> +        :raises GitRepositoryCreatorNotMemberOfOwnerTeam: if the namespace
> +            owner is a team and the registrant is not in that team.
> +        :raises GitRepositoryCreatorNotOwner: if the namespace owner is an
> +            individual and the registrant is not the owner.
> +        :raises GitRepositoryCreationForbidden: if the registrant is not
> +            allowed to create a repository in this namespace due to privacy
> +            rules.
> +        """
> +
> +    def validateRepositoryName(name):
> +        """Check the repository `name`.
> +
> +        :param name: A branch name, either string or unicode.
> +        :raises GitRepositoryExists: if a branch with the `name` already
> +            exists in the namespace.
> +        :raises LaunchpadValidationError: if the name doesn't match the
> +            validation constraints on IGitRepository.name.
> +        """
> +
> +    def validateMove(repository, mover, name=None):
> +        """Check that 'mover' can move 'repository' into this namespace.
> +
> +        :param repository: An `IGitRepository` that might be moved.
> +        :param mover: The `IPerson` who would move it.
> +        :param name: A new name for the repository.  If None, the repository
> +            name is used.
> +        :raises GitRepositoryCreatorNotMemberOfOwnerTeam: if the namespace
> +            owner is a team and 'mover' is not in that team.
> +        :raises GitRepositoryCreatorNotOwner: if the namespace owner is an
> +            individual and 'mover' is not the owner.
> +        :raises GitRepositoryCreationForbidden: if 'mover' is not allowed to
> +            create a repository in this namespace due to privacy rules.
> +        :raises GitRepositoryExists: if a repository with the new name
> +            already exists in the namespace.
> +        """
> +
> +
> +class IGitNamespaceSet(Interface):
> +    """Interface for getting Git repository namespaces."""
> +
> +    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 names of Launchpad components.
> +            The first segment is the username, *not* preceded by a '~`.

"An iterable of names of Launchpad components" is very vague, and false now that "+source" is involved.

> +        :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(person, project=None, distribution=None,
> +                      sourcepackagename=None):
> +    return getUtility(IGitNamespaceSet).get(
> +        person, project=project, distribution=distribution,
> +        sourcepackagename=sourcepackagename)

Is this method valuable while get_git_namespace_for_target exists?

> +
> +
> +def get_git_namespace_for_target(target, owner):
> +    if IProduct.providedBy(target):
> +        return get_git_namespace(owner, project=target)
> +    elif IDistributionSourcePackage.providedBy(target):
> +        return get_git_namespace(
> +            owner, distribution=target.distribution,
> +            sourcepackagename=target.sourcepackagename)
> +    else:
> +        return get_git_namespace(owner)

Ah, the target=None -> personal issue that I mentioned in https://code.launchpad.net/~cjwatson/launchpad/git-basic-model/+merge/248976. I'm not sure which way is best, but it would be nice to be consistent.

> +
> +
> +def lookup_git_namespace(namespace_name):
> +    return getUtility(IGitNamespaceSet).lookup(namespace_name)

Does this need to exist?

> +
> +
> +def split_git_unique_name(unique_name):
> +    """Return the namespace and repository names of a unique name."""
> +    try:
> +        namespace_name, literal, repository_name = unique_name.rsplit("/", 2)
> +    except ValueError:
> +        raise InvalidNamespace(unique_name)
> +    if literal != "g":
> +        raise InvalidNamespace(unique_name)
> +    return namespace_name, repository_name
> 
> === modified file 'lib/lp/code/interfaces/gitrepository.py'
> --- lib/lp/code/interfaces/gitrepository.py	2015-02-07 10:28:57 +0000
> +++ lib/lp/code/interfaces/gitrepository.py	2015-02-07 10:28:57 +0000
> @@ -142,6 +142,9 @@
>          schema=IHasGitRepositories,
>          description=_("The target of the repository."))
>  
> +    namespace = Attribute(
> +        "The namespace of this repository, as an `IGitNamespace`.")
> +
>      information_type = Choice(
>          title=_("Information Type"), vocabulary=InformationType,
>          required=True, readonly=True, default=InformationType.PUBLIC,
> 
> === modified file 'lib/lp/code/model/branchnamespace.py'
> --- lib/lp/code/model/branchnamespace.py	2015-01-29 13:09:37 +0000
> +++ lib/lp/code/model/branchnamespace.py	2015-02-07 10:28:57 +0000
> @@ -6,9 +6,11 @@
>  __metaclass__ = type
>  __all__ = [
>      'BranchNamespaceSet',
> +    'BRANCH_POLICY_ALLOWED_TYPES',
> +    'BRANCH_POLICY_DEFAULT_TYPES',
> +    'BRANCH_POLICY_REQUIRED_GRANTS',
>      'PackageNamespace',
>      'PersonalNamespace',
> -    'BRANCH_POLICY_ALLOWED_TYPES',
>      'ProductNamespace',
>      ]
>  
> 
> === added file 'lib/lp/code/model/gitnamespace.py'
> --- lib/lp/code/model/gitnamespace.py	1970-01-01 00:00:00 +0000
> +++ lib/lp/code/model/gitnamespace.py	2015-02-07 10:28:57 +0000
> @@ -0,0 +1,575 @@
> +# Copyright 2015 Canonical Ltd.  This software is licensed under the
> +# GNU Affero General Public License version 3 (see the file LICENSE).
> +
> +"""Implementations of `IGitNamespace`."""
> +
> +__metaclass__ = type
> +__all__ = [
> +    'GitNamespaceSet',
> +    'PackageGitNamespace',
> +    'PersonalGitNamespace',
> +    'ProjectGitNamespace',
> +    ]
> +
> +from urlparse import urljoin
> +
> +from lazr.lifecycle.event import ObjectCreatedEvent
> +import requests
> +from storm.locals import And
> +import transaction
> +from zope.component import getUtility
> +from zope.event import notify
> +from zope.interface import implements
> +from zope.security.proxy import removeSecurityProxy
> +
> +from lp.app.enums import (
> +    FREE_INFORMATION_TYPES,
> +    InformationType,
> +    NON_EMBARGOED_INFORMATION_TYPES,
> +    PUBLIC_INFORMATION_TYPES,
> +    )
> +from lp.app.interfaces.services import IService
> +from lp.code.errors import (
> +    GitRepositoryCreationFault,
> +    GitRepositoryCreationForbidden,
> +    GitRepositoryCreatorNotMemberOfOwnerTeam,
> +    GitRepositoryCreatorNotOwner,
> +    GitRepositoryExists,
> +    InvalidNamespace,
> +    NoSuchGitRepository,
> +    )
> +from lp.code.interfaces.gitnamespace import (
> +    IGitNamespace,
> +    IGitNamespacePolicy,
> +    IGitNamespaceSet,
> +    )
> +from lp.code.interfaces.gitrepository import (
> +    IGitRepository,
> +    user_has_special_git_repository_access,
> +    )
> +from lp.code.interfaces.hasgitrepositories import IHasGitRepositories
> +from lp.code.model.branchnamespace import (
> +    BRANCH_POLICY_ALLOWED_TYPES,
> +    BRANCH_POLICY_DEFAULT_TYPES,
> +    BRANCH_POLICY_REQUIRED_GRANTS,
> +    )
> +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 (
> +    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.config import config
> +from lp.services.database.constants import DEFAULT
> +from lp.services.database.interfaces import IStore
> +from lp.services.propertycache import get_property_cache
> +
> +
> +class GitHostingClient:
> +    """A client for the internal API provided by the Git hosting system."""

Does this really belong in gitnamespace?

> +
> +    @property
> +    def endpoint(self):
> +        return config.codehosting.internal_git_endpoint
> +
> +    def create(self, path):
> +        print("GitHostingClient.create: %s" % path)

Hm :)

> +        response = requests.post(
> +            urljoin(self.endpoint, "create"), data={"path": path})

I believe this will respect http_proxy by default, which we probably don't want.

> +        if response.status_code != 200:
> +            raise GitRepositoryCreationFault(
> +                "Failed to create Git repository: %s" % response.text)
> +
> +
> +class _BaseGitNamespace:
> +    """Common code for Git repository namespaces."""
> +
> +    def createRepository(self, registrant, name, information_type=None,
> +                         date_created=DEFAULT, with_hosting=True):
> +        """See `IGitNamespace`."""
> +
> +        self.validateRegistrant(registrant)
> +        self.validateRepositoryName(name)
> +
> +        project = getattr(self, "project", None)
> +        dsp = getattr(self, "distro_source_package", None)
> +        if dsp is None:
> +            distribution = None
> +            sourcepackagename = None
> +        else:
> +            distribution = dsp.distribution
> +            sourcepackagename = dsp.sourcepackagename
> +
> +        if information_type is None:
> +            information_type = self.getDefaultInformationType(registrant)
> +            if information_type is None:
> +                raise GitRepositoryCreationForbidden()
> +
> +        repository = GitRepository(
> +            registrant, self.owner, name, information_type, date_created,
> +            project=project, distribution=distribution,
> +            sourcepackagename=sourcepackagename)
> +        repository._reconcileAccess()
> +
> +        # Commit the transaction so that we can get the id column and thus
> +        # construct the hosting path.
> +        transaction.commit()
> +        # XXX cjwatson 2015-02-02: If the hosting service is unavailable, we
> +        # should defer to a job and (at least in the browser case) issue a
> +        # notification.
> +        if with_hosting:
> +            GitHostingClient().create(repository.getInternalPath())

We might also be able to get away with doing this on the first repository access.

> +
> +        notify(ObjectCreatedEvent(repository))
> +
> +        return repository
> +
> +    def isNameUsed(self, repository_name):
> +        """See `IGitNamespace`."""
> +        return self.getByName(repository_name) is not None
> +
> +    def findUnusedName(self, prefix):
> +        """See `IGitNamespace`."""
> +        name = prefix
> +        count = 0
> +        while self.isNameUsed(name):
> +            count += 1
> +            name = "%s-%s" % (prefix, count)
> +        return name
> +
> +    def validateRegistrant(self, registrant):
> +        """See `IGitNamespace`."""
> +        if user_has_special_git_repository_access(registrant):
> +            return
> +        owner = self.owner
> +        if not registrant.inTeam(owner):
> +            if owner.is_team:
> +                raise GitRepositoryCreatorNotMemberOfOwnerTeam(
> +                    "%s is not a member of %s"
> +                    % (registrant.displayname, owner.displayname))
> +            else:
> +                raise GitRepositoryCreatorNotOwner(
> +                    "%s cannot create Git repositories owned by %s"
> +                    % (registrant.displayname, owner.displayname))
> +
> +        if not self.getAllowedInformationTypes(registrant):
> +            raise GitRepositoryCreationForbidden(
> +                'You cannot create Git repositories in "%s"' % self.name)
> +
> +    def validateRepositoryName(self, name):
> +        """See `IGitNamespace`."""
> +        existing_repository = self.getByName(name)
> +        if existing_repository is not None:
> +            raise GitRepositoryExists(existing_repository)
> +
> +        # Not all code paths that lead to Git repository creation go via a
> +        # schema-validated form, so we validate the repository name here to
> +        # give a nicer error message than 'ERROR: new row for relation
> +        # "gitrepository" violates check constraint "valid_name"...'.
> +        IGitRepository['name'].validate(unicode(name))
> +
> +    def validateMove(self, repository, mover, name=None):
> +        """See `IGitNamespace`."""
> +        if name is None:
> +            name = repository.name
> +        self.validateRepositoryName(name)
> +        self.validateRegistrant(mover)
> +
> +    def moveRepository(self, repository, mover, new_name=None,
> +                       rename_if_necessary=False):
> +        """See `IGitNamespace`."""
> +        # Check to see if the repository is already in this namespace.
> +        old_namespace = repository.namespace
> +        if self.name == old_namespace.name:
> +            return
> +        if new_name is None:
> +            new_name = repository.name
> +        if rename_if_necessary:
> +            new_name = self.findUnusedName(new_name)
> +        self.validateMove(repository, mover, new_name)
> +        # Remove the security proxy of the repository as the owner and
> +        # target attributes are read-only through the interface.
> +        naked_repository = removeSecurityProxy(repository)
> +        naked_repository.owner = self.owner
> +        self._retargetRepository(naked_repository)
> +        naked_repository.name = new_name
> +
> +    def getRepositories(self):
> +        """See `IGitNamespace`."""
> +        return IStore(GitRepository).find(
> +            GitRepository, self._getRepositoriesClause())
> +
> +    def getByName(self, repository_name, default=None):
> +        """See `IGitNamespace`."""
> +        match = IStore(GitRepository).find(
> +            GitRepository, self._getRepositoriesClause(),
> +            GitRepository.name == repository_name).one()
> +        if match is None:
> +            match = default
> +        return match
> +
> +    def getAllowedInformationTypes(self, who=None):
> +        """See `IGitNamespace`."""
> +        raise NotImplementedError
> +
> +    def getDefaultInformationType(self, who=None):
> +        """See `IGitNamespace`."""
> +        raise NotImplementedError
> +
> +    def __eq__(self, other):
> +        """See `IGitNamespace`."""
> +        return self.target == other.target
> +
> +    def __ne__(self, other):
> +        """See `IGitNamespace`."""
> +        return not self == other
> +
> +
> +class PersonalGitNamespace(_BaseGitNamespace):
> +    """A namespace for personal repositories.
> +
> +    Repositories in this namespace have names like "~foo/g/bar".
> +    """
> +
> +    implements(IGitNamespace, IGitNamespacePolicy)
> +
> +    def __init__(self, person):
> +        self.owner = person
> +
> +    def _getRepositoriesClause(self):
> +        return And(
> +            GitRepository.owner == self.owner,
> +            GitRepository.project == None,
> +            GitRepository.distribution == None,
> +            GitRepository.sourcepackagename == None)
> +
> +    @property
> +    def name(self):
> +        """See `IGitNamespace`."""
> +        return "~%s" % self.owner.name
> +
> +    @property
> +    def target(self):
> +        """See `IGitNamespace`."""
> +        return IHasGitRepositories(self.owner)
> +
> +    def _retargetRepository(self, repository):
> +        repository.project = None
> +        repository.distribution = None
> +        repository.sourcepackagename = None
> +        del get_property_cache(repository).distro_source_package
> +
> +    @property
> +    def _is_private_team(self):
> +        return (
> +            self.owner.is_team
> +            and self.owner.visibility == PersonVisibility.PRIVATE)
> +
> +    def getAllowedInformationTypes(self, who=None):
> +        """See `IGitNamespace`."""
> +        # Private teams get private branches, everyone else gets public ones.
> +        if self._is_private_team:
> +            return NON_EMBARGOED_INFORMATION_TYPES
> +        else:
> +            return FREE_INFORMATION_TYPES
> +
> +    def getDefaultInformationType(self, who=None):
> +        """See `IGitNamespace`."""
> +        if self._is_private_team:
> +            return InformationType.PROPRIETARY
> +        else:
> +            return InformationType.PUBLIC
> +
> +
> +class ProjectGitNamespace(_BaseGitNamespace):
> +    """A namespace for project repositories.
> +
> +    This namespace is for all the repositories owned by a particular person
> +    in a particular project.
> +    """
> +
> +    implements(IGitNamespace, IGitNamespacePolicy)
> +
> +    def __init__(self, person, project):
> +        self.owner = person
> +        self.project = project
> +
> +    def _getRepositoriesClause(self):
> +        return And(
> +            GitRepository.owner == self.owner,
> +            GitRepository.project == self.project)
> +
> +    @property
> +    def name(self):
> +        """See `IGitNamespace`."""
> +        return '~%s/%s' % (self.owner.name, self.project.name)
> +
> +    @property
> +    def target(self):
> +        """See `IGitNamespace`."""
> +        return IHasGitRepositories(self.project)
> +
> +    def _retargetRepository(self, repository):
> +        repository.project = self.project
> +        repository.distribution = None
> +        repository.sourcepackagename = None
> +        del get_property_cache(repository).distro_source_package
> +
> +    def getAllowedInformationTypes(self, who=None):
> +        """See `IGitNamespace`."""
> +        # Some policies require that the repository owner or current user
> +        # have full access to an information type.  If it's required and the
> +        # user doesn't hold it, no information types are legal.
> +        required_grant = BRANCH_POLICY_REQUIRED_GRANTS[
> +            self.project.branch_sharing_policy]
> +        if (required_grant is not None
> +            and not getUtility(IService, 'sharing').checkPillarAccess(
> +                [self.project], required_grant, self.owner)
> +            and (who is None
> +                or not getUtility(IService, 'sharing').checkPillarAccess(
> +                    [self.project], required_grant, who))):
> +            return []
> +
> +        return BRANCH_POLICY_ALLOWED_TYPES[self.project.branch_sharing_policy]
> +
> +    def getDefaultInformationType(self, who=None):
> +        """See `IGitNamespace`."""
> +        default_type = BRANCH_POLICY_DEFAULT_TYPES[
> +            self.project.branch_sharing_policy]
> +        if default_type not in self.getAllowedInformationTypes(who):
> +            return None
> +        return default_type
> +
> +
> +class PackageGitNamespace(_BaseGitNamespace):
> +    """A namespace for distribution source package repositories.
> +
> +    This namespace is for all the repositories owned by a particular person
> +    in a particular source package in a particular distribution.
> +    """
> +
> +    implements(IGitNamespace, IGitNamespacePolicy)
> +
> +    def __init__(self, person, distro_source_package):
> +        self.owner = person
> +        self.distro_source_package = distro_source_package
> +
> +    def _getRepositoriesClause(self):
> +        dsp = self.distro_source_package
> +        return And(
> +            GitRepository.owner == self.owner,
> +            GitRepository.distribution == dsp.distribution,
> +            GitRepository.sourcepackagename == dsp.sourcepackagename)
> +
> +    @property
> +    def name(self):
> +        """See `IGitNamespace`."""
> +        dsp = self.distro_source_package
> +        return '~%s/%s/+source/%s' % (
> +            self.owner.name, dsp.distribution.name, dsp.sourcepackagename.name)
> +
> +    @property
> +    def target(self):
> +        """See `IGitNamespace`."""
> +        return IHasGitRepositories(self.distro_source_package)
> +
> +    def _retargetRepository(self, repository):
> +        dsp = self.distro_source_package
> +        repository.project = None
> +        repository.distribution = dsp.distribution
> +        repository.sourcepackagename = dsp.sourcepackagename
> +        get_property_cache(repository).distro_source_package = dsp
> +
> +    def getAllowedInformationTypes(self, who=None):
> +        """See `IGitNamespace`."""
> +        return PUBLIC_INFORMATION_TYPES
> +
> +    def getDefaultInformationType(self, who=None):
> +        """See `IGitNamespace`."""
> +        return InformationType.PUBLIC
> +
> +    def __eq__(self, other):
> +        """See `IGitNamespace`."""
> +        # We may have different DSP objects that are functionally the same.
> +        self_dsp = self.distro_source_package
> +        other_dsp = IDistributionSourcePackage(other.target)
> +        return (
> +            self_dsp.distribution == other_dsp.distribution and
> +            self_dsp.sourcepackagename == other_dsp.sourcepackagename)
> +
> +
> +class GitNamespaceSet:
> +    """Only implementation of `IGitNamespaceSet`."""
> +
> +    implements(IGitNamespaceSet)
> +
> +    def get(self, person, project=None, distribution=None,
> +            sourcepackagename=None):
> +        """See `IGitNamespaceSet`."""
> +        if project is not None:
> +            assert distribution is None and sourcepackagename is None, (
> +                "project implies no distribution or sourcepackagename. "
> +                "Got %r, %r, %r."
> +                % (project, distribution, sourcepackagename))
> +            return ProjectGitNamespace(person, project)
> +        elif distribution is not None:
> +            assert sourcepackagename is not None, (
> +                "distribution implies sourcepackagename. Got %r, %r"
> +                % (distribution, sourcepackagename))
> +            return PackageGitNamespace(
> +                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)
> +
> +    def _findPillar(self, pillar_name):
> +        """Find and return the pillar with the given name.
> +
> +        If the given name is 'g' or None, return None.

I'd explain the "g" special case.

> +
> +        :raise NoSuchProduct if there's no pillar with the given name or it
> +            is a project group.
> +        """
> +        if pillar_name == "g":
> +            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)
> +
> +    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)
> +
> +    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:
> +            distribution_name = get_next_segment()
> +            distribution = self._findDistroSeries(pillar, distribution_name)
> +            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=distribution,
> +                sourcepackagename=sourcepackagename)
> +            git_literal = get_next_segment()
> +        if git_literal != "g":
> +            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-07 10:28:57 +0000
> +++ lib/lp/code/model/gitrepository.py	2015-02-07 10:28:57 +0000
> @@ -34,6 +34,10 @@
>  from lp.app.interfaces.informationtype import IInformationType
>  from lp.app.interfaces.launchpad import IPrivacy
>  from lp.app.interfaces.services import IService
> +from lp.code.interfaces.gitnamespace import (
> +    get_git_namespace_for_target,
> +    IGitNamespacePolicy,
> +    )
>  from lp.code.interfaces.gitrepository import (
>      GitPathMixin,
>      IGitRepository,
> @@ -156,9 +160,27 @@
>  
>      def setTarget(self, user, target):
>          """See `IGitRepository`."""
> -        # XXX cjwatson 2015-02-06: Fill this in once IGitNamespace is in
> -        # place.
> -        raise NotImplementedError
> +        if IPerson.providedBy(target):
> +            owner = IPerson(target)
> +            if (self.information_type in PRIVATE_INFORMATION_TYPES and
> +                (not owner.is_team or
> +                 owner.visibility != PersonVisibility.PRIVATE)):
> +                raise GitTargetError(
> +                    "Only private teams may have personal private "
> +                    "repositories.")
> +        namespace = get_git_namespace_for_target(target, self.owner)
> +        if (self.information_type not in
> +            namespace.getAllowedInformationTypes(user)):
> +            raise GitTargetError(
> +                "%s repositories are not allowed for target %s." % (
> +                    self.information_type.title, target.displayname))
> +        namespace.moveRepository(self, user, rename_if_necessary=True)
> +        self._reconcileAccess()
> +
> +    @property
> +    def namespace(self):
> +        """See `IGitRepository`."""
> +        return get_git_namespace_for_target(self.target, self.owner)
>  
>      @property
>      def displayname(self):
> @@ -249,9 +271,8 @@
>              types = set(PUBLIC_INFORMATION_TYPES + PRIVATE_INFORMATION_TYPES)
>          else:
>              # Otherwise the permitted types are defined by the namespace.
> -            # XXX cjwatson 2015-01-19: Define permitted types properly.  For
> -            # now, non-admins only get public repository access.
> -            types = set(PUBLIC_INFORMATION_TYPES)
> +            policy = IGitNamespacePolicy(self.namespace)
> +            types = set(policy.getAllowedInformationTypes(user))
>          return types
>  
>      def transitionToInformationType(self, information_type, user,
> @@ -284,9 +305,8 @@
>  
>      def setOwner(self, new_owner, user):
>          """See `IGitRepository`."""
> -        # XXX cjwatson 2015-02-06: Fill this in once IGitNamespace is in
> -        # place.
> -        raise NotImplementedError
> +        new_namespace = get_git_namespace_for_target(self.target, new_owner)
> +        new_namespace.moveRepository(self, user, rename_if_necessary=True)
>  
>      def destroySelf(self):
>          raise NotImplementedError
> @@ -300,28 +320,27 @@
>      def new(self, registrant, owner, target, name, information_type=None,
>              date_created=DEFAULT):
>          """See `IGitRepositorySet`."""
> -        # XXX cjwatson 2015-02-06: Fill this in once IGitNamespace is in
> -        # place.
> -        raise NotImplementedError
> +        namespace = get_git_namespace_for_target(target, owner)
> +        return namespace.createRepository(
> +            registrant, name, information_type=information_type,
> +            date_created=date_created)
>  
>      def getPersonalRepository(self, person, repository_name):
>          """See `IGitRepositorySet`."""
> -        # XXX cjwatson 2015-02-06: Fill this in once IGitNamespace is in
> -        # place.
> -        raise NotImplementedError
> +        namespace = get_git_namespace_for_target(person, person)
> +        return namespace.getByName(repository_name)
>  
>      def getProjectRepository(self, person, project, repository_name=None):
>          """See `IGitRepositorySet`."""
> -        # XXX cjwatson 2015-02-06: Fill this in once IGitNamespace is in
> -        # place.
> -        raise NotImplementedError
> +        namespace = get_git_namespace_for_target(project, person)
> +        return namespace.getByName(repository_name)
>  
>      def getPackageRepository(self, person, distribution, sourcepackagename,
>                                repository_name=None):
>          """See `IGitRepositorySet`."""
> -        # XXX cjwatson 2015-02-06: Fill this in once IGitNamespace is in
> -        # place.
> -        raise NotImplementedError
> +        namespace = get_git_namespace_for_target(
> +            distribution.getSourcePackage(sourcepackagename), person)
> +        return namespace.getByName(repository_name)
>  
>      def getByPath(self, user, path):
>          """See `IGitRepositorySet`."""
> 


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


References