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.")) > + > + 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 Though that would make filtering to just personal branches, as target=None sounds like no target filter. So scratch that. > + 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. _______________________________________________ Mailing list: https://launchpad.net/~launchpad-reviewers Post to : [email protected] Unsubscribe : https://launchpad.net/~launchpad-reviewers More help : https://help.launchpad.net/ListHelp

