launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #23440
[Merge] lp:~cjwatson/launchpad/code-async-delete into lp:launchpad
Colin Watson has proposed merging lp:~cjwatson/launchpad/code-async-delete into lp:launchpad with lp:~cjwatson/launchpad/git-repository-delete-job as a prerequisite.
Commit message:
Delete branches and Git repositories asynchronously.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
Bug #1793266 in Launchpad itself: "Unable to delete repository"
https://bugs.launchpad.net/launchpad/+bug/1793266
For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/code-async-delete/+merge/364912
--
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/code-async-delete into lp:launchpad.
=== modified file 'lib/lp/code/browser/branch.py'
--- lib/lp/code/browser/branch.py 2019-01-30 16:41:12 +0000
+++ lib/lp/code/browser/branch.py 2019-03-21 16:27:30 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2019 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Branch views."""
@@ -87,7 +87,10 @@
from lp.code.browser.decorations import DecoratedBranch
from lp.code.browser.sourcepackagerecipelisting import HasRecipesMenuMixin
from lp.code.browser.widgets.branchtarget import BranchTargetWidget
-from lp.code.enums import BranchType
+from lp.code.enums import (
+ BranchDeletionStatus,
+ BranchType,
+ )
from lp.code.errors import (
BranchCreationForbidden,
BranchExists,
@@ -254,7 +257,8 @@
@enabled_with_permission('launchpad.Edit')
def delete(self):
text = 'Delete branch'
- return Link('+delete', text, icon='trash-icon')
+ enabled = self.context.deletion_status != BranchDeletionStatus.DELETING
+ return Link('+delete', text, icon='trash-icon', enabled=enabled)
@enabled_with_permission('launchpad.AnyPerson')
def edit_whiteboard(self):
@@ -992,7 +996,7 @@
# somewhere valid to send them next.
self.next_url = canonical_url(branch.target)
message = "Branch %s deleted." % branch.unique_name
- self.context.destroySelf(break_references=True)
+ self.context.delete()
self.request.response.addNotification(message)
else:
self.request.response.addNotification(
=== modified file 'lib/lp/code/browser/gitrepository.py'
--- lib/lp/code/browser/gitrepository.py 2019-01-30 16:41:12 +0000
+++ lib/lp/code/browser/gitrepository.py 2019-03-21 16:27:30 +0000
@@ -87,6 +87,7 @@
)
from lp.code.enums import (
GitGranteeType,
+ GitRepositoryDeletionStatus,
GitRepositoryType,
)
from lp.code.errors import (
@@ -276,7 +277,10 @@
@enabled_with_permission("launchpad.Edit")
def delete(self):
text = "Delete repository"
- return Link("+delete", text, icon="trash-icon")
+ enabled = (
+ self.context.deletion_status !=
+ GitRepositoryDeletionStatus.DELETING)
+ return Link("+delete", text, icon="trash-icon", enabled=enabled)
class GitRepositoryContextMenu(ContextMenu, HasRecipesMenuMixin):
@@ -1294,7 +1298,7 @@
# have somewhere valid to send them next.
self.next_url = canonical_url(repository.target)
message = "Repository %s deleted." % repository.unique_name
- self.context.destroySelf(break_references=True)
+ self.context.delete()
self.request.response.addNotification(message)
else:
self.request.response.addNotification(
=== modified file 'lib/lp/code/browser/tests/test_gitrepository.py'
--- lib/lp/code/browser/tests/test_gitrepository.py 2019-01-30 17:24:15 +0000
+++ lib/lp/code/browser/tests/test_gitrepository.py 2019-03-21 16:27:30 +0000
@@ -16,6 +16,7 @@
from textwrap import dedent
from fixtures import FakeLogger
+from mechanize import LinkNotFoundError
import pytz
import soupmatchers
from storm.store import Store
@@ -1829,6 +1830,25 @@
["Repository %s deleted." % name],
get_feedback_messages(browser.contents))
+ def test_deleting_repository(self):
+ # Repository deletion is asynchronous. If the user finds the
+ # repository again before the deletion completes, then they see an
+ # indication that it is being deleted, and there is no "Delete
+ # repository" action.
+ owner = self.factory.makePerson()
+ repository = self.factory.makeGitRepository(owner=owner)
+ with person_logged_in(repository.owner):
+ repository.delete()
+ self.assertEndsWith(repository.unique_name, "-deleting")
+ browser = self.getViewBrowser(
+ repository, "+index", rootsite="code", user=repository.owner)
+ self.assertEqual(
+ "This repository is being deleted.",
+ extract_text(find_tag_by_id(
+ browser.contents, "repository-deleting")))
+ self.assertRaises(
+ LinkNotFoundError, browser.getLink, "Delete repository")
+
class TestGitRepositoryActivityView(BrowserTestCase):
=== modified file 'lib/lp/code/interfaces/branch.py'
--- lib/lp/code/interfaces/branch.py 2019-03-21 16:27:29 +0000
+++ lib/lp/code/interfaces/branch.py 2019-03-21 16:27:30 +0000
@@ -1228,9 +1228,6 @@
branch.
"""
- @call_with(break_references=True)
- @export_destructor_operation()
- @operation_for_version('beta')
def destroySelf(break_references=False):
"""Delete the specified branch.
@@ -1239,7 +1236,23 @@
:param break_references: If supplied, break any references to this
branch by deleting items with mandatory references and
NULLing other references.
- :raise: CannotDeleteBranch if the branch cannot be deleted.
+ :raises CannotDeleteBranch: if the branch cannot be deleted.
+ """
+
+ @export_destructor_operation()
+ @operation_for_version('beta')
+ def delete():
+ """Request deletion of the specified branch.
+
+ The branch will be deleted asynchronously.
+
+ This will delete any merge proposals that use this branch as their
+ source or target, and any recipes that use this branch. It will
+ clear the prerequisite from any merge proposals that use this branch
+ as a prerequisite. It will detach the branch from any snap packages
+ that build from it.
+
+ :raises CannotDeleteBranch: if the branch cannot be deleted.
"""
=== modified file 'lib/lp/code/interfaces/gitrepository.py'
--- lib/lp/code/interfaces/gitrepository.py 2019-03-21 16:27:29 +0000
+++ lib/lp/code/interfaces/gitrepository.py 2019-03-21 16:27:30 +0000
@@ -1,4 +1,4 @@
-# Copyright 2015-2018 Canonical Ltd. This software is licensed under the
+# Copyright 2015-2019 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Git repository interfaces."""
@@ -881,16 +881,29 @@
object needs to be touched.
"""
- @call_with(break_references=True)
- @export_destructor_operation()
- @operation_for_version("devel")
def destroySelf(break_references=False):
"""Delete the specified repository.
:param break_references: If supplied, break any references to this
repository by deleting items with mandatory references and
NULLing other references.
- :raise: CannotDeleteGitRepository if the repository cannot be deleted.
+ :raises CannotDeleteGitRepository: if the repository cannot be deleted.
+ """
+
+ @export_destructor_operation()
+ @operation_for_version("devel")
+ def delete():
+ """Request deletion of the specified repository.
+
+ The repository will be deleted asynchronously.
+
+ This will delete any merge proposals that use this repository as
+ their source or target, and any recipes that use this repository.
+ It will clear the prerequisite from any merge proposals that use
+ this repository as a prerequisite. It will detach the repository
+ from any snap packages that build from it.
+
+ :raises CannotDeleteGitRepository: if the repository cannot be deleted.
"""
=== modified file 'lib/lp/code/model/branch.py'
--- lib/lp/code/model/branch.py 2019-03-21 16:27:29 +0000
+++ lib/lp/code/model/branch.py 2019-03-21 16:27:30 +0000
@@ -112,6 +112,11 @@
)
from lp.code.interfaces.branchcollection import IAllBranches
from lp.code.interfaces.branchhosting import IBranchHostingClient
+from lp.code.interfaces.branchjob import (
+ IBranchDeleteJobSource,
+ IBranchUpgradeJobSource,
+ IReclaimBranchSpaceJobSource,
+ )
from lp.code.interfaces.branchlookup import IBranchLookup
from lp.code.interfaces.branchmergeproposal import (
BRANCH_MERGE_PROPOSAL_FINAL_STATES,
@@ -1455,7 +1460,6 @@
def destroySelf(self, break_references=False):
"""See `IBranch`."""
- from lp.code.interfaces.branchjob import IReclaimBranchSpaceJobSource
if break_references:
self._breakReferences()
if not self.canBeDeleted():
@@ -1473,6 +1477,22 @@
job = getUtility(IReclaimBranchSpaceJobSource).create(branch_id)
job.celeryRunOnCommit()
+ def delete(self):
+ """See `IBranch`."""
+ if self.deletion_status == BranchDeletionStatus.DELETING:
+ raise CannotDeleteBranch(
+ "This branch is already being deleted: %s" % self.unique_name)
+ if not self.getStackedBranches().is_empty():
+ # If there are branches stacked on this one, then we won't be
+ # able to delete this branch even after breaking references.
+ raise CannotDeleteBranch(
+ "Cannot delete branch: %s" % self.unique_name)
+ # Destructor operations can't take arguments.
+ user = getUtility(ILaunchBag).user
+ getUtility(IBranchDeleteJobSource).create(self, user)
+ self.name = self.namespace.findUnusedName(self.name + "-deleting")
+ self.deletion_status = BranchDeletionStatus.DELETING
+
def checkUpgrade(self):
if self.branch_type is not BranchType.HOSTED:
raise CannotUpgradeNonHosted(self)
@@ -1509,7 +1529,6 @@
def requestUpgrade(self, requester):
"""See `IBranch`."""
- from lp.code.interfaces.branchjob import IBranchUpgradeJobSource
job = getUtility(IBranchUpgradeJobSource).create(self, requester)
job.celeryRunOnCommit()
return job
=== modified file 'lib/lp/code/model/gitrepository.py'
--- lib/lp/code/model/gitrepository.py 2019-03-21 16:27:29 +0000
+++ lib/lp/code/model/gitrepository.py 2019-03-21 16:27:30 +0000
@@ -112,7 +112,11 @@
IGitCollection,
)
from lp.code.interfaces.githosting import IGitHostingClient
-from lp.code.interfaces.gitjob import IGitRefScanJobSource
+from lp.code.interfaces.gitjob import (
+ IGitRefScanJobSource,
+ IGitRepositoryDeleteJobSource,
+ IReclaimGitRepositorySpaceJobSource,
+ )
from lp.code.interfaces.gitlookup import IGitLookup
from lp.code.interfaces.gitnamespace import (
get_git_namespace,
@@ -1551,11 +1555,6 @@
def destroySelf(self, break_references=False):
"""See `IGitRepository`."""
- # Circular import.
- from lp.code.interfaces.gitjob import (
- IReclaimGitRepositorySpaceJobSource,
- )
-
if break_references:
self._breakReferences()
if not self.canBeDeleted():
@@ -1583,6 +1582,18 @@
getUtility(IReclaimGitRepositorySpaceJobSource).create(
repository_name, repository_path)
+ def delete(self):
+ """See `IGitRepository`."""
+ if self.deletion_status == GitRepositoryDeletionStatus.DELETING:
+ raise CannotDeleteGitRepository(
+ "This repository is already being deleted: %s" %
+ self.unique_name)
+ # Destructor operations can't take arguments.
+ user = getUtility(ILaunchBag).user
+ getUtility(IGitRepositoryDeleteJobSource).create(self, user)
+ self.name = self.namespace.findUnusedName(self.name + "-deleting")
+ self.deletion_status = GitRepositoryDeletionStatus.DELETING
+
class DeletionOperation:
"""Represent an operation to perform as part of branch deletion."""
=== modified file 'lib/lp/code/model/tests/test_branch.py'
--- lib/lp/code/model/tests/test_branch.py 2019-03-21 16:27:29 +0000
+++ lib/lp/code/model/tests/test_branch.py 2019-03-21 16:27:30 +0000
@@ -53,6 +53,7 @@
RepositoryFormat,
)
from lp.code.enums import (
+ BranchDeletionStatus,
BranchLifecycleStatus,
BranchSubscriptionNotificationLevel,
BranchType,
@@ -74,6 +75,7 @@
IBranch,
)
from lp.code.interfaces.branchjob import (
+ IBranchDeleteJobSource,
IBranchScanJobSource,
IBranchUpgradeJobSource,
)
@@ -1483,6 +1485,34 @@
transaction.commit()
self.assertRaises(LostObjectError, getattr, webhook, 'target')
+ def test_delete_schedules_job(self):
+ # Calling the delete method schedules a deletion job, which can be
+ # processed.
+ branch_id = self.branch.id
+ branch_name = self.branch.unique_name
+ self.branch.delete()
+ update_trigger_modified_fields(self.branch)
+ self.assertEqual(branch_name + "-deleting", self.branch.unique_name)
+ self.assertEqual(
+ BranchDeletionStatus.DELETING, self.branch.deletion_status)
+ [job] = getUtility(IBranchDeleteJobSource).iterReady()
+ self.assertEqual(branch_id, job.branch_id)
+ self.assertEqual(self.user, job.requester)
+ with dbuser(config.IBranchDeleteJobSource.dbuser):
+ JobRunner([job]).runAll()
+
+ def test_delete_already_deleting(self):
+ # Trying to delete a branch that is already being deleted raises
+ # CannotDeleteBranch.
+ self.branch.delete()
+ self.assertRaises(CannotDeleteBranch, self.branch.delete)
+
+ def test_delete_stacked_branches(self):
+ # Trying to delete a branch that is stacked upon raises
+ # CannotDeleteBranch.
+ self.factory.makeAnyBranch(stacked_on=self.branch)
+ self.assertRaises(CannotDeleteBranch, self.branch.delete)
+
class TestBranchDeletionConsequences(TestCase):
"""Test determination and application of branch deletion consequences."""
=== modified file 'lib/lp/code/model/tests/test_gitrepository.py'
--- lib/lp/code/model/tests/test_gitrepository.py 2019-03-21 16:27:29 +0000
+++ lib/lp/code/model/tests/test_gitrepository.py 2019-03-21 16:27:30 +0000
@@ -52,6 +52,7 @@
CodeReviewNotificationLevel,
GitGranteeType,
GitObjectType,
+ GitRepositoryDeletionStatus,
GitRepositoryType,
TargetRevisionControlSystems,
)
@@ -71,6 +72,7 @@
from lp.code.interfaces.defaultgit import ICanHasDefaultGitRepository
from lp.code.interfaces.gitjob import (
IGitRefScanJobSource,
+ IGitRepositoryDeleteJobSource,
IGitRepositoryModifiedMailJobSource,
)
from lp.code.interfaces.gitlookup import IGitLookup
@@ -818,6 +820,29 @@
GitActivity, GitActivity.repository_id == repository_id)
self.assertEqual([], list(activities))
+ def test_delete_schedules_job(self):
+ # Calling the delete method schedules a deletion job, which can be
+ # processed.
+ repository_id = self.repository.id
+ repository_name = self.repository.unique_name
+ self.repository.delete()
+ self.assertEqual(
+ repository_name + "-deleting", self.repository.unique_name)
+ self.assertEqual(
+ GitRepositoryDeletionStatus.DELETING,
+ self.repository.deletion_status)
+ [job] = getUtility(IGitRepositoryDeleteJobSource).iterReady()
+ self.assertEqual(repository_id, job.repository_id)
+ self.assertEqual(self.user, job.requester)
+ with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
+ JobRunner([job]).runAll()
+
+ def test_delete_already_deleting(self):
+ # Trying to delete a repository that is already being deleted raises
+ # CannotDeleteGitRepository.
+ self.repository.delete()
+ self.assertRaises(CannotDeleteGitRepository, self.repository.delete)
+
class TestGitRepositoryDeletionConsequences(TestCaseWithFactory):
"""Test determination and application of repository deletion
=== modified file 'lib/lp/code/stories/branches/xx-branch-deletion.txt'
--- lib/lp/code/stories/branches/xx-branch-deletion.txt 2018-05-13 10:35:52 +0000
+++ lib/lp/code/stories/branches/xx-branch-deletion.txt 2019-03-21 16:27:30 +0000
@@ -52,6 +52,20 @@
>>> print_feedback_messages(browser.contents)
Branch ~alice/earthlynx/to-delete deleted...
+Branch deletion is asynchronous. If the user finds the branch again before
+the deletion completes, then they see an indication that it is being
+deleted, and there is no 'Delete branch' action.
+
+ >>> browser.open(
+ ... 'http://code.launchpad.dev/~alice/earthlynx/to-delete-deleting')
+ >>> print(extract_text(find_tag_by_id(
+ ... browser.contents, 'branch-deleting')))
+ This branch is being deleted.
+ >>> browser.getLink('Delete branch')
+ Traceback (most recent call last):
+ ...
+ LinkNotFoundError
+
If the branch is junk, then the user is taken back to the code listing for
the deleted branch's owner.
=== modified file 'lib/lp/code/stories/webservice/xx-branch.txt'
--- lib/lp/code/stories/webservice/xx-branch.txt 2019-03-21 16:27:29 +0000
+++ lib/lp/code/stories/webservice/xx-branch.txt 2019-03-21 16:27:30 +0000
@@ -300,4 +300,17 @@
>>> print_branches(webservice, '/widgets')
~eric/fooix/trunk - Development
-
+ ~eric/fooix/feature-branch-deleting - Experimental
+ ~mary/blob/bar-deleting - Development
+
+ >>> from zope.component import getUtility
+ >>> from lp.code.interfaces.branchjob import IBranchDeleteJobSource
+ >>> from lp.services.config import config
+ >>> from lp.services.job.runner import JobRunner
+
+ >>> with permissive_security_policy(config.IBranchDeleteJobSource.dbuser):
+ ... job_source = getUtility(IBranchDeleteJobSource)
+ ... JobRunner.fromReady(job_source).runAll()
+
+ >>> print_branches(webservice, '/widgets')
+ ~eric/fooix/trunk - Development
=== modified file 'lib/lp/code/templates/branch-index.pt'
--- lib/lp/code/templates/branch-index.pt 2019-01-23 17:52:34 +0000
+++ lib/lp/code/templates/branch-index.pt 2019-03-21 16:27:30 +0000
@@ -67,6 +67,10 @@
<div metal:fill-slot="main">
<div class="yui-g first">
+ <div tal:condition="python: context.deletion_status.name == 'DELETING'"
+ id="branch-deleting" class="warning message">
+ This branch is being deleted.
+ </div>
<tal:branch-errors tal:replace="structure context/@@+messages" />
</div>
=== modified file 'lib/lp/code/templates/gitrepository-index.pt'
--- lib/lp/code/templates/gitrepository-index.pt 2019-01-28 15:36:56 +0000
+++ lib/lp/code/templates/gitrepository-index.pt 2019-03-21 16:27:30 +0000
@@ -33,6 +33,13 @@
<div metal:fill-slot="main">
+ <div class="yui-g first">
+ <div tal:condition="python: context.deletion_status.name == 'DELETING'"
+ id="repository-deleting" class="warning message">
+ This repository is being deleted.
+ </div>
+ </div>
+
<div id="repository-description" tal:condition="context/description"
class="summary"
tal:content="structure context/description/fmt:text-to-html" />
=== modified file 'lib/lp/scripts/garbo.py'
--- lib/lp/scripts/garbo.py 2019-01-30 11:23:33 +0000
+++ lib/lp/scripts/garbo.py 2019-03-21 16:27:30 +0000
@@ -40,6 +40,7 @@
Or,
Row,
SQL,
+ Update,
)
from storm.info import ClassAlias
from storm.store import EmptyResultSet
@@ -57,13 +58,19 @@
BugWatchScheduler,
MAX_SAMPLE_SIZE,
)
+from lp.code.enums import (
+ BranchDeletionStatus,
+ GitRepositoryDeletionStatus,
+ )
from lp.code.interfaces.revision import IRevisionSet
+from lp.code.model.branch import Branch
from lp.code.model.codeimportevent import CodeImportEvent
from lp.code.model.codeimportresult import CodeImportResult
from lp.code.model.diff import (
Diff,
PreviewDiff,
)
+from lp.code.model.gitrepository import GitRepository
from lp.code.model.revision import (
RevisionAuthor,
RevisionCache,
@@ -1611,6 +1618,62 @@
""" % (SnapBuildJobType.STORE_UPLOAD.value, JobStatus.COMPLETED.value)
+class BranchDeletionStatusPopulator(TunableLoop):
+ """Populates Branch.deletion_status with ACTIVE."""
+
+ maximum_chunk_size = 5000
+
+ def __init__(self, log, abort_time=None):
+ super(BranchDeletionStatusPopulator, self).__init__(log, abort_time)
+ self.start_at = 1
+ self.store = IMasterStore(Branch)
+
+ def findBranches(self):
+ return self.store.find(
+ Branch,
+ Branch.id >= self.start_at,
+ Branch._deletion_status == None).order_by(Branch.id)
+
+ def isDone(self):
+ return self.findBranches().is_empty()
+
+ def __call__(self, chunk_size):
+ ids = [branch.id for branch in self.findBranches()]
+ self.store.execute(Update(
+ {Branch._deletion_status: BranchDeletionStatus.ACTIVE.value},
+ where=Branch.id.is_in(ids), table=Branch))
+ transaction.commit()
+
+
+class GitRepositoryDeletionStatusPopulator(TunableLoop):
+ """Populates GitRepository.deletion_status with ACTIVE."""
+
+ maximum_chunk_size = 5000
+
+ def __init__(self, log, abort_time=None):
+ super(GitRepositoryDeletionStatusPopulator, self).__init__(
+ log, abort_time)
+ self.start_at = 1
+ self.store = IMasterStore(GitRepository)
+
+ def findRepositories(self):
+ return self.store.find(
+ GitRepository,
+ GitRepository.id >= self.start_at,
+ GitRepository._deletion_status == None).order_by(GitRepository.id)
+
+ def isDone(self):
+ return self.findRepositories().is_empty()
+
+ def __call__(self, chunk_size):
+ ids = [branch.id for branch in self.findRepositories()]
+ self.store.execute(Update(
+ {GitRepository._deletion_status:
+ GitRepositoryDeletionStatus.ACTIVE.value},
+ where=GitRepository.id.is_in(ids), table=GitRepository))
+ transaction.commit()
+
+
class BaseDatabaseGarbageCollector(LaunchpadCronScript):
"""Abstract base class to run a collection of TunableLoops."""
script_name = None # Script name for locking and database user. Override.
@@ -1884,6 +1947,7 @@
script_name = 'garbo-daily'
tunable_loops = [
AnswerContactPruner,
+ BranchDeletionStatusPopulator,
BranchJobPruner,
BugNotificationPruner,
BugWatchActivityPruner,
@@ -1891,6 +1955,7 @@
CodeImportResultPruner,
DiffPruner,
GitJobPruner,
+ GitRepositoryDeletionStatusPopulator,
HWSubmissionEmailLinker,
LiveFSFilePruner,
LoginTokenPruner,
=== modified file 'lib/lp/scripts/tests/test_garbo.py'
--- lib/lp/scripts/tests/test_garbo.py 2019-01-30 11:23:33 +0000
+++ lib/lp/scripts/tests/test_garbo.py 2019-03-21 16:27:30 +0000
@@ -15,6 +15,7 @@
from StringIO import StringIO
import time
+from psycopg2 import IntegrityError
from pytz import UTC
from storm.exceptions import LostObjectError
from storm.expr import (
@@ -54,7 +55,11 @@
BranchFormat,
RepositoryFormat,
)
-from lp.code.enums import CodeImportResultStatus
+from lp.code.enums import (
+ BranchDeletionStatus,
+ CodeImportResultStatus,
+ GitRepositoryDeletionStatus,
+ )
from lp.code.interfaces.codeimportevent import ICodeImportEventSet
from lp.code.interfaces.gitrepository import IGitRepositorySet
from lp.code.model.branchjob import (
@@ -1606,6 +1611,84 @@
# retained.
self._test_SnapFilePruner('foo.snap', None, 30, expected_count=1)
+ def test_BranchDeletionStatusPopulator(self):
+ switch_dbuser('testadmin')
+ old_branches = [self.factory.makeAnyBranch() for _ in range(2)]
+ for branch in old_branches:
+ removeSecurityProxy(branch)._deletion_status = None
+ try:
+ Store.of(old_branches[0]).flush()
+ except IntegrityError:
+ # Now enforced by DB NOT NULL constraint; backfilling is no
+ # longer necessary.
+ return
+ active_branches = [self.factory.makeAnyBranch() for _ in range(2)]
+ for branch in active_branches:
+ removeSecurityProxy(branch)._deletion_status = (
+ BranchDeletionStatus.ACTIVE)
+ deleting_branches = [self.factory.makeAnyBranch() for _ in range(2)]
+ for branch in deleting_branches:
+ removeSecurityProxy(branch)._deletion_status = (
+ BranchDeletionStatus.DELETING)
+ transaction.commit()
+
+ self.runDaily()
+
+ # Old branches are backfilled.
+ for branch in old_branches:
+ self.assertEqual(
+ BranchDeletionStatus.ACTIVE,
+ removeSecurityProxy(branch)._deletion_status)
+ # Other branches are left alone.
+ for branch in active_branches:
+ self.assertEqual(
+ BranchDeletionStatus.ACTIVE,
+ removeSecurityProxy(branch)._deletion_status)
+ for branch in deleting_branches:
+ self.assertEqual(
+ BranchDeletionStatus.DELETING,
+ removeSecurityProxy(branch)._deletion_status)
+
+ def test_GitRepositoryDeletionStatusPopulator(self):
+ switch_dbuser('testadmin')
+ old_repositories = [self.factory.makeGitRepository() for _ in range(2)]
+ for repository in old_repositories:
+ removeSecurityProxy(repository)._deletion_status = None
+ try:
+ Store.of(old_repositories[0]).flush()
+ except IntegrityError:
+ # Now enforced by DB NOT NULL constraint; backfilling is no
+ # longer necessary.
+ return
+ active_repositories = [
+ self.factory.makeGitRepository() for _ in range(2)]
+ for repository in active_repositories:
+ removeSecurityProxy(repository)._deletion_status = (
+ GitRepositoryDeletionStatus.ACTIVE)
+ deleting_repositories = [
+ self.factory.makeGitRepository() for _ in range(2)]
+ for repository in deleting_repositories:
+ removeSecurityProxy(repository)._deletion_status = (
+ GitRepositoryDeletionStatus.DELETING)
+ transaction.commit()
+
+ self.runDaily()
+
+ # Old repositories are backfilled.
+ for repository in old_repositories:
+ self.assertEqual(
+ GitRepositoryDeletionStatus.ACTIVE,
+ removeSecurityProxy(repository)._deletion_status)
+ # Other repositories are left alone.
+ for repository in active_repositories:
+ self.assertEqual(
+ GitRepositoryDeletionStatus.ACTIVE,
+ removeSecurityProxy(repository)._deletion_status)
+ for repository in deleting_repositories:
+ self.assertEqual(
+ GitRepositoryDeletionStatus.DELETING,
+ removeSecurityProxy(repository)._deletion_status)
+
class TestGarboTasks(TestCaseWithFactory):
layer = LaunchpadZopelessLayer
Follow ups