launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #17927
[Merge] lp:~cjwatson/launchpad/git-defaults into lp:launchpad
Colin Watson has proposed merging lp:~cjwatson/launchpad/git-defaults into lp:launchpad.
Commit message:
Implement target and owner-target default Git repositories.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
Bug #1032731 in Launchpad itself: "Support for Launchpad-hosted Git repositories"
https://bugs.launchpad.net/launchpad/+bug/1032731
For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/git-defaults/+merge/250474
Implement target and owner-target default Git repositories.
This presents two different layers because of the different ways we'll need to call this. For the webservice, it's simplest to just have setDefaultGitRepository methods on Product, DistributionSourcePackage, and Person (we don't want to have to export PersonProduct and PersonDistributionSourcePackage as well, I think). But for some internal purposes, it's useful to have an ICanHasDefaultGitRepository abstraction that's sortable and so on, much like ICanHasLinkedBranch: this is useful when working out Git identities and when setting defaults from the XML-RPC endpoint.
--
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/git-defaults into lp:launchpad.
=== modified file 'lib/lp/code/configure.zcml'
--- lib/lp/code/configure.zcml 2015-02-19 23:57:34 +0000
+++ lib/lp/code/configure.zcml 2015-02-20 16:35:44 +0000
@@ -1,4 +1,4 @@
-<!-- Copyright 2009-2013 Canonical Ltd. This software is licensed under the
+<!-- Copyright 2009-2015 Canonical Ltd. This software is licensed under the
GNU Affero General Public License version 3 (see the file LICENSE).
-->
@@ -858,6 +858,13 @@
<allow interface="lp.code.interfaces.gitnamespace.IGitNamespaceSet" />
</securedutility>
+ <!-- Default Git repositories -->
+
+ <adapter factory="lp.code.model.defaultgit.ProjectDefaultGitRepository" />
+ <adapter factory="lp.code.model.defaultgit.PackageDefaultGitRepository" />
+ <adapter factory="lp.code.model.defaultgit.OwnerProjectDefaultGitRepository" />
+ <adapter factory="lp.code.model.defaultgit.OwnerPackageDefaultGitRepository" />
+
<lp:help-folder folder="help" name="+help-code" />
<!-- Diffs -->
=== modified file 'lib/lp/code/errors.py'
--- lib/lp/code/errors.py 2015-02-11 17:10:09 +0000
+++ lib/lp/code/errors.py 2015-02-20 16:35:44 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Errors used in the lp/code modules."""
@@ -22,6 +22,7 @@
'CannotDeleteBranch',
'CannotUpgradeBranch',
'CannotUpgradeNonHosted',
+ 'CannotHaveDefaultGitRepository',
'CannotHaveLinkedBranch',
'CodeImportAlreadyRequested',
'CodeImportAlreadyRunning',
@@ -37,8 +38,10 @@
'GitRepositoryExists',
'GitTargetError',
'InvalidBranchMergeProposal',
+ 'InvalidGitRepositoryException',
'InvalidMergeQueueConfig',
'InvalidNamespace',
+ 'NoDefaultGitRepository',
'NoLinkedBranch',
'NoSuchBranch',
'NoSuchGitRepository',
@@ -375,6 +378,41 @@
"""Raised when there is an error determining a Git repository target."""
+class InvalidGitRepositoryException(Exception):
+ """Base exception for an error resolving a Git repository for a component.
+
+ Subclasses should set _msg_template to match their required display
+ message.
+ """
+
+ _msg_template = "Invalid Git repository for: %s"
+
+ def __init__(self, component):
+ self.component = component
+ # It's expected that components have a name attribute,
+ # so let's assume they will and deal with any error if it occurs.
+ try:
+ component_name = component.name
+ except AttributeError:
+ component_name = str(component)
+ # The display_message contains something readable for the user.
+ self.display_message = self._msg_template % component_name
+ Exception.__init__(self, self._msg_template % (repr(component),))
+
+
+class CannotHaveDefaultGitRepository(InvalidGitRepositoryException):
+ """Raised when we try to get the default Git repository for a thing that
+ can't."""
+
+ _msg_template = "%s cannot have default Git repositories."""
+
+
+class NoDefaultGitRepository(InvalidGitRepositoryException):
+ """Raised when there's no default Git repository for a thing."""
+
+ _msg_template = "%s has no default Git repository."
+
+
class NoSuchGitRepository(NameLookupFailed):
"""Raised when we try to load a Git repository that does not exist."""
=== added file 'lib/lp/code/interfaces/defaultgit.py'
--- lib/lp/code/interfaces/defaultgit.py 1970-01-01 00:00:00 +0000
+++ lib/lp/code/interfaces/defaultgit.py 2015-02-20 16:35:44 +0000
@@ -0,0 +1,60 @@
+# Copyright 2015 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Interface for objects that have a default Git repository.
+
+A default Git repository is a repository that's somehow officially related
+to an object. It might be a project, a distribution source package, or a
+combination of one of those with an owner to represent that owner's default
+repository for that target.
+"""
+
+__metaclass__ = type
+__all__ = [
+ 'get_default_git_repository',
+ 'ICanHasDefaultGitRepository',
+ ]
+
+from zope.interface import (
+ Attribute,
+ Interface,
+ )
+
+from lp.code.errors import (
+ CannotHaveDefaultGitRepository,
+ NoDefaultGitRepository,
+ )
+
+
+class ICanHasDefaultGitRepository(Interface):
+ """Something that has a default Git repository."""
+
+ context = Attribute("The object that can have a default Git repository.")
+ repository = Attribute("The default Git repository.")
+ path = Attribute(
+ "The path for the default Git repository. "
+ "Note that this will be set even if there is no default repository.")
+
+ def setRepository(repository):
+ """Set the default repository.
+
+ :param repository: An `IGitRepository`. After calling this,
+ `ICanHasDefaultGitRepository.repository` will be `repository`.
+ """
+
+
+def get_default_git_repository(provided):
+ """Get the `ICanHasDefaultGitRepository` for 'provided', whatever that is.
+
+ :raise CannotHaveDefaultGitRepository: If 'provided' can never have a
+ default Git repository.
+ :raise NoDefaultGitRepository: If 'provided' could have a default Git
+ repository, but doesn't.
+ :return: The `ICanHasDefaultGitRepository` object.
+ """
+ has_default_repository = ICanHasDefaultGitRepository(provided, None)
+ if has_default_repository is None:
+ raise CannotHaveDefaultGitRepository(provided)
+ if has_default_repository.repository is None:
+ raise NoDefaultGitRepository(provided)
+ return has_default_repository
=== modified file 'lib/lp/code/interfaces/gitnamespace.py'
--- lib/lp/code/interfaces/gitnamespace.py 2015-02-13 18:34:45 +0000
+++ lib/lp/code/interfaces/gitnamespace.py 2015-02-20 16:35:44 +0000
@@ -76,6 +76,14 @@
:return: `IGitRepository` if found, otherwise 'default'.
"""
+ def getDefault():
+ """Find the default repository for this namespace.
+
+ This returns owner-target defaults, e.g. `lp:~owner/project`. For
+ target defaults such as `lp:project`, use
+ `IGitRepositorySet.getDefaultForTarget` instead.
+ """
+
def __eq__(other):
"""Is this namespace the same as another namespace?"""
=== modified file 'lib/lp/code/interfaces/gitrepository.py'
--- lib/lp/code/interfaces/gitrepository.py 2015-02-19 23:57:34 +0000
+++ lib/lp/code/interfaces/gitrepository.py 2015-02-20 16:35:44 +0000
@@ -16,6 +16,7 @@
import re
from lazr.restful.fields import Reference
+from zope.component import getUtility
from zope.interface import (
Attribute,
Interface,
@@ -32,7 +33,16 @@
from lp import _
from lp.app.enums import InformationType
from lp.app.validators import LaunchpadValidationError
+from lp.code.interfaces.defaultgit import ICanHasDefaultGitRepository
from lp.code.interfaces.hasgitrepositories import IHasGitRepositories
+from lp.registry.interfaces.distributionsourcepackage import (
+ IDistributionSourcePackage,
+ )
+from lp.registry.interfaces.persondistributionsourcepackage import (
+ IPersonDistributionSourcePackageFactory,
+ )
+from lp.registry.interfaces.personproduct import IPersonProductFactory
+from lp.registry.interfaces.product import IProduct
from lp.registry.interfaces.role import IPersonRoles
from lp.services.fields import (
PersonChoice,
@@ -349,9 +359,23 @@
def getRepositoryDefaults(self):
"""See `IGitRepository`."""
- # XXX cjwatson 2015-02-06: This will return shortcut defaults once
- # they're implemented.
- return []
+ defaults = []
+ if self.target_default:
+ defaults.append(ICanHasDefaultGitRepository(self.target))
+ if self.owner_default:
+ if IProduct.providedBy(self.target):
+ factory = getUtility(IPersonProductFactory)
+ default = factory.create(self.owner, self.target)
+ elif IDistributionSourcePackage.providedBy(self.target):
+ factory = getUtility(IPersonDistributionSourcePackageFactory)
+ default = factory.create(self.owner, self.target)
+ else:
+ # Also enforced by database constraint.
+ raise AssertionError(
+ "Only projects or packages can have owner-target default "
+ "repositories.")
+ defaults.append(ICanHasDefaultGitRepository(default))
+ return sorted(defaults)
def getRepositoryIdentities(self):
"""See `IGitRepository`."""
=== added file 'lib/lp/code/model/defaultgit.py'
--- lib/lp/code/model/defaultgit.py 1970-01-01 00:00:00 +0000
+++ lib/lp/code/model/defaultgit.py 2015-02-20 16:35:44 +0000
@@ -0,0 +1,249 @@
+# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Implementation of `ICanHasDefaultGitRepository`."""
+
+__metaclass__ = type
+# Don't export anything -- anything you need from this module you can get by
+# adapting another object.
+__all__ = []
+
+from lazr.enum import (
+ EnumeratedType,
+ Item,
+ )
+from zope.component import adapts
+from zope.interface import implements
+
+from lp.code.interfaces.defaultgit import ICanHasDefaultGitRepository
+from lp.registry.interfaces.distributionsourcepackage import (
+ IDistributionSourcePackage,
+ )
+from lp.registry.interfaces.persondistributionsourcepackage import (
+ IPersonDistributionSourcePackage,
+ )
+from lp.registry.interfaces.personproduct import IPersonProduct
+from lp.registry.interfaces.product import IProduct
+
+
+class DefaultGitRepositoryOrder(EnumeratedType):
+ """An enum used only for ordering."""
+
+ PROJECT = Item("Project shortcut")
+ DISTRIBUTION_SOURCE_PACKAGE = Item("Distribution source package shortcut")
+ OWNER_PROJECT = Item("Owner's default for a project")
+ OWNER_DISTRIBUTION_SOURCE_PACKAGE = Item(
+ "Owner's default for a distribution source package")
+
+
+class BaseDefaultGitRepository:
+ """Provides the common sorting algorithm."""
+
+ def __cmp__(self, other):
+ if not ICanHasDefaultGitRepository.providedBy(other):
+ raise AssertionError("Can't compare with: %r" % other)
+ return cmp(self.sort_order, other.sort_order)
+
+ def __eq__(self, other):
+ return (
+ isinstance(other, self.__class__) and
+ self.context == other.context)
+
+ def __ne__(self, other):
+ return not self == other
+
+
+class ProjectDefaultGitRepository(BaseDefaultGitRepository):
+ """Implement a default Git repository for a project."""
+
+ adapts(IProduct)
+ implements(ICanHasDefaultGitRepository)
+
+ sort_order = DefaultGitRepositoryOrder.PROJECT
+
+ def __init__(self, project):
+ self.context = project
+
+ @property
+ def project(self):
+ return self.context
+
+ def __cmp__(self, other):
+ result = super(ProjectDefaultGitRepository, self).__cmp__(other)
+ if result != 0:
+ return result
+ else:
+ return cmp(self.project.name, other.project.name)
+
+ @property
+ def repository(self):
+ """See `ICanHasDefaultGitRepository`."""
+ return self.context.getDefaultGitRepository()
+
+ def setRepository(self, repository):
+ """See `ICanHasDefaultGitRepository`."""
+ self.context.setDefaultGitRepository(repository)
+
+ @property
+ def path(self):
+ """See `ICanHasDefaultGitRepository`."""
+ return self.project.name
+
+
+class PackageDefaultGitRepository(BaseDefaultGitRepository):
+ """Implement a default Git repository for a distribution source package."""
+
+ adapts(IDistributionSourcePackage)
+ implements(ICanHasDefaultGitRepository)
+
+ sort_order = DefaultGitRepositoryOrder.DISTRIBUTION_SOURCE_PACKAGE
+
+ def __init__(self, distro_source_package):
+ self.context = distro_source_package
+
+ @property
+ def distro_source_package(self):
+ return self.context
+
+ @property
+ def distribution(self):
+ return self.context.distribution
+
+ @property
+ def sourcepackagename(self):
+ return self.context.sourcepackagename
+
+ def __cmp__(self, other):
+ result = super(PackageDefaultGitRepository, self).__cmp__(other)
+ if result != 0:
+ return result
+ else:
+ my_names = (self.distribution.name, self.sourcepackagename.name)
+ other_names = (
+ other.distribution.name, other.sourcepackagename.name)
+ return cmp(my_names, other_names)
+
+ @property
+ def repository(self):
+ """See `ICanHasDefaultGitRepository`."""
+ return self.context.getDefaultGitRepository()
+
+ def setRepository(self, repository):
+ """See `ICanHasDefaultGitRepository`."""
+ self.context.setDefaultGitRepository(repository)
+
+ @property
+ def path(self):
+ """See `ICanHasDefaultGitRepository`."""
+ return "%s/+source/%s" % (
+ self.distribution.name, self.sourcepackagename.name)
+
+
+class OwnerProjectDefaultGitRepository(BaseDefaultGitRepository):
+ """Implement an owner's default Git repository for a project."""
+
+ adapts(IPersonProduct)
+ implements(ICanHasDefaultGitRepository)
+
+ sort_order = DefaultGitRepositoryOrder.OWNER_PROJECT
+
+ def __init__(self, person_project):
+ self.context = person_project
+
+ @property
+ def person_project(self):
+ return self.context
+
+ @property
+ def person(self):
+ return self.context.person
+
+ @property
+ def project(self):
+ return self.context.product
+
+ def __cmp__(self, other):
+ result = super(OwnerProjectDefaultGitRepository, self).__cmp__(other)
+ if result != 0:
+ return result
+ else:
+ my_names = (self.person.name, self.project.name)
+ other_names = (other.person.name, other.project.name)
+ return cmp(my_names, other_names)
+
+ @property
+ def repository(self):
+ """See `ICanHasDefaultGitRepository`."""
+ return self.context.getDefaultGitRepository(self.project)
+
+ def setRepository(self, repository):
+ """See `ICanHasDefaultGitRepository`."""
+ self.context.setDefaultGitRepository(self.project, repository)
+
+ @property
+ def path(self):
+ """See `ICanHasDefaultGitRepository`."""
+ return "~%s/%s" % (self.person.name, self.project.name)
+
+
+class OwnerPackageDefaultGitRepository(BaseDefaultGitRepository):
+ """Implement an owner's default Git repository for a distribution source
+ package."""
+
+ adapts(IPersonDistributionSourcePackage)
+ implements(ICanHasDefaultGitRepository)
+
+ sort_order = DefaultGitRepositoryOrder.OWNER_DISTRIBUTION_SOURCE_PACKAGE
+
+ def __init__(self, person_distro_source_package):
+ self.context = person_distro_source_package
+
+ @property
+ def person_distro_source_package(self):
+ return self.context
+
+ @property
+ def person(self):
+ return self.context.person
+
+ @property
+ def distro_source_package(self):
+ return self.context.distro_source_package
+
+ @property
+ def distribution(self):
+ return self.distro_source_package.distribution
+
+ @property
+ def sourcepackagename(self):
+ return self.distro_source_package.sourcepackagename
+
+ def __cmp__(self, other):
+ result = super(OwnerPackageDefaultGitRepository, self).__cmp__(other)
+ if result != 0:
+ return result
+ else:
+ my_names = (
+ self.person.name, self.distribution.name,
+ self.sourcepackagename.name)
+ other_names = (
+ other.person.name, other.distribution.name,
+ other.sourcepackagename.name)
+ return cmp(my_names, other_names)
+
+ @property
+ def repository(self):
+ """See `ICanHasDefaultGitRepository`."""
+ return self.context.getDefaultGitRepository(self.distro_source_package)
+
+ def setRepository(self, repository):
+ """See `ICanHasDefaultGitRepository`."""
+ self.context.setDefaultGitRepository(
+ self.distro_source_package, repository)
+
+ @property
+ def path(self):
+ """See `ICanHasDefaultGitRepository`."""
+ return "~%s/%s/+source/%s" % (
+ self.person.name, self.distribution.name,
+ self.sourcepackagename.name)
=== modified file 'lib/lp/code/model/gitnamespace.py'
--- lib/lp/code/model/gitnamespace.py 2015-02-13 18:34:45 +0000
+++ lib/lp/code/model/gitnamespace.py 2015-02-20 16:35:44 +0000
@@ -185,6 +185,12 @@
match = default
return match
+ def getDefault(self):
+ """See `IGitNamespace`."""
+ return IStore(GitRepository).find(
+ GitRepository, self._getRepositoriesClause(),
+ GitRepository.owner_default == True).one()
+
def getAllowedInformationTypes(self, who=None):
"""See `IGitNamespace`."""
raise NotImplementedError
=== modified file 'lib/lp/code/model/tests/test_gitrepository.py'
--- lib/lp/code/model/tests/test_gitrepository.py 2015-02-20 00:56:57 +0000
+++ lib/lp/code/model/tests/test_gitrepository.py 2015-02-20 16:35:44 +0000
@@ -24,12 +24,17 @@
GitRepositoryCreatorNotOwner,
GitTargetError,
)
+from lp.code.interfaces.defaultgit import ICanHasDefaultGitRepository
from lp.code.interfaces.gitnamespace import (
IGitNamespacePolicy,
IGitNamespaceSet,
)
from lp.code.interfaces.gitrepository import IGitRepository
from lp.registry.enums import BranchSharingPolicy
+from lp.registry.interfaces.persondistributionsourcepackage import (
+ IPersonDistributionSourcePackageFactory,
+ )
+from lp.registry.interfaces.personproduct import IPersonProductFactory
from lp.services.database.constants import UTC_NOW
from lp.services.webapp.authorization import check_permission
from lp.testing import (
@@ -111,6 +116,51 @@
repository = self.factory.makeGitRepository()
self.assertGitIdentity(repository, repository.unique_name)
+ def test_git_identity_default_for_project(self):
+ # If a repository is the default for a project, then its Git
+ # identity is the project name.
+ project = self.factory.makeProduct()
+ repository = self.factory.makeGitRepository(
+ owner=project.owner, target=project)
+ with person_logged_in(project.owner):
+ project.setDefaultGitRepository(repository)
+ self.assertGitIdentity(repository, project.name)
+
+ def test_git_identity_default_for_package(self):
+ # If a repository is the default for a package, then its Git
+ # identity uses the path to that package.
+ dsp = self.factory.makeDistributionSourcePackage()
+ repository = self.factory.makeGitRepository(target=dsp)
+ with admin_logged_in():
+ dsp.setDefaultGitRepository(repository)
+ self.assertGitIdentity(
+ repository,
+ "%s/+source/%s" % (
+ dsp.distribution.name, dsp.sourcepackagename.name))
+
+ def test_git_identity_owner_default_for_project(self):
+ # If a repository is a person's default for a project, then its Git
+ # identity is a combination of the person and project names.
+ project = self.factory.makeProduct()
+ repository = self.factory.makeGitRepository(target=project)
+ with person_logged_in(repository.owner):
+ repository.owner.setDefaultGitRepository(project, repository)
+ self.assertGitIdentity(
+ repository, "~%s/%s" % (repository.owner.name, project.name))
+
+ def test_git_identity_owner_default_for_package(self):
+ # If a repository is a person's default for a package, then its Git
+ # identity is a combination of the person name and the package path.
+ dsp = self.factory.makeDistributionSourcePackage()
+ repository = self.factory.makeGitRepository(target=dsp)
+ with person_logged_in(repository.owner):
+ repository.owner.setDefaultGitRepository(dsp, repository)
+ self.assertGitIdentity(
+ repository,
+ "~%s/%s/+source/%s" % (
+ repository.owner.name, dsp.distribution.name,
+ dsp.sourcepackagename.name))
+
def test_identities_no_defaults(self):
# If there are no defaults, the only repository identity is the
# unique name.
@@ -119,8 +169,52 @@
[(repository.unique_name, repository)],
repository.getRepositoryIdentities())
- # XXX cjwatson 2015-02-12: This will need to be expanded once support
- # for default repositories is in place.
+ def test_default_for_project(self):
+ # If a repository is the default for a project, then that is the
+ # preferred identity. Target defaults are preferred over
+ # owner-target defaults.
+ eric = self.factory.makePerson(name="eric")
+ fooix = self.factory.makeProduct(name="fooix", owner=eric)
+ repository = self.factory.makeGitRepository(
+ owner=eric, target=fooix, name=u"fooix-repo")
+ with person_logged_in(fooix.owner):
+ repository.owner.setDefaultGitRepository(fooix, repository)
+ fooix.setDefaultGitRepository(repository)
+ eric_fooix = getUtility(IPersonProductFactory).create(eric, fooix)
+ self.assertEqual(
+ [ICanHasDefaultGitRepository(target)
+ for target in (fooix, eric_fooix)],
+ repository.getRepositoryDefaults())
+ self.assertEqual(
+ [("fooix", fooix), ("~eric/fooix", eric_fooix),
+ ("~eric/fooix/+git/fooix-repo", repository)],
+ repository.getRepositoryIdentities())
+
+ def test_default_for_package(self):
+ # If a repository is the default for a package, then that is the
+ # preferred identity. Target defaults are preferred over
+ # owner-target defaults.
+ mint = self.factory.makeDistribution(name="mint")
+ eric = self.factory.makePerson(name="eric")
+ mint_choc = self.factory.makeDistributionSourcePackage(
+ distribution=mint, sourcepackagename="choc")
+ repository = self.factory.makeGitRepository(
+ owner=eric, target=mint_choc, name=u"choc-repo")
+ dsp = repository.target
+ with admin_logged_in():
+ repository.owner.setDefaultGitRepository(dsp, repository)
+ dsp.setDefaultGitRepository(repository)
+ eric_dsp = getUtility(IPersonDistributionSourcePackageFactory).create(
+ eric, dsp)
+ self.assertEqual(
+ [ICanHasDefaultGitRepository(target)
+ for target in (dsp, eric_dsp)],
+ repository.getRepositoryDefaults())
+ self.assertEqual(
+ [("mint/+source/choc", dsp),
+ ("~eric/mint/+source/choc", eric_dsp),
+ ("~eric/mint/+source/choc/+git/choc-repo", repository)],
+ repository.getRepositoryIdentities())
class TestGitRepositoryDateLastModified(TestCaseWithFactory):
=== modified file 'lib/lp/registry/configure.zcml'
--- lib/lp/registry/configure.zcml 2015-02-13 15:54:36 +0000
+++ lib/lp/registry/configure.zcml 2015-02-20 16:35:44 +0000
@@ -516,6 +516,7 @@
findRelatedArchivePublications
findRelatedArchives
getBranches
+ getDefaultGitRepository
getMergeProposals
getReleasesAndPublishingHistory
getUsedBugTagsWithOpenCounts
@@ -528,11 +529,17 @@
personHasDriverRights
publishing_history
releases
+ setDefaultGitRepository
sourcepackagename
subscribers
summary
title
upstream_product"/>
+ <require
+ permission="launchpad.Edit"
+ set_attributes="
+ setDefaultGitRepository
+ " />
<!-- IStructuralSubscriptionTarget -->
=== modified file 'lib/lp/registry/interfaces/distributionsourcepackage.py'
--- lib/lp/registry/interfaces/distributionsourcepackage.py 2015-02-06 15:29:45 +0000
+++ lib/lp/registry/interfaces/distributionsourcepackage.py 2015-02-20 16:35:44 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Source package in Distribution interfaces."""
@@ -175,6 +175,18 @@
on how this criteria will be centrally encoded.
""")
+ def getDefaultGitRepository():
+ """Get the default Git repository for this package.
+
+ :return: An `IGitRepository`, or None.
+ """
+
+ def setDefaultGitRepository(git_repository):
+ """Set the default Git repository for this package.
+
+ :param git_repository: An `IGitRepository`, or None to unset.
+ """
+
def __eq__(other):
"""IDistributionSourcePackage comparison method.
=== modified file 'lib/lp/registry/interfaces/person.py'
--- lib/lp/registry/interfaces/person.py 2015-02-09 16:49:01 +0000
+++ lib/lp/registry/interfaces/person.py 2015-02-20 16:35:44 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2014 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Person interfaces."""
@@ -1581,6 +1581,13 @@
If no orderby is provided, Person.sortingColumns is used.
"""
+ def getDefaultGitRepository(target):
+ """Get this person's default Git repository for this target.
+
+ :param target: An `IHasGitRepositories`.
+ :return: An `IGitRepository`, or None.
+ """
+
class IPersonEditRestricted(Interface):
"""IPerson attributes that require launchpad.Edit permission."""
@@ -1779,6 +1786,13 @@
:return: a PPA `IArchive` record.
"""
+ def setDefaultGitRepository(target, git_repository):
+ """Set this person's default Git repository for this target.
+
+ :param target: An `IHasGitRepositories`.
+ :param git_repository: An `IGitRepository`, or None to unset.
+ """
+
class IPersonSpecialRestricted(Interface):
"""IPerson methods that require launchpad.Special permission to use."""
=== modified file 'lib/lp/registry/interfaces/product.py'
--- lib/lp/registry/interfaces/product.py 2015-02-09 16:49:01 +0000
+++ lib/lp/registry/interfaces/product.py 2015-02-20 16:35:44 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Interfaces including and related to IProduct."""
@@ -837,6 +837,12 @@
The number of milestones returned per series is limited.
"""
+ def getDefaultGitRepository():
+ """Get the default Git repository for this project.
+
+ :return: An `IGitRepository`, or None.
+ """
+
class IProductEditRestricted(IOfficialBugTagTargetRestricted):
"""`IProduct` properties which require launchpad.Edit permission."""
@@ -884,6 +890,12 @@
permitted.
"""
+ def setDefaultGitRepository(git_repository):
+ """Set the default Git repository for this project.
+
+ :param git_repository: An `IGitRepository`, or None to unset.
+ """
+
class IProduct(
IBugTarget, IHasBugSupervisor, IHasDrivers, IProductEditRestricted,
=== modified file 'lib/lp/registry/model/distributionsourcepackage.py'
--- lib/lp/registry/model/distributionsourcepackage.py 2015-02-13 17:16:22 +0000
+++ lib/lp/registry/model/distributionsourcepackage.py 2015-02-20 16:35:44 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Classes to represent source packages in a distribution."""
@@ -32,6 +32,7 @@
Unicode,
)
import transaction
+from zope.component import getUtility
from zope.interface import implements
from lp.bugs.interfaces.bugsummary import IBugSummaryDimension
@@ -39,6 +40,8 @@
from lp.bugs.model.structuralsubscription import (
StructuralSubscriptionTargetMixin,
)
+from lp.code.errors import GitTargetError
+from lp.code.interfaces.gitrepository import IGitRepositorySet
from lp.code.model.hasbranches import (
HasBranchesMixin,
HasMergeProposalsMixin,
@@ -448,6 +451,23 @@
return [dspr for (dspr, pubs) in
self.getReleasesAndPublishingHistory()]
+ def getDefaultGitRepository(self):
+ """See `IDistributionSourcePackage`."""
+ return getUtility(IGitRepositorySet).getDefaultRepository(self)
+
+ def setDefaultGitRepository(self, git_repository):
+ """See `IDistributionSourcePackage`."""
+ if git_repository is not None:
+ if git_repository.target != self:
+ raise GitTargetError(
+ "Cannot set default Git repository to one attached to "
+ "another target.")
+ git_repository.setTargetDefault(True)
+ else:
+ previous = self.getDefaultGitRepository()
+ if previous is not None:
+ previous.setTargetDefault(False)
+
def __eq__(self, other):
"""See `IDistributionSourcePackage`."""
return (
=== modified file 'lib/lp/registry/model/person.py'
--- lib/lp/registry/model/person.py 2015-02-06 15:29:45 +0000
+++ lib/lp/registry/model/person.py 2015-02-20 16:35:44 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2014 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Implementation classes for a Person."""
@@ -140,7 +140,9 @@
)
from lp.bugs.model.bugtarget import HasBugsBase
from lp.bugs.model.structuralsubscription import StructuralSubscription
+from lp.code.errors import GitTargetError
from lp.code.interfaces.branchcollection import IAllBranches
+from lp.code.interfaces.gitrepository import IGitRepositorySet
from lp.code.model.hasbranches import (
HasBranchesMixin,
HasMergeProposalsMixin,
@@ -3249,6 +3251,27 @@
grants = [(policy, self, self)]
getUtility(IAccessPolicyGrantSource).grant(grants)
+ def getDefaultGitRepository(self, target):
+ """See `IPerson`."""
+ return getUtility(IGitRepositorySet).getDefaultRepository(target, self)
+
+ def setDefaultGitRepository(self, target, git_repository):
+ """See `IPerson`."""
+ if IPerson.providedBy(target):
+ raise GitTargetError(
+ "Cannot set a person's default Git repository for a person, "
+ "only for a project or a package.")
+ if git_repository is not None:
+ if git_repository.target != target:
+ raise GitTargetError(
+ "Cannot set default Git repository to one attached to "
+ "another target.")
+ git_repository.setOwnerDefault(True)
+ else:
+ previous = self.getDefaultGitRepository(target)
+ if previous is not None:
+ previous.setOwnerDefault(False)
+
class PersonSet:
"""The set of persons."""
=== modified file 'lib/lp/registry/model/persondistributionsourcepackage.py'
--- lib/lp/registry/model/persondistributionsourcepackage.py 2015-02-06 13:37:58 +0000
+++ lib/lp/registry/model/persondistributionsourcepackage.py 2015-02-20 16:35:44 +0000
@@ -37,3 +37,12 @@
def displayname(self):
return '%s in %s' % (
self.person.displayname, self.distro_source_package.displayname)
+
+ def __eq__(self, other):
+ return (
+ IPersonDistributionSourcePackage.providedBy(other) and
+ self.person.id == other.person.id and
+ self.distro_source_package == other.distro_source_package)
+
+ def __ne__(self, other):
+ return not self == other
=== modified file 'lib/lp/registry/model/personproduct.py'
--- lib/lp/registry/model/personproduct.py 2011-12-19 23:38:16 +0000
+++ lib/lp/registry/model/personproduct.py 2015-02-20 16:35:44 +0000
@@ -38,3 +38,12 @@
def displayname(self):
return '%s in %s' % (
self.person.displayname, self.product.displayname)
+
+ def __eq__(self, other):
+ return (
+ IPersonProduct.providedBy(other) and
+ self.person.id == other.person.id and
+ self.product.id == other.product.id)
+
+ def __ne__(self, other):
+ return not self == other
=== modified file 'lib/lp/registry/model/product.py'
--- lib/lp/registry/model/product.py 2015-02-13 17:16:22 +0000
+++ lib/lp/registry/model/product.py 2015-02-20 16:35:44 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Database classes including and related to Product."""
@@ -116,7 +116,9 @@
StructuralSubscriptionTargetMixin,
)
from lp.code.enums import BranchType
+from lp.code.errors import GitTargetError
from lp.code.interfaces.branch import DEFAULT_BRANCH_STATUS_IN_LISTING
+from lp.code.interfaces.gitrepository import IGitRepositorySet
from lp.code.model.branch import Branch
from lp.code.model.branchnamespace import BRANCH_POLICY_ALLOWED_TYPES
from lp.code.model.hasbranches import (
@@ -1569,6 +1571,23 @@
return True
return False
+ def getDefaultGitRepository(self):
+ """See `IProductView`."""
+ return getUtility(IGitRepositorySet).getDefaultRepository(self)
+
+ def setDefaultGitRepository(self, git_repository):
+ """See `IProductEditRestricted`."""
+ if git_repository is not None:
+ if git_repository.target != self:
+ raise GitTargetError(
+ "Cannot set default Git repository to one attached to "
+ "another target.")
+ git_repository.setTargetDefault(True)
+ else:
+ previous = self.getDefaultGitRepository()
+ if previous is not None:
+ previous.setTargetDefault(False)
+
def get_precached_products(products, need_licences=False,
need_projectgroups=False, need_series=False,
=== modified file 'lib/lp/registry/tests/test_distributionsourcepackage.py'
--- lib/lp/registry/tests/test_distributionsourcepackage.py 2014-04-24 06:45:51 +0000
+++ lib/lp/registry/tests/test_distributionsourcepackage.py 2015-02-20 16:35:44 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Tests for DistributionSourcePackage."""
@@ -11,6 +11,7 @@
from zope.component import getUtility
from zope.security.proxy import removeSecurityProxy
+from lp.code.errors import GitTargetError
from lp.registry.interfaces.distribution import IDistributionSet
from lp.registry.model.distributionsourcepackage import (
DistributionSourcePackage,
@@ -22,6 +23,7 @@
from lp.soyuz.enums import PackagePublishingStatus
from lp.soyuz.tests.test_publishing import SoyuzTestPublisher
from lp.testing import (
+ person_logged_in,
StormStatementRecorder,
TestCaseWithFactory,
)
@@ -165,6 +167,36 @@
driver = distribution.drivers[0]
self.assertTrue(dsp.personHasDriverRights(driver))
+ def test_default_git_repository_round_trip(self):
+ # A default Git repository set using setDefaultGitRepository can be
+ # retrieved using getDefaultGitRepository.
+ dsp = self.factory.makeDistributionSourcePackage()
+ self.assertIsNone(dsp.getDefaultGitRepository())
+ repository = self.factory.makeGitRepository(target=dsp)
+ with person_logged_in(dsp.distribution.owner):
+ dsp.setDefaultGitRepository(repository)
+ self.assertEqual(repository, dsp.getDefaultGitRepository())
+
+ def test_setDefaultGitRepository_None(self):
+ # setDefaultGitRepository(None) clears the default.
+ dsp = self.factory.makeDistributionSourcePackage()
+ repository = self.factory.makeGitRepository(target=dsp)
+ with person_logged_in(dsp.distribution.owner):
+ dsp.setDefaultGitRepository(repository)
+ dsp.setDefaultGitRepository(None)
+ self.assertIsNone(dsp.getDefaultGitRepository())
+
+ def test_setDefaultGitRepository_different_target(self):
+ # setDefaultGitRepository refuses if the repository is attached to a
+ # different target.
+ dsp = self.factory.makeDistributionSourcePackage()
+ other_dsp = self.factory.makeDistributionSourcePackage(
+ distribution=dsp.distribution)
+ repository = self.factory.makeGitRepository(target=other_dsp)
+ with person_logged_in(dsp.distribution.owner):
+ self.assertRaises(
+ GitTargetError, dsp.setDefaultGitRepository, repository)
+
class TestDistributionSourcePackageFindRelatedArchives(TestCaseWithFactory):
=== modified file 'lib/lp/registry/tests/test_person.py'
--- lib/lp/registry/tests/test_person.py 2015-01-07 00:35:41 +0000
+++ lib/lp/registry/tests/test_person.py 2015-02-20 16:35:44 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
__metaclass__ = type
@@ -39,6 +39,7 @@
IllegalRelatedBugTasksParams,
)
from lp.bugs.model.bug import Bug
+from lp.code.errors import GitTargetError
from lp.registry.enums import (
PersonVisibility,
TeamMembershipPolicy,
@@ -774,6 +775,51 @@
self.assertContentEqual(
[], getUtility(IAccessPolicySource).findByTeam([team]))
+ def test_default_git_repository_round_trip(self):
+ # A default Git repository set using setDefaultGitRepository can be
+ # retrieved using getDefaultGitRepository.
+ person = self.factory.makePerson()
+ project = self.factory.makeProduct()
+ self.assertIsNone(person.getDefaultGitRepository(project))
+ repository = self.factory.makeGitRepository(
+ owner=person, target=project)
+ with person_logged_in(person):
+ person.setDefaultGitRepository(project, repository)
+ self.assertEqual(repository, person.getDefaultGitRepository(project))
+
+ def test_setDefaultGitRepository_None(self):
+ # setDefaultGitRepository(target, None) clears the default.
+ person = self.factory.makePerson()
+ project = self.factory.makeProduct()
+ repository = self.factory.makeGitRepository(
+ owner=person, target=project)
+ with person_logged_in(person):
+ person.setDefaultGitRepository(project, repository)
+ person.setDefaultGitRepository(project, None)
+ self.assertIsNone(person.getDefaultGitRepository(project))
+
+ def test_setDefaultGitRepository_refuses_person(self):
+ # setDefaultGitRepository refuses if the target is a person.
+ person = self.factory.makePerson()
+ repository = self.factory.makeGitRepository(owner=person)
+ with person_logged_in(person):
+ self.assertRaises(
+ GitTargetError, person.setDefaultGitRepository, person,
+ repository)
+
+ def test_setDefaultGitRepository_different_target(self):
+ # setDefaultGitRepository refuses if the repository is attached to a
+ # different target.
+ person = self.factory.makePerson()
+ project = self.factory.makeProduct()
+ other_project = self.factory.makeProduct()
+ repository = self.factory.makeGitRepository(
+ owner=person, target=other_project)
+ with person_logged_in(person):
+ self.assertRaises(
+ GitTargetError, person.setDefaultGitRepository, project,
+ repository)
+
class TestPersonStates(TestCaseWithFactory):
=== modified file 'lib/lp/registry/tests/test_product.py'
--- lib/lp/registry/tests/test_product.py 2015-02-19 19:23:23 +0000
+++ lib/lp/registry/tests/test_product.py 2015-02-20 16:35:44 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
__metaclass__ = type
@@ -56,6 +56,7 @@
from lp.bugs.interfaces.bugsummary import IBugSummaryDimension
from lp.bugs.interfaces.bugsupervisor import IHasBugSupervisor
from lp.bugs.interfaces.bugtarget import BUG_POLICY_ALLOWED_TYPES
+from lp.code.errors import GitTargetError
from lp.code.model.branchnamespace import BRANCH_POLICY_ALLOWED_TYPES
from lp.registry.enums import (
BranchSharingPolicy,
@@ -1227,6 +1228,35 @@
self.assertIsNot(None, getUtility(IAccessPolicySource).find(
[(product, InformationType.EMBARGOED)]).one())
+ def test_default_git_repository_round_trip(self):
+ # A default Git repository set using setDefaultGitRepository can be
+ # retrieved using getDefaultGitRepository.
+ product = self.factory.makeProduct()
+ self.assertIsNone(product.getDefaultGitRepository())
+ repository = self.factory.makeGitRepository(target=product)
+ with person_logged_in(product.owner):
+ product.setDefaultGitRepository(repository)
+ self.assertEqual(repository, product.getDefaultGitRepository())
+
+ def test_setDefaultGitRepository_None(self):
+ # setDefaultGitRepository(None) clears the default.
+ product = self.factory.makeProduct()
+ repository = self.factory.makeGitRepository(target=product)
+ with person_logged_in(product.owner):
+ product.setDefaultGitRepository(repository)
+ product.setDefaultGitRepository(None)
+ self.assertIsNone(product.getDefaultGitRepository())
+
+ def test_setDefaultGitRepository_different_target(self):
+ # setDefaultGitRepository refuses if the repository is attached to a
+ # different target.
+ product = self.factory.makeProduct()
+ other_product = self.factory.makeProduct()
+ repository = self.factory.makeGitRepository(target=other_product)
+ with person_logged_in(product.owner):
+ self.assertRaises(
+ GitTargetError, product.setDefaultGitRepository, repository)
+
class TestProductBugInformationTypes(TestCaseWithFactory):
Follow ups