Merge lp:~cjwatson/launchpad/git-repository-delete-job into lp:launchpad

Proposed by Colin Watson
Status: Rejected
Rejected by: Colin Watson
Proposed branch: lp:~cjwatson/launchpad/git-repository-delete-job
Merge into: lp:launchpad
Prerequisite: lp:~cjwatson/launchpad/branch-delete-job
Diff against target: 752 lines (+317/-33)
10 files modified
database/schema/security.cfg (+7/-0)
lib/lp/code/configure.zcml (+9/-0)
lib/lp/code/enums.py (+12/-0)
lib/lp/code/interfaces/gitjob.py (+22/-1)
lib/lp/code/interfaces/gitrepository.py (+6/-0)
lib/lp/code/model/gitjob.py (+73/-0)
lib/lp/code/model/gitrepository.py (+18/-0)
lib/lp/code/model/tests/test_gitjob.py (+104/-0)
lib/lp/code/model/tests/test_gitrepository.py (+60/-32)
lib/lp/services/config/schema-lazr.conf (+6/-0)
To merge this branch: bzr merge lp:~cjwatson/launchpad/git-repository-delete-job
Reviewer Review Type Date Requested Status
Launchpad code reviewers Pending
Review via email: mp+364910@code.launchpad.net

Commit message

Add a Git repository deletion job.

To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) wrote :

This seems unnecessary since adding some more indexes has made deletion fast enough, so withdrawing this until we have a clear need.

Unmerged revisions

18913. By Colin Watson

Add a Git repository deletion job.

18912. By Colin Watson

