← Back to team overview

launchpad-reviewers team mailing list archive

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

 

Review: Approve code



Diff comments:

> === modified file 'configs/development/launchpad-lazr.conf'
> --- configs/development/launchpad-lazr.conf	2014-02-27 08:39:44 +0000
> +++ configs/development/launchpad-lazr.conf	2015-02-18 14:02:32 +0000
> @@ -48,6 +48,10 @@
>  access_log: /var/tmp/bazaar.launchpad.dev/codehosting-access.log
>  blacklisted_hostnames:
>  use_forking_daemon: True
> +internal_git_endpoint: http://git.launchpad.dev:19417/

It's actually the Git API endpoint, not actual git.

> +git_browse_root: https://git.launchpad.dev/
> +git_anon_root: git://git.launchpad.dev/
> +git_ssh_root: git+ssh://git.launchpad.dev/
>  
>  [codeimport]
>  bazaar_branch_store: file:///tmp/bazaar-branches
> 
> === modified file 'lib/lp/code/configure.zcml'
> --- lib/lp/code/configure.zcml	2015-02-09 11:38:30 +0000
> +++ lib/lp/code/configure.zcml	2015-02-18 14:02:32 +0000
> @@ -807,6 +807,37 @@
>    <adapter factory="lp.code.model.linkedbranch.PackageLinkedBranch" />
>    <adapter factory="lp.code.model.linkedbranch.DistributionPackageLinkedBranch" />
>  
> +  <!-- GitRepository -->
> +
> +  <class class="lp.code.model.gitrepository.GitRepository">
> +    <require
> +        permission="launchpad.View"
> +        interface="lp.app.interfaces.launchpad.IPrivacy
> +                   lp.code.interfaces.gitrepository.IGitRepositoryView
> +                   lp.code.interfaces.gitrepository.IGitRepositoryModerateAttributes" />
> +    <require
> +        permission="launchpad.Moderate"
> +        interface="lp.code.interfaces.gitrepository.IGitRepositoryModerate"
> +        set_schema="lp.code.interfaces.gitrepository.IGitRepositoryModerateAttributes" />
> +    <require
> +        permission="launchpad.Edit"
> +        interface="lp.code.interfaces.gitrepository.IGitRepositoryEdit" />
> +  </class>
> +  <subscriber
> +      for="lp.code.interfaces.gitrepository.IGitRepository zope.lifecycleevent.interfaces.IObjectModifiedEvent"
> +      handler="lp.code.model.gitrepository.git_repository_modified"/>
> +
> +  <!-- GitRepositorySet -->
> +
> +  <class class="lp.code.model.gitrepository.GitRepositorySet">
> +    <allow interface="lp.code.interfaces.gitrepository.IGitRepositorySet" />
> +  </class>
> +  <securedutility
> +      class="lp.code.model.gitrepository.GitRepositorySet"
> +      provides="lp.code.interfaces.gitrepository.IGitRepositorySet">
> +    <allow interface="lp.code.interfaces.gitrepository.IGitRepositorySet" />
> +  </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-18 14:02:32 +0000
> @@ -28,6 +28,8 @@
>      'CodeImportNotInReviewedState',
>      'ClaimReviewFailed',
>      'DiffNotFound',
> +    'GitDefaultConflict',
> +    'GitTargetError',
>      'InvalidBranchMergeProposal',
>      'InvalidMergeQueueConfig',
>      'InvalidNamespace',
> @@ -312,6 +314,35 @@
>      """Raised when the user specifies an unrecognized branch type."""
>  
>  
> +class GitTargetError(Exception):
> +    """Raised when there is an error determining a Git repository target."""
> +
> +
> +@error_status(httplib.CONFLICT)
> +class GitDefaultConflict(Exception):
> +    """Raised when trying to set a Git repository as the default for
> +    something that already has a default."""
> +
> +    def __init__(self, existing_repository, target, owner=None):
> +        params = {
> +            "unique_name": existing_repository.unique_name,
> +            "target": target.displayname,
> +            "owner": owner.displayname,
> +            }
> +        if owner is None:
> +            message = (
> +                "The default repository for '%(target)s' is already set to "
> +                "%(unique_name)s." % params)
> +        else:
> +            message = (
> +                "%(owner)'s default repository for '%(target)s' is already "
> +                "set to %(unique_name)s." % params)
> +        self.existing_repository = existing_repository
> +        self.target = target
> +        self.owner = owner
> +        Exception.__init__(self, message)
> +
> +
>  @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/gitrepository.py'
> --- lib/lp/code/interfaces/gitrepository.py	1970-01-01 00:00:00 +0000
> +++ lib/lp/code/interfaces/gitrepository.py	2015-02-18 14:02:32 +0000
> @@ -0,0 +1,372 @@
> +# Copyright 2015 Canonical Ltd.  This software is licensed under the
> +# GNU Affero General Public License version 3 (see the file LICENSE).
> +
> +"""Git repository interfaces."""
> +
> +__metaclass__ = type
> +
> +__all__ = [
> +    'GitIdentityMixin',
> +    'git_repository_name_validator',
> +    'IGitRepository',
> +    'IGitRepositorySet',
> +    'user_has_special_git_repository_access',
> +    ]
> +
> +import re
> +
> +from lazr.restful.fields import Reference
> +from zope.interface import (
> +    Attribute,
> +    Interface,
> +    )
> +from zope.schema import (
> +    Bool,
> +    Choice,
> +    Datetime,
> +    Int,
> +    Text,
> +    TextLine,
> +    )
> +
> +from lp import _
> +from lp.app.enums import InformationType
> +from lp.app.validators import LaunchpadValidationError
> +from lp.code.interfaces.hasgitrepositories import IHasGitRepositories
> +from lp.registry.interfaces.role import IPersonRoles
> +from lp.services.fields import (
> +    PersonChoice,
> +    PublicPersonChoice,
> +    )
> +
> +
> +GIT_REPOSITORY_NAME_VALIDATION_ERROR_MESSAGE = _(
> +    "Git repository names must start with a number or letter.  The characters "
> +    "+, -, _, . and @ are also allowed after the first character.  Repository "
> +    "names must not end with \".git\".")
> +
> +
> +# This is a copy of the pattern in database/schema/patch-2209-61-0.sql.
> +# Don't change it without changing that.
> +valid_git_repository_name_pattern = re.compile(
> +    r"^(?i)[a-z0-9][a-z0-9+\.\-@_]*\Z")
> +
> +
> +def valid_git_repository_name(name):
> +    """Return True iff the name is valid as a Git repository name.
> +
> +    The rules for what is a valid Git repository name are described in
> +    GIT_REPOSITORY_NAME_VALIDATION_ERROR_MESSAGE.
> +    """
> +    if (not name.endswith(".git") and
> +        valid_git_repository_name_pattern.match(name)):
> +        return True
> +    return False
> +
> +
> +def git_repository_name_validator(name):
> +    """Return True if the name is valid, or raise a LaunchpadValidationError.
> +    """
> +    if not valid_git_repository_name(name):
> +        raise LaunchpadValidationError(
> +            _("Invalid Git repository name '${name}'. ${message}",
> +              mapping={
> +                  "name": name,
> +                  "message": GIT_REPOSITORY_NAME_VALIDATION_ERROR_MESSAGE,
> +                  }))
> +    return True
> +
> +
> +class IGitRepositoryView(Interface):
> +    """IGitRepository attributes that require launchpad.View permission."""
> +
> +    id = Int(title=_("ID"), readonly=True, required=True)
> +
> +    date_created = Datetime(
> +        title=_("Date created"), required=True, readonly=True)
> +
> +    date_last_modified = Datetime(
> +        title=_("Date last modified"), required=True, readonly=True)
> +
> +    registrant = PublicPersonChoice(
> +        title=_("Registrant"), required=True, readonly=True,
> +        vocabulary="ValidPersonOrTeam",
> +        description=_("The person who registered this Git repository."))
> +
> +    owner = PersonChoice(
> +        title=_("Owner"), required=True, readonly=False,
> +        vocabulary="AllUserTeamsParticipationPlusSelf",
> +        description=_(
> +            "The owner of this Git repository. This controls who can modify "
> +            "the repository."))
> +
> +    target = Reference(
> +        title=_("Target"), required=True, readonly=True,
> +        schema=IHasGitRepositories,
> +        description=_("The target of the repository."))
> +
> +    information_type = Choice(
> +        title=_("Information Type"), vocabulary=InformationType,

The rest of the titles are sentence case.

> +        required=True, readonly=True, default=InformationType.PUBLIC,
> +        description=_(
> +            "The type of information contained in this repository."))
> +
> +    owner_default = Bool(
> +        title=_("Owner default"), required=True, readonly=True,
> +        description=_(
> +            "Whether this repository is the default for its owner and "
> +            "target."))
> +
> +    target_default = Bool(
> +        title=_("Target default"), required=True, readonly=True,
> +        description=_(
> +            "Whether this repository is the default for its target."))
> +
> +    unique_name = Text(
> +        title=_("Unique name"), readonly=True,
> +        description=_(
> +            "Unique name of the repository, including the owner and project "
> +            "names."))
> +
> +    displayname = Text(
> +        title=_("Display name"), readonly=True,
> +        description=_("Display name of the repository."))

