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