← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~cjwatson/launchpad/snap-source-deletion into lp:launchpad

 

Colin Watson has proposed merging lp:~cjwatson/launchpad/snap-source-deletion into lp:launchpad.

Commit message:
Add Branch.snaps and GitRepository.snaps.  Delete snap branches when their source branch/repository is deleted.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #1476405 in Launchpad itself: "Add support for building snaps"
  https://bugs.launchpad.net/launchpad/+bug/1476405

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/snap-source-deletion/+merge/266866

Add Branch.snaps and GitRepository.snaps.  Delete snap branches when their source branch/repository is deleted.

This will make it feasible to add the tests requested in a review comment on https://code.launchpad.net/~cjwatson/launchpad/snap-build-behaviour/+merge/266736.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/snap-source-deletion into lp:launchpad.
=== modified file 'lib/lp/code/interfaces/branch.py'
--- lib/lp/code/interfaces/branch.py	2015-05-26 12:18:12 +0000
+++ lib/lp/code/interfaces/branch.py	2015-08-04 13:10:52 +0000
@@ -105,6 +105,7 @@
     structured,
     )
 from lp.services.webapp.interfaces import ITableBatchNavigator
+from lp.snappy.interfaces.hassnaps import IHasSnaps
 
 
 DEFAULT_BRANCH_STATUS_IN_LISTING = (
@@ -275,7 +276,7 @@
 
 
 class IBranchView(IHasOwner, IHasBranchTarget, IHasMergeProposals,
-                  IHasRecipes):
+                  IHasRecipes, IHasSnaps):
     """IBranch attributes that require launchpad.View permission."""
 
     id = Int(title=_('ID'), readonly=True, required=True)

=== modified file 'lib/lp/code/interfaces/gitrepository.py'
--- lib/lp/code/interfaces/gitrepository.py	2015-07-01 07:58:25 +0000
+++ lib/lp/code/interfaces/gitrepository.py	2015-08-04 13:10:52 +0000
@@ -78,6 +78,7 @@
     PublicPersonChoice,
     )
 from lp.services.webhooks.interfaces import IWebhookTarget