Should this be display_name?

> +
> +    shortened_path = Attribute(
> +        "The shortest reasonable version of the path to this repository.")
> +
> +    git_identity = Text(
> +        title=_("Git identity"), readonly=True,
> +        description=_(
> +            "If this is the default repository for some target, then this is "
> +            "'lp:' plus a shortcut version of the path via that target.  "
> +            "Otherwise it is simply 'lp:' plus the unique name."))
> +
> +    def setOwnerDefault(value):
> +        """Set whether this repository is the default for its owner-target.
> +
> +        This is for internal use; the caller should ensure permission to edit
> +        the owner, should arrange to remove any existing owner-target default
> +        (including any target default with the same owner), and should check
> +        that this repository is attached to the desired target.

This paragraph is inaccurate now that owner-target default doesn't imply target default.

> +
> +        :raises Unauthorized: if lacking permission to edit the owner.
> +        :param value: True if this repository should be the owner-target
> +        default, otherwise False.

Model methods rarely use check_permission. Since the caller has to check permissions anyway, I'd consider dropping the permission check here.

> +        """
> +
> +    def setTargetDefault(value):
> +        """Set whether this repository is the default for its target.
> +
> +        This is for internal use; the caller should ensure permission to edit
> +        the target, should arrange to remove any existing target default, and
> +        should check that this repository is attached to the desired target.
> +
> +        :raises Unauthorized: if lacking permission to edit the target.
> +        :param value: True if this repository should be the target default,
> +        otherwise False.
> +        """
> +
> +    def getCodebrowseUrl():
> +        """Construct a browsing URL for this Git repository."""
> +
> +    def visibleByUser(user):
> +        """Can the specified user see this repository?"""
> +
> +    def getAllowedInformationTypes(user):
> +        """Get a list of acceptable `InformationType`s for this repository.
> +
> +        If the user is a Launchpad admin, any type is acceptable.
> +        """
> +
> +    def getInternalPath():
> +        """Get the internal path to this repository.
> +
> +        This is used on the storage backend.
> +        """
> +
> +    def getRepositoryDefaults():
> +        """Return a sorted list of `ICanHasDefaultGitRepository` objects.
> +
> +        There is one result for each related object for which this
> +        repository is the default.  For example, in the case where a
> +        repository is the default for a project and is also its owner's
> +        default repository for that project, the objects for both the
> +        project and the person-project are returned.
> +
> +        More important related objects are sorted first.
> +        """
> +
> +    def getRepositoryIdentities():
> +        """A list of aliases for a repository.
> +
> +        Returns a list of tuples of path and context object.  There is at
> +        least one alias for any repository, and that is the repository
> +        itself.  For default repositories, the context object is the
> +        appropriate default object.
> +
> +        Where a repository is the default for a product or a distribution
> +        source package, the repository is available through a number of
> +        different URLs.  These URLs are the aliases for the repository.
> +
> +        For example, a repository which is the default for the 'fooix'
> +        project and which is also its owner's default repository for that
> +        project is accessible using:
> +          fooix - the context object is the project fooix
> +          ~fooix-owner/fooix - the context object is the person-project
> +              ~fooix-owner and fooix
> +          ~fooix-owner/fooix/+git/fooix - the unique name of the repository
> +              where the context object is the repository itself.

##GITNAMESPACE##?

> +        """
> +
> +
> +class IGitRepositoryModerateAttributes(Interface):
> +    """IGitRepository attributes that can be edited by more than one community.
> +    """
> +
> +    # XXX cjwatson 2015-01-29: Add some advice about default repository
> +    # naming.
> +    name = TextLine(
> +        title=_("Name"), required=True,
> +        constraint=git_repository_name_validator,
> +        description=_(
> +            "The repository name. Keep very short, unique, and descriptive, "
> +            "because it will be used in URLs."))
> +
> +
> +class IGitRepositoryModerate(Interface):
> +    """IGitRepository methods that can be called by more than one community."""
> +
> +    def transitionToInformationType(information_type, user,
> +                                    verify_policy=True):
> +        """Set the information type for this repository.
> +
> +        :param information_type: The `InformationType` to transition to.
> +        :param user: The `IPerson` who is making the change.
> +        :param verify_policy: Check if the new information type complies
> +            with the `IGitNamespacePolicy`.
> +        """
> +
> +
> +class IGitRepositoryEdit(Interface):
> +    """IGitRepository methods that require launchpad.Edit permission."""
> +
> +    def setOwner(new_owner, user):
> +        """Set the owner of the repository to be `new_owner`."""
> +
> +    def setTarget(target, user):
> +        """Set the target of the repository."""
> +
> +    def destroySelf():
> +        """Delete the specified repository."""
> +
> +
> +class IGitRepository(IGitRepositoryView, IGitRepositoryModerateAttributes,
> +                     IGitRepositoryModerate, IGitRepositoryEdit):
> +    """A Git repository."""
> +
> +    private = Bool(
> +        title=_("Repository is confidential"), required=False, readonly=True,

I think "Private" is probably more understandable.

> +        description=_("This repository is visible only to its subscribers."))
> +
> +
> +class IGitRepositorySet(Interface):
> +    """Interface representing the set of Git repositories."""
> +
> +    def new(registrant, owner, target, name, information_type=None,
> +            date_created=None):
> +        """Create a Git repository and return it.
> +
> +        :param registrant: The `IPerson` who registered the new repository.
> +        :param owner: The `IPerson` who owns the new repository.
> +        :param target: The `IProduct`, `IDistributionSourcePackage`, or
> +            `IPerson` that the new repository is associated with.
> +        :param name: The repository name.
> +        :param information_type: Set the repository's information type to
> +            one different from the target's default.  The type must conform
> +            to the target's code sharing policy.  (optional)
> +        """
> +
> +    # Marker for references to Git URL layouts: ##GITNAMESPACE##
> +    def getByPath(user, 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 None if no match was found.
> +        """
> +
> +    def getDefaultRepository(target, owner=None):
> +        """Get the default repository for a target or owner-target.
> +
> +        :param target: An `IHasGitRepositories`.
> +        :param owner: An `IPerson`, in which case search for that person's
> +            default repository for this target; or None, in which case
> +            search for the overall default repository for this target.
> +
> +        :raises GitTargetError: if `target` is an `IPerson`.
> +        :return: An `IGitRepository`, or None.
> +        """
> +
> +    def getRepositories():
> +        """Return an empty collection of repositories.
> +
> +        This only exists to keep lazr.restful happy.
> +        """
> +
> +
> +class GitIdentityMixin:
> +    """This mixin class determines Git repository paths.
> +
> +    Used by both the model GitRepository class and the browser repository
> +    listing item.  This allows the browser code to cache the associated
> +    context objects which reduces query counts.
> +    """
> +
> +    @property
> +    def shortened_path(self):
> +        """See `IGitRepository`."""
> +        path, context = self.getRepositoryIdentities()[0]
> +        return path
> +
> +    @property
> +    def git_identity(self):
> +        """See `IGitRepository`."""
> +        return "lp:" + self.shortened_path
> +
> +    def getRepositoryDefaults(self):
> +        """See `IGitRepository`."""
> +        # XXX cjwatson 2015-02-06: This will return shortcut defaults once
> +        # they're implemented.
> +        return []
> +
> +    def getRepositoryIdentities(self):
> +        """See `IGitRepository`."""
> +        identities = [
> +            (default.path, default.context)
> +            for default in self.getRepositoryDefaults()]
> +        identities.append((self.unique_name, self))
> +        return identities
> +
> +
> +def user_has_special_git_repository_access(user):
> +    """Admins have special access.
> +
> +    :param user: An `IPerson` or None.
> +    """
> +    if user is None:
> +        return False
> +    roles = IPersonRoles(user)
> +    if roles.in_admin:
> +        return True
> +    return False
> 
> === added file 'lib/lp/code/interfaces/hasgitrepositories.py'
> --- lib/lp/code/interfaces/hasgitrepositories.py	1970-01-01 00:00:00 +0000
> +++ lib/lp/code/interfaces/hasgitrepositories.py	2015-02-18 14:02:32 +0000
> @@ -0,0 +1,40 @@
> +# Copyright 2015 Canonical Ltd.  This software is licensed under the
> +# GNU Affero General Public License version 3 (see the file LICENSE).
> +
> +"""Interfaces relating to targets of Git repositories."""
> +
> +__metaclass__ = type
> +
> +__all__ = [
> +    'IHasGitRepositories',
> +    ]
> +
> +from zope.interface import Interface
> +
> +
> +class IHasGitRepositories(Interface):
> +    """An object that has related Git repositories.
> +
> +    A project contains Git repositories, a source package on a distribution
> +    contains branches, and a person contains "personal" branches.
> +    """
> +
> +    def getGitRepositories(visible_by_user=None, eager_load=False):
> +        """Returns all Git repositories related to this object.
> +
> +        :param visible_by_user: Normally the user who is asking.
> +        :param eager_load: If True, load related objects for the whole
> +            collection.
> +        :returns: A list of `IGitRepository` objects.
> +        """
> +
> +    def createGitRepository(registrant, owner, name, information_type=None):
> +        """Create a Git repository for this target and return it.
> +
> +        :param registrant: The `IPerson` who registered the new repository.
> +        :param owner: The `IPerson` who owns the new repository.
> +        :param name: The repository name.
> +        :param information_type: Set the repository's information type to
> +            one different from the target's default.  The type must conform
> +            to the target's code sharing policy.  (optional)
> +        """
> 
> === modified file 'lib/lp/code/model/branch.py'
> --- lib/lp/code/model/branch.py	2014-01-15 00:59:48 +0000
> +++ lib/lp/code/model/branch.py	2015-02-18 14:02:32 +0000
> @@ -208,7 +208,6 @@
>      mirror_status_message = StringCol(default=None)
>      information_type = EnumCol(
>          enum=InformationType, default=InformationType.PUBLIC)
> -    access_policy = IntCol()
>  
>      @property
>      def private(self):
> @@ -1661,7 +1660,7 @@
>  
>      policy_grant_query = Coalesce(
>          ArrayIntersects(
> -            Array(branch_class.access_policy),
> +            Array(SQL('%s.access_policy' % branch_class.__storm_table__)),
>              Select(
>                  ArrayAgg(AccessPolicyGrant.policy_id),
>                  tables=(AccessPolicyGrant,
> 
> === added file 'lib/lp/code/model/gitrepository.py'
> --- lib/lp/code/model/gitrepository.py	1970-01-01 00:00:00 +0000
> +++ lib/lp/code/model/gitrepository.py	2015-02-18 14:02:32 +0000
> @@ -0,0 +1,403 @@
> +# Copyright 2015 Canonical Ltd.  This software is licensed under the
> +# GNU Affero General Public License version 3 (see the file LICENSE).
> +
> +__metaclass__ = type
> +__all__ = [
> +    'get_git_repository_privacy_filter',
> +    'GitRepository',
> +    'GitRepositorySet',
> +    ]
> +
> +from bzrlib import urlutils
> +import pytz
> +from storm.expr import (
> +    Coalesce,
> +    Join,
> +    Or,
> +    Select,
> +    SQL,
> +    )
> +from storm.locals import (
> +    Bool,
> +    DateTime,
> +    Int,
> +    Reference,
> +    Unicode,
> +    )
> +from zope.component import getUtility
> +from zope.interface import implements
> +from zope.security.interfaces import Unauthorized
> +
> +from lp.app.enums import (
> +    InformationType,
> +    PRIVATE_INFORMATION_TYPES,
> +    PUBLIC_INFORMATION_TYPES,
> +    )
> +from lp.app.interfaces.informationtype import IInformationType
> +from lp.app.interfaces.launchpad import IPrivacy
> +from lp.app.interfaces.services import IService
> +from lp.code.errors import (
> +    GitDefaultConflict,
> +    GitTargetError,
> +    )
> +from lp.code.interfaces.gitrepository import (
> +    GitIdentityMixin,
> +    IGitRepository,
> +    IGitRepositorySet,
> +    user_has_special_git_repository_access,
> +    )
> +from lp.registry.errors import CannotChangeInformationType
> +from lp.registry.interfaces.accesspolicy import (
> +    IAccessArtifactSource,
> +    IAccessPolicySource,
> +    )
> +from lp.registry.interfaces.distributionsourcepackage import (
> +    IDistributionSourcePackage,
> +    )
> +from lp.registry.interfaces.product import IProduct
> +from lp.registry.interfaces.role import IHasOwner
> +from lp.registry.interfaces.sharingjob import (
> +    IRemoveArtifactSubscriptionsJobSource,
> +    )
> +from lp.registry.model.accesspolicy import (
> +    AccessPolicyGrant,
> +    reconcile_access_for_artifact,
> +    )
> +from lp.registry.model.teammembership import TeamParticipation
> +from lp.services.config import config
> +from lp.services.database.constants import (
> +    DEFAULT,
> +    UTC_NOW,
> +    )
> +from lp.services.database.enumcol import EnumCol
> +from lp.services.database.interfaces import IStore
> +from lp.services.database.stormbase import StormBase
> +from lp.services.database.stormexpr import (
> +    Array,
> +    ArrayAgg,
> +    ArrayIntersects,
> +    )
> +from lp.services.propertycache import cachedproperty
> +from lp.services.webapp.authorization import check_permission
> +
> +
> +def git_repository_modified(repository, event):
> +    """Update the date_last_modified property when a GitRepository is modified.
> +
> +    This method is registered as a subscriber to `IObjectModifiedEvent`
> +    events on Git repositories.
> +    """
> +    repository.date_last_modified = UTC_NOW
> +
> +
> +class GitRepository(StormBase, GitIdentityMixin):
> +    """See `IGitRepository`."""
> +
> +    __storm_table__ = 'GitRepository'
> +
> +    implements(IGitRepository, IHasOwner, IPrivacy, IInformationType)
> +
> +    id = Int(primary=True)
> +
> +    date_created = DateTime(
> +        name='date_created', tzinfo=pytz.UTC, allow_none=False)
> +    date_last_modified = DateTime(
> +        name='date_last_modified', tzinfo=pytz.UTC, allow_none=False)
> +
> +    registrant_id = Int(name='registrant', allow_none=False)
> +    registrant = Reference(registrant_id, 'Person.id')
> +
> +    owner_id = Int(name='owner', allow_none=False)
> +    owner = Reference(owner_id, 'Person.id')
> +
> +    project_id = Int(name='project', allow_none=True)
> +    project = Reference(project_id, 'Product.id')
> +
> +    distribution_id = Int(name='distribution', allow_none=True)
> +    distribution = Reference(distribution_id, 'Distribution.id')
> +
> +    sourcepackagename_id = Int(name='sourcepackagename', allow_none=True)
> +    sourcepackagename = Reference(sourcepackagename_id, 'SourcePackageName.id')
> +
> +    name = Unicode(name='name', allow_none=False)
> +
> +    information_type = EnumCol(enum=InformationType, notNull=True)
> +    owner_default = Bool(name='owner_default', allow_none=False)
> +    target_default = Bool(name='target_default', allow_none=False)
> +
> +    def __init__(self, registrant, owner, target, name, information_type,
> +                 date_created):
> +        super(GitRepository, self).__init__()
> +        self.registrant = registrant
> +        self.owner = owner
> +        self.name = name
> +        self.information_type = information_type
> +        self.date_created = date_created
> +        self.date_last_modified = date_created
> +        self.project = None
> +        self.distribution = None
> +        self.sourcepackagename = None
> +        if IProduct.providedBy(target):
> +            self.project = target
> +        elif IDistributionSourcePackage.providedBy(target):
> +            self.distribution = target.distribution
> +            self.sourcepackagename = target.sourcepackagename
> +        self.owner_default = False
> +        self.target_default = False
> +
> +    # Marker for references to Git URL layouts: ##GITNAMESPACE##
> +    @property
> +    def unique_name(self):
> +        names = {"owner": self.owner.name, "repository": self.name}
> +        if self.project is not None:
> +            fmt = "~%(owner)s/%(project)s"
> +            names["project"] = self.project.name
> +        elif self.distribution is not None:
> +            fmt = "~%(owner)s/%(distribution)s/+source/%(source)s"
> +            names["distribution"] = self.distribution.name
> +            names["source"] = self.sourcepackagename.name
> +        else:
> +            fmt = "~%(owner)s"
> +        fmt += "/+git/%(repository)s"
> +        return fmt % names
> +
> +    def __repr__(self):
> +        return "<GitRepository %r (%d)>" % (self.unique_name, self.id)
> +
> +    @cachedproperty
> +    def target(self):
> +        """See `IGitRepository`."""
> +        if self.project is None:
> +            if self.distribution is None:
> +                return self.owner
> +            else:
> +                return self.distribution.getSourcePackage(
> +                    self.sourcepackagename)
> +        else:
> +            return self.project

This would make more sense as an if/elif/else.

> +
> +    def setTarget(self, target, user):
> +        """See `IGitRepository`."""
> +        # XXX cjwatson 2015-02-06: Fill this in once IGitNamespace is in
> +        # place.
> +        raise NotImplementedError
> +
> +    def setOwnerDefault(self, value):
> +        """See `IGitRepository`."""
> +        if not check_permission("launchpad.Edit", self.owner):
> +            raise Unauthorized(
> +                "You don't have permission to change the default repository "
> +                "for %s on '%s'." %
> +                (self.owner.displayname, self.target.displayname))
> +        if value:
> +            # Check for an existing owner-target default.
> +            existing = getUtility(IGitRepositorySet).getDefaultRepository(
> +                self.target, owner=self.owner)
> +            if existing is not None:
> +                raise GitDefaultConflict(
> +                    existing, self.target, owner=self.owner)
> +        self.owner_default = value
> +
> +    def setTargetDefault(self, value):
> +        """See `IGitRepository`."""
> +        if not check_permission("launchpad.Edit", self.target):
> +            raise Unauthorized(
> +                "You don't have permission to change the default repository "
> +                "for '%s'." % self.target.displayname)
> +        if value:
> +            # Check for an existing target default.
> +            existing = getUtility(IGitRepositorySet).getDefaultRepository(
> +                self.target)
> +            if existing is not None:
> +                raise GitDefaultConflict(existing, self.target)
> +        self.target_default = value
> +
> +    @property
> +    def displayname(self):
> +        return self.git_identity
> +
> +    def getInternalPath(self):
> +        """See `IGitRepository`."""
> +        # This may need to change later to improve support for sharding.
> +        return str(self.id)
> +
> +    def getCodebrowseUrl(self):
> +        """See `IGitRepository`."""
> +        return urlutils.join(
> +            config.codehosting.git_browse_root, self.unique_name)
> +
> +    @property
> +    def private(self):
> +        return self.information_type in PRIVATE_INFORMATION_TYPES
> +
> +    def _reconcileAccess(self):
> +        """Reconcile the repository's sharing information.
> +
> +        Takes the information_type and target and makes the related
> +        AccessArtifact and AccessPolicyArtifacts match.
> +        """
> +        wanted_links = None
> +        pillars = []
> +        # For private personal repositories, we calculate the wanted grants.
> +        if (not self.project and not self.distribution and
> +            not self.information_type in PUBLIC_INFORMATION_TYPES):
> +            aasource = getUtility(IAccessArtifactSource)
> +            [abstract_artifact] = aasource.ensure([self])
> +            wanted_links = set(
> +                (abstract_artifact, policy) for policy in
> +                getUtility(IAccessPolicySource).findByTeam([self.owner]))
> +        else:
> +            # We haven't yet quite worked out how distribution privacy
> +            # works, so only work for projects for now.
> +            if self.project is not None:
> +                pillars = [self.project]
> +        reconcile_access_for_artifact(
> +            self, self.information_type, pillars, wanted_links)
> +
> +    @cachedproperty
> +    def _known_viewers(self):
> +        """A set of known persons able to view this repository.
> +
> +        This method must return an empty set or repository searches will
> +        trigger late evaluation.  Any 'should be set on load' properties
> +        must be done by the repository search.
> +
> +        If you are tempted to change this method, don't. Instead see
> +        visibleByUser which defines the just-in-time policy for repository
> +        visibility, and IGitCollection which honours visibility rules.
> +        """
> +        return set()
> +
> +    def visibleByUser(self, user):
> +        """See `IGitRepository`."""
> +        if self.information_type in PUBLIC_INFORMATION_TYPES:
> +            return True
> +        elif user is None:
> +            return False
> +        elif user.id in self._known_viewers:
> +            return True
> +        else:
> +            # XXX cjwatson 2015-02-06: Fill this in once IGitCollection is
> +            # in place.
> +            return False
> +
> +    def getAllowedInformationTypes(self, user):
> +        """See `IGitRepository`."""
> +        if user_has_special_git_repository_access(user):
> +            # Admins can set any type.
> +            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)
> +        return types
> +
> +    def transitionToInformationType(self, information_type, user,
> +                                    verify_policy=True):
> +        """See `IGitRepository`."""
> +        if self.information_type == information_type:
> +            return
> +        if (verify_policy and
> +            information_type not in self.getAllowedInformationTypes(user)):
> +            raise CannotChangeInformationType("Forbidden by project policy.")
> +        self.information_type = information_type
> +        self._reconcileAccess()
> +        # XXX cjwatson 2015-02-05: Once we have repository subscribers, we
> +        # need to grant them access if necessary.  For now, treat the owner
> +        # as always subscribed, which is just about enough to make the
> +        # GitCollection tests pass.
> +        if information_type in PRIVATE_INFORMATION_TYPES:
> +            # Grant the subscriber access if they can't see the repository.
> +            service = getUtility(IService, "sharing")
> +            blind_subscribers = service.getPeopleWithoutAccess(
> +                self, [self.owner])
> +            if len(blind_subscribers):
> +                service.ensureAccessGrants(
> +                    blind_subscribers, user, gitrepositories=[self],
> +                    ignore_permissions=True)
> +        # As a result of the transition, some subscribers may no longer have
> +        # access to the repository.  We need to run a job to remove any such
> +        # subscriptions.
> +        getUtility(IRemoveArtifactSubscriptionsJobSource).create(user, [self])
> +
> +    def setOwner(self, new_owner, user):
> +        """See `IGitRepository`."""
> +        # XXX cjwatson 2015-02-06: Fill this in once IGitNamespace is in
> +        # place.
> +        raise NotImplementedError
> +
> +    def destroySelf(self):
> +        raise NotImplementedError
> +
> +
> +class GitRepositorySet:
> +    """See `IGitRepositorySet`."""
> +
> +    implements(IGitRepositorySet)
> +
> +    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
> +
> +    def getByPath(self, user, path):
> +        """See `IGitRepositorySet`."""
> +        # XXX cjwatson 2015-02-06: Fill this in once IGitLookup is in place.
> +        raise NotImplementedError
> +
> +    def getDefaultRepository(self, target, owner=None):
> +        """See `IGitRepositorySet`."""
> +        clauses = []
> +        if IProduct.providedBy(target):
> +            clauses.append(GitRepository.project == target)
> +        elif IDistributionSourcePackage.providedBy(target):
> +            clauses.append(GitRepository.distribution == target.distribution)
> +            clauses.append(
> +                GitRepository.sourcepackagename == target.sourcepackagename)
> +        else:
> +            raise GitTargetError(
> +                "Personal repositories cannot be defaults for any target.")
> +        if owner is not None:
> +            clauses.append(GitRepository.owner == owner)
> +            clauses.append(GitRepository.owner_default == True)
> +        else:
> +            clauses.append(GitRepository.target_default == True)
> +        return IStore(GitRepository).find(GitRepository, *clauses).one()
> +
> +    def getRepositories(self):
> +        """See `IGitRepositorySet`."""
> +        return []
> +
> +
> +def get_git_repository_privacy_filter(user):
> +    public_filter = GitRepository.information_type.is_in(
> +        PUBLIC_INFORMATION_TYPES)
> +
> +    if user is None:
> +        return [public_filter]
> +
> +    artifact_grant_query = Coalesce(
> +        ArrayIntersects(
> +            SQL("GitRepository.access_grants"),
> +            Select(
> +                ArrayAgg(TeamParticipation.teamID),
> +                tables=TeamParticipation,
> +                where=(TeamParticipation.person == user)
> +            )), False)
> +
> +    policy_grant_query = Coalesce(
> +        ArrayIntersects(
> +            Array(SQL("GitRepository.access_policy")),
> +            Select(
> +                ArrayAgg(AccessPolicyGrant.policy_id),
> +                tables=(AccessPolicyGrant,
> +                        Join(TeamParticipation,
> +                            TeamParticipation.teamID ==
> +                            AccessPolicyGrant.grantee_id)),
> +                where=(TeamParticipation.person == user)
> +            )), False)
> +
> +    return [Or(public_filter, artifact_grant_query, policy_grant_query)]
> 
> === added file 'lib/lp/code/model/hasgitrepositories.py'
> --- lib/lp/code/model/hasgitrepositories.py	1970-01-01 00:00:00 +0000
> +++ lib/lp/code/model/hasgitrepositories.py	2015-02-18 14:02:32 +0000
> @@ -0,0 +1,28 @@
> +# Copyright 2015 Canonical Ltd.  This software is licensed under the
> +# GNU Affero General Public License version 3 (see the file LICENSE).
> +
> +__metaclass__ = type
> +__all__ = [
> +    'HasGitRepositoriesMixin',
> +    ]
> +
> +from zope.component import getUtility
> +
> +from lp.code.interfaces.gitrepository import IGitRepositorySet
> +
> +
> +class HasGitRepositoriesMixin:
> +    """A mixin implementation for `IHasGitRepositories`."""
> +
> +    def createGitRepository(self, registrant, owner, name,
> +                            information_type=None):
> +        """See `IHasGitRepositories`."""
> +        return getUtility(IGitRepositorySet).new(
> +            registrant, owner, self, name,
> +            information_type=information_type)
> +
> +    def getGitRepositories(self, visible_by_user=None, eager_load=False):
> +        """See `IHasGitRepositories`."""
> +        # XXX cjwatson 2015-02-06: Fill this in once IGitCollection is in
> +        # place.
> +        raise NotImplementedError
> 
> === added file 'lib/lp/code/model/tests/test_hasgitrepositories.py'
> --- lib/lp/code/model/tests/test_hasgitrepositories.py	1970-01-01 00:00:00 +0000
> +++ lib/lp/code/model/tests/test_hasgitrepositories.py	2015-02-18 14:02:32 +0000
> @@ -0,0 +1,34 @@
> +# Copyright 2015 Canonical Ltd.  This software is licensed under the
> +# GNU Affero General Public License version 3 (see the file LICENSE).
> +
> +"""Tests for classes that implement IHasGitRepositories."""
> +
> +__metaclass__ = type
> +
> +from lp.code.interfaces.hasgitrepositories import IHasGitRepositories
> +from lp.testing import (
> +    TestCaseWithFactory,
> +    verifyObject,
> +    )
> +from lp.testing.layers import DatabaseFunctionalLayer
> +
> +
> +class TestIHasGitRepositories(TestCaseWithFactory):
> +    """Test that the correct objects implement the interface."""
> +
> +    layer = DatabaseFunctionalLayer
> +
> +    def test_project_implements_hasgitrepositories(self):
> +        # Projects should implement IHasGitRepositories.
> +        project = self.factory.makeProduct()
> +        verifyObject(IHasGitRepositories, project)
> +
> +    def test_dsp_implements_hasgitrepositories(self):
> +        # DistributionSourcePackages should implement IHasGitRepositories.
> +        dsp = self.factory.makeDistributionSourcePackage()
> +        verifyObject(IHasGitRepositories, dsp)
> +
> +    def test_person_implements_hasgitrepositories(self):
> +        # People should implement IHasGitRepositories.
> +        person = self.factory.makePerson()
> +        verifyObject(IHasGitRepositories, person)
> 
> === modified file 'lib/lp/registry/configure.zcml'
> --- lib/lp/registry/configure.zcml	2015-02-09 17:42:48 +0000
> +++ lib/lp/registry/configure.zcml	2015-02-18 14:02:32 +0000
> @@ -556,6 +556,11 @@
>                  bug_reporting_guidelines
>                  enable_bugfiling_duplicate_search
>                  "/>
> +
> +        <!-- IHasGitRepositories -->
> +
> +        <allow
> +            interface="lp.code.interfaces.hasgitrepositories.IHasGitRepositories" />
>      </class>
>      <adapter
>          provides="lp.registry.interfaces.distribution.IDistribution"
> 
> === modified file 'lib/lp/registry/interfaces/distributionsourcepackage.py'
> --- lib/lp/registry/interfaces/distributionsourcepackage.py	2014-11-28 22:28:40 +0000
> +++ lib/lp/registry/interfaces/distributionsourcepackage.py	2015-02-18 14:02:32 +0000
> @@ -34,6 +34,7 @@
>      IHasBranches,
>      IHasMergeProposals,
>      )
> +from lp.code.interfaces.hasgitrepositories import IHasGitRepositories
>  from lp.registry.interfaces.distribution import IDistribution
>  from lp.registry.interfaces.role import IHasDrivers
>  from lp.soyuz.enums import ArchivePurpose
> @@ -42,7 +43,8 @@
>  class IDistributionSourcePackage(IHeadingContext, IBugTarget, IHasBranches,
>                                   IHasMergeProposals, IHasOfficialBugTags,
>                                   IStructuralSubscriptionTarget,
> -                                 IQuestionTarget, IHasDrivers):
> +                                 IQuestionTarget, IHasDrivers,
> +                                 IHasGitRepositories):
>      """Represents a source package in a distribution.
>  
>      Create IDistributionSourcePackages by invoking
> 
> === modified file 'lib/lp/registry/interfaces/person.py'
> --- lib/lp/registry/interfaces/person.py	2015-01-30 18:24:07 +0000
> +++ lib/lp/registry/interfaces/person.py	2015-02-18 14:02:32 +0000
> @@ -111,6 +111,7 @@
>      IHasMergeProposals,
>      IHasRequestedReviews,
>      )
> +from lp.code.interfaces.hasgitrepositories import IHasGitRepositories
>  from lp.code.interfaces.hasrecipes import IHasRecipes
>  from lp.registry.enums import (
>      EXCLUSIVE_TEAM_POLICY,
> @@ -688,7 +689,7 @@
>                      IHasMergeProposals, IHasMugshot,
>                      IHasLocation, IHasRequestedReviews, IObjectWithLocation,
>                      IHasBugs, IHasRecipes, IHasTranslationImports,
> -                    IPersonSettings, IQuestionsPerson):
> +                    IPersonSettings, IQuestionsPerson, IHasGitRepositories):
>      """IPerson attributes that require launchpad.View permission."""
>      account = Object(schema=IAccount)
>      accountID = Int(title=_('Account ID'), required=True, readonly=True)
> 
> === modified file 'lib/lp/registry/interfaces/product.py'
> --- lib/lp/registry/interfaces/product.py	2015-01-30 18:24:07 +0000
> +++ lib/lp/registry/interfaces/product.py	2015-02-18 14:02:32 +0000
> @@ -102,6 +102,7 @@
>      IHasCodeImports,
>      IHasMergeProposals,
>      )
> +from lp.code.interfaces.hasgitrepositories import IHasGitRepositories
>  from lp.code.interfaces.hasrecipes import IHasRecipes
>  from lp.registry.enums import (
>      BranchSharingPolicy,
> @@ -475,7 +476,7 @@
>      IHasMugshot, IHasSprints, IHasTranslationImports,
>      ITranslationPolicy, IKarmaContext, IMakesAnnouncements,
>      IOfficialBugTagTargetPublic, IHasOOPSReferences,
> -    IHasRecipes, IHasCodeImports, IServiceUsage):
> +    IHasRecipes, IHasCodeImports, IServiceUsage, IHasGitRepositories):
>      """Public IProduct properties."""
>  
>      registrant = exported(
> 
> === modified file 'lib/lp/registry/model/distributionsourcepackage.py'
> --- lib/lp/registry/model/distributionsourcepackage.py	2014-11-27 20:52:37 +0000
> +++ lib/lp/registry/model/distributionsourcepackage.py	2015-02-18 14:02:32 +0000
> @@ -43,6 +43,7 @@
>      HasBranchesMixin,
>      HasMergeProposalsMixin,
>      )
> +from lp.code.model.hasgitrepositories import HasGitRepositoriesMixin
>  from lp.registry.interfaces.distributionsourcepackage import (
>      IDistributionSourcePackage,
>      )
> @@ -119,7 +120,7 @@
>                                  HasBranchesMixin,
>                                  HasCustomLanguageCodesMixin,
>                                  HasMergeProposalsMixin,
> -                                HasDriversMixin):
> +                                HasDriversMixin, HasGitRepositoriesMixin):
>      """This is a "Magic Distribution Source Package". It is not an
>      SQLObject, but instead it represents a source package with a particular
>      name in a particular distribution. You can then ask it all sorts of
> 
> === modified file 'lib/lp/registry/model/person.py'
> --- lib/lp/registry/model/person.py	2015-01-28 16:10:51 +0000
> +++ lib/lp/registry/model/person.py	2015-02-18 14:02:32 +0000
> @@ -146,6 +146,7 @@
>      HasMergeProposalsMixin,
>      HasRequestedReviewsMixin,
>      )
> +from lp.code.model.hasgitrepositories import HasGitRepositoriesMixin
>  from lp.registry.enums import (
>      EXCLUSIVE_TEAM_POLICY,
>      INCLUSIVE_TEAM_POLICY,
> @@ -476,7 +477,7 @@
>  class Person(
>      SQLBase, HasBugsBase, HasSpecificationsMixin, HasTranslationImportsMixin,
>      HasBranchesMixin, HasMergeProposalsMixin, HasRequestedReviewsMixin,
> -    QuestionsPersonMixin):
> +    QuestionsPersonMixin, HasGitRepositoriesMixin):
>      """A Person."""
>  
>      implements(IPerson, IHasIcon, IHasLogo, IHasMugshot)
> 
> === modified file 'lib/lp/registry/model/product.py'
> --- lib/lp/registry/model/product.py	2015-01-29 16:28:30 +0000
> +++ lib/lp/registry/model/product.py	2015-02-18 14:02:32 +0000
> @@ -124,6 +124,7 @@
>      HasCodeImportsMixin,
>      HasMergeProposalsMixin,
>      )
> +from lp.code.model.hasgitrepositories import HasGitRepositoriesMixin
>  from lp.code.model.sourcepackagerecipe import SourcePackageRecipe
>  from lp.code.model.sourcepackagerecipedata import SourcePackageRecipeData
>  from lp.registry.enums import (
> @@ -361,7 +362,7 @@
>                OfficialBugTagTargetMixin, HasBranchesMixin,
>                HasCustomLanguageCodesMixin, HasMergeProposalsMixin,
>                HasCodeImportsMixin, InformationTypeMixin,
> -              TranslationPolicyMixin):
> +              TranslationPolicyMixin, HasGitRepositoriesMixin):
>      """A Product."""
>  
>      implements(
> 
> === modified file 'lib/lp/registry/tests/test_product.py'
> --- lib/lp/registry/tests/test_product.py	2015-01-29 16:28:30 +0000
> +++ lib/lp/registry/tests/test_product.py	2015-02-18 14:02:32 +0000
> @@ -858,10 +858,10 @@
>              'getCustomLanguageCode', 'getDefaultBugInformationType',
>              'getDefaultSpecificationInformationType',
>              'getEffectiveTranslationPermission', 'getExternalBugTracker',
> -            'getFAQ', 'getFirstEntryToImport', 'getLinkedBugWatches',
> -            'getMergeProposals', 'getMilestone', 'getMilestonesAndReleases',
> -            'getQuestion', 'getQuestionLanguages', 'getPackage', 'getRelease',
> -            'getSeries', 'getSubscription',
> +            'getFAQ', 'getFirstEntryToImport', 'getGitRepositories',
> +            'getLinkedBugWatches', 'getMergeProposals', 'getMilestone',
> +            'getMilestonesAndReleases', 'getQuestion', 'getQuestionLanguages',
> +            'getPackage', 'getRelease', 'getSeries', 'getSubscription',
>              'getSubscriptions', 'getSupportedLanguages', 'getTimeline',
>              'getTopContributors', 'getTopContributorsGroupedByCategory',
>              'getTranslationGroups', 'getTranslationImportQueueEntries',
> @@ -902,7 +902,8 @@
>          'launchpad.Edit': set((
>              'addOfficialBugTag', 'removeOfficialBugTag',
>              'setBranchSharingPolicy', 'setBugSharingPolicy',
> -            'setSpecificationSharingPolicy', 'checkInformationType')),
> +            'setSpecificationSharingPolicy', 'checkInformationType',
> +            'createGitRepository')),
>          'launchpad.Moderate': set((
>              'is_permitted', 'license_approved', 'project_reviewed',
>              'reviewer_whiteboard', 'setAliases')),
> 
> === modified file 'lib/lp/security.py'
> --- lib/lp/security.py	2015-01-06 04:52:44 +0000
> +++ lib/lp/security.py	2015-02-18 14:02:32 +0000
> @@ -83,6 +83,10 @@
>      )
>  from lp.code.interfaces.codereviewvote import ICodeReviewVoteReference
>  from lp.code.interfaces.diff import IPreviewDiff
> +from lp.code.interfaces.gitrepository import (
> +    IGitRepository,
> +    user_has_special_git_repository_access,
> +    )
>  from lp.code.interfaces.sourcepackagerecipe import ISourcePackageRecipe
>  from lp.code.interfaces.sourcepackagerecipebuild import (
>      ISourcePackageRecipeBuild,
> @@ -1151,14 +1155,37 @@
>  
>  
>  class EditDistributionSourcePackage(AuthorizationBase):
> -    """DistributionSourcePackage is not editable.
> -
> -    But EditStructuralSubscription needs launchpad.Edit defined on all
> -    targets.
> -    """
>      permission = 'launchpad.Edit'
>      usedfor = IDistributionSourcePackage
>  
> +    def _checkUpload(self, user, archive, distroseries):
> +        # We use verifyUpload() instead of checkUpload() because we don't
> +        # have a pocket.  It returns the reason the user can't upload or
> +        # None if they are allowed.
> +        if distroseries is None:
> +            return False
> +        reason = archive.verifyUpload(
> +            user.person, sourcepackagename=self.obj.sourcepackagename,
> +            component=None, distroseries=distroseries, strict_component=False)
> +        return reason is None
> +
> +    def checkAuthenticated(self, user):
> +        """Anyone who can upload a package can edit it.
> +
> +        Checking upload permission requires a distroseries; a reasonable
> +        approximation is to check whether the user can upload the package to
> +        the current series.
> +        """
> +        if user.in_admin:
> +            return True
> +
> +        distribution = self.obj.distribution
> +        if user.inTeam(distribution.owner):
> +            return True
> +
> +        return self._checkUpload(
> +            user, distribution.main_archive, distribution.currentseries)
> +
>  
>  class BugTargetOwnerOrBugSupervisorOrAdmins(AuthorizationBase):
>      """Product's owner and bug supervisor can set official bug tags."""
> @@ -2176,6 +2203,57 @@
>          return user.in_admin
>  
>  
> +class ViewGitRepository(AuthorizationBase):
> +    """Controls visibility of Git repositories.
> +
> +    A person can see the repository if the repository is public, they are
> +    the owner of the repository, they are in the team that owns the
> +    repository, they have an access grant to the repository, or they are a
> +    Launchpad administrator.
> +    """
> +    permission = 'launchpad.View'
> +    usedfor = IGitRepository
> +
> +    def checkAuthenticated(self, user):
> +        return self.obj.visibleByUser(user.person)
> +
> +    def checkUnauthenticated(self):
> +        return self.obj.visibleByUser(None)
> +
> +
> +class EditGitRepository(AuthorizationBase):
> +    """The owner or admins can edit Git repositories."""
> +    permission = 'launchpad.Edit'
> +    usedfor = IGitRepository
> +
> +    def checkAuthenticated(self, user):
> +        # XXX cjwatson 2015-01-23: People who can upload source packages to
> +        # a distribution should be able to push to the corresponding
> +        # "official" repositories, once those are defined.
> +        return (
> +            user.inTeam(self.obj.owner) or
> +            user_has_special_git_repository_access(user.person))
> +
> +
> +class ModerateGitRepository(EditGitRepository):
> +    """The owners, project owners, and admins can moderate Git repositories."""
> +    permission = 'launchpad.Moderate'
> +
> +    def checkAuthenticated(self, user):
> +        if super(ModerateGitRepository, self).checkAuthenticated(user):
> +            return True
> +        target = self.obj.target
> +        if (target is not None and IProduct.providedBy(target) and
> +            user.inTeam(target.owner)):
> +            return True
> +        return user.in_commercial_admin
> +
> +
> +class AdminGitRepository(AdminByAdminsTeam):
> +    """The admins can administer Git repositories."""
> +    usedfor = IGitRepository
> +
> +
>  class AdminDistroSeriesTranslations(AuthorizationBase):
>      permission = 'launchpad.TranslationsAdmin'
>      usedfor = IDistroSeries
> @@ -2858,8 +2936,7 @@
>      usedfor = IPublisherConfig
>  
>  
> -class EditSourcePackage(AuthorizationBase):
> -    permission = 'launchpad.Edit'
> +class EditSourcePackage(EditDistributionSourcePackage):
>      usedfor = ISourcePackage
>  
>      def checkAuthenticated(self, user):
> @@ -2871,15 +2948,8 @@
>          if user.inTeam(distribution.owner):
>              return True
>  
> -        # We use verifyUpload() instead of checkUpload() because
> -        # we don't have a pocket.
> -        # It returns the reason the user can't upload
> -        # or None if they are allowed.
> -        reason = distribution.main_archive.verifyUpload(
> -            user.person, distroseries=self.obj.distroseries,
> -            sourcepackagename=self.obj.sourcepackagename,
> -            component=None, strict_component=False)
> -        return reason is None
> +        return self._checkUpload(
> +            user, distribution.main_archive, self.obj.distroseries)
>  
>  
>  class ViewLiveFS(DelegatedAuthorization):
> 
> === modified file 'lib/lp/services/config/schema-lazr.conf'
> --- lib/lp/services/config/schema-lazr.conf	2014-08-05 08:58:14 +0000
> +++ lib/lp/services/config/schema-lazr.conf	2015-02-18 14:02:32 +0000
> @@ -335,6 +335,27 @@
>  # of shutting down and so should not receive any more connections.
>  web_status_port = tcp:8022
>  
> +# The URL of the internal Git hosting API endpoint.
> +internal_git_endpoint: none
> +
> +# The URL prefix for links to the Git code browser.  Links are formed by
> +# appending the repository's path to the root URL.
> +#
> +# datatype: urlbase
> +git_browse_root: none
> +
> +# The URL prefix for anonymous Git protocol fetches.  Links are formed by
> +# appending the repository's path to the root URL.
> +#
> +# datatype: urlbase
> +git_anon_root: none
> +
> +# The URL prefix for Git-over-SSH.  Links are formed by appending the
> +# repository's path to the root URL.
> +#
> +# datatype: urlbase
> +git_ssh_root: none
> +
>  
>  [codeimport]
>  # Where the Bazaar imports are stored.
> 


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


References