Merge lp:~cjwatson/launchpad/git-repository-delete-job into lp:launchpad
- git-repository-delete-job
- Merge into devel
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 | ||||
Related bugs: |
|
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.
Description of the change
To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) wrote : | # |
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
1 | === modified file 'database/schema/security.cfg' |
2 | --- database/schema/security.cfg 2019-03-21 16:22:19 +0000 |
3 | +++ database/schema/security.cfg 2019-03-21 16:22:19 +0000 |
4 | @@ -2621,6 +2621,13 @@ |
5 | public.distributionsourcepackage = SELECT, DELETE |
6 | public.distroseries = SELECT |
7 | public.emailaddress = SELECT |
8 | +public.gitactivity = SELECT, DELETE |
9 | +public.gitjob = SELECT, INSERT, DELETE |
10 | +public.gitref = SELECT, DELETE |
11 | +public.gitrepository = SELECT, UPDATE, DELETE |
12 | +public.gitrule = SELECT, DELETE |
13 | +public.gitrulegrant = SELECT, DELETE |
14 | +public.gitsubscription = SELECT, DELETE |
15 | public.job = SELECT, INSERT, UPDATE, DELETE |
16 | public.person = SELECT |
17 | public.previewdiff = SELECT, DELETE |
18 | |
19 | === modified file 'lib/lp/code/configure.zcml' |
20 | --- lib/lp/code/configure.zcml 2019-03-21 16:22:19 +0000 |
21 | +++ lib/lp/code/configure.zcml 2019-03-21 16:22:19 +0000 |
22 | @@ -1092,6 +1092,11 @@ |
23 | provides="lp.code.interfaces.gitjob.IGitRepositoryModifiedMailJobSource"> |
24 | <allow interface="lp.code.interfaces.gitjob.IGitRepositoryModifiedMailJobSource" /> |
25 | </securedutility> |
26 | + <securedutility |
27 | + component="lp.code.model.gitjob.GitRepositoryDeleteJob" |
28 | + provides="lp.code.interfaces.gitjob.IGitRepositoryDeleteJobSource"> |
29 | + <allow interface="lp.code.interfaces.gitjob.IGitRepositoryDeleteJobSource" /> |
30 | + </securedutility> |
31 | <class class="lp.code.model.gitjob.GitRefScanJob"> |
32 | <allow interface="lp.code.interfaces.gitjob.IGitJob" /> |
33 | <allow interface="lp.code.interfaces.gitjob.IGitRefScanJob" /> |
34 | @@ -1104,6 +1109,10 @@ |
35 | <allow interface="lp.code.interfaces.gitjob.IGitJob" /> |
36 | <allow interface="lp.code.interfaces.gitjob.IGitRepositoryModifiedMailJob" /> |
37 | </class> |
38 | + <class class="lp.code.model.gitjob.GitRepositoryDeleteJob"> |
39 | + <allow interface="lp.code.interfaces.gitjob.IGitJob" /> |
40 | + <allow interface="lp.code.interfaces.gitjob.IGitRepositoryDeleteJob" /> |
41 | + </class> |
42 | |
43 | <lp:help-folder folder="help" name="+help-code" /> |
44 | |
45 | |
46 | === modified file 'lib/lp/code/enums.py' |
47 | --- lib/lp/code/enums.py 2019-03-21 16:22:19 +0000 |
48 | +++ lib/lp/code/enums.py 2019-03-21 16:22:19 +0000 |
49 | @@ -26,6 +26,7 @@ |
50 | 'GitGranteeType', |
51 | 'GitObjectType', |
52 | 'GitPermissionType', |
53 | + 'GitRepositoryDeletionStatus', |
54 | 'GitRepositoryType', |
55 | 'NON_CVS_RCS_TYPES', |
56 | 'RevisionControlSystems', |
57 | @@ -158,6 +159,17 @@ |
58 | """) |
59 | |
60 | |
61 | +class GitRepositoryDeletionStatus(DBEnumeratedType): |
62 | + """Git Repository Deletion Status |
63 | + |
64 | + The deletion status of a repository is used to track asynchronous |
65 | + deletion. |
66 | + """ |
67 | + |
68 | + ACTIVE = DBItem(0, "Active") |
69 | + DELETING = DBItem(1, "Deleting") |
70 | + |
71 | + |
72 | class GitObjectType(DBEnumeratedType): |
73 | """Git Object Type |
74 | |
75 | |
76 | === modified file 'lib/lp/code/interfaces/gitjob.py' |
77 | --- lib/lp/code/interfaces/gitjob.py 2015-09-01 17:10:46 +0000 |
78 | +++ lib/lp/code/interfaces/gitjob.py 2019-03-21 16:22:19 +0000 |
79 | @@ -9,6 +9,8 @@ |
80 | 'IGitJob', |
81 | 'IGitRefScanJob', |
82 | 'IGitRefScanJobSource', |
83 | + 'IGitRepositoryDeleteJob', |
84 | + 'IGitRepositoryDeleteJobSource', |
85 | 'IGitRepositoryModifiedMailJob', |
86 | 'IGitRepositoryModifiedMailJobSource', |
87 | 'IReclaimGitRepositorySpaceJob', |
88 | @@ -20,7 +22,10 @@ |
89 | Attribute, |
90 | Interface, |
91 | ) |
92 | -from zope.schema import Text |
93 | +from zope.schema import ( |
94 | + Int, |
95 | + Text, |
96 | + ) |
97 | |
98 | from lp import _ |
99 | from lp.code.interfaces.gitrepository import IGitRepository |
100 | @@ -93,3 +98,19 @@ |
101 | :param repository_delta: An `IGitRepositoryDelta` describing the |
102 | changes. |
103 | """ |
104 | + |
105 | + |
106 | +class IGitRepositoryDeleteJob(IRunnableJob): |
107 | + """A Job that deletes a repository from the database.""" |
108 | + |
109 | + repository_id = Int(title=_("The id of the repository to delete.")) |
110 | + |
111 | + |
112 | +class IGitRepositoryDeleteJobSource(IJobSource): |
113 | + |
114 | + def create(repository, requester): |
115 | + """Delete a repository from the database. |
116 | + |
117 | + :param repository: The `IGitRepository` to delete. |
118 | + :param requester: The `IPerson` who requested the deletion. |
119 | + """ |
120 | |
121 | === modified file 'lib/lp/code/interfaces/gitrepository.py' |
122 | --- lib/lp/code/interfaces/gitrepository.py 2019-01-28 17:19:44 +0000 |
123 | +++ lib/lp/code/interfaces/gitrepository.py 2019-03-21 16:22:19 +0000 |
124 | @@ -64,6 +64,7 @@ |
125 | BranchSubscriptionDiffSize, |
126 | BranchSubscriptionNotificationLevel, |
127 | CodeReviewNotificationLevel, |
128 | + GitRepositoryDeletionStatus, |
129 | GitRepositoryType, |
130 | ) |
131 | from lp.code.interfaces.defaultgit import ICanHasDefaultGitRepository |
132 | @@ -139,6 +140,11 @@ |
133 | "The way this repository is hosted: directly on Launchpad, or " |
134 | "imported from somewhere else."))) |
135 | |
136 | + deletion_status = exported(Choice( |
137 | + title=_("Deletion status"), required=True, readonly=True, |
138 | + vocabulary=GitRepositoryDeletionStatus, |
139 | + description=_("The deletion status of this repository."))) |
140 | + |
141 | registrant = exported(PublicPersonChoice( |
142 | title=_("Registrant"), required=True, readonly=True, |
143 | vocabulary="ValidPersonOrTeam", |
144 | |
145 | === modified file 'lib/lp/code/model/gitjob.py' |
146 | --- lib/lp/code/model/gitjob.py 2018-10-22 00:37:07 +0000 |
147 | +++ lib/lp/code/model/gitjob.py 2019-03-21 16:22:19 +0000 |
148 | @@ -8,6 +8,7 @@ |
149 | 'GitJob', |
150 | 'GitJobType', |
151 | 'GitRefScanJob', |
152 | + 'GitRepositoryDeleteJob', |
153 | 'GitRepositoryModifiedMailJob', |
154 | 'ReclaimGitRepositorySpaceJob', |
155 | ] |
156 | @@ -25,6 +26,7 @@ |
157 | SQL, |
158 | Store, |
159 | ) |
160 | +import transaction |
161 | from zope.component import getUtility |
162 | from zope.interface import ( |
163 | implementer, |
164 | @@ -36,17 +38,22 @@ |
165 | from lp.code.enums import ( |
166 | GitActivityType, |
167 | GitPermissionType, |
168 | + GitRepositoryDeletionStatus, |
169 | ) |
170 | +from lp.code.errors import CannotDeleteGitRepository |
171 | from lp.code.interfaces.githosting import IGitHostingClient |
172 | from lp.code.interfaces.gitjob import ( |
173 | IGitJob, |
174 | IGitRefScanJob, |
175 | IGitRefScanJobSource, |
176 | + IGitRepositoryDeleteJob, |
177 | + IGitRepositoryDeleteJobSource, |
178 | IGitRepositoryModifiedMailJob, |
179 | IGitRepositoryModifiedMailJobSource, |
180 | IReclaimGitRepositorySpaceJob, |
181 | IReclaimGitRepositorySpaceJobSource, |
182 | ) |
183 | +from lp.code.interfaces.gitlookup import IGitLookup |
184 | from lp.code.interfaces.gitrule import describe_git_permissions |
185 | from lp.code.mail.branch import BranchMailer |
186 | from lp.registry.interfaces.person import IPersonSet |
187 | @@ -99,6 +106,12 @@ |
188 | modifications. |
189 | """) |
190 | |
191 | + DELETE_REPOSITORY = DBItem(3, """ |
192 | + Delete repository |
193 | + |
194 | + This job deletes a repository from the database. |
195 | + """) |
196 | + |
197 | |
198 | @implementer(IGitJob) |
199 | class GitJob(StormBase): |
200 | @@ -405,3 +418,63 @@ |
201 | def run(self): |
202 | """See `IGitRepositoryModifiedMailJob`.""" |
203 | self.getMailer().sendAll() |
204 | + |
205 | + |
206 | +@implementer(IGitRepositoryDeleteJob) |
207 | +@provider(IGitRepositoryDeleteJobSource) |
208 | +class GitRepositoryDeleteJob(GitJobDerived): |
209 | + """A Job that deletes a repository from the database.""" |
210 | + |
211 | + class_job_type = GitJobType.DELETE_REPOSITORY |
212 | + |
213 | + user_error_types = (CannotDeleteGitRepository,) |
214 | + |
215 | + config = config.IGitRepositoryDeleteJobSource |
216 | + |
217 | + def getOperationDescription(self): |
218 | + return "deleting a repository" |
219 | + |
220 | + @classmethod |
221 | + def create(cls, repository, requester): |
222 | + """See `IGitRepositoryDeleteJobSource`.""" |
223 | + metadata = { |
224 | + "repository_id": repository.id, |
225 | + "repository_name": repository.unique_name, |
226 | + } |
227 | + # The GitJob has a repository of None, because we don't want to |
228 | + # delete this job while trying to delete the repository. |
229 | + git_job = GitJob( |
230 | + None, cls.class_job_type, metadata, requester=requester) |
231 | + job = cls(git_job) |
232 | + job.celeryRunOnCommit() |
233 | + return job |
234 | + |
235 | + @property |
236 | + def repository_id(self): |
237 | + return self.metadata["repository_id"] |
238 | + |
239 | + def run(self): |
240 | + """See `IGitRepositoryDeleteJob`.""" |
241 | + repository = getUtility(IGitLookup).get(self.repository_id) |
242 | + if repository is None: |
243 | + log.info( |
244 | + "Skipping repository %s because it has already been deleted." % |
245 | + self._cached_repository_name) |
246 | + elif (repository.deletion_status != |
247 | + GitRepositoryDeletionStatus.DELETING): |
248 | + log.warning( |
249 | + "Skipping repository %s because its deletion status is not " |
250 | + "DELETING." % self._cached_repository_name) |
251 | + else: |
252 | + try: |
253 | + repository.destroySelf(break_references=True) |
254 | + except CannotDeleteGitRepository: |
255 | + # Set the deletion status back to ACTIVE so that it's |
256 | + # possible to try again. We don't attempt to undo the |
257 | + # renaming at the moment. Do this in its own transaction |
258 | + # since the job runner will abort the transaction. |
259 | + transaction.abort() |
260 | + removeSecurityProxy(repository).deletion_status = ( |
261 | + GitRepositoryDeletionStatus.ACTIVE) |
262 | + transaction.commit() |
263 | + raise |
264 | |
265 | === modified file 'lib/lp/code/model/gitrepository.py' |
266 | --- lib/lp/code/model/gitrepository.py 2019-03-21 16:22:19 +0000 |
267 | +++ lib/lp/code/model/gitrepository.py 2019-03-21 16:22:19 +0000 |
268 | @@ -91,6 +91,7 @@ |
269 | GitGranteeType, |
270 | GitObjectType, |
271 | GitPermissionType, |
272 | + GitRepositoryDeletionStatus, |
273 | GitRepositoryType, |
274 | ) |
275 | from lp.code.errors import ( |
276 | @@ -282,6 +283,23 @@ |
277 | repository_type = EnumCol( |
278 | dbName='repository_type', enum=GitRepositoryType, notNull=True) |
279 | |
280 | + _deletion_status = EnumCol( |
281 | + dbName='deletion_status', enum=GitRepositoryDeletionStatus, |
282 | + default=GitRepositoryDeletionStatus.ACTIVE) |
283 | + |
284 | + @property |
285 | + def deletion_status(self): |
286 | + # XXX cjwatson 2019-03-19: Remove once this column has been |
287 | + # backfilled. |
288 | + if self._deletion_status is None: |
289 | + return GitRepositoryDeletionStatus.ACTIVE |
290 | + else: |
291 | + return self._deletion_status |
292 | + |
293 | + @deletion_status.setter |
294 | + def deletion_status(self, value): |
295 | + self._deletion_status = value |
296 | + |
297 | registrant_id = Int(name='registrant', allow_none=False) |
298 | registrant = Reference(registrant_id, 'Person.id') |
299 | |
300 | |
301 | === modified file 'lib/lp/code/model/tests/test_gitjob.py' |
302 | --- lib/lp/code/model/tests/test_gitjob.py 2018-10-21 17:38:05 +0000 |
303 | +++ lib/lp/code/model/tests/test_gitjob.py 2019-03-21 16:22:19 +0000 |
304 | @@ -13,6 +13,10 @@ |
305 | ) |
306 | import hashlib |
307 | |
308 | +from fixtures import ( |
309 | + FakeLogger, |
310 | + MockPatch, |
311 | + ) |
312 | from lazr.lifecycle.snapshot import Snapshot |
313 | import pytz |
314 | from testtools.matchers import ( |
315 | @@ -23,6 +27,7 @@ |
316 | MatchesStructure, |
317 | ) |
318 | import transaction |
319 | +from zope.component import getUtility |
320 | from zope.interface import providedBy |
321 | from zope.security.proxy import removeSecurityProxy |
322 | |
323 | @@ -30,6 +35,7 @@ |
324 | from lp.code.enums import ( |
325 | GitGranteeType, |
326 | GitObjectType, |
327 | + GitRepositoryDeletionStatus, |
328 | ) |
329 | from lp.code.interfaces.branchmergeproposal import ( |
330 | BRANCH_MERGE_PROPOSAL_WEBHOOKS_FEATURE_FLAG, |
331 | @@ -37,8 +43,11 @@ |
332 | from lp.code.interfaces.gitjob import ( |
333 | IGitJob, |
334 | IGitRefScanJob, |
335 | + IGitRepositoryDeleteJob, |
336 | + IGitRepositoryDeleteJobSource, |
337 | IReclaimGitRepositorySpaceJob, |
338 | ) |
339 | +from lp.code.interfaces.gitlookup import IGitLookup |
340 | from lp.code.model.gitjob import ( |
341 | describe_repository_delta, |
342 | GitJob, |
343 | @@ -52,6 +61,7 @@ |
344 | from lp.services.database.constants import UTC_NOW |
345 | from lp.services.features.testing import FeatureFixture |
346 | from lp.services.job.runner import JobRunner |
347 | +from lp.services.mail.sendmail import format_address_for_person |
348 | from lp.services.utils import seconds_since_epoch |
349 | from lp.services.webapp import canonical_url |
350 | from lp.services.webapp.snapshot import notify_modified |
351 | @@ -65,6 +75,7 @@ |
352 | DatabaseFunctionalLayer, |
353 | ZopelessDatabaseLayer, |
354 | ) |
355 | +from lp.testing.mail_helpers import pop_notifications |
356 | |
357 | |
358 | class TestGitJob(TestCaseWithFactory): |
359 | @@ -473,5 +484,98 @@ |
360 | snapshot, repository) |
361 | |
362 | |
363 | +class TestGitRepositoryDeleteJob(TestCaseWithFactory): |
364 | + |
365 | + layer = ZopelessDatabaseLayer |
366 | + |
367 | + def test_providesInterface(self): |
368 | + repository = self.factory.makeGitRepository() |
369 | + requester = repository.registrant |
370 | + job = getUtility(IGitRepositoryDeleteJobSource).create( |
371 | + repository, requester) |
372 | + self.assertProvides(job, IGitRepositoryDeleteJob) |
373 | + |
374 | + def test_run(self): |
375 | + repository = self.factory.makeGitRepository() |
376 | + repository_id = repository.id |
377 | + requester = repository.registrant |
378 | + job = getUtility(IGitRepositoryDeleteJobSource).create( |
379 | + repository, requester) |
380 | + removeSecurityProxy(repository).deletion_status = ( |
381 | + GitRepositoryDeletionStatus.DELETING) |
382 | + logger = self.useFixture(FakeLogger()) |
383 | + with dbuser(config.IGitRepositoryDeleteJobSource.dbuser): |
384 | + job.run() |
385 | + self.assertEqual('', logger.output) |
386 | + self.assertIsNone(getUtility(IGitLookup).get(repository_id)) |
387 | + |
388 | + def test_already_deleted(self): |
389 | + repository = self.factory.makeGitRepository() |
390 | + repository_name = repository.unique_name |
391 | + requester = repository.registrant |
392 | + job = getUtility(IGitRepositoryDeleteJobSource).create( |
393 | + repository, requester) |
394 | + repository.destroySelf() |
395 | + logger = self.useFixture(FakeLogger()) |
396 | + with dbuser(config.IGitRepositoryDeleteJobSource.dbuser): |
397 | + job.run() |
398 | + self.assertEqual( |
399 | + "Skipping repository %s because it has already been " |
400 | + "deleted.\n" % repository_name, |
401 | + logger.output) |
402 | + |
403 | + def test_not_deleting(self): |
404 | + # The job skips repositories that aren't DELETING. This shouldn't |
405 | + # be possible in practice, but is a guard against accidents. |
406 | + repository = self.factory.makeGitRepository() |
407 | + repository_id = repository.id |
408 | + repository_name = repository.unique_name |
409 | + self.assertNotEqual( |
410 | + GitRepositoryDeletionStatus.DELETING, repository.deletion_status) |
411 | + requester = repository.registrant |
412 | + job = getUtility(IGitRepositoryDeleteJobSource).create( |
413 | + repository, requester) |
414 | + logger = self.useFixture(FakeLogger()) |
415 | + with dbuser(config.IGitRepositoryDeleteJobSource.dbuser): |
416 | + job.run() |
417 | + self.assertEqual( |
418 | + "Skipping repository %s because its deletion status is not " |
419 | + "DELETING.\n" % repository_name, |
420 | + logger.output) |
421 | + self.assertEqual(repository, getUtility(IGitLookup).get(repository_id)) |
422 | + |
423 | + def test_error(self): |
424 | + # If deleting the repository fails, an error message is sent to the |
425 | + # requester and the deletion status is set back to ACTIVE. This |
426 | + # can't normally happen because the job always breaks references to |
427 | + # the repository, so we patch in a failure to allow testing the |
428 | + # error path. |
429 | + repository = self.factory.makeGitRepository() |
430 | + repository_id = repository.id |
431 | + requester = repository.registrant |
432 | + job = getUtility(IGitRepositoryDeleteJobSource).create( |
433 | + repository, requester) |
434 | + removeSecurityProxy(repository).deletion_status = ( |
435 | + GitRepositoryDeletionStatus.DELETING) |
436 | + logger = self.useFixture(FakeLogger()) |
437 | + with dbuser(config.IGitRepositoryDeleteJobSource.dbuser): |
438 | + with MockPatch( |
439 | + "lp.code.model.gitrepository.GitRepository.canBeDeleted", |
440 | + return_value=False): |
441 | + JobRunner([job]).runJobHandleError(job) |
442 | + self.assertIn( |
443 | + "failed with user error CannotDeleteGitRepository", logger.output) |
444 | + self.assertEqual(repository, getUtility(IGitLookup).get(repository_id)) |
445 | + self.assertEqual( |
446 | + GitRepositoryDeletionStatus.ACTIVE, repository.deletion_status) |
447 | + self.assertEqual([], self.oopses) |
448 | + [mail] = pop_notifications() |
449 | + self.assertEqual(format_address_for_person(requester), mail["to"]) |
450 | + self.assertEqual( |
451 | + "Launchpad error while deleting a repository", mail["subject"]) |
452 | + self.assertIn( |
453 | + "Cannot delete Git repository", mail.get_payload(decode=True)) |
454 | + |
455 | + |
456 | # XXX cjwatson 2015-03-12: We should test that the jobs work via Celery too, |
457 | # but that isn't feasible until we have a proper turnip fixture. |
458 | |
459 | === modified file 'lib/lp/code/model/tests/test_gitrepository.py' |
460 | --- lib/lp/code/model/tests/test_gitrepository.py 2019-02-11 12:31:06 +0000 |
461 | +++ lib/lp/code/model/tests/test_gitrepository.py 2019-03-21 16:22:19 +0000 |
462 | @@ -638,7 +638,8 @@ |
463 | self.repository.canBeDeleted(), |
464 | "A newly created repository should be able to be deleted.") |
465 | repository_id = self.repository.id |
466 | - self.repository.destroySelf() |
467 | + with dbuser(config.IGitRepositoryDeleteJobSource.dbuser): |
468 | + self.repository.destroySelf() |
469 | self.assertIsNone( |
470 | getUtility(IGitLookup).get(repository_id), |
471 | "The repository has not been deleted.") |
472 | @@ -650,7 +651,8 @@ |
473 | CodeReviewNotificationLevel.NOEMAIL, self.user) |
474 | self.assertTrue(self.repository.canBeDeleted()) |
475 | repository_id = self.repository.id |
476 | - self.repository.destroySelf() |
477 | + with dbuser(config.IGitRepositoryDeleteJobSource.dbuser): |
478 | + self.repository.destroySelf() |
479 | self.assertIsNone( |
480 | getUtility(IGitLookup).get(repository_id), |
481 | "The repository has not been deleted.") |
482 | @@ -665,7 +667,8 @@ |
483 | CodeReviewNotificationLevel.NOEMAIL, self.user) |
484 | self.assertTrue(self.repository.canBeDeleted()) |
485 | repository_id = self.repository.id |
486 | - self.repository.destroySelf() |
487 | + with dbuser(config.IGitRepositoryDeleteJobSource.dbuser): |
488 | + self.repository.destroySelf() |
489 | self.assertIsNone( |
490 | getUtility(IGitLookup).get(repository_id), |
491 | "The repository has not been deleted.") |
492 | @@ -687,8 +690,9 @@ |
493 | self.assertFalse( |
494 | self.repository.canBeDeleted(), |
495 | "A repository with a landing target is not deletable.") |
496 | - self.assertRaises( |
497 | - CannotDeleteGitRepository, self.repository.destroySelf) |
498 | + with dbuser(config.IGitRepositoryDeleteJobSource.dbuser): |
499 | + self.assertRaises( |
500 | + CannotDeleteGitRepository, self.repository.destroySelf) |
501 | |
502 | def test_landing_candidate_disables_deletion(self): |
503 | # A repository with a landing candidate cannot be deleted. |
504 | @@ -698,8 +702,9 @@ |
505 | self.assertFalse( |
506 | self.repository.canBeDeleted(), |
507 | "A repository with a landing candidate is not deletable.") |
508 | - self.assertRaises( |
509 | - CannotDeleteGitRepository, self.repository.destroySelf) |
510 | + with dbuser(config.IGitRepositoryDeleteJobSource.dbuser): |
511 | + self.assertRaises( |
512 | + CannotDeleteGitRepository, self.repository.destroySelf) |
513 | |
514 | def test_prerequisite_repository_disables_deletion(self): |
515 | # A repository that is a prerequisite repository cannot be deleted. |
516 | @@ -711,14 +716,16 @@ |
517 | self.assertFalse( |
518 | self.repository.canBeDeleted(), |
519 | "A repository with a prerequisite target is not deletable.") |
520 | - self.assertRaises( |
521 | - CannotDeleteGitRepository, self.repository.destroySelf) |
522 | + with dbuser(config.IGitRepositoryDeleteJobSource.dbuser): |
523 | + self.assertRaises( |
524 | + CannotDeleteGitRepository, self.repository.destroySelf) |
525 | |
526 | def test_related_GitJobs_deleted(self): |
527 | # A repository with an associated job will delete those jobs. |
528 | GitAPI(None, None).notify(self.repository.getInternalPath()) |
529 | store = Store.of(self.repository) |
530 | - self.repository.destroySelf() |
531 | + with dbuser(config.IGitRepositoryDeleteJobSource.dbuser): |
532 | + self.repository.destroySelf() |
533 | # Need to commit the transaction to fire off the constraint checks. |
534 | transaction.commit() |
535 | jobs = store.find(GitJob, GitJob.job_type == GitJobType.REF_SCAN) |
536 | @@ -729,7 +736,8 @@ |
537 | # to remove the repository from disk as well. |
538 | repository_path = self.repository.getInternalPath() |
539 | store = Store.of(self.repository) |
540 | - self.repository.destroySelf() |
541 | + with dbuser(config.IGitRepositoryDeleteJobSource.dbuser): |
542 | + self.repository.destroySelf() |
543 | jobs = store.find( |
544 | GitJob, |
545 | GitJob.job_type == GitJobType.RECLAIM_REPOSITORY_SPACE) |
546 | @@ -742,14 +750,16 @@ |
547 | # If repository is a base_git_repository in a recipe, it is deleted. |
548 | recipe = self.factory.makeSourcePackageRecipe( |
549 | branches=self.factory.makeGitRefs(owner=self.user)) |
550 | - recipe.base_git_repository.destroySelf(break_references=True) |
551 | + with dbuser(config.IGitRepositoryDeleteJobSource.dbuser): |
552 | + recipe.base_git_repository.destroySelf(break_references=True) |
553 | |
554 | def test_destroySelf_with_SourcePackageRecipe_as_non_base(self): |
555 | # If repository is referred to by a recipe, it is deleted. |
556 | [ref1] = self.factory.makeGitRefs(owner=self.user) |
557 | [ref2] = self.factory.makeGitRefs(owner=self.user) |
558 | self.factory.makeSourcePackageRecipe(branches=[ref1, ref2]) |
559 | - ref2.repository.destroySelf(break_references=True) |
560 | + with dbuser(config.IGitRepositoryDeleteJobSource.dbuser): |
561 | + ref2.repository.destroySelf(break_references=True) |
562 | |
563 | def test_destroySelf_with_inline_comments_draft(self): |
564 | # Draft inline comments related to a deleted repository (source or |
565 | @@ -763,7 +773,8 @@ |
566 | previewdiff_id=preview_diff.id, |
567 | person=self.user, |
568 | comments={"1": "Should vanish."}) |
569 | - self.repository.destroySelf(break_references=True) |
570 | + with dbuser(config.IGitRepositoryDeleteJobSource.dbuser): |
571 | + self.repository.destroySelf(break_references=True) |
572 | |
573 | def test_destroySelf_with_inline_comments_published(self): |
574 | # Published inline comments related to a deleted repository (source |
575 | @@ -779,12 +790,14 @@ |
576 | previewdiff_id=preview_diff.id, |
577 | inline_comments={"1": "Must disappear."}, |
578 | ) |
579 | - self.repository.destroySelf(break_references=True) |
580 | + with dbuser(config.IGitRepositoryDeleteJobSource.dbuser): |
581 | + self.repository.destroySelf(break_references=True) |
582 | |
583 | def test_related_webhooks_deleted(self): |
584 | webhook = self.factory.makeWebhook(target=self.repository) |
585 | webhook.ping() |
586 | - self.repository.destroySelf() |
587 | + with dbuser(config.IGitRepositoryDeleteJobSource.dbuser): |
588 | + self.repository.destroySelf() |
589 | transaction.commit() |
590 | self.assertRaises(LostObjectError, getattr, webhook, 'target') |
591 | |
592 | @@ -796,7 +809,8 @@ |
593 | activities = store.find( |
594 | GitActivity, GitActivity.repository_id == repository_id) |
595 | self.assertNotEqual([], list(activities)) |
596 | - self.repository.destroySelf() |
597 | + with dbuser(config.IGitRepositoryDeleteJobSource.dbuser): |
598 | + self.repository.destroySelf() |
599 | transaction.commit() |
600 | self.assertRaises(LostObjectError, getattr, grant, 'rule') |
601 | self.assertRaises(LostObjectError, getattr, rule, 'repository') |
602 | @@ -895,7 +909,8 @@ |
603 | merge_proposal1, merge_proposal2 = self.makeMergeProposals() |
604 | merge_proposal1_id = merge_proposal1.id |
605 | BranchMergeProposal.get(merge_proposal1_id) |
606 | - self.repository.destroySelf(break_references=True) |
607 | + with dbuser(config.IGitRepositoryDeleteJobSource.dbuser): |
608 | + self.repository.destroySelf(break_references=True) |
609 | self.assertRaises( |
610 | SQLObjectNotFound, BranchMergeProposal.get, merge_proposal1_id) |
611 | |
612 | @@ -905,8 +920,9 @@ |
613 | merge_proposal1, merge_proposal2 = self.makeMergeProposals() |
614 | merge_proposal1_id = merge_proposal1.id |
615 | BranchMergeProposal.get(merge_proposal1_id) |
616 | - merge_proposal1.target_git_repository.destroySelf( |
617 | - break_references=True) |
618 | + with dbuser(config.IGitRepositoryDeleteJobSource.dbuser): |
619 | + merge_proposal1.target_git_repository.destroySelf( |
620 | + break_references=True) |
621 | self.assertRaises(SQLObjectNotFound, |
622 | BranchMergeProposal.get, merge_proposal1_id) |
623 | |
624 | @@ -914,8 +930,9 @@ |
625 | # Merge proposal prerequisite repositories can be deleted with |
626 | # break_references. |
627 | merge_proposal1, merge_proposal2 = self.makeMergeProposals() |
628 | - merge_proposal1.prerequisite_git_repository.destroySelf( |
629 | - break_references=True) |
630 | + with dbuser(config.IGitRepositoryDeleteJobSource.dbuser): |
631 | + merge_proposal1.prerequisite_git_repository.destroySelf( |
632 | + break_references=True) |
633 | self.assertIsNone(merge_proposal1.prerequisite_git_repository) |
634 | |
635 | def test_delete_source_CodeReviewComment(self): |
636 | @@ -923,7 +940,8 @@ |
637 | comment = self.factory.makeCodeReviewComment(git=True) |
638 | comment_id = comment.id |
639 | repository = comment.branch_merge_proposal.source_git_repository |
640 | - repository.destroySelf(break_references=True) |
641 | + with dbuser(config.IGitRepositoryDeleteJobSource.dbuser): |
642 | + repository.destroySelf(break_references=True) |
643 | self.assertRaises( |
644 | SQLObjectNotFound, CodeReviewComment.get, comment_id) |
645 | |
646 | @@ -932,7 +950,8 @@ |
647 | comment = self.factory.makeCodeReviewComment(git=True) |
648 | comment_id = comment.id |
649 | repository = comment.branch_merge_proposal.target_git_repository |
650 | - repository.destroySelf(break_references=True) |
651 | + with dbuser(config.IGitRepositoryDeleteJobSource.dbuser): |
652 | + repository.destroySelf(break_references=True) |
653 | self.assertRaises( |
654 | SQLObjectNotFound, CodeReviewComment.get, comment_id) |
655 | |
656 | @@ -941,14 +960,18 @@ |
657 | merge_proposal = self.factory.makeBranchMergeProposalForGit() |
658 | merge_proposal.nominateReviewer( |
659 | self.factory.makePerson(), self.factory.makePerson()) |
660 | - merge_proposal.source_git_repository.destroySelf(break_references=True) |
661 | + with dbuser(config.IGitRepositoryDeleteJobSource.dbuser): |
662 | + merge_proposal.source_git_repository.destroySelf( |
663 | + break_references=True) |
664 | |
665 | def test_targetBranchWithCodeReviewVoteReference(self): |
666 | # break_references handles CodeReviewVoteReference target repository. |
667 | merge_proposal = self.factory.makeBranchMergeProposalForGit() |
668 | merge_proposal.nominateReviewer( |
669 | self.factory.makePerson(), self.factory.makePerson()) |
670 | - merge_proposal.target_git_repository.destroySelf(break_references=True) |
671 | + with dbuser(config.IGitRepositoryDeleteJobSource.dbuser): |
672 | + merge_proposal.target_git_repository.destroySelf( |
673 | + break_references=True) |
674 | |
675 | def test_code_import_requirements(self): |
676 | # Code imports are not included explicitly in deletion requirements. |
677 | @@ -967,7 +990,8 @@ |
678 | code_import = self.factory.makeCodeImport( |
679 | target_rcs_type=TargetRevisionControlSystems.GIT) |
680 | code_import_id = code_import.id |
681 | - code_import.git_repository.destroySelf(break_references=True) |
682 | + with dbuser(config.IGitRepositoryDeleteJobSource.dbuser): |
683 | + code_import.git_repository.destroySelf(break_references=True) |
684 | self.assertRaises( |
685 | NotFoundError, getUtility(ICodeImportSet).get, code_import_id) |
686 | |
687 | @@ -988,7 +1012,8 @@ |
688 | repository=repository, paths=["refs/heads/1", "refs/heads/2"]) |
689 | snap1 = self.factory.makeSnap(git_ref=ref1) |
690 | snap2 = self.factory.makeSnap(git_ref=ref2) |
691 | - repository.destroySelf(break_references=True) |
692 | + with dbuser(config.IGitRepositoryDeleteJobSource.dbuser): |
693 | + repository.destroySelf(break_references=True) |
694 | transaction.commit() |
695 | self.assertIsNone(snap1.git_repository) |
696 | self.assertIsNone(snap1.git_path) |
697 | @@ -1001,7 +1026,8 @@ |
698 | merge_proposal = removeSecurityProxy(self.makeMergeProposals()[0]) |
699 | with person_logged_in( |
700 | merge_proposal.prerequisite_git_repository.owner): |
701 | - ClearPrerequisiteRepository(merge_proposal)() |
702 | + with dbuser(config.IGitRepositoryDeleteJobSource.dbuser): |
703 | + ClearPrerequisiteRepository(merge_proposal)() |
704 | self.assertIsNone(merge_proposal.prerequisite_git_repository) |
705 | |
706 | def test_DeletionOperation(self): |
707 | @@ -1012,8 +1038,9 @@ |
708 | # DeletionCallable must invoke the callable. |
709 | merge_proposal = self.factory.makeBranchMergeProposalForGit() |
710 | merge_proposal_id = merge_proposal.id |
711 | - DeletionCallable( |
712 | - merge_proposal, "blah", merge_proposal.deleteProposal)() |
713 | + with dbuser(config.IGitRepositoryDeleteJobSource.dbuser): |
714 | + DeletionCallable( |
715 | + merge_proposal, "blah", merge_proposal.deleteProposal)() |
716 | self.assertRaises( |
717 | SQLObjectNotFound, BranchMergeProposal.get, merge_proposal_id) |
718 | |
719 | @@ -1023,7 +1050,8 @@ |
720 | code_import = self.factory.makeCodeImport( |
721 | target_rcs_type=TargetRevisionControlSystems.GIT) |
722 | code_import_id = code_import.id |
723 | - DeleteCodeImport(code_import)() |
724 | + with dbuser(config.IGitRepositoryDeleteJobSource.dbuser): |
725 | + DeleteCodeImport(code_import)() |
726 | self.assertRaises( |
727 | NotFoundError, getUtility(ICodeImportSet).get, code_import_id) |
728 | |
729 | |
730 | === modified file 'lib/lp/services/config/schema-lazr.conf' |
731 | --- lib/lp/services/config/schema-lazr.conf 2019-03-21 16:22:19 +0000 |
732 | +++ lib/lp/services/config/schema-lazr.conf 2019-03-21 16:22:19 +0000 |
733 | @@ -1871,6 +1871,7 @@ |
734 | IBranchModifiedMailJobSource, |
735 | ICommercialExpiredJobSource, |
736 | IExpiringMembershipNotificationJobSource, |
737 | + IGitRepositoryDeleteJobSource, |
738 | IGitRepositoryModifiedMailJobSource, |
739 | IMembershipNotificationJobSource, |
740 | IPackageUploadNotificationJobSource, |
741 | @@ -1931,6 +1932,11 @@ |
742 | module: lp.code.interfaces.gitjob |
743 | dbuser: branchscanner |
744 | |
745 | +[IGitRepositoryDeleteJobSource] |
746 | +module: lp.code.interfaces.gitjob |
747 | +dbuser: branch-delete-job |
748 | +crontab_group: MAIN |
749 | + |
750 | [IGitRepositoryModifiedMailJobSource] |
751 | module: lp.code.interfaces.gitjob |
752 | dbuser: send-branch-mail |
This seems unnecessary since adding some more indexes has made deletion fast enough, so withdrawing this until we have a clear need.