launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #23439
[Merge] lp:~cjwatson/launchpad/git-repository-delete-job into lp:launchpad
Colin Watson has proposed merging lp:~cjwatson/launchpad/git-repository-delete-job into lp:launchpad with lp:~cjwatson/launchpad/branch-delete-job as a prerequisite.
Commit message:
Add a Git repository deletion job.
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/git-repository-delete-job/+merge/364910
As in https://code.launchpad.net/~cjwatson/launchpad/branch-delete-job/+merge/364907.
--
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/git-repository-delete-job into lp:launchpad.
=== modified file 'database/schema/security.cfg'
--- database/schema/security.cfg 2019-03-21 16:22:19 +0000
+++ database/schema/security.cfg 2019-03-21 16:22:19 +0000
@@ -2621,6 +2621,13 @@
public.distributionsourcepackage = SELECT, DELETE
public.distroseries = SELECT
public.emailaddress = SELECT
+public.gitactivity = SELECT, DELETE
+public.gitjob = SELECT, INSERT, DELETE
+public.gitref = SELECT, DELETE
+public.gitrepository = SELECT, UPDATE, DELETE
+public.gitrule = SELECT, DELETE
+public.gitrulegrant = SELECT, DELETE
+public.gitsubscription = SELECT, DELETE
public.job = SELECT, INSERT, UPDATE, DELETE
public.person = SELECT
public.previewdiff = SELECT, DELETE
=== modified file 'lib/lp/code/configure.zcml'
--- lib/lp/code/configure.zcml 2019-03-21 16:22:19 +0000
+++ lib/lp/code/configure.zcml 2019-03-21 16:22:19 +0000
@@ -1092,6 +1092,11 @@
provides="lp.code.interfaces.gitjob.IGitRepositoryModifiedMailJobSource">
<allow interface="lp.code.interfaces.gitjob.IGitRepositoryModifiedMailJobSource" />
</securedutility>
+ <securedutility
+ component="lp.code.model.gitjob.GitRepositoryDeleteJob"
+ provides="lp.code.interfaces.gitjob.IGitRepositoryDeleteJobSource">
+ <allow interface="lp.code.interfaces.gitjob.IGitRepositoryDeleteJobSource" />
+ </securedutility>
<class class="lp.code.model.gitjob.GitRefScanJob">
<allow interface="lp.code.interfaces.gitjob.IGitJob" />
<allow interface="lp.code.interfaces.gitjob.IGitRefScanJob" />
@@ -1104,6 +1109,10 @@
<allow interface="lp.code.interfaces.gitjob.IGitJob" />
<allow interface="lp.code.interfaces.gitjob.IGitRepositoryModifiedMailJob" />
</class>
+ <class class="lp.code.model.gitjob.GitRepositoryDeleteJob">
+ <allow interface="lp.code.interfaces.gitjob.IGitJob" />
+ <allow interface="lp.code.interfaces.gitjob.IGitRepositoryDeleteJob" />
+ </class>
<lp:help-folder folder="help" name="+help-code" />
=== modified file 'lib/lp/code/enums.py'
--- lib/lp/code/enums.py 2019-03-21 16:22:19 +0000
+++ lib/lp/code/enums.py 2019-03-21 16:22:19 +0000
@@ -26,6 +26,7 @@
'GitGranteeType',
'GitObjectType',
'GitPermissionType',
+ 'GitRepositoryDeletionStatus',
'GitRepositoryType',
'NON_CVS_RCS_TYPES',
'RevisionControlSystems',
@@ -158,6 +159,17 @@
""")
+class GitRepositoryDeletionStatus(DBEnumeratedType):
+ """Git Repository Deletion Status
+
+ The deletion status of a repository is used to track asynchronous
+ deletion.
+ """
+
+ ACTIVE = DBItem(0, "Active")
+ DELETING = DBItem(1, "Deleting")
+
+
class GitObjectType(DBEnumeratedType):
"""Git Object Type
=== modified file 'lib/lp/code/interfaces/gitjob.py'
--- lib/lp/code/interfaces/gitjob.py 2015-09-01 17:10:46 +0000
+++ lib/lp/code/interfaces/gitjob.py 2019-03-21 16:22:19 +0000
@@ -9,6 +9,8 @@
'IGitJob',
'IGitRefScanJob',
'IGitRefScanJobSource',
+ 'IGitRepositoryDeleteJob',
+ 'IGitRepositoryDeleteJobSource',
'IGitRepositoryModifiedMailJob',
'IGitRepositoryModifiedMailJobSource',
'IReclaimGitRepositorySpaceJob',
@@ -20,7 +22,10 @@
Attribute,
Interface,
)
-from zope.schema import Text
+from zope.schema import (
+ Int,
+ Text,
+ )
from lp import _
from lp.code.interfaces.gitrepository import IGitRepository
@@ -93,3 +98,19 @@
:param repository_delta: An `IGitRepositoryDelta` describing the
changes.
"""
+
+
+class IGitRepositoryDeleteJob(IRunnableJob):
+ """A Job that deletes a repository from the database."""
+
+ repository_id = Int(title=_("The id of the repository to delete."))
+
+
+class IGitRepositoryDeleteJobSource(IJobSource):
+
+ def create(repository, requester):
+ """Delete a repository from the database.
+
+ :param repository: The `IGitRepository` to delete.
+ :param requester: The `IPerson` who requested the deletion.
+ """
=== modified file 'lib/lp/code/interfaces/gitrepository.py'
--- lib/lp/code/interfaces/gitrepository.py 2019-01-28 17:19:44 +0000
+++ lib/lp/code/interfaces/gitrepository.py 2019-03-21 16:22:19 +0000
@@ -64,6 +64,7 @@
BranchSubscriptionDiffSize,
BranchSubscriptionNotificationLevel,
CodeReviewNotificationLevel,
+ GitRepositoryDeletionStatus,
GitRepositoryType,
)
from lp.code.interfaces.defaultgit import ICanHasDefaultGitRepository
@@ -139,6 +140,11 @@
"The way this repository is hosted: directly on Launchpad, or "
"imported from somewhere else.")))
+ deletion_status = exported(Choice(
+ title=_("Deletion status"), required=True, readonly=True,
+ vocabulary=GitRepositoryDeletionStatus,
+ description=_("The deletion status of this repository.")))
+
registrant = exported(PublicPersonChoice(
title=_("Registrant"), required=True, readonly=True,
vocabulary="ValidPersonOrTeam",
=== modified file 'lib/lp/code/model/gitjob.py'
--- lib/lp/code/model/gitjob.py 2018-10-22 00:37:07 +0000
+++ lib/lp/code/model/gitjob.py 2019-03-21 16:22:19 +0000
@@ -8,6 +8,7 @@
'GitJob',
'GitJobType',
'GitRefScanJob',
+ 'GitRepositoryDeleteJob',
'GitRepositoryModifiedMailJob',
'ReclaimGitRepositorySpaceJob',
]
@@ -25,6 +26,7 @@
SQL,
Store,
)
+import transaction
from zope.component import getUtility
from zope.interface import (
implementer,
@@ -36,17 +38,22 @@
from lp.code.enums import (
GitActivityType,
GitPermissionType,
+ GitRepositoryDeletionStatus,
)
+from lp.code.errors import CannotDeleteGitRepository
from lp.code.interfaces.githosting import IGitHostingClient
from lp.code.interfaces.gitjob import (
IGitJob,
IGitRefScanJob,
IGitRefScanJobSource,
+ IGitRepositoryDeleteJob,
+ IGitRepositoryDeleteJobSource,
IGitRepositoryModifiedMailJob,
IGitRepositoryModifiedMailJobSource,
IReclaimGitRepositorySpaceJob,
IReclaimGitRepositorySpaceJobSource,
)
+from lp.code.interfaces.gitlookup import IGitLookup
from lp.code.interfaces.gitrule import describe_git_permissions
from lp.code.mail.branch import BranchMailer
from lp.registry.interfaces.person import IPersonSet
@@ -99,6 +106,12 @@
modifications.
""")
+ DELETE_REPOSITORY = DBItem(3, """
+ Delete repository
+
+ This job deletes a repository from the database.
+ """)
+
@implementer(IGitJob)
class GitJob(StormBase):
@@ -405,3 +418,63 @@
def run(self):
"""See `IGitRepositoryModifiedMailJob`."""
self.getMailer().sendAll()
+
+
+@implementer(IGitRepositoryDeleteJob)
+@provider(IGitRepositoryDeleteJobSource)
+class GitRepositoryDeleteJob(GitJobDerived):
+ """A Job that deletes a repository from the database."""
+
+ class_job_type = GitJobType.DELETE_REPOSITORY
+
+ user_error_types = (CannotDeleteGitRepository,)
+
+ config = config.IGitRepositoryDeleteJobSource
+
+ def getOperationDescription(self):
+ return "deleting a repository"
+
+ @classmethod
+ def create(cls, repository, requester):
+ """See `IGitRepositoryDeleteJobSource`."""
+ metadata = {
+ "repository_id": repository.id,
+ "repository_name": repository.unique_name,
+ }
+ # The GitJob has a repository of None, because we don't want to
+ # delete this job while trying to delete the repository.
+ git_job = GitJob(
+ None, cls.class_job_type, metadata, requester=requester)
+ job = cls(git_job)
+ job.celeryRunOnCommit()
+ return job
+
+ @property
+ def repository_id(self):
+ return self.metadata["repository_id"]
+
+ def run(self):
+ """See `IGitRepositoryDeleteJob`."""
+ repository = getUtility(IGitLookup).get(self.repository_id)
+ if repository is None:
+ log.info(
+ "Skipping repository %s because it has already been deleted." %
+ self._cached_repository_name)
+ elif (repository.deletion_status !=
+ GitRepositoryDeletionStatus.DELETING):
+ log.warning(
+ "Skipping repository %s because its deletion status is not "
+ "DELETING." % self._cached_repository_name)
+ else:
+ try:
+ repository.destroySelf(break_references=True)
+ except CannotDeleteGitRepository:
+ # Set the deletion status back to ACTIVE so that it's
+ # possible to try again. We don't attempt to undo the
+ # renaming at the moment. Do this in its own transaction
+ # since the job runner will abort the transaction.
+ transaction.abort()
+ removeSecurityProxy(repository).deletion_status = (
+ GitRepositoryDeletionStatus.ACTIVE)
+ transaction.commit()
+ raise
=== modified file 'lib/lp/code/model/gitrepository.py'
--- lib/lp/code/model/gitrepository.py 2019-03-21 16:22:19 +0000
+++ lib/lp/code/model/gitrepository.py 2019-03-21 16:22:19 +0000
@@ -91,6 +91,7 @@
GitGranteeType,
GitObjectType,
GitPermissionType,
+ GitRepositoryDeletionStatus,
GitRepositoryType,
)
from lp.code.errors import (
@@ -282,6 +283,23 @@
repository_type = EnumCol(
dbName='repository_type', enum=GitRepositoryType, notNull=True)
+ _deletion_status = EnumCol(
+ dbName='deletion_status', enum=GitRepositoryDeletionStatus,
+ default=GitRepositoryDeletionStatus.ACTIVE)
+
+ @property
+ def deletion_status(self):
+ # XXX cjwatson 2019-03-19: Remove once this column has been
+ # backfilled.
+ if self._deletion_status is None:
+ return GitRepositoryDeletionStatus.ACTIVE
+ else:
+ return self._deletion_status
+
+ @deletion_status.setter
+ def deletion_status(self, value):
+ self._deletion_status = value
+
registrant_id = Int(name='registrant', allow_none=False)
registrant = Reference(registrant_id, 'Person.id')
=== modified file 'lib/lp/code/model/tests/test_gitjob.py'
--- lib/lp/code/model/tests/test_gitjob.py 2018-10-21 17:38:05 +0000
+++ lib/lp/code/model/tests/test_gitjob.py 2019-03-21 16:22:19 +0000
@@ -13,6 +13,10 @@
)
import hashlib
+from fixtures import (
+ FakeLogger,
+ MockPatch,
+ )
from lazr.lifecycle.snapshot import Snapshot
import pytz
from testtools.matchers import (
@@ -23,6 +27,7 @@
MatchesStructure,
)
import transaction
+from zope.component import getUtility
from zope.interface import providedBy
from zope.security.proxy import removeSecurityProxy
@@ -30,6 +35,7 @@
from lp.code.enums import (
GitGranteeType,
GitObjectType,
+ GitRepositoryDeletionStatus,
)
from lp.code.interfaces.branchmergeproposal import (
BRANCH_MERGE_PROPOSAL_WEBHOOKS_FEATURE_FLAG,
@@ -37,8 +43,11 @@
from lp.code.interfaces.gitjob import (
IGitJob,
IGitRefScanJob,
+ IGitRepositoryDeleteJob,
+ IGitRepositoryDeleteJobSource,
IReclaimGitRepositorySpaceJob,
)
+from lp.code.interfaces.gitlookup import IGitLookup
from lp.code.model.gitjob import (
describe_repository_delta,
GitJob,
@@ -52,6 +61,7 @@
from lp.services.database.constants import UTC_NOW
from lp.services.features.testing import FeatureFixture
from lp.services.job.runner import JobRunner
+from lp.services.mail.sendmail import format_address_for_person
from lp.services.utils import seconds_since_epoch
from lp.services.webapp import canonical_url
from lp.services.webapp.snapshot import notify_modified
@@ -65,6 +75,7 @@
DatabaseFunctionalLayer,
ZopelessDatabaseLayer,
)
+from lp.testing.mail_helpers import pop_notifications
class TestGitJob(TestCaseWithFactory):
@@ -473,5 +484,98 @@
snapshot, repository)
+class TestGitRepositoryDeleteJob(TestCaseWithFactory):
+
+ layer = ZopelessDatabaseLayer
+
+ def test_providesInterface(self):
+ repository = self.factory.makeGitRepository()
+ requester = repository.registrant
+ job = getUtility(IGitRepositoryDeleteJobSource).create(
+ repository, requester)
+ self.assertProvides(job, IGitRepositoryDeleteJob)
+
+ def test_run(self):
+ repository = self.factory.makeGitRepository()
+ repository_id = repository.id
+ requester = repository.registrant
+ job = getUtility(IGitRepositoryDeleteJobSource).create(
+ repository, requester)
+ removeSecurityProxy(repository).deletion_status = (
+ GitRepositoryDeletionStatus.DELETING)
+ logger = self.useFixture(FakeLogger())
+ with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
+ job.run()
+ self.assertEqual('', logger.output)
+ self.assertIsNone(getUtility(IGitLookup).get(repository_id))
+
+ def test_already_deleted(self):
+ repository = self.factory.makeGitRepository()
+ repository_name = repository.unique_name
+ requester = repository.registrant
+ job = getUtility(IGitRepositoryDeleteJobSource).create(
+ repository, requester)
+ repository.destroySelf()
+ logger = self.useFixture(FakeLogger())
+ with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
+ job.run()
+ self.assertEqual(
+ "Skipping repository %s because it has already been "
+ "deleted.\n" % repository_name,
+ logger.output)
+
+ def test_not_deleting(self):
+ # The job skips repositories that aren't DELETING. This shouldn't
+ # be possible in practice, but is a guard against accidents.
+ repository = self.factory.makeGitRepository()
+ repository_id = repository.id
+ repository_name = repository.unique_name
+ self.assertNotEqual(
+ GitRepositoryDeletionStatus.DELETING, repository.deletion_status)
+ requester = repository.registrant
+ job = getUtility(IGitRepositoryDeleteJobSource).create(
+ repository, requester)
+ logger = self.useFixture(FakeLogger())
+ with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
+ job.run()
+ self.assertEqual(
+ "Skipping repository %s because its deletion status is not "
+ "DELETING.\n" % repository_name,
+ logger.output)
+ self.assertEqual(repository, getUtility(IGitLookup).get(repository_id))
+
+ def test_error(self):
+ # If deleting the repository fails, an error message is sent to the
+ # requester and the deletion status is set back to ACTIVE. This
+ # can't normally happen because the job always breaks references to
+ # the repository, so we patch in a failure to allow testing the
+ # error path.
+ repository = self.factory.makeGitRepository()
+ repository_id = repository.id
+ requester = repository.registrant
+ job = getUtility(IGitRepositoryDeleteJobSource).create(
+ repository, requester)
+ removeSecurityProxy(repository).deletion_status = (
+ GitRepositoryDeletionStatus.DELETING)
+ logger = self.useFixture(FakeLogger())
+ with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
+ with MockPatch(
+ "lp.code.model.gitrepository.GitRepository.canBeDeleted",
+ return_value=False):
+ JobRunner([job]).runJobHandleError(job)
+ self.assertIn(
+ "failed with user error CannotDeleteGitRepository", logger.output)
+ self.assertEqual(repository, getUtility(IGitLookup).get(repository_id))
+ self.assertEqual(
+ GitRepositoryDeletionStatus.ACTIVE, repository.deletion_status)
+ self.assertEqual([], self.oopses)
+ [mail] = pop_notifications()
+ self.assertEqual(format_address_for_person(requester), mail["to"])
+ self.assertEqual(
+ "Launchpad error while deleting a repository", mail["subject"])
+ self.assertIn(
+ "Cannot delete Git repository", mail.get_payload(decode=True))
+
+
# XXX cjwatson 2015-03-12: We should test that the jobs work via Celery too,
# but that isn't feasible until we have a proper turnip fixture.
=== modified file 'lib/lp/code/model/tests/test_gitrepository.py'
--- lib/lp/code/model/tests/test_gitrepository.py 2019-02-11 12:31:06 +0000
+++ lib/lp/code/model/tests/test_gitrepository.py 2019-03-21 16:22:19 +0000
@@ -638,7 +638,8 @@
self.repository.canBeDeleted(),
"A newly created repository should be able to be deleted.")
repository_id = self.repository.id
- self.repository.destroySelf()
+ with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
+ self.repository.destroySelf()
self.assertIsNone(
getUtility(IGitLookup).get(repository_id),
"The repository has not been deleted.")
@@ -650,7 +651,8 @@
CodeReviewNotificationLevel.NOEMAIL, self.user)
self.assertTrue(self.repository.canBeDeleted())
repository_id = self.repository.id
- self.repository.destroySelf()
+ with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
+ self.repository.destroySelf()
self.assertIsNone(
getUtility(IGitLookup).get(repository_id),
"The repository has not been deleted.")
@@ -665,7 +667,8 @@
CodeReviewNotificationLevel.NOEMAIL, self.user)
self.assertTrue(self.repository.canBeDeleted())
repository_id = self.repository.id
- self.repository.destroySelf()
+ with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
+ self.repository.destroySelf()
self.assertIsNone(
getUtility(IGitLookup).get(repository_id),
"The repository has not been deleted.")
@@ -687,8 +690,9 @@
self.assertFalse(
self.repository.canBeDeleted(),
"A repository with a landing target is not deletable.")
- self.assertRaises(
- CannotDeleteGitRepository, self.repository.destroySelf)
+ with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
+ self.assertRaises(
+ CannotDeleteGitRepository, self.repository.destroySelf)
def test_landing_candidate_disables_deletion(self):
# A repository with a landing candidate cannot be deleted.
@@ -698,8 +702,9 @@
self.assertFalse(
self.repository.canBeDeleted(),
"A repository with a landing candidate is not deletable.")
- self.assertRaises(
- CannotDeleteGitRepository, self.repository.destroySelf)
+ with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
+ self.assertRaises(
+ CannotDeleteGitRepository, self.repository.destroySelf)
def test_prerequisite_repository_disables_deletion(self):
# A repository that is a prerequisite repository cannot be deleted.
@@ -711,14 +716,16 @@
self.assertFalse(
self.repository.canBeDeleted(),
"A repository with a prerequisite target is not deletable.")
- self.assertRaises(
- CannotDeleteGitRepository, self.repository.destroySelf)
+ with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
+ self.assertRaises(
+ CannotDeleteGitRepository, self.repository.destroySelf)
def test_related_GitJobs_deleted(self):
# A repository with an associated job will delete those jobs.
GitAPI(None, None).notify(self.repository.getInternalPath())
store = Store.of(self.repository)
- self.repository.destroySelf()
+ with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
+ self.repository.destroySelf()
# Need to commit the transaction to fire off the constraint checks.
transaction.commit()
jobs = store.find(GitJob, GitJob.job_type == GitJobType.REF_SCAN)
@@ -729,7 +736,8 @@
# to remove the repository from disk as well.
repository_path = self.repository.getInternalPath()
store = Store.of(self.repository)
- self.repository.destroySelf()
+ with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
+ self.repository.destroySelf()
jobs = store.find(
GitJob,
GitJob.job_type == GitJobType.RECLAIM_REPOSITORY_SPACE)
@@ -742,14 +750,16 @@
# If repository is a base_git_repository in a recipe, it is deleted.
recipe = self.factory.makeSourcePackageRecipe(
branches=self.factory.makeGitRefs(owner=self.user))
- recipe.base_git_repository.destroySelf(break_references=True)
+ with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
+ recipe.base_git_repository.destroySelf(break_references=True)
def test_destroySelf_with_SourcePackageRecipe_as_non_base(self):
# If repository is referred to by a recipe, it is deleted.
[ref1] = self.factory.makeGitRefs(owner=self.user)
[ref2] = self.factory.makeGitRefs(owner=self.user)
self.factory.makeSourcePackageRecipe(branches=[ref1, ref2])
- ref2.repository.destroySelf(break_references=True)
+ with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
+ ref2.repository.destroySelf(break_references=True)
def test_destroySelf_with_inline_comments_draft(self):
# Draft inline comments related to a deleted repository (source or
@@ -763,7 +773,8 @@
previewdiff_id=preview_diff.id,
person=self.user,
comments={"1": "Should vanish."})
- self.repository.destroySelf(break_references=True)
+ with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
+ self.repository.destroySelf(break_references=True)
def test_destroySelf_with_inline_comments_published(self):
# Published inline comments related to a deleted repository (source
@@ -779,12 +790,14 @@
previewdiff_id=preview_diff.id,
inline_comments={"1": "Must disappear."},
)
- self.repository.destroySelf(break_references=True)
+ with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
+ self.repository.destroySelf(break_references=True)
def test_related_webhooks_deleted(self):
webhook = self.factory.makeWebhook(target=self.repository)
webhook.ping()
- self.repository.destroySelf()
+ with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
+ self.repository.destroySelf()
transaction.commit()
self.assertRaises(LostObjectError, getattr, webhook, 'target')
@@ -796,7 +809,8 @@
activities = store.find(
GitActivity, GitActivity.repository_id == repository_id)
self.assertNotEqual([], list(activities))
- self.repository.destroySelf()
+ with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
+ self.repository.destroySelf()
transaction.commit()
self.assertRaises(LostObjectError, getattr, grant, 'rule')
self.assertRaises(LostObjectError, getattr, rule, 'repository')
@@ -895,7 +909,8 @@
merge_proposal1, merge_proposal2 = self.makeMergeProposals()
merge_proposal1_id = merge_proposal1.id
BranchMergeProposal.get(merge_proposal1_id)
- self.repository.destroySelf(break_references=True)
+ with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
+ self.repository.destroySelf(break_references=True)
self.assertRaises(
SQLObjectNotFound, BranchMergeProposal.get, merge_proposal1_id)
@@ -905,8 +920,9 @@
merge_proposal1, merge_proposal2 = self.makeMergeProposals()
merge_proposal1_id = merge_proposal1.id
BranchMergeProposal.get(merge_proposal1_id)
- merge_proposal1.target_git_repository.destroySelf(
- break_references=True)
+ with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
+ merge_proposal1.target_git_repository.destroySelf(
+ break_references=True)
self.assertRaises(SQLObjectNotFound,
BranchMergeProposal.get, merge_proposal1_id)
@@ -914,8 +930,9 @@
# Merge proposal prerequisite repositories can be deleted with
# break_references.
merge_proposal1, merge_proposal2 = self.makeMergeProposals()
- merge_proposal1.prerequisite_git_repository.destroySelf(
- break_references=True)
+ with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
+ merge_proposal1.prerequisite_git_repository.destroySelf(
+ break_references=True)
self.assertIsNone(merge_proposal1.prerequisite_git_repository)
def test_delete_source_CodeReviewComment(self):
@@ -923,7 +940,8 @@
comment = self.factory.makeCodeReviewComment(git=True)
comment_id = comment.id
repository = comment.branch_merge_proposal.source_git_repository
- repository.destroySelf(break_references=True)
+ with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
+ repository.destroySelf(break_references=True)
self.assertRaises(
SQLObjectNotFound, CodeReviewComment.get, comment_id)
@@ -932,7 +950,8 @@
comment = self.factory.makeCodeReviewComment(git=True)
comment_id = comment.id
repository = comment.branch_merge_proposal.target_git_repository
- repository.destroySelf(break_references=True)
+ with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
+ repository.destroySelf(break_references=True)
self.assertRaises(
SQLObjectNotFound, CodeReviewComment.get, comment_id)
@@ -941,14 +960,18 @@
merge_proposal = self.factory.makeBranchMergeProposalForGit()
merge_proposal.nominateReviewer(
self.factory.makePerson(), self.factory.makePerson())
- merge_proposal.source_git_repository.destroySelf(break_references=True)
+ with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
+ merge_proposal.source_git_repository.destroySelf(
+ break_references=True)
def test_targetBranchWithCodeReviewVoteReference(self):
# break_references handles CodeReviewVoteReference target repository.
merge_proposal = self.factory.makeBranchMergeProposalForGit()
merge_proposal.nominateReviewer(
self.factory.makePerson(), self.factory.makePerson())
- merge_proposal.target_git_repository.destroySelf(break_references=True)
+ with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
+ merge_proposal.target_git_repository.destroySelf(
+ break_references=True)
def test_code_import_requirements(self):
# Code imports are not included explicitly in deletion requirements.
@@ -967,7 +990,8 @@
code_import = self.factory.makeCodeImport(
target_rcs_type=TargetRevisionControlSystems.GIT)
code_import_id = code_import.id
- code_import.git_repository.destroySelf(break_references=True)
+ with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
+ code_import.git_repository.destroySelf(break_references=True)
self.assertRaises(
NotFoundError, getUtility(ICodeImportSet).get, code_import_id)
@@ -988,7 +1012,8 @@
repository=repository, paths=["refs/heads/1", "refs/heads/2"])
snap1 = self.factory.makeSnap(git_ref=ref1)
snap2 = self.factory.makeSnap(git_ref=ref2)
- repository.destroySelf(break_references=True)
+ with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
+ repository.destroySelf(break_references=True)
transaction.commit()
self.assertIsNone(snap1.git_repository)
self.assertIsNone(snap1.git_path)
@@ -1001,7 +1026,8 @@
merge_proposal = removeSecurityProxy(self.makeMergeProposals()[0])
with person_logged_in(
merge_proposal.prerequisite_git_repository.owner):
- ClearPrerequisiteRepository(merge_proposal)()
+ with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
+ ClearPrerequisiteRepository(merge_proposal)()
self.assertIsNone(merge_proposal.prerequisite_git_repository)
def test_DeletionOperation(self):
@@ -1012,8 +1038,9 @@
# DeletionCallable must invoke the callable.
merge_proposal = self.factory.makeBranchMergeProposalForGit()
merge_proposal_id = merge_proposal.id
- DeletionCallable(
- merge_proposal, "blah", merge_proposal.deleteProposal)()
+ with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
+ DeletionCallable(
+ merge_proposal, "blah", merge_proposal.deleteProposal)()
self.assertRaises(
SQLObjectNotFound, BranchMergeProposal.get, merge_proposal_id)
@@ -1023,7 +1050,8 @@
code_import = self.factory.makeCodeImport(
target_rcs_type=TargetRevisionControlSystems.GIT)
code_import_id = code_import.id
- DeleteCodeImport(code_import)()
+ with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
+ DeleteCodeImport(code_import)()
self.assertRaises(
NotFoundError, getUtility(ICodeImportSet).get, code_import_id)
=== modified file 'lib/lp/services/config/schema-lazr.conf'
--- lib/lp/services/config/schema-lazr.conf 2019-03-21 16:22:19 +0000
+++ lib/lp/services/config/schema-lazr.conf 2019-03-21 16:22:19 +0000
@@ -1871,6 +1871,7 @@
IBranchModifiedMailJobSource,
ICommercialExpiredJobSource,
IExpiringMembershipNotificationJobSource,
+ IGitRepositoryDeleteJobSource,
IGitRepositoryModifiedMailJobSource,
IMembershipNotificationJobSource,
IPackageUploadNotificationJobSource,
@@ -1931,6 +1932,11 @@
module: lp.code.interfaces.gitjob
dbuser: branchscanner
+[IGitRepositoryDeleteJobSource]
+module: lp.code.interfaces.gitjob
+dbuser: branch-delete-job
+crontab_group: MAIN
+
[IGitRepositoryModifiedMailJobSource]
module: lp.code.interfaces.gitjob
dbuser: send-branch-mail
Follow ups