Add a branch deletion job.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== 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 @@
2621public.distributionsourcepackage = SELECT, DELETE2621public.distributionsourcepackage = SELECT, DELETE
2622public.distroseries = SELECT2622public.distroseries = SELECT
2623public.emailaddress = SELECT2623public.emailaddress = SELECT
2624public.gitactivity = SELECT, DELETE
2625public.gitjob = SELECT, INSERT, DELETE
2626public.gitref = SELECT, DELETE
2627public.gitrepository = SELECT, UPDATE, DELETE
2628public.gitrule = SELECT, DELETE
2629public.gitrulegrant = SELECT, DELETE
2630public.gitsubscription = SELECT, DELETE
2624public.job = SELECT, INSERT, UPDATE, DELETE2631public.job = SELECT, INSERT, UPDATE, DELETE
2625public.person = SELECT2632public.person = SELECT
2626public.previewdiff = SELECT, DELETE2633public.previewdiff = SELECT, DELETE
26272634
=== 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 @@
1092 provides="lp.code.interfaces.gitjob.IGitRepositoryModifiedMailJobSource">1092 provides="lp.code.interfaces.gitjob.IGitRepositoryModifiedMailJobSource">
1093 <allow interface="lp.code.interfaces.gitjob.IGitRepositoryModifiedMailJobSource" />1093 <allow interface="lp.code.interfaces.gitjob.IGitRepositoryModifiedMailJobSource" />
1094 </securedutility>1094 </securedutility>
1095 <securedutility
1096 component="lp.code.model.gitjob.GitRepositoryDeleteJob"
1097 provides="lp.code.interfaces.gitjob.IGitRepositoryDeleteJobSource">
1098 <allow interface="lp.code.interfaces.gitjob.IGitRepositoryDeleteJobSource" />
1099 </securedutility>
1095 <class class="lp.code.model.gitjob.GitRefScanJob">1100 <class class="lp.code.model.gitjob.GitRefScanJob">
1096 <allow interface="lp.code.interfaces.gitjob.IGitJob" />1101 <allow interface="lp.code.interfaces.gitjob.IGitJob" />
1097 <allow interface="lp.code.interfaces.gitjob.IGitRefScanJob" />1102 <allow interface="lp.code.interfaces.gitjob.IGitRefScanJob" />
@@ -1104,6 +1109,10 @@
1104 <allow interface="lp.code.interfaces.gitjob.IGitJob" />1109 <allow interface="lp.code.interfaces.gitjob.IGitJob" />
1105 <allow interface="lp.code.interfaces.gitjob.IGitRepositoryModifiedMailJob" />1110 <allow interface="lp.code.interfaces.gitjob.IGitRepositoryModifiedMailJob" />
1106 </class>1111 </class>
1112 <class class="lp.code.model.gitjob.GitRepositoryDeleteJob">
1113 <allow interface="lp.code.interfaces.gitjob.IGitJob" />
1114 <allow interface="lp.code.interfaces.gitjob.IGitRepositoryDeleteJob" />
1115 </class>
11071116
1108 <lp:help-folder folder="help" name="+help-code" />1117 <lp:help-folder folder="help" name="+help-code" />
11091118
11101119
=== 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 @@
26 'GitGranteeType',26 'GitGranteeType',
27 'GitObjectType',27 'GitObjectType',
28 'GitPermissionType',28 'GitPermissionType',
29 'GitRepositoryDeletionStatus',
29 'GitRepositoryType',30 'GitRepositoryType',
30 'NON_CVS_RCS_TYPES',31 'NON_CVS_RCS_TYPES',
31 'RevisionControlSystems',32 'RevisionControlSystems',
@@ -158,6 +159,17 @@
158 """)159 """)
159160
160161
162class GitRepositoryDeletionStatus(DBEnumeratedType):
163 """Git Repository Deletion Status
164
165 The deletion status of a repository is used to track asynchronous
166 deletion.
167 """
168
169 ACTIVE = DBItem(0, "Active")
170 DELETING = DBItem(1, "Deleting")
171
172
161class GitObjectType(DBEnumeratedType):173class GitObjectType(DBEnumeratedType):
162 """Git Object Type174 """Git Object Type
163175
164176
=== 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 @@
9 'IGitJob',9 'IGitJob',
10 'IGitRefScanJob',10 'IGitRefScanJob',
11 'IGitRefScanJobSource',11 'IGitRefScanJobSource',
12 'IGitRepositoryDeleteJob',
13 'IGitRepositoryDeleteJobSource',
12 'IGitRepositoryModifiedMailJob',14 'IGitRepositoryModifiedMailJob',
13 'IGitRepositoryModifiedMailJobSource',15 'IGitRepositoryModifiedMailJobSource',
14 'IReclaimGitRepositorySpaceJob',16 'IReclaimGitRepositorySpaceJob',
@@ -20,7 +22,10 @@
20 Attribute,22 Attribute,
21 Interface,23 Interface,
22 )24 )
23from zope.schema import Text25from zope.schema import (
26 Int,
27 Text,
28 )
2429
25from lp import _30from lp import _
26from lp.code.interfaces.gitrepository import IGitRepository31from lp.code.interfaces.gitrepository import IGitRepository
@@ -93,3 +98,19 @@
93 :param repository_delta: An `IGitRepositoryDelta` describing the98 :param repository_delta: An `IGitRepositoryDelta` describing the
94 changes.99 changes.
95 """100 """
101
102
103class IGitRepositoryDeleteJob(IRunnableJob):
104 """A Job that deletes a repository from the database."""
105
106 repository_id = Int(title=_("The id of the repository to delete."))
107
108
109class IGitRepositoryDeleteJobSource(IJobSource):
110
111 def create(repository, requester):
112 """Delete a repository from the database.
113
114 :param repository: The `IGitRepository` to delete.
115 :param requester: The `IPerson` who requested the deletion.
116 """
96117
=== 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 @@
64 BranchSubscriptionDiffSize,64 BranchSubscriptionDiffSize,
65 BranchSubscriptionNotificationLevel,65 BranchSubscriptionNotificationLevel,
66 CodeReviewNotificationLevel,66 CodeReviewNotificationLevel,
67 GitRepositoryDeletionStatus,
67 GitRepositoryType,68 GitRepositoryType,
68 )69 )
69from lp.code.interfaces.defaultgit import ICanHasDefaultGitRepository70from lp.code.interfaces.defaultgit import ICanHasDefaultGitRepository
@@ -139,6 +140,11 @@
139 "The way this repository is hosted: directly on Launchpad, or "140 "The way this repository is hosted: directly on Launchpad, or "
140 "imported from somewhere else.")))141 "imported from somewhere else.")))
141142
143 deletion_status = exported(Choice(
144 title=_("Deletion status"), required=True, readonly=True,
145 vocabulary=GitRepositoryDeletionStatus,
146 description=_("The deletion status of this repository.")))
147
142 registrant = exported(PublicPersonChoice(148 registrant = exported(PublicPersonChoice(
143 title=_("Registrant"), required=True, readonly=True,149 title=_("Registrant"), required=True, readonly=True,
144 vocabulary="ValidPersonOrTeam",150 vocabulary="ValidPersonOrTeam",
145151
=== 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 @@
8 'GitJob',8 'GitJob',
9 'GitJobType',9 'GitJobType',
10 'GitRefScanJob',10 'GitRefScanJob',
11 'GitRepositoryDeleteJob',
11 'GitRepositoryModifiedMailJob',12 'GitRepositoryModifiedMailJob',
12 'ReclaimGitRepositorySpaceJob',13 'ReclaimGitRepositorySpaceJob',
13 ]14 ]
@@ -25,6 +26,7 @@
25 SQL,26 SQL,
26 Store,27 Store,
27 )28 )
29import transaction
28from zope.component import getUtility30from zope.component import getUtility
29from zope.interface import (31from zope.interface import (
30 implementer,32 implementer,
@@ -36,17 +38,22 @@
36from lp.code.enums import (38from lp.code.enums import (
37 GitActivityType,39 GitActivityType,
38 GitPermissionType,40 GitPermissionType,
41 GitRepositoryDeletionStatus,
39 )42 )
43from lp.code.errors import CannotDeleteGitRepository
40from lp.code.interfaces.githosting import IGitHostingClient44from lp.code.interfaces.githosting import IGitHostingClient
41from lp.code.interfaces.gitjob import (45from lp.code.interfaces.gitjob import (
42 IGitJob,46 IGitJob,
43 IGitRefScanJob,47 IGitRefScanJob,
44 IGitRefScanJobSource,48 IGitRefScanJobSource,
49 IGitRepositoryDeleteJob,
50 IGitRepositoryDeleteJobSource,
45 IGitRepositoryModifiedMailJob,51 IGitRepositoryModifiedMailJob,
46 IGitRepositoryModifiedMailJobSource,52 IGitRepositoryModifiedMailJobSource,
47 IReclaimGitRepositorySpaceJob,53 IReclaimGitRepositorySpaceJob,
48 IReclaimGitRepositorySpaceJobSource,54 IReclaimGitRepositorySpaceJobSource,
49 )55 )
56from lp.code.interfaces.gitlookup import IGitLookup
50from lp.code.interfaces.gitrule import describe_git_permissions57from lp.code.interfaces.gitrule import describe_git_permissions
51from lp.code.mail.branch import BranchMailer58from lp.code.mail.branch import BranchMailer
52from lp.registry.interfaces.person import IPersonSet59from lp.registry.interfaces.person import IPersonSet
@@ -99,6 +106,12 @@
99 modifications.106 modifications.
100 """)107 """)
101108
109 DELETE_REPOSITORY = DBItem(3, """
110 Delete repository
111
112 This job deletes a repository from the database.
113 """)
114
102115
103@implementer(IGitJob)116@implementer(IGitJob)
104class GitJob(StormBase):117class GitJob(StormBase):
@@ -405,3 +418,63 @@
405 def run(self):418 def run(self):
406 """See `IGitRepositoryModifiedMailJob`."""419 """See `IGitRepositoryModifiedMailJob`."""
407 self.getMailer().sendAll()420 self.getMailer().sendAll()
421
422
423@implementer(IGitRepositoryDeleteJob)
424@provider(IGitRepositoryDeleteJobSource)
425class GitRepositoryDeleteJob(GitJobDerived):
426 """A Job that deletes a repository from the database."""
427
428 class_job_type = GitJobType.DELETE_REPOSITORY
429
430 user_error_types = (CannotDeleteGitRepository,)
431
432 config = config.IGitRepositoryDeleteJobSource
433
434 def getOperationDescription(self):
435 return "deleting a repository"
436
437 @classmethod
438 def create(cls, repository, requester):
439 """See `IGitRepositoryDeleteJobSource`."""
440 metadata = {
441 "repository_id": repository.id,
442 "repository_name": repository.unique_name,
443 }
444 # The GitJob has a repository of None, because we don't want to
445 # delete this job while trying to delete the repository.
446 git_job = GitJob(
447 None, cls.class_job_type, metadata, requester=requester)
448 job = cls(git_job)
449 job.celeryRunOnCommit()
450 return job
451
452 @property
453 def repository_id(self):
454 return self.metadata["repository_id"]
455
456 def run(self):
457 """See `IGitRepositoryDeleteJob`."""
458 repository = getUtility(IGitLookup).get(self.repository_id)
459 if repository is None:
460 log.info(
461 "Skipping repository %s because it has already been deleted." %
462 self._cached_repository_name)
463 elif (repository.deletion_status !=
464 GitRepositoryDeletionStatus.DELETING):
465 log.warning(
466 "Skipping repository %s because its deletion status is not "
467 "DELETING." % self._cached_repository_name)
468 else:
469 try:
470 repository.destroySelf(break_references=True)
471 except CannotDeleteGitRepository:
472 # Set the deletion status back to ACTIVE so that it's
473 # possible to try again. We don't attempt to undo the
474 # renaming at the moment. Do this in its own transaction
475 # since the job runner will abort the transaction.
476 transaction.abort()
477 removeSecurityProxy(repository).deletion_status = (
478 GitRepositoryDeletionStatus.ACTIVE)
479 transaction.commit()
480 raise
408481
=== 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 @@
91 GitGranteeType,91 GitGranteeType,
92 GitObjectType,92 GitObjectType,
93 GitPermissionType,93 GitPermissionType,
94 GitRepositoryDeletionStatus,
94 GitRepositoryType,95 GitRepositoryType,
95 )96 )
96from lp.code.errors import (97from lp.code.errors import (
@@ -282,6 +283,23 @@
282 repository_type = EnumCol(283 repository_type = EnumCol(
283 dbName='repository_type', enum=GitRepositoryType, notNull=True)284 dbName='repository_type', enum=GitRepositoryType, notNull=True)
284285
286 _deletion_status = EnumCol(
287 dbName='deletion_status', enum=GitRepositoryDeletionStatus,
288 default=GitRepositoryDeletionStatus.ACTIVE)
289
290 @property
291 def deletion_status(self):
292 # XXX cjwatson 2019-03-19: Remove once this column has been
293 # backfilled.
294 if self._deletion_status is None:
295 return GitRepositoryDeletionStatus.ACTIVE
296 else:
297 return self._deletion_status
298
299 @deletion_status.setter
300 def deletion_status(self, value):
301 self._deletion_status = value
302
285 registrant_id = Int(name='registrant', allow_none=False)303 registrant_id = Int(name='registrant', allow_none=False)
286 registrant = Reference(registrant_id, 'Person.id')304 registrant = Reference(registrant_id, 'Person.id')
287305
288306
=== 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 @@
13 )13 )
14import hashlib14import hashlib
1515
16from fixtures import (
17 FakeLogger,
18 MockPatch,
19 )
16from lazr.lifecycle.snapshot import Snapshot20from lazr.lifecycle.snapshot import Snapshot
17import pytz21import pytz
18from testtools.matchers import (22from testtools.matchers import (
@@ -23,6 +27,7 @@
23 MatchesStructure,27 MatchesStructure,
24 )28 )
25import transaction29import transaction
30from zope.component import getUtility
26from zope.interface import providedBy31from zope.interface import providedBy
27from zope.security.proxy import removeSecurityProxy32from zope.security.proxy import removeSecurityProxy
2833
@@ -30,6 +35,7 @@
30from lp.code.enums import (35from lp.code.enums import (
31 GitGranteeType,36 GitGranteeType,
32 GitObjectType,37 GitObjectType,
38 GitRepositoryDeletionStatus,
33 )39 )
34from lp.code.interfaces.branchmergeproposal import (40from lp.code.interfaces.branchmergeproposal import (
35 BRANCH_MERGE_PROPOSAL_WEBHOOKS_FEATURE_FLAG,41 BRANCH_MERGE_PROPOSAL_WEBHOOKS_FEATURE_FLAG,
@@ -37,8 +43,11 @@
37from lp.code.interfaces.gitjob import (43from lp.code.interfaces.gitjob import (
38 IGitJob,44 IGitJob,
39 IGitRefScanJob,45 IGitRefScanJob,
46 IGitRepositoryDeleteJob,
47 IGitRepositoryDeleteJobSource,
40 IReclaimGitRepositorySpaceJob,48 IReclaimGitRepositorySpaceJob,
41 )49 )
50from lp.code.interfaces.gitlookup import IGitLookup
42from lp.code.model.gitjob import (51from lp.code.model.gitjob import (
43 describe_repository_delta,52 describe_repository_delta,
44 GitJob,53 GitJob,
@@ -52,6 +61,7 @@
52from lp.services.database.constants import UTC_NOW61from lp.services.database.constants import UTC_NOW
53from lp.services.features.testing import FeatureFixture62from lp.services.features.testing import FeatureFixture
54from lp.services.job.runner import JobRunner63from lp.services.job.runner import JobRunner
64from lp.services.mail.sendmail import format_address_for_person
55from lp.services.utils import seconds_since_epoch65from lp.services.utils import seconds_since_epoch
56from lp.services.webapp import canonical_url66from lp.services.webapp import canonical_url
57from lp.services.webapp.snapshot import notify_modified67from lp.services.webapp.snapshot import notify_modified
@@ -65,6 +75,7 @@
65 DatabaseFunctionalLayer,75 DatabaseFunctionalLayer,
66 ZopelessDatabaseLayer,76 ZopelessDatabaseLayer,
67 )77 )
78from lp.testing.mail_helpers import pop_notifications
6879
6980
70class TestGitJob(TestCaseWithFactory):81class TestGitJob(TestCaseWithFactory):
@@ -473,5 +484,98 @@
473 snapshot, repository)484 snapshot, repository)
474485
475486
487class TestGitRepositoryDeleteJob(TestCaseWithFactory):
488
489 layer = ZopelessDatabaseLayer
490
491 def test_providesInterface(self):
492 repository = self.factory.makeGitRepository()
493 requester = repository.registrant
494 job = getUtility(IGitRepositoryDeleteJobSource).create(
495 repository, requester)
496 self.assertProvides(job, IGitRepositoryDeleteJob)
497
498 def test_run(self):
499 repository = self.factory.makeGitRepository()
500 repository_id = repository.id
501 requester = repository.registrant
502 job = getUtility(IGitRepositoryDeleteJobSource).create(
503 repository, requester)
504 removeSecurityProxy(repository).deletion_status = (
505 GitRepositoryDeletionStatus.DELETING)
506 logger = self.useFixture(FakeLogger())
507 with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
508 job.run()
509 self.assertEqual('', logger.output)
510 self.assertIsNone(getUtility(IGitLookup).get(repository_id))
511
512 def test_already_deleted(self):
513 repository = self.factory.makeGitRepository()
514 repository_name = repository.unique_name
515 requester = repository.registrant
516 job = getUtility(IGitRepositoryDeleteJobSource).create(
517 repository, requester)
518 repository.destroySelf()
519 logger = self.useFixture(FakeLogger())
520 with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
521 job.run()
522 self.assertEqual(
523 "Skipping repository %s because it has already been "
524 "deleted.\n" % repository_name,
525 logger.output)
526
527 def test_not_deleting(self):
528 # The job skips repositories that aren't DELETING. This shouldn't
529 # be possible in practice, but is a guard against accidents.
530 repository = self.factory.makeGitRepository()
531 repository_id = repository.id
532 repository_name = repository.unique_name
533 self.assertNotEqual(
534 GitRepositoryDeletionStatus.DELETING, repository.deletion_status)
535 requester = repository.registrant
536 job = getUtility(IGitRepositoryDeleteJobSource).create(
537 repository, requester)
538 logger = self.useFixture(FakeLogger())
539 with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
540 job.run()
541 self.assertEqual(
542 "Skipping repository %s because its deletion status is not "
543 "DELETING.\n" % repository_name,
544 logger.output)
545 self.assertEqual(repository, getUtility(IGitLookup).get(repository_id))
546
547 def test_error(self):
548 # If deleting the repository fails, an error message is sent to the
549 # requester and the deletion status is set back to ACTIVE. This
550 # can't normally happen because the job always breaks references to
551 # the repository, so we patch in a failure to allow testing the
552 # error path.
553 repository = self.factory.makeGitRepository()
554 repository_id = repository.id
555 requester = repository.registrant
556 job = getUtility(IGitRepositoryDeleteJobSource).create(
557 repository, requester)
558 removeSecurityProxy(repository).deletion_status = (
559 GitRepositoryDeletionStatus.DELETING)
560 logger = self.useFixture(FakeLogger())
561 with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
562 with MockPatch(
563 "lp.code.model.gitrepository.GitRepository.canBeDeleted",
564 return_value=False):
565 JobRunner([job]).runJobHandleError(job)
566 self.assertIn(
567 "failed with user error CannotDeleteGitRepository", logger.output)
568 self.assertEqual(repository, getUtility(IGitLookup).get(repository_id))
569 self.assertEqual(
570 GitRepositoryDeletionStatus.ACTIVE, repository.deletion_status)
571 self.assertEqual([], self.oopses)
572 [mail] = pop_notifications()
573 self.assertEqual(format_address_for_person(requester), mail["to"])
574 self.assertEqual(
575 "Launchpad error while deleting a repository", mail["subject"])
576 self.assertIn(
577 "Cannot delete Git repository", mail.get_payload(decode=True))
578
579
476# XXX cjwatson 2015-03-12: We should test that the jobs work via Celery too,580# XXX cjwatson 2015-03-12: We should test that the jobs work via Celery too,
477# but that isn't feasible until we have a proper turnip fixture.581# but that isn't feasible until we have a proper turnip fixture.
478582
=== 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 @@
638 self.repository.canBeDeleted(),638 self.repository.canBeDeleted(),
639 "A newly created repository should be able to be deleted.")639 "A newly created repository should be able to be deleted.")
640 repository_id = self.repository.id640 repository_id = self.repository.id
641 self.repository.destroySelf()641 with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
642 self.repository.destroySelf()
642 self.assertIsNone(643 self.assertIsNone(
643 getUtility(IGitLookup).get(repository_id),644 getUtility(IGitLookup).get(repository_id),
644 "The repository has not been deleted.")645 "The repository has not been deleted.")
@@ -650,7 +651,8 @@
650 CodeReviewNotificationLevel.NOEMAIL, self.user)651 CodeReviewNotificationLevel.NOEMAIL, self.user)
651 self.assertTrue(self.repository.canBeDeleted())652 self.assertTrue(self.repository.canBeDeleted())
652 repository_id = self.repository.id653 repository_id = self.repository.id
653 self.repository.destroySelf()654 with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
655 self.repository.destroySelf()
654 self.assertIsNone(656 self.assertIsNone(
655 getUtility(IGitLookup).get(repository_id),657 getUtility(IGitLookup).get(repository_id),
656 "The repository has not been deleted.")658 "The repository has not been deleted.")
@@ -665,7 +667,8 @@
665 CodeReviewNotificationLevel.NOEMAIL, self.user)667 CodeReviewNotificationLevel.NOEMAIL, self.user)
666 self.assertTrue(self.repository.canBeDeleted())668 self.assertTrue(self.repository.canBeDeleted())
667 repository_id = self.repository.id669 repository_id = self.repository.id
668 self.repository.destroySelf()670 with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
671 self.repository.destroySelf()
669 self.assertIsNone(672 self.assertIsNone(
670 getUtility(IGitLookup).get(repository_id),673 getUtility(IGitLookup).get(repository_id),
671 "The repository has not been deleted.")674 "The repository has not been deleted.")
@@ -687,8 +690,9 @@
687 self.assertFalse(690 self.assertFalse(
688 self.repository.canBeDeleted(),691 self.repository.canBeDeleted(),
689 "A repository with a landing target is not deletable.")692 "A repository with a landing target is not deletable.")
690 self.assertRaises(693 with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
691 CannotDeleteGitRepository, self.repository.destroySelf)694 self.assertRaises(
695 CannotDeleteGitRepository, self.repository.destroySelf)
692696
693 def test_landing_candidate_disables_deletion(self):697 def test_landing_candidate_disables_deletion(self):
694 # A repository with a landing candidate cannot be deleted.698 # A repository with a landing candidate cannot be deleted.
@@ -698,8 +702,9 @@
698 self.assertFalse(702 self.assertFalse(
699 self.repository.canBeDeleted(),703 self.repository.canBeDeleted(),
700 "A repository with a landing candidate is not deletable.")704 "A repository with a landing candidate is not deletable.")
701 self.assertRaises(705 with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
702 CannotDeleteGitRepository, self.repository.destroySelf)706 self.assertRaises(
707 CannotDeleteGitRepository, self.repository.destroySelf)
703708
704 def test_prerequisite_repository_disables_deletion(self):709 def test_prerequisite_repository_disables_deletion(self):
705 # A repository that is a prerequisite repository cannot be deleted.710 # A repository that is a prerequisite repository cannot be deleted.
@@ -711,14 +716,16 @@
711 self.assertFalse(716 self.assertFalse(
712 self.repository.canBeDeleted(),717 self.repository.canBeDeleted(),
713 "A repository with a prerequisite target is not deletable.")718 "A repository with a prerequisite target is not deletable.")
714 self.assertRaises(719 with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
715 CannotDeleteGitRepository, self.repository.destroySelf)720 self.assertRaises(
721 CannotDeleteGitRepository, self.repository.destroySelf)
716722
717 def test_related_GitJobs_deleted(self):723 def test_related_GitJobs_deleted(self):
718 # A repository with an associated job will delete those jobs.724 # A repository with an associated job will delete those jobs.
719 GitAPI(None, None).notify(self.repository.getInternalPath())725 GitAPI(None, None).notify(self.repository.getInternalPath())
720 store = Store.of(self.repository)726 store = Store.of(self.repository)
721 self.repository.destroySelf()727 with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
728 self.repository.destroySelf()
722 # Need to commit the transaction to fire off the constraint checks.729 # Need to commit the transaction to fire off the constraint checks.
723 transaction.commit()730 transaction.commit()
724 jobs = store.find(GitJob, GitJob.job_type == GitJobType.REF_SCAN)731 jobs = store.find(GitJob, GitJob.job_type == GitJobType.REF_SCAN)
@@ -729,7 +736,8 @@
729 # to remove the repository from disk as well.736 # to remove the repository from disk as well.
730 repository_path = self.repository.getInternalPath()737 repository_path = self.repository.getInternalPath()
731 store = Store.of(self.repository)738 store = Store.of(self.repository)
732 self.repository.destroySelf()739 with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
740 self.repository.destroySelf()
733 jobs = store.find(741 jobs = store.find(
734 GitJob,742 GitJob,
735 GitJob.job_type == GitJobType.RECLAIM_REPOSITORY_SPACE)743 GitJob.job_type == GitJobType.RECLAIM_REPOSITORY_SPACE)
@@ -742,14 +750,16 @@
742 # If repository is a base_git_repository in a recipe, it is deleted.750 # If repository is a base_git_repository in a recipe, it is deleted.
743 recipe = self.factory.makeSourcePackageRecipe(751 recipe = self.factory.makeSourcePackageRecipe(
744 branches=self.factory.makeGitRefs(owner=self.user))752 branches=self.factory.makeGitRefs(owner=self.user))
745 recipe.base_git_repository.destroySelf(break_references=True)753 with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
754 recipe.base_git_repository.destroySelf(break_references=True)
746755
747 def test_destroySelf_with_SourcePackageRecipe_as_non_base(self):756 def test_destroySelf_with_SourcePackageRecipe_as_non_base(self):
748 # If repository is referred to by a recipe, it is deleted.757 # If repository is referred to by a recipe, it is deleted.
749 [ref1] = self.factory.makeGitRefs(owner=self.user)758 [ref1] = self.factory.makeGitRefs(owner=self.user)
750 [ref2] = self.factory.makeGitRefs(owner=self.user)759 [ref2] = self.factory.makeGitRefs(owner=self.user)
751 self.factory.makeSourcePackageRecipe(branches=[ref1, ref2])760 self.factory.makeSourcePackageRecipe(branches=[ref1, ref2])
752 ref2.repository.destroySelf(break_references=True)761 with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
762 ref2.repository.destroySelf(break_references=True)
753763
754 def test_destroySelf_with_inline_comments_draft(self):764 def test_destroySelf_with_inline_comments_draft(self):
755 # Draft inline comments related to a deleted repository (source or765 # Draft inline comments related to a deleted repository (source or
@@ -763,7 +773,8 @@
763 previewdiff_id=preview_diff.id,773 previewdiff_id=preview_diff.id,
764 person=self.user,774 person=self.user,
765 comments={"1": "Should vanish."})775 comments={"1": "Should vanish."})
766 self.repository.destroySelf(break_references=True)776 with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
777 self.repository.destroySelf(break_references=True)
767778
768 def test_destroySelf_with_inline_comments_published(self):779 def test_destroySelf_with_inline_comments_published(self):
769 # Published inline comments related to a deleted repository (source780 # Published inline comments related to a deleted repository (source
@@ -779,12 +790,14 @@
779 previewdiff_id=preview_diff.id,790 previewdiff_id=preview_diff.id,
780 inline_comments={"1": "Must disappear."},791 inline_comments={"1": "Must disappear."},
781 )792 )
782 self.repository.destroySelf(break_references=True)793 with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
794 self.repository.destroySelf(break_references=True)
783795
784 def test_related_webhooks_deleted(self):796 def test_related_webhooks_deleted(self):
785 webhook = self.factory.makeWebhook(target=self.repository)797 webhook = self.factory.makeWebhook(target=self.repository)
786 webhook.ping()798 webhook.ping()
787 self.repository.destroySelf()799 with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
800 self.repository.destroySelf()
788 transaction.commit()801 transaction.commit()
789 self.assertRaises(LostObjectError, getattr, webhook, 'target')802 self.assertRaises(LostObjectError, getattr, webhook, 'target')
790803
@@ -796,7 +809,8 @@
796 activities = store.find(809 activities = store.find(
797 GitActivity, GitActivity.repository_id == repository_id)810 GitActivity, GitActivity.repository_id == repository_id)
798 self.assertNotEqual([], list(activities))811 self.assertNotEqual([], list(activities))
799 self.repository.destroySelf()812 with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
813 self.repository.destroySelf()
800 transaction.commit()814 transaction.commit()
801 self.assertRaises(LostObjectError, getattr, grant, 'rule')815 self.assertRaises(LostObjectError, getattr, grant, 'rule')
802 self.assertRaises(LostObjectError, getattr, rule, 'repository')816 self.assertRaises(LostObjectError, getattr, rule, 'repository')
@@ -895,7 +909,8 @@
895 merge_proposal1, merge_proposal2 = self.makeMergeProposals()909 merge_proposal1, merge_proposal2 = self.makeMergeProposals()
896 merge_proposal1_id = merge_proposal1.id910 merge_proposal1_id = merge_proposal1.id
897 BranchMergeProposal.get(merge_proposal1_id)911 BranchMergeProposal.get(merge_proposal1_id)
898 self.repository.destroySelf(break_references=True)912 with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
913 self.repository.destroySelf(break_references=True)
899 self.assertRaises(914 self.assertRaises(
900 SQLObjectNotFound, BranchMergeProposal.get, merge_proposal1_id)915 SQLObjectNotFound, BranchMergeProposal.get, merge_proposal1_id)
901916
@@ -905,8 +920,9 @@
905 merge_proposal1, merge_proposal2 = self.makeMergeProposals()920 merge_proposal1, merge_proposal2 = self.makeMergeProposals()
906 merge_proposal1_id = merge_proposal1.id921 merge_proposal1_id = merge_proposal1.id
907 BranchMergeProposal.get(merge_proposal1_id)922 BranchMergeProposal.get(merge_proposal1_id)
908 merge_proposal1.target_git_repository.destroySelf(923 with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
909 break_references=True)924 merge_proposal1.target_git_repository.destroySelf(
925 break_references=True)
910 self.assertRaises(SQLObjectNotFound,926 self.assertRaises(SQLObjectNotFound,
911 BranchMergeProposal.get, merge_proposal1_id)927 BranchMergeProposal.get, merge_proposal1_id)
912928
@@ -914,8 +930,9 @@
914 # Merge proposal prerequisite repositories can be deleted with930 # Merge proposal prerequisite repositories can be deleted with
915 # break_references.931 # break_references.
916 merge_proposal1, merge_proposal2 = self.makeMergeProposals()932 merge_proposal1, merge_proposal2 = self.makeMergeProposals()
917 merge_proposal1.prerequisite_git_repository.destroySelf(933 with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
918 break_references=True)934 merge_proposal1.prerequisite_git_repository.destroySelf(
935 break_references=True)
919 self.assertIsNone(merge_proposal1.prerequisite_git_repository)936 self.assertIsNone(merge_proposal1.prerequisite_git_repository)
920937
921 def test_delete_source_CodeReviewComment(self):938 def test_delete_source_CodeReviewComment(self):
@@ -923,7 +940,8 @@
923 comment = self.factory.makeCodeReviewComment(git=True)940 comment = self.factory.makeCodeReviewComment(git=True)
924 comment_id = comment.id941 comment_id = comment.id
925 repository = comment.branch_merge_proposal.source_git_repository942 repository = comment.branch_merge_proposal.source_git_repository
926 repository.destroySelf(break_references=True)943 with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
944 repository.destroySelf(break_references=True)
927 self.assertRaises(945 self.assertRaises(
928 SQLObjectNotFound, CodeReviewComment.get, comment_id)946 SQLObjectNotFound, CodeReviewComment.get, comment_id)
929947
@@ -932,7 +950,8 @@
932 comment = self.factory.makeCodeReviewComment(git=True)950 comment = self.factory.makeCodeReviewComment(git=True)
933 comment_id = comment.id951 comment_id = comment.id
934 repository = comment.branch_merge_proposal.target_git_repository952 repository = comment.branch_merge_proposal.target_git_repository
935 repository.destroySelf(break_references=True)953 with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
954 repository.destroySelf(break_references=True)
936 self.assertRaises(955 self.assertRaises(
937 SQLObjectNotFound, CodeReviewComment.get, comment_id)956 SQLObjectNotFound, CodeReviewComment.get, comment_id)
938957
@@ -941,14 +960,18 @@
941 merge_proposal = self.factory.makeBranchMergeProposalForGit()960 merge_proposal = self.factory.makeBranchMergeProposalForGit()
942 merge_proposal.nominateReviewer(961 merge_proposal.nominateReviewer(
943 self.factory.makePerson(), self.factory.makePerson())962 self.factory.makePerson(), self.factory.makePerson())
944 merge_proposal.source_git_repository.destroySelf(break_references=True)963 with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
964 merge_proposal.source_git_repository.destroySelf(
965 break_references=True)
945966
946 def test_targetBranchWithCodeReviewVoteReference(self):967 def test_targetBranchWithCodeReviewVoteReference(self):
947 # break_references handles CodeReviewVoteReference target repository.968 # break_references handles CodeReviewVoteReference target repository.
948 merge_proposal = self.factory.makeBranchMergeProposalForGit()969 merge_proposal = self.factory.makeBranchMergeProposalForGit()
949 merge_proposal.nominateReviewer(970 merge_proposal.nominateReviewer(
950 self.factory.makePerson(), self.factory.makePerson())971 self.factory.makePerson(), self.factory.makePerson())
951 merge_proposal.target_git_repository.destroySelf(break_references=True)972 with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
973 merge_proposal.target_git_repository.destroySelf(
974 break_references=True)
952975
953 def test_code_import_requirements(self):976 def test_code_import_requirements(self):
954 # Code imports are not included explicitly in deletion requirements.977 # Code imports are not included explicitly in deletion requirements.
@@ -967,7 +990,8 @@
967 code_import = self.factory.makeCodeImport(990 code_import = self.factory.makeCodeImport(
968 target_rcs_type=TargetRevisionControlSystems.GIT)991 target_rcs_type=TargetRevisionControlSystems.GIT)
969 code_import_id = code_import.id992 code_import_id = code_import.id
970 code_import.git_repository.destroySelf(break_references=True)993 with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
994 code_import.git_repository.destroySelf(break_references=True)
971 self.assertRaises(995 self.assertRaises(
972 NotFoundError, getUtility(ICodeImportSet).get, code_import_id)996 NotFoundError, getUtility(ICodeImportSet).get, code_import_id)
973997
@@ -988,7 +1012,8 @@
988 repository=repository, paths=["refs/heads/1", "refs/heads/2"])1012 repository=repository, paths=["refs/heads/1", "refs/heads/2"])
989 snap1 = self.factory.makeSnap(git_ref=ref1)1013 snap1 = self.factory.makeSnap(git_ref=ref1)
990 snap2 = self.factory.makeSnap(git_ref=ref2)1014 snap2 = self.factory.makeSnap(git_ref=ref2)
991 repository.destroySelf(break_references=True)1015 with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
1016 repository.destroySelf(break_references=True)
992 transaction.commit()1017 transaction.commit()
993 self.assertIsNone(snap1.git_repository)1018 self.assertIsNone(snap1.git_repository)
994 self.assertIsNone(snap1.git_path)1019 self.assertIsNone(snap1.git_path)
@@ -1001,7 +1026,8 @@
1001 merge_proposal = removeSecurityProxy(self.makeMergeProposals()[0])1026 merge_proposal = removeSecurityProxy(self.makeMergeProposals()[0])
1002 with person_logged_in(1027 with person_logged_in(
1003 merge_proposal.prerequisite_git_repository.owner):1028 merge_proposal.prerequisite_git_repository.owner):
1004 ClearPrerequisiteRepository(merge_proposal)()1029 with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
1030 ClearPrerequisiteRepository(merge_proposal)()
1005 self.assertIsNone(merge_proposal.prerequisite_git_repository)1031 self.assertIsNone(merge_proposal.prerequisite_git_repository)
10061032
1007 def test_DeletionOperation(self):1033 def test_DeletionOperation(self):
@@ -1012,8 +1038,9 @@
1012 # DeletionCallable must invoke the callable.1038 # DeletionCallable must invoke the callable.
1013 merge_proposal = self.factory.makeBranchMergeProposalForGit()1039 merge_proposal = self.factory.makeBranchMergeProposalForGit()
1014 merge_proposal_id = merge_proposal.id1040 merge_proposal_id = merge_proposal.id
1015 DeletionCallable(1041 with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
1016 merge_proposal, "blah", merge_proposal.deleteProposal)()1042 DeletionCallable(
1043 merge_proposal, "blah", merge_proposal.deleteProposal)()
1017 self.assertRaises(1044 self.assertRaises(
1018 SQLObjectNotFound, BranchMergeProposal.get, merge_proposal_id)1045 SQLObjectNotFound, BranchMergeProposal.get, merge_proposal_id)
10191046
@@ -1023,7 +1050,8 @@
1023 code_import = self.factory.makeCodeImport(1050 code_import = self.factory.makeCodeImport(
1024 target_rcs_type=TargetRevisionControlSystems.GIT)1051 target_rcs_type=TargetRevisionControlSystems.GIT)
1025 code_import_id = code_import.id1052 code_import_id = code_import.id
1026 DeleteCodeImport(code_import)()1053 with dbuser(config.IGitRepositoryDeleteJobSource.dbuser):
1054 DeleteCodeImport(code_import)()
1027 self.assertRaises(1055 self.assertRaises(
1028 NotFoundError, getUtility(ICodeImportSet).get, code_import_id)1056 NotFoundError, getUtility(ICodeImportSet).get, code_import_id)
10291057
10301058
=== 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 @@
1871 IBranchModifiedMailJobSource,1871 IBranchModifiedMailJobSource,
1872 ICommercialExpiredJobSource,1872 ICommercialExpiredJobSource,
1873 IExpiringMembershipNotificationJobSource,1873 IExpiringMembershipNotificationJobSource,
1874 IGitRepositoryDeleteJobSource,
1874 IGitRepositoryModifiedMailJobSource,1875 IGitRepositoryModifiedMailJobSource,
1875 IMembershipNotificationJobSource,1876 IMembershipNotificationJobSource,
1876 IPackageUploadNotificationJobSource,1877 IPackageUploadNotificationJobSource,
@@ -1931,6 +1932,11 @@
1931module: lp.code.interfaces.gitjob1932module: lp.code.interfaces.gitjob
1932dbuser: branchscanner1933dbuser: branchscanner
19331934
1935[IGitRepositoryDeleteJobSource]
1936module: lp.code.interfaces.gitjob
1937dbuser: branch-delete-job
1938crontab_group: MAIN
1939
1934[IGitRepositoryModifiedMailJobSource]1940[IGitRepositoryModifiedMailJobSource]
1935module: lp.code.interfaces.gitjob1941module: lp.code.interfaces.gitjob
1936dbuser: send-branch-mail1942dbuser: send-branch-mail