launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #17853
Re: [Merge] lp:~cjwatson/launchpad/git-basic-model into lp:launchpad
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-07 09:52:36 +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/
> +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-01-28 16:38:13 +0000
> +++ lib/lp/code/configure.zcml 2015-02-07 09:52:36 +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 -->
>
> === 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-07 09:52:36 +0000
> @@ -0,0 +1,384 @@
> +# 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__ = [
> + 'GitPathMixin',
> + 'git_repository_name_validator',
> + 'IGitRepository',
> + 'IGitRepositorySet',
> + 'user_has_special_git_repository_access',
> + ]
> +
> +import re
> +
> +from lazr.restful.fields import (
> + Reference,
> + ReferenceChoice,
> + )
> +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.distributionsourcepackage import (
> + IDistributionSourcePackage,
> + )
> +from lp.registry.interfaces.product import IProduct
> +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=False)
> +
> + 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."))
> +
> + project = ReferenceChoice(
> + title=_("Project"), required=False, readonly=True,
> + vocabulary="Product", schema=IProduct,
> + description=_(
> + "The project that this Git repository belongs to. None if it "
> + "belongs to a distribution source package instead."))
> +
> + # The distribution and sourcepackagename attributes are exported
> + # together as distro_source_package.
> + distribution = Choice(
> + title=_("Distribution"), required=False,
> + vocabulary="Distribution",
> + description=_(
> + "The distribution that this Git repository belongs to. None if it "
> + "belongs to a project instead."))
> +
> + sourcepackagename = Choice(
> + title=_("Source Package Name"), required=False,
> + vocabulary="SourcePackageName",
> + description=_(
> + "The source package that this Git repository belongs to. None if "
> + "it belongs to a project instead. Source package repositories "
> + "always belong to a distribution."))
> +
> + distro_source_package = Reference(
> + title=_(
> + "The IDistributionSourcePackage that this Git repository belongs "
> + "to. None if it belongs to a project instead."),
> + schema=IDistributionSourcePackage, required=False, readonly=True)
> +
> + target = Reference(
> + title=_("Target"), required=True, readonly=True,
> + schema=IHasGitRepositories,
> + description=_("The target of the repository."))
> +
> + information_type = Choice(
> + title=_("Information Type"), vocabulary=InformationType,
> + required=True, readonly=True, default=InformationType.PUBLIC,
> + description=_(
> + "The type of information contained in this repository."))
> +
> + 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."))
> +
> + 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."))
It is, but I only currently plan to export git_identity on the webservice so I wanted its description to be useful in isolation.
> +
> + def codebrowse_url():
> + """Construct a browsing URL for this branch."""
> +
> + def addToLaunchBag(launchbag):
> + """Add information about this branch to `launchbag'.
> +
> + Use this when traversing to this branch in the web UI.
> +
> + In particular, add information about the branch's target to the
> + launchbag. If the branch has a product, add that; if it has a source
> + package, add lots of information about that.
> +
> + :param launchbag: `ILaunchBag`.
> + """
> +
> + 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 getRepositoryLinks():
> + """Return a sorted list of `ICanHasLinkedGitRepository` objects.
> +
> + There is one result for each related object that the repository is
> + linked to. For example, in the case where a branch is linked to a
> + project and is also its owner's preferred branch for that project,
> + the link objects for both the project and the person-project are
> + returned.
> +
> + The sorting uses the defined order of the linked objects where the
> + more important links 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 linked repositories, the context object is the
> + appropriate linked object.
> +
> + Where a repository is linked to 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 linked to the 'fooix' project and
> + which is also its owner's preferred repository for that project is
> + accessible using:
> + fooix - the linked object is the project fooix
> + ~fooix-owner/fooix - the linked object is the person-project
> + ~fooix-owner and fooix
> + ~fooix-owner/fooix/g/fooix - the unique name of the repository
> + where the linked object is the repository itself.
> + """
> +
> +
> +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(user, target):
> + """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,
> + 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)
> + """
> +
> + 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/g/NAME
> + ~OWNER/DISTRO/+source/SOURCE/g/NAME
> + ~OWNER/g/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 getPersonalRepository(person, repository_name):
> + """Find a personal repository."""
> +
> + def getProjectRepository(person, project, repository_name=None):
> + """Find a project repository."""
> +
> + def getPackageRepository(person, distribution, sourcepackagename,
> + repository_name=None):
> + """Find a package repository."""
> +
> + def getRepositories(limit=50, eager_load=True):
> + """Return a collection of repositories.
> +
> + :param eager_load: If True (the default because this is used in the
> + web service and it needs the related objects to create links)
> + eager load related objects (projects, etc.).
> + """
> +
> +
> +class GitPathMixin:
> + """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
> + links 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 getRepositoryLinks(self):
> + """See `IGitRepository`."""
> + # XXX cjwatson 2015-02-06: This will return shortcut links once
> + # they're implemented.
> + return []
> +
> + def getRepositoryIdentities(self):
> + """See `IGitRepository`."""
> + identities = [
> + (link.path, link.context) for link in self.getRepositoryLinks()]
> + 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-07 09:52:36 +0000
> @@ -0,0 +1,50 @@
> +# 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',
> + 'IHasGitRepositoriesEdit',
> + 'IHasGitRepositoriesView',
> + ]
> +
> +from zope.interface import Interface
> +
> +
> +class IHasGitRepositoriesView(Interface):
> + """Viewing an object that has related Git repositories."""
> +
> + 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.
> + """
> +
> +
> +class IHasGitRepositoriesEdit(Interface):
> + """Editing an object that has related Git repositories."""
> +
> + 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)
> + """
> +
> +
> +class IHasGitRepositories(IHasGitRepositoriesView, IHasGitRepositoriesEdit):
> + """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.
> + """
>
> === 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-07 09:52:36 +0000
> @@ -0,0 +1,366 @@
> +# 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 (
> + DateTime,
> + Int,
> + Reference,
> + Unicode,
> + )
> +from zope.component import getUtility
> +from zope.interface import implements
> +
> +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.interfaces.gitrepository import (
> + GitPathMixin,
> + 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.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.stormbase import StormBase
> +from lp.services.database.stormexpr import (
> + Array,
> + ArrayAgg,
> + ArrayIntersects,
> + )
> +from lp.services.propertycache import cachedproperty
> +
> +
> +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, GitPathMixin):
> + """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)
> + access_policy = Int(name='access_policy')
> +
> + def __init__(self, registrant, owner, name, information_type, date_created,
> + project=None, distribution=None, sourcepackagename=None):
> + 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 = project
> + self.distribution = distribution
> + self.sourcepackagename = sourcepackagename
> +
> + @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 += "/g/%(repository)s"
> + return fmt % names
> +
> + def __repr__(self):
> + return "<GitRepository %r (%d)>" % (self.unique_name, self.id)
> +
> + @property
> + def target(self):
> + """See `IGitRepository`."""
> + if self.project is None:
> + if self.distribution is None:
> + return self.owner
> + else:
> + return self.distro_source_package
> + else:
> + return self.project
> +
> + def setTarget(self, user, target):
> + """See `IGitRepository`."""
> + # XXX cjwatson 2015-02-06: Fill this in once IGitNamespace is in
> + # place.
> + raise NotImplementedError
> +
> + @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 codebrowse_url(self):
> + """See `IGitRepository`."""
> + return urlutils.join(
> + config.codehosting.git_browse_root, self.unique_name)
> +
> + @cachedproperty
> + def distro_source_package(self):
> + """See `IGitRepository`."""
> + if self.distribution is not None:
> + return self.distribution.getSourcePackage(self.sourcepackagename)
> + else:
> + return None
> +
> + @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)
> +
> + def addToLaunchBag(self, launchbag):
> + """See `IGitRepository`."""
> + launchbag.add(self.project)
> + launchbag.add(self.distribution)
> +
> + @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 getPersonalRepository(self, person, repository_name):
> + """See `IGitRepositorySet`."""
> + # XXX cjwatson 2015-02-06: Fill this in once IGitNamespace is in
> + # place.
> + raise NotImplementedError
> +
> + def getProjectRepository(self, person, project, repository_name=None):
> + """See `IGitRepositorySet`."""
> + # XXX cjwatson 2015-02-06: Fill this in once IGitNamespace is in
> + # place.
> + raise NotImplementedError
> +
> + def getPackageRepository(self, person, distribution, sourcepackagename,
> + repository_name=None):
> + """See `IGitRepositorySet`."""
> + # XXX cjwatson 2015-02-06: Fill this in once IGitNamespace is in
> + # place.
> + raise NotImplementedError
> +
> + def getByPath(self, user, path):
> + """See `IGitRepositorySet`."""
> + # XXX cjwatson 2015-02-06: Fill this in once IGitLookup is in place.
> + raise NotImplementedError
> +
> + def getRepositories(self, limit=50, eager_load=True):
> + """See `IGitRepositorySet`."""
> + # XXX cjwatson 2015-02-06: Fill this in once IGitCollection is in
> + # place.
> + raise NotImplementedError
> +
> +
> +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(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-07 09:52:36 +0000
> @@ -0,0 +1,29 @@
> +# 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',
> + 'HasGitShortcutsMixin',
> + ]
> +
> +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-07 09:52:36 +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-01-29 16:28:30 +0000
> +++ lib/lp/registry/configure.zcml 2015-02-07 09:52:36 +0000
> @@ -556,6 +556,14 @@
> bug_reporting_guidelines
> enable_bugfiling_duplicate_search
> "/>
> +
> + <!-- IHasGitRepositories -->
> +
> + <allow
> + interface="lp.code.interfaces.hasgitrepositories.IHasGitRepositoriesView" />
> + <require
> + permission="launchpad.Edit"
> + interface="lp.code.interfaces.hasgitrepositories.IHasGitRepositoriesEdit" />
> </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-07 09:52:36 +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-07 09:52:36 +0000
> @@ -111,6 +111,11 @@
> IHasMergeProposals,
> IHasRequestedReviews,
> )
> +from lp.code.interfaces.hasgitrepositories import (
> + IHasGitRepositories,
> + IHasGitRepositoriesEdit,
> + IHasGitRepositoriesView,
> + )
> from lp.code.interfaces.hasrecipes import IHasRecipes
> from lp.registry.enums import (
> EXCLUSIVE_TEAM_POLICY,
> @@ -688,7 +693,8 @@
> IHasMergeProposals, IHasMugshot,
> IHasLocation, IHasRequestedReviews, IObjectWithLocation,
> IHasBugs, IHasRecipes, IHasTranslationImports,
> - IPersonSettings, IQuestionsPerson):
> + IPersonSettings, IQuestionsPerson,
> + IHasGitRepositoriesView):
> """IPerson attributes that require launchpad.View permission."""
> account = Object(schema=IAccount)
> accountID = Int(title=_('Account ID'), required=True, readonly=True)
> @@ -1581,7 +1587,7 @@
> """
>
>
> -class IPersonEditRestricted(Interface):
> +class IPersonEditRestricted(IHasGitRepositoriesEdit):
> """IPerson attributes that require launchpad.Edit permission."""
>
> @call_with(requester=REQUEST_USER)
> @@ -1870,7 +1876,7 @@
> class IPerson(IPersonPublic, IPersonLimitedView, IPersonViewRestricted,
> IPersonEditRestricted, IPersonModerateRestricted,
> IPersonSpecialRestricted, IHasStanding, ISetLocation,
> - IHeadingContext):
> + IHeadingContext, IHasGitRepositories):
> """A Person."""
> export_as_webservice_entry(plural_name='people')
>
>
> === 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-07 09:52:36 +0000
> @@ -102,6 +102,11 @@
> IHasCodeImports,
> IHasMergeProposals,
> )
> +from lp.code.interfaces.hasgitrepositories import (
> + IHasGitRepositories,
> + IHasGitRepositoriesEdit,
> + IHasGitRepositoriesView,
> + )
> from lp.code.interfaces.hasrecipes import IHasRecipes
> from lp.registry.enums import (
> BranchSharingPolicy,
> @@ -475,7 +480,8 @@
> IHasMugshot, IHasSprints, IHasTranslationImports,
> ITranslationPolicy, IKarmaContext, IMakesAnnouncements,
> IOfficialBugTagTargetPublic, IHasOOPSReferences,
> - IHasRecipes, IHasCodeImports, IServiceUsage):
> + IHasRecipes, IHasCodeImports, IServiceUsage,
> + IHasGitRepositoriesView):
> """Public IProduct properties."""
>
> registrant = exported(
> @@ -837,7 +843,8 @@
> """
>
>
> -class IProductEditRestricted(IOfficialBugTagTargetRestricted):
> +class IProductEditRestricted(IOfficialBugTagTargetRestricted,
> + IHasGitRepositoriesEdit):
> """`IProduct` properties which require launchpad.Edit permission."""
>
> @mutator_for(IProductView['bug_sharing_policy'])
> @@ -889,7 +896,7 @@
> IProductModerateRestricted, IProductDriverRestricted, IProductView,
> IProductLimitedView, IProductPublic, IQuestionTarget,
> ISpecificationTarget, IStructuralSubscriptionTarget, IInformationType,
> - IPillar):
> + IPillar, IHasGitRepositories):
> """A Product.
>
> The Launchpad Registry describes the open source world as ProjectGroups
>
> === 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-07 09:52:36 +0000
> @@ -43,6 +43,7 @@
> HasBranchesMixin,
> HasMergeProposalsMixin,
> )
> +from lp.code.model.hasgitrepositories import HasGitRepositoriesMixin
> from lp.registry.interfaces.distributionsourcepackage import (
> IDistributionSourcePackage,
> )
> @@ -119,7 +120,8 @@
> 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-07 09:52:36 +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-07 09:52:36 +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,8 @@
> 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-07 09:52:36 +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-07 09:52:36 +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,34 @@
>
>
> class EditDistributionSourcePackage(AuthorizationBase):
> - """DistributionSourcePackage is not editable.
> -
> - But EditStructuralSubscription needs launchpad.Edit defined on all
> - targets.
> - """
> permission = 'launchpad.Edit'
> usedfor = IDistributionSourcePackage
>
> + 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
> +
> + # 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 distribution.currentseries is None:
> + return False
> + reason = distribution.main_archive.verifyUpload(
> + user.person, sourcepackagename=self.obj.sourcepackagename,
> + component=None, distroseries=distribution.currentseries,
> + strict_component=False)
> + return reason is None
> +
>
> class BugTargetOwnerOrBugSupervisorOrAdmins(AuthorizationBase):
> """Product's owner and bug supervisor can set official bug tags."""
> @@ -2176,6 +2200,56 @@
> 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
> + project = self.obj.project
> + if project is not None and user.inTeam(project.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
>
> === 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-07 09:52:36 +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: https://git.launchpad.net/
> +
> +# 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: git://git.launchpad.net/
> +
> +# 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: git+ssh://git.launchpad.net/
> +
>
> [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