+from lp.snappy.interfaces.hassnaps import IHasSnaps
 
 
 GIT_REPOSITORY_NAME_VALIDATION_ERROR_MESSAGE = _(
@@ -117,7 +118,7 @@
     return True
 
 
-class IGitRepositoryView(Interface):
+class IGitRepositoryView(IHasSnaps):
     """IGitRepository attributes that require launchpad.View permission."""
 
     id = Int(title=_("ID"), readonly=True, required=True)

=== modified file 'lib/lp/code/model/branch.py'
--- lib/lp/code/model/branch.py	2015-07-08 16:05:11 +0000
+++ lib/lp/code/model/branch.py	2015-08-04 13:10:52 +0000
@@ -179,6 +179,7 @@
 from lp.services.propertycache import cachedproperty
 from lp.services.webapp import urlappend
 from lp.services.webapp.authorization import check_permission
+from lp.snappy.interfaces.snap import ISnapSet
 
 
 @implementer(IBranch, IPrivacy, IInformationType)
@@ -814,6 +815,8 @@
         deletion_operations.extend(
             DeletionCallable.forSourcePackageRecipe(recipe)
             for recipe in self.recipes)
+        alteration_operations.extend(
+            ClearSnapBranch(snap, self) for snap in self.snaps)
         return (alteration_operations, deletion_operations)
 
     def deletionRequirements(self):
@@ -1420,6 +1423,11 @@
         hook = SourcePackageRecipe.preLoadDataForSourcePackageRecipes
         return DecoratedResultSet(self._recipes, pre_iter_hook=hook)
 
+    @property
+    def snaps(self):
+        """See `IHasSnaps`."""
+        return getUtility(ISnapSet).findByBranch(self)
+
 
 class DeletionOperation:
     """Represent an operation to perform as part of branch deletion."""
@@ -1515,6 +1523,19 @@
         CodeImportSet().delete(self.affected_object)
 
 
+class ClearSnapBranch(DeletionOperation):
+    """Deletion operation that clears a snap package's branch."""
+
+    def __init__(self, snap, branch):
+        DeletionOperation.__init__(
+            self, snap, _('This snap package uses this branch.'))
+        self.branch = branch
+
+    def __call__(self):
+        if self.affected_object.branch == self.branch:
+            self.affected_object.branch = None
+
+
 @implementer(IBranchSet)
 class BranchSet:
     """The set of all branches."""

=== modified file 'lib/lp/code/model/gitrepository.py'
--- lib/lp/code/model/gitrepository.py	2015-07-17 07:34:21 +0000
+++ lib/lp/code/model/gitrepository.py	2015-08-04 13:10:52 +0000
@@ -148,6 +148,7 @@
 from lp.services.webapp.authorization import available_with_permission
 from lp.services.webhooks.interfaces import IWebhookSource
 from lp.services.webhooks.model import WebhookTargetMixin
+from lp.snappy.interfaces.snap import ISnapSet
 
 
 object_type_map = {
@@ -961,6 +962,11 @@
                     self._markProposalMerged(
                         proposal, merged_revision_id, logger=logger)
 
+    @property
+    def snaps(self):
+        """See `IHasSnaps`."""
+        return getUtility(ISnapSet).findByGitRepository(self)
+
     def canBeDeleted(self):
         """See `IGitRepository`."""
         # Can't delete if the repository is associated with anything.
@@ -1002,6 +1008,8 @@
             prerequisite_git_repository=self):
             alteration_operations.append(
                 ClearPrerequisiteRepository(merge_proposal))
+        alteration_operations.extend(
+            ClearSnapRepository(snap, self) for snap in self.snaps)
 
         return (alteration_operations, deletion_operations)
 
@@ -1127,6 +1135,20 @@
         self.affected_object.prerequisite_git_commit_sha1 = None
 
 
+class ClearSnapRepository(DeletionOperation):
+    """Deletion operation that clears a snap package's repository."""
+
+    def __init__(self, snap, repository):
+        DeletionOperation.__init__(
+            self, snap, msg("This snap package uses this repository."))
+        self.repository = repository
+
+    def __call__(self):
+        if self.affected_object.git_repository == self.repository:
+            self.affected_object.git_repository = None
+            self.affected_object.git_path = None
+
+
 @implementer(IGitRepositorySet)
 class GitRepositorySet:
     """See `IGitRepositorySet`."""

=== modified file 'lib/lp/code/model/tests/test_branch.py'
--- lib/lp/code/model/tests/test_branch.py	2015-07-24 09:16:50 +0000
+++ lib/lp/code/model/tests/test_branch.py	2015-08-04 13:10:52 +0000
@@ -72,7 +72,6 @@
 from lp.code.interfaces.branchjob import (
     IBranchScanJobSource,
     IBranchUpgradeJobSource,
-    IReclaimBranchSpaceJobSource,
     )
 from lp.code.interfaces.branchlookup import IBranchLookup
 from lp.code.interfaces.branchmergeproposal import (
@@ -93,6 +92,7 @@
     ClearDependentBranch,
     ClearOfficialPackageBranch,
     ClearSeriesBranch,
+    ClearSnapBranch,
     DeleteCodeImport,
     DeletionCallable,
     DeletionOperation,
@@ -143,6 +143,7 @@
     IOpenLaunchBag,
     OAuthPermission,
     )
+from lp.snappy.interfaces.snap import SNAP_FEATURE_FLAG
 from lp.testing import (
     admin_logged_in,
     ANONYMOUS,
@@ -1672,6 +1673,24 @@
                                         self.factory.makePerson())
         merge_proposal.target_branch.destroySelf(break_references=True)
 
+    def test_snap_requirements(self):
+        # If a branch is used by a snap package, the deletion requirements
+        # indicate this.
+        self.useFixture(FeatureFixture({SNAP_FEATURE_FLAG: u"on"}))
+        snap = self.factory.makeSnap(branch=self.branch)
+        self.assertEqual(
+            {snap: ('alter', _('This snap package uses this branch.'))},
+            self.branch.deletionRequirements())
+
+    def test_snap_deletion(self):
+        # break_references allows deleting a branch used by a snap package.
+        self.useFixture(FeatureFixture({SNAP_FEATURE_FLAG: u"on"}))
+        snap1 = self.factory.makeSnap(branch=self.branch)
+        snap2 = self.factory.makeSnap(branch=self.branch)
+        self.branch.destroySelf(break_references=True)
+        self.assertIsNone(snap1.branch)
+        self.assertIsNone(snap2.branch)
+
     def test_ClearDependentBranch(self):
         """ClearDependent.__call__ must clear the prerequisite branch."""
         merge_proposal = removeSecurityProxy(self.makeMergeProposals()[0])
@@ -1701,6 +1720,13 @@
         ClearSeriesBranch(series, self.branch)()
         self.assertEqual(None, series.branch)
 
+    def test_ClearSnapBranch(self):
+        """ClearSnapBranch.__call__ must clear the branch."""
+        self.useFixture(FeatureFixture({SNAP_FEATURE_FLAG: u"on"}))
+        snap = self.factory.makeSnap(branch=self.branch)
+        ClearSnapBranch(snap, self.branch)()
+        self.assertIsNone(snap.branch)
+
     def test_DeletionOperation(self):
         """DeletionOperation.__call__ is not implemented."""
         self.assertRaises(NotImplementedError, DeletionOperation('a', 'b'))

=== modified file 'lib/lp/code/model/tests/test_gitrepository.py'
--- lib/lp/code/model/tests/test_gitrepository.py	2015-07-17 07:34:21 +0000
+++ lib/lp/code/model/tests/test_gitrepository.py	2015-08-04 13:10:52 +0000
@@ -81,6 +81,7 @@
     )
 from lp.code.model.gitrepository import (
     ClearPrerequisiteRepository,
+    ClearSnapRepository,
     DeletionCallable,
     DeletionOperation,
     GitRepository,
@@ -104,12 +105,14 @@
 from lp.services.config import config
 from lp.services.database.constants import UTC_NOW
 from lp.services.database.interfaces import IStore
+from lp.services.features.testing import FeatureFixture
 from lp.services.job.interfaces.job import JobStatus
 from lp.services.job.model.job import Job
 from lp.services.job.runner import JobRunner
 from lp.services.mail import stub
 from lp.services.webapp.authorization import check_permission
 from lp.services.webapp.interfaces import OAuthPermission
+from lp.snappy.interfaces.snap import SNAP_FEATURE_FLAG
 from lp.testing import (
     admin_logged_in,
     ANONYMOUS,
@@ -620,6 +623,30 @@
             self.factory.makePerson(), self.factory.makePerson())
         merge_proposal.target_git_repository.destroySelf(break_references=True)
 
+    def test_snap_requirements(self):
+        # If a repository is used by a snap package, the deletion
+        # requirements indicate this.
+        self.useFixture(FeatureFixture({SNAP_FEATURE_FLAG: u"on"}))
+        [ref] = self.factory.makeGitRefs()
+        snap = self.factory.makeSnap(git_ref=ref)
+        self.assertEqual(
+            {snap: ("alter", _("This snap package uses this repository."))},
+            ref.repository.getDeletionRequirements())
+
+    def test_snap_deletion(self):
+        # break_references allows deleting a repository used by a snap package.
+        self.useFixture(FeatureFixture({SNAP_FEATURE_FLAG: u"on"}))
+        repository = self.factory.makeGitRepository()
+        [ref1, ref2] = self.factory.makeGitRefs(
+            repository=repository, paths=[u"refs/heads/1", u"refs/heads/2"])
+        snap1 = self.factory.makeSnap(git_ref=ref1)
+        snap2 = self.factory.makeSnap(git_ref=ref2)
+        repository.destroySelf(break_references=True)
+        self.assertIsNone(snap1.git_repository)
+        self.assertIsNone(snap1.git_path)
+        self.assertIsNone(snap2.git_repository)
+        self.assertIsNone(snap2.git_path)
+
     def test_ClearPrerequisiteRepository(self):
         # ClearPrerequisiteRepository.__call__ must clear the prerequisite
         # repository.
@@ -629,6 +656,15 @@
             ClearPrerequisiteRepository(merge_proposal)()
         self.assertIsNone(merge_proposal.prerequisite_git_repository)
 
+    def test_ClearSnapRepository(self):
+        # ClearSnapRepository.__call__ must clear the repository and path.
+        self.useFixture(FeatureFixture({SNAP_FEATURE_FLAG: u"on"}))
+        [ref] = self.factory.makeGitRefs()
+        snap = self.factory.makeSnap(git_ref=ref)
+        ClearSnapRepository(snap, ref.repository)()
+        self.assertIsNone(snap.git_repository)
+        self.assertIsNone(snap.git_path)
+
     def test_DeletionOperation(self):
         # DeletionOperation.__call__ is not implemented.
         self.assertRaises(NotImplementedError, DeletionOperation("a", "b"))

=== added file 'lib/lp/snappy/interfaces/hassnaps.py'
--- lib/lp/snappy/interfaces/hassnaps.py	1970-01-01 00:00:00 +0000
+++ lib/lp/snappy/interfaces/hassnaps.py	2015-08-04 13:10:52 +0000
@@ -0,0 +1,31 @@
+# Copyright 2015 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Interface definitions for IHasSnaps."""
+
+__metaclass__ = type
+__all__ = [
+    'IHasSnaps',
+    ]
+
+
+from lazr.lifecycle.snapshot import doNotSnapshot
+from lazr.restful.declarations import exported
+from lazr.restful.fields import (
+    CollectionField,
+    Reference,
+    )
+from zope.interface import Interface
+
+from lp import _
+
+
+class IHasSnaps(Interface):
+    """An object that has snap packages."""
+
+    snaps = exported(doNotSnapshot(
+        CollectionField(
+            title=_("All snap packages associated with the object."),
+            # Really ISnap.
+            value_type=Reference(schema=Interface),
+            readonly=True)))

=== modified file 'lib/lp/snappy/interfaces/snap.py'
--- lib/lp/snappy/interfaces/snap.py	2015-08-03 13:16:48 +0000
+++ lib/lp/snappy/interfaces/snap.py	2015-08-04 13:10:52 +0000
@@ -347,6 +347,12 @@
     def findByPerson(owner):
         """Return all snap packages with the given `owner`."""
 
+    def findByBranch(branch):
+        """Return all snap packages for the given Bazaar branch."""
+
+    def findByGitRepository(repository):
+        """Return all snap packages for the given Git repository."""
+
     @collection_default_content()
     def empty_list():
         """Return an empty collection of snap packages.

=== modified file 'lib/lp/snappy/interfaces/webservice.py'
--- lib/lp/snappy/interfaces/webservice.py	2015-08-03 11:45:43 +0000
+++ lib/lp/snappy/interfaces/webservice.py	2015-08-04 13:10:52 +0000
@@ -20,6 +20,7 @@
     patch_entry_return_type,
     patch_reference_property,
     )
+from lp.snappy.interfaces.hassnaps import IHasSnaps
 from lp.snappy.interfaces.snap import (
     ISnap,
     ISnapSet,
@@ -31,6 +32,9 @@
     )
 
 
+# IHasSnaps
+patch_collection_property(IHasSnaps, 'snaps', ISnap)
+
 # ISnapFile
 patch_reference_property(ISnapFile, 'snapbuild', ISnapBuild)
 

=== modified file 'lib/lp/snappy/model/snap.py'
--- lib/lp/snappy/model/snap.py	2015-08-03 13:16:48 +0000
+++ lib/lp/snappy/model/snap.py	2015-08-04 13:10:52 +0000
@@ -334,6 +334,14 @@
         """See `ISnapSet`."""
         return IStore(Snap).find(Snap, Snap.owner == owner)
 
+    def findByBranch(self, branch):
+        """See `ISnapSet`."""
+        return IStore(Snap).find(Snap, Snap.branch == branch)
+
+    def findByGitRepository(self, repository):
+        """See `ISnapSet`."""
+        return IStore(Snap).find(Snap, Snap.git_repository == repository)
+
     def empty_list(self):
         """See `ISnapSet`."""
         return []

=== added file 'lib/lp/snappy/tests/test_hassnaps.py'
--- lib/lp/snappy/tests/test_hassnaps.py	1970-01-01 00:00:00 +0000
+++ lib/lp/snappy/tests/test_hassnaps.py	2015-08-04 13:10:52 +0000
@@ -0,0 +1,52 @@
+# 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 IHasSnaps."""
+
+__metaclass__ = type
+
+from lp.services.features.testing import FeatureFixture
+from lp.snappy.interfaces.hassnaps import IHasSnaps
+from lp.snappy.interfaces.snap import SNAP_FEATURE_FLAG
+from lp.testing import TestCaseWithFactory
+from lp.testing.layers import DatabaseFunctionalLayer
+
+
+class TestIHasSnaps(TestCaseWithFactory):
+    """Test that the correct objects implement the interface."""
+
+    layer = DatabaseFunctionalLayer
+
+    def setUp(self):
+        super(TestIHasSnaps, self).setUp()
+        self.useFixture(FeatureFixture({SNAP_FEATURE_FLAG: u"on"}))
+
+    def test_branch_implements_hasrecipes(self):
+        # Branches should implement IHasSnaps.
+        branch = self.factory.makeBranch()
+        self.assertProvides(branch, IHasSnaps)
+
+    def test_branch_recipes(self):
+        # IBranch.snaps should provide all the Snaps attached to that
+        # branch.
+        branch = self.factory.makeBranch()
+        self.factory.makeSnap(branch=branch)
+        self.factory.makeSnap(branch=branch)
+        self.factory.makeSnap()
+        self.assertEqual(2, branch.snaps.count())
+
+    def test_git_repository_implements_hasrecipes(self):
+        # Git repositories should implement IHasSnaps.
+        repository = self.factory.makeGitRepository()
+        self.assertProvides(repository, IHasSnaps)
+
+    def test_git_repository_recipes(self):
+        # IGitRepository.snaps should provide all the Snaps attached to that
+        # repository.
+        repository = self.factory.makeGitRepository()
+        [ref1, ref2] = self.factory.makeGitRefs(
+            repository=repository, paths=[u"refs/heads/1", u"refs/heads/2"])
+        self.factory.makeSnap(git_ref=ref1)
+        self.factory.makeSnap(git_ref=ref2)
+        self.factory.makeSnap()
+        self.assertEqual(2, repository.snaps.count())

=== modified file 'lib/lp/snappy/tests/test_snap.py'
--- lib/lp/snappy/tests/test_snap.py	2015-08-03 13:16:48 +0000
+++ lib/lp/snappy/tests/test_snap.py	2015-08-04 13:10:52 +0000
@@ -451,6 +451,34 @@
         self.assertContentEqual(
             snaps[2:], getUtility(ISnapSet).findByPerson(owners[1]))
 
+    def test_findByBranch(self):
+        # ISnapSet.findByBranch returns all Snaps with the given Bazaar branch.
+        branches = [self.factory.makeAnyBranch() for i in range(2)]
+        snaps = []
+        for branch in branches:
+            for i in range(2):
+                snaps.append(self.factory.makeSnap(branch=branch))
+        self.assertContentEqual(
+            snaps[:2], getUtility(ISnapSet).findByBranch(branches[0]))
+        self.assertContentEqual(
+            snaps[2:], getUtility(ISnapSet).findByBranch(branches[1]))
+
+    def test_findByGitRepository(self):
+        # ISnapSet.findByGitRepository returns all Snaps with the given Git
+        # repository.
+        repositories = [self.factory.makeGitRepository() for i in range(2)]
+        snaps = []
+        for repository in repositories:
+            for i in range(2):
+                [ref] = self.factory.makeGitRefs(repository=repository)
+                snaps.append(self.factory.makeSnap(git_ref=ref))
+        self.assertContentEqual(
+            snaps[:2],
+            getUtility(ISnapSet).findByGitRepository(repositories[0]))
+        self.assertContentEqual(
+            snaps[2:],
+            getUtility(ISnapSet).findByGitRepository(repositories[1]))
+
 
 class TestSnapProcessors(TestCaseWithFactory):
 


Follow ups