Merge lp:~cjwatson/launchpad/code-async-delete into lp:launchpad
- code-async-delete
- Merge into devel
Proposed by
Colin Watson
Status: | Rejected | ||||
---|---|---|---|---|---|
Rejected by: | Colin Watson | ||||
Proposed branch: | lp:~cjwatson/launchpad/code-async-delete | ||||
Merge into: | lp:launchpad | ||||
Prerequisite: | lp:~cjwatson/launchpad/git-repository-delete-job | ||||
Diff against target: |
704 lines (+350/-25) 15 files modified
lib/lp/code/browser/branch.py (+8/-4) lib/lp/code/browser/gitrepository.py (+6/-2) lib/lp/code/browser/tests/test_gitrepository.py (+20/-0) lib/lp/code/interfaces/branch.py (+17/-4) lib/lp/code/interfaces/gitrepository.py (+18/-5) lib/lp/code/model/branch.py (+21/-2) lib/lp/code/model/gitrepository.py (+17/-6) lib/lp/code/model/tests/test_branch.py (+30/-0) lib/lp/code/model/tests/test_gitrepository.py (+25/-0) lib/lp/code/stories/branches/xx-branch-deletion.txt (+14/-0) lib/lp/code/stories/webservice/xx-branch.txt (+14/-1) lib/lp/code/templates/branch-index.pt (+4/-0) lib/lp/code/templates/gitrepository-index.pt (+7/-0) lib/lp/scripts/garbo.py (+65/-0) lib/lp/scripts/tests/test_garbo.py (+84/-1) |
||||
To merge this branch: | bzr merge lp:~cjwatson/launchpad/code-async-delete | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Launchpad code reviewers | Pending | ||
Review via email: mp+364912@code.launchpad.net |
Commit message
Delete branches and Git repositories asynchronously.
Description of the change
To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) wrote : | # |
Unmerged revisions
- 18914. By Colin Watson
-
Delete branches and Git repositories asynchronously.
- 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 'lib/lp/code/browser/branch.py' | |||
2 | --- lib/lp/code/browser/branch.py 2019-01-30 16:41:12 +0000 | |||
3 | +++ lib/lp/code/browser/branch.py 2019-03-21 16:27:30 +0000 | |||
4 | @@ -1,4 +1,4 @@ | |||
6 | 1 | # Copyright 2009-2018 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2009-2019 Canonical Ltd. This software is licensed under the |
7 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
8 | 3 | 3 | ||
9 | 4 | """Branch views.""" | 4 | """Branch views.""" |
10 | @@ -87,7 +87,10 @@ | |||
11 | 87 | from lp.code.browser.decorations import DecoratedBranch | 87 | from lp.code.browser.decorations import DecoratedBranch |
12 | 88 | from lp.code.browser.sourcepackagerecipelisting import HasRecipesMenuMixin | 88 | from lp.code.browser.sourcepackagerecipelisting import HasRecipesMenuMixin |
13 | 89 | from lp.code.browser.widgets.branchtarget import BranchTargetWidget | 89 | from lp.code.browser.widgets.branchtarget import BranchTargetWidget |
15 | 90 | from lp.code.enums import BranchType | 90 | from lp.code.enums import ( |
16 | 91 | BranchDeletionStatus, | ||
17 | 92 | BranchType, | ||
18 | 93 | ) | ||
19 | 91 | from lp.code.errors import ( | 94 | from lp.code.errors import ( |
20 | 92 | BranchCreationForbidden, | 95 | BranchCreationForbidden, |
21 | 93 | BranchExists, | 96 | BranchExists, |
22 | @@ -254,7 +257,8 @@ | |||
23 | 254 | @enabled_with_permission('launchpad.Edit') | 257 | @enabled_with_permission('launchpad.Edit') |
24 | 255 | def delete(self): | 258 | def delete(self): |
25 | 256 | text = 'Delete branch' | 259 | text = 'Delete branch' |
27 | 257 | return Link('+delete', text, icon='trash-icon') | 260 | enabled = self.context.deletion_status != BranchDeletionStatus.DELETING |
28 | 261 | return Link('+delete', text, icon='trash-icon', enabled=enabled) | ||
29 | 258 | 262 | ||
30 | 259 | @enabled_with_permission('launchpad.AnyPerson') | 263 | @enabled_with_permission('launchpad.AnyPerson') |
31 | 260 | def edit_whiteboard(self): | 264 | def edit_whiteboard(self): |
32 | @@ -992,7 +996,7 @@ | |||
33 | 992 | # somewhere valid to send them next. | 996 | # somewhere valid to send them next. |
34 | 993 | self.next_url = canonical_url(branch.target) | 997 | self.next_url = canonical_url(branch.target) |
35 | 994 | message = "Branch %s deleted." % branch.unique_name | 998 | message = "Branch %s deleted." % branch.unique_name |
37 | 995 | self.context.destroySelf(break_references=True) | 999 | self.context.delete() |
38 | 996 | self.request.response.addNotification(message) | 1000 | self.request.response.addNotification(message) |
39 | 997 | else: | 1001 | else: |
40 | 998 | self.request.response.addNotification( | 1002 | self.request.response.addNotification( |
41 | 999 | 1003 | ||
42 | === modified file 'lib/lp/code/browser/gitrepository.py' | |||
43 | --- lib/lp/code/browser/gitrepository.py 2019-01-30 16:41:12 +0000 | |||
44 | +++ lib/lp/code/browser/gitrepository.py 2019-03-21 16:27:30 +0000 | |||
45 | @@ -87,6 +87,7 @@ | |||
46 | 87 | ) | 87 | ) |
47 | 88 | from lp.code.enums import ( | 88 | from lp.code.enums import ( |
48 | 89 | GitGranteeType, | 89 | GitGranteeType, |
49 | 90 | GitRepositoryDeletionStatus, | ||
50 | 90 | GitRepositoryType, | 91 | GitRepositoryType, |
51 | 91 | ) | 92 | ) |
52 | 92 | from lp.code.errors import ( | 93 | from lp.code.errors import ( |
53 | @@ -276,7 +277,10 @@ | |||
54 | 276 | @enabled_with_permission("launchpad.Edit") | 277 | @enabled_with_permission("launchpad.Edit") |
55 | 277 | def delete(self): | 278 | def delete(self): |
56 | 278 | text = "Delete repository" | 279 | text = "Delete repository" |
58 | 279 | return Link("+delete", text, icon="trash-icon") | 280 | enabled = ( |
59 | 281 | self.context.deletion_status != | ||
60 | 282 | GitRepositoryDeletionStatus.DELETING) | ||
61 | 283 | return Link("+delete", text, icon="trash-icon", enabled=enabled) | ||
62 | 280 | 284 | ||
63 | 281 | 285 | ||
64 | 282 | class GitRepositoryContextMenu(ContextMenu, HasRecipesMenuMixin): | 286 | class GitRepositoryContextMenu(ContextMenu, HasRecipesMenuMixin): |
65 | @@ -1294,7 +1298,7 @@ | |||
66 | 1294 | # have somewhere valid to send them next. | 1298 | # have somewhere valid to send them next. |
67 | 1295 | self.next_url = canonical_url(repository.target) | 1299 | self.next_url = canonical_url(repository.target) |
68 | 1296 | message = "Repository %s deleted." % repository.unique_name | 1300 | message = "Repository %s deleted." % repository.unique_name |
70 | 1297 | self.context.destroySelf(break_references=True) | 1301 | self.context.delete() |
71 | 1298 | self.request.response.addNotification(message) | 1302 | self.request.response.addNotification(message) |
72 | 1299 | else: | 1303 | else: |
73 | 1300 | self.request.response.addNotification( | 1304 | self.request.response.addNotification( |
74 | 1301 | 1305 | ||
75 | === modified file 'lib/lp/code/browser/tests/test_gitrepository.py' | |||
76 | --- lib/lp/code/browser/tests/test_gitrepository.py 2019-01-30 17:24:15 +0000 | |||
77 | +++ lib/lp/code/browser/tests/test_gitrepository.py 2019-03-21 16:27:30 +0000 | |||
78 | @@ -16,6 +16,7 @@ | |||
79 | 16 | from textwrap import dedent | 16 | from textwrap import dedent |
80 | 17 | 17 | ||
81 | 18 | from fixtures import FakeLogger | 18 | from fixtures import FakeLogger |
82 | 19 | from mechanize import LinkNotFoundError | ||
83 | 19 | import pytz | 20 | import pytz |
84 | 20 | import soupmatchers | 21 | import soupmatchers |
85 | 21 | from storm.store import Store | 22 | from storm.store import Store |
86 | @@ -1829,6 +1830,25 @@ | |||
87 | 1829 | ["Repository %s deleted." % name], | 1830 | ["Repository %s deleted." % name], |
88 | 1830 | get_feedback_messages(browser.contents)) | 1831 | get_feedback_messages(browser.contents)) |
89 | 1831 | 1832 | ||
90 | 1833 | def test_deleting_repository(self): | ||
91 | 1834 | # Repository deletion is asynchronous. If the user finds the | ||
92 | 1835 | # repository again before the deletion completes, then they see an | ||
93 | 1836 | # indication that it is being deleted, and there is no "Delete | ||
94 | 1837 | # repository" action. | ||
95 | 1838 | owner = self.factory.makePerson() | ||
96 | 1839 | repository = self.factory.makeGitRepository(owner=owner) | ||
97 | 1840 | with person_logged_in(repository.owner): | ||
98 | 1841 | repository.delete() | ||
99 | 1842 | self.assertEndsWith(repository.unique_name, "-deleting") | ||
100 | 1843 | browser = self.getViewBrowser( | ||
101 | 1844 | repository, "+index", rootsite="code", user=repository.owner) | ||
102 | 1845 | self.assertEqual( | ||
103 | 1846 | "This repository is being deleted.", | ||
104 | 1847 | extract_text(find_tag_by_id( | ||
105 | 1848 | browser.contents, "repository-deleting"))) | ||
106 | 1849 | self.assertRaises( | ||
107 | 1850 | LinkNotFoundError, browser.getLink, "Delete repository") | ||
108 | 1851 | |||
109 | 1832 | 1852 | ||
110 | 1833 | class TestGitRepositoryActivityView(BrowserTestCase): | 1853 | class TestGitRepositoryActivityView(BrowserTestCase): |
111 | 1834 | 1854 | ||
112 | 1835 | 1855 | ||
113 | === modified file 'lib/lp/code/interfaces/branch.py' | |||
114 | --- lib/lp/code/interfaces/branch.py 2019-03-21 16:27:29 +0000 | |||
115 | +++ lib/lp/code/interfaces/branch.py 2019-03-21 16:27:30 +0000 | |||
116 | @@ -1228,9 +1228,6 @@ | |||
117 | 1228 | branch. | 1228 | branch. |
118 | 1229 | """ | 1229 | """ |
119 | 1230 | 1230 | ||
120 | 1231 | @call_with(break_references=True) | ||
121 | 1232 | @export_destructor_operation() | ||
122 | 1233 | @operation_for_version('beta') | ||
123 | 1234 | def destroySelf(break_references=False): | 1231 | def destroySelf(break_references=False): |
124 | 1235 | """Delete the specified branch. | 1232 | """Delete the specified branch. |
125 | 1236 | 1233 | ||
126 | @@ -1239,7 +1236,23 @@ | |||
127 | 1239 | :param break_references: If supplied, break any references to this | 1236 | :param break_references: If supplied, break any references to this |
128 | 1240 | branch by deleting items with mandatory references and | 1237 | branch by deleting items with mandatory references and |
129 | 1241 | NULLing other references. | 1238 | NULLing other references. |
131 | 1242 | :raise: CannotDeleteBranch if the branch cannot be deleted. | 1239 | :raises CannotDeleteBranch: if the branch cannot be deleted. |
132 | 1240 | """ | ||
133 | 1241 | |||
134 | 1242 | @export_destructor_operation() | ||
135 | 1243 | @operation_for_version('beta') | ||
136 | 1244 | def delete(): | ||
137 | 1245 | """Request deletion of the specified branch. | ||
138 | 1246 | |||
139 | 1247 | The branch will be deleted asynchronously. | ||
140 | 1248 | |||
141 | 1249 | This will delete any merge proposals that use this branch as their | ||
142 | 1250 | source or target, and any recipes that use this branch. It will | ||
143 | 1251 | clear the prerequisite from any merge proposals that use this branch | ||
144 | 1252 | as a prerequisite. It will detach the branch from any snap packages | ||
145 | 1253 | that build from it. | ||
146 | 1254 | |||
147 | 1255 | :raises CannotDeleteBranch: if the branch cannot be deleted. | ||
148 | 1243 | """ | 1256 | """ |
149 | 1244 | 1257 | ||
150 | 1245 | 1258 | ||
151 | 1246 | 1259 | ||
152 | === modified file 'lib/lp/code/interfaces/gitrepository.py' | |||
153 | --- lib/lp/code/interfaces/gitrepository.py 2019-03-21 16:27:29 +0000 | |||
154 | +++ lib/lp/code/interfaces/gitrepository.py 2019-03-21 16:27:30 +0000 | |||
155 | @@ -1,4 +1,4 @@ | |||
157 | 1 | # Copyright 2015-2018 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2015-2019 Canonical Ltd. This software is licensed under the |
158 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
159 | 3 | 3 | ||
160 | 4 | """Git repository interfaces.""" | 4 | """Git repository interfaces.""" |
161 | @@ -881,16 +881,29 @@ | |||
162 | 881 | object needs to be touched. | 881 | object needs to be touched. |
163 | 882 | """ | 882 | """ |
164 | 883 | 883 | ||
165 | 884 | @call_with(break_references=True) | ||
166 | 885 | @export_destructor_operation() | ||
167 | 886 | @operation_for_version("devel") | ||
168 | 887 | def destroySelf(break_references=False): | 884 | def destroySelf(break_references=False): |
169 | 888 | """Delete the specified repository. | 885 | """Delete the specified repository. |
170 | 889 | 886 | ||
171 | 890 | :param break_references: If supplied, break any references to this | 887 | :param break_references: If supplied, break any references to this |
172 | 891 | repository by deleting items with mandatory references and | 888 | repository by deleting items with mandatory references and |
173 | 892 | NULLing other references. | 889 | NULLing other references. |
175 | 893 | :raise: CannotDeleteGitRepository if the repository cannot be deleted. | 890 | :raises CannotDeleteGitRepository: if the repository cannot be deleted. |
176 | 891 | """ | ||
177 | 892 | |||
178 | 893 | @export_destructor_operation() | ||
179 | 894 | @operation_for_version("devel") | ||
180 | 895 | def delete(): | ||
181 | 896 | """Request deletion of the specified repository. | ||
182 | 897 | |||
183 | 898 | The repository will be deleted asynchronously. | ||
184 | 899 | |||
185 | 900 | This will delete any merge proposals that use this repository as | ||
186 | 901 | their source or target, and any recipes that use this repository. | ||
187 | 902 | It will clear the prerequisite from any merge proposals that use | ||
188 | 903 | this repository as a prerequisite. It will detach the repository | ||
189 | 904 | from any snap packages that build from it. | ||
190 | 905 | |||
191 | 906 | :raises CannotDeleteGitRepository: if the repository cannot be deleted. | ||
192 | 894 | """ | 907 | """ |
193 | 895 | 908 | ||
194 | 896 | 909 | ||
195 | 897 | 910 | ||
196 | === modified file 'lib/lp/code/model/branch.py' | |||
197 | --- lib/lp/code/model/branch.py 2019-03-21 16:27:29 +0000 | |||
198 | +++ lib/lp/code/model/branch.py 2019-03-21 16:27:30 +0000 | |||
199 | @@ -112,6 +112,11 @@ | |||
200 | 112 | ) | 112 | ) |
201 | 113 | from lp.code.interfaces.branchcollection import IAllBranches | 113 | from lp.code.interfaces.branchcollection import IAllBranches |
202 | 114 | from lp.code.interfaces.branchhosting import IBranchHostingClient | 114 | from lp.code.interfaces.branchhosting import IBranchHostingClient |
203 | 115 | from lp.code.interfaces.branchjob import ( | ||
204 | 116 | IBranchDeleteJobSource, | ||
205 | 117 | IBranchUpgradeJobSource, | ||
206 | 118 | IReclaimBranchSpaceJobSource, | ||
207 | 119 | ) | ||
208 | 115 | from lp.code.interfaces.branchlookup import IBranchLookup | 120 | from lp.code.interfaces.branchlookup import IBranchLookup |
209 | 116 | from lp.code.interfaces.branchmergeproposal import ( | 121 | from lp.code.interfaces.branchmergeproposal import ( |
210 | 117 | BRANCH_MERGE_PROPOSAL_FINAL_STATES, | 122 | BRANCH_MERGE_PROPOSAL_FINAL_STATES, |
211 | @@ -1455,7 +1460,6 @@ | |||
212 | 1455 | 1460 | ||
213 | 1456 | def destroySelf(self, break_references=False): | 1461 | def destroySelf(self, break_references=False): |
214 | 1457 | """See `IBranch`.""" | 1462 | """See `IBranch`.""" |
215 | 1458 | from lp.code.interfaces.branchjob import IReclaimBranchSpaceJobSource | ||
216 | 1459 | if break_references: | 1463 | if break_references: |
217 | 1460 | self._breakReferences() | 1464 | self._breakReferences() |
218 | 1461 | if not self.canBeDeleted(): | 1465 | if not self.canBeDeleted(): |
219 | @@ -1473,6 +1477,22 @@ | |||
220 | 1473 | job = getUtility(IReclaimBranchSpaceJobSource).create(branch_id) | 1477 | job = getUtility(IReclaimBranchSpaceJobSource).create(branch_id) |
221 | 1474 | job.celeryRunOnCommit() | 1478 | job.celeryRunOnCommit() |
222 | 1475 | 1479 | ||
223 | 1480 | def delete(self): | ||
224 | 1481 | """See `IBranch`.""" | ||
225 | 1482 | if self.deletion_status == BranchDeletionStatus.DELETING: | ||
226 | 1483 | raise CannotDeleteBranch( | ||
227 | 1484 | "This branch is already being deleted: %s" % self.unique_name) | ||
228 | 1485 | if not self.getStackedBranches().is_empty(): | ||
229 | 1486 | # If there are branches stacked on this one, then we won't be | ||
230 | 1487 | # able to delete this branch even after breaking references. | ||
231 | 1488 | raise CannotDeleteBranch( | ||
232 | 1489 | "Cannot delete branch: %s" % self.unique_name) | ||
233 | 1490 | # Destructor operations can't take arguments. | ||
234 | 1491 | user = getUtility(ILaunchBag).user | ||
235 | 1492 | getUtility(IBranchDeleteJobSource).create(self, user) | ||
236 | 1493 | self.name = self.namespace.findUnusedName(self.name + "-deleting") | ||
237 | 1494 | self.deletion_status = BranchDeletionStatus.DELETING | ||
238 | 1495 | |||
239 | 1476 | def checkUpgrade(self): | 1496 | def checkUpgrade(self): |
240 | 1477 | if self.branch_type is not BranchType.HOSTED: | 1497 | if self.branch_type is not BranchType.HOSTED: |
241 | 1478 | raise CannotUpgradeNonHosted(self) | 1498 | raise CannotUpgradeNonHosted(self) |
242 | @@ -1509,7 +1529,6 @@ | |||
243 | 1509 | 1529 | ||
244 | 1510 | def requestUpgrade(self, requester): | 1530 | def requestUpgrade(self, requester): |
245 | 1511 | """See `IBranch`.""" | 1531 | """See `IBranch`.""" |
246 | 1512 | from lp.code.interfaces.branchjob import IBranchUpgradeJobSource | ||
247 | 1513 | job = getUtility(IBranchUpgradeJobSource).create(self, requester) | 1532 | job = getUtility(IBranchUpgradeJobSource).create(self, requester) |
248 | 1514 | job.celeryRunOnCommit() | 1533 | job.celeryRunOnCommit() |
249 | 1515 | return job | 1534 | return job |
250 | 1516 | 1535 | ||
251 | === modified file 'lib/lp/code/model/gitrepository.py' | |||
252 | --- lib/lp/code/model/gitrepository.py 2019-03-21 16:27:29 +0000 | |||
253 | +++ lib/lp/code/model/gitrepository.py 2019-03-21 16:27:30 +0000 | |||
254 | @@ -112,7 +112,11 @@ | |||
255 | 112 | IGitCollection, | 112 | IGitCollection, |
256 | 113 | ) | 113 | ) |
257 | 114 | from lp.code.interfaces.githosting import IGitHostingClient | 114 | from lp.code.interfaces.githosting import IGitHostingClient |
259 | 115 | from lp.code.interfaces.gitjob import IGitRefScanJobSource | 115 | from lp.code.interfaces.gitjob import ( |
260 | 116 | IGitRefScanJobSource, | ||
261 | 117 | IGitRepositoryDeleteJobSource, | ||
262 | 118 | IReclaimGitRepositorySpaceJobSource, | ||
263 | 119 | ) | ||
264 | 116 | from lp.code.interfaces.gitlookup import IGitLookup | 120 | from lp.code.interfaces.gitlookup import IGitLookup |
265 | 117 | from lp.code.interfaces.gitnamespace import ( | 121 | from lp.code.interfaces.gitnamespace import ( |
266 | 118 | get_git_namespace, | 122 | get_git_namespace, |
267 | @@ -1551,11 +1555,6 @@ | |||
268 | 1551 | 1555 | ||
269 | 1552 | def destroySelf(self, break_references=False): | 1556 | def destroySelf(self, break_references=False): |
270 | 1553 | """See `IGitRepository`.""" | 1557 | """See `IGitRepository`.""" |
271 | 1554 | # Circular import. | ||
272 | 1555 | from lp.code.interfaces.gitjob import ( | ||
273 | 1556 | IReclaimGitRepositorySpaceJobSource, | ||
274 | 1557 | ) | ||
275 | 1558 | |||
276 | 1559 | if break_references: | 1558 | if break_references: |
277 | 1560 | self._breakReferences() | 1559 | self._breakReferences() |
278 | 1561 | if not self.canBeDeleted(): | 1560 | if not self.canBeDeleted(): |
279 | @@ -1583,6 +1582,18 @@ | |||
280 | 1583 | getUtility(IReclaimGitRepositorySpaceJobSource).create( | 1582 | getUtility(IReclaimGitRepositorySpaceJobSource).create( |
281 | 1584 | repository_name, repository_path) | 1583 | repository_name, repository_path) |
282 | 1585 | 1584 | ||
283 | 1585 | def delete(self): | ||
284 | 1586 | """See `IGitRepository`.""" | ||
285 | 1587 | if self.deletion_status == GitRepositoryDeletionStatus.DELETING: | ||
286 | 1588 | raise CannotDeleteGitRepository( | ||
287 | 1589 | "This repository is already being deleted: %s" % | ||
288 | 1590 | self.unique_name) | ||
289 | 1591 | # Destructor operations can't take arguments. | ||
290 | 1592 | user = getUtility(ILaunchBag).user | ||
291 | 1593 | getUtility(IGitRepositoryDeleteJobSource).create(self, user) | ||
292 | 1594 | self.name = self.namespace.findUnusedName(self.name + "-deleting") | ||
293 | 1595 | self.deletion_status = GitRepositoryDeletionStatus.DELETING | ||
294 | 1596 | |||
295 | 1586 | 1597 | ||
296 | 1587 | class DeletionOperation: | 1598 | class DeletionOperation: |
297 | 1588 | """Represent an operation to perform as part of branch deletion.""" | 1599 | """Represent an operation to perform as part of branch deletion.""" |
298 | 1589 | 1600 | ||
299 | === modified file 'lib/lp/code/model/tests/test_branch.py' | |||
300 | --- lib/lp/code/model/tests/test_branch.py 2019-03-21 16:27:29 +0000 | |||
301 | +++ lib/lp/code/model/tests/test_branch.py 2019-03-21 16:27:30 +0000 | |||
302 | @@ -53,6 +53,7 @@ | |||
303 | 53 | RepositoryFormat, | 53 | RepositoryFormat, |
304 | 54 | ) | 54 | ) |
305 | 55 | from lp.code.enums import ( | 55 | from lp.code.enums import ( |
306 | 56 | BranchDeletionStatus, | ||
307 | 56 | BranchLifecycleStatus, | 57 | BranchLifecycleStatus, |
308 | 57 | BranchSubscriptionNotificationLevel, | 58 | BranchSubscriptionNotificationLevel, |
309 | 58 | BranchType, | 59 | BranchType, |
310 | @@ -74,6 +75,7 @@ | |||
311 | 74 | IBranch, | 75 | IBranch, |
312 | 75 | ) | 76 | ) |
313 | 76 | from lp.code.interfaces.branchjob import ( | 77 | from lp.code.interfaces.branchjob import ( |
314 | 78 | IBranchDeleteJobSource, | ||
315 | 77 | IBranchScanJobSource, | 79 | IBranchScanJobSource, |
316 | 78 | IBranchUpgradeJobSource, | 80 | IBranchUpgradeJobSource, |
317 | 79 | ) | 81 | ) |
318 | @@ -1483,6 +1485,34 @@ | |||
319 | 1483 | transaction.commit() | 1485 | transaction.commit() |
320 | 1484 | self.assertRaises(LostObjectError, getattr, webhook, 'target') | 1486 | self.assertRaises(LostObjectError, getattr, webhook, 'target') |
321 | 1485 | 1487 | ||
322 | 1488 | def test_delete_schedules_job(self): | ||
323 | 1489 | # Calling the delete method schedules a deletion job, which can be | ||
324 | 1490 | # processed. | ||
325 | 1491 | branch_id = self.branch.id | ||
326 | 1492 | branch_name = self.branch.unique_name | ||
327 | 1493 | self.branch.delete() | ||
328 | 1494 | update_trigger_modified_fields(self.branch) | ||
329 | 1495 | self.assertEqual(branch_name + "-deleting", self.branch.unique_name) | ||
330 | 1496 | self.assertEqual( | ||
331 | 1497 | BranchDeletionStatus.DELETING, self.branch.deletion_status) | ||
332 | 1498 | [job] = getUtility(IBranchDeleteJobSource).iterReady() | ||
333 | 1499 | self.assertEqual(branch_id, job.branch_id) | ||
334 | 1500 | self.assertEqual(self.user, job.requester) | ||
335 | 1501 | with dbuser(config.IBranchDeleteJobSource.dbuser): | ||
336 | 1502 | JobRunner([job]).runAll() | ||
337 | 1503 | |||
338 | 1504 | def test_delete_already_deleting(self): | ||
339 | 1505 | # Trying to delete a branch that is already being deleted raises | ||
340 | 1506 | # CannotDeleteBranch. | ||
341 | 1507 | self.branch.delete() | ||
342 | 1508 | self.assertRaises(CannotDeleteBranch, self.branch.delete) | ||
343 | 1509 | |||
344 | 1510 | def test_delete_stacked_branches(self): | ||
345 | 1511 | # Trying to delete a branch that is stacked upon raises | ||
346 | 1512 | # CannotDeleteBranch. | ||
347 | 1513 | self.factory.makeAnyBranch(stacked_on=self.branch) | ||
348 | 1514 | self.assertRaises(CannotDeleteBranch, self.branch.delete) | ||
349 | 1515 | |||
350 | 1486 | 1516 | ||
351 | 1487 | class TestBranchDeletionConsequences(TestCase): | 1517 | class TestBranchDeletionConsequences(TestCase): |
352 | 1488 | """Test determination and application of branch deletion consequences.""" | 1518 | """Test determination and application of branch deletion consequences.""" |
353 | 1489 | 1519 | ||
354 | === modified file 'lib/lp/code/model/tests/test_gitrepository.py' | |||
355 | --- lib/lp/code/model/tests/test_gitrepository.py 2019-03-21 16:27:29 +0000 | |||
356 | +++ lib/lp/code/model/tests/test_gitrepository.py 2019-03-21 16:27:30 +0000 | |||
357 | @@ -52,6 +52,7 @@ | |||
358 | 52 | CodeReviewNotificationLevel, | 52 | CodeReviewNotificationLevel, |
359 | 53 | GitGranteeType, | 53 | GitGranteeType, |
360 | 54 | GitObjectType, | 54 | GitObjectType, |
361 | 55 | GitRepositoryDeletionStatus, | ||
362 | 55 | GitRepositoryType, | 56 | GitRepositoryType, |
363 | 56 | TargetRevisionControlSystems, | 57 | TargetRevisionControlSystems, |
364 | 57 | ) | 58 | ) |
365 | @@ -71,6 +72,7 @@ | |||
366 | 71 | from lp.code.interfaces.defaultgit import ICanHasDefaultGitRepository | 72 | from lp.code.interfaces.defaultgit import ICanHasDefaultGitRepository |
367 | 72 | from lp.code.interfaces.gitjob import ( | 73 | from lp.code.interfaces.gitjob import ( |
368 | 73 | IGitRefScanJobSource, | 74 | IGitRefScanJobSource, |
369 | 75 | IGitRepositoryDeleteJobSource, | ||
370 | 74 | IGitRepositoryModifiedMailJobSource, | 76 | IGitRepositoryModifiedMailJobSource, |
371 | 75 | ) | 77 | ) |
372 | 76 | from lp.code.interfaces.gitlookup import IGitLookup | 78 | from lp.code.interfaces.gitlookup import IGitLookup |
373 | @@ -818,6 +820,29 @@ | |||
374 | 818 | GitActivity, GitActivity.repository_id == repository_id) | 820 | GitActivity, GitActivity.repository_id == repository_id) |
375 | 819 | self.assertEqual([], list(activities)) | 821 | self.assertEqual([], list(activities)) |
376 | 820 | 822 | ||
377 | 823 | def test_delete_schedules_job(self): | ||
378 | 824 | # Calling the delete method schedules a deletion job, which can be | ||
379 | 825 | # processed. | ||
380 | 826 | repository_id = self.repository.id | ||
381 | 827 | repository_name = self.repository.unique_name | ||
382 | 828 | self.repository.delete() | ||
383 | 829 | self.assertEqual( | ||
384 | 830 | repository_name + "-deleting", self.repository.unique_name) | ||
385 | 831 | self.assertEqual( | ||
386 | 832 | GitRepositoryDeletionStatus.DELETING, | ||
387 | 833 | self.repository.deletion_status) | ||
388 | 834 | [job] = getUtility(IGitRepositoryDeleteJobSource).iterReady() | ||
389 | 835 | self.assertEqual(repository_id, job.repository_id) | ||
390 | 836 | self.assertEqual(self.user, job.requester) | ||
391 | 837 | with dbuser(config.IGitRepositoryDeleteJobSource.dbuser): | ||
392 | 838 | JobRunner([job]).runAll() | ||
393 | 839 | |||
394 | 840 | def test_delete_already_deleting(self): | ||
395 | 841 | # Trying to delete a repository that is already being deleted raises | ||
396 | 842 | # CannotDeleteGitRepository. | ||
397 | 843 | self.repository.delete() | ||
398 | 844 | self.assertRaises(CannotDeleteGitRepository, self.repository.delete) | ||
399 | 845 | |||
400 | 821 | 846 | ||
401 | 822 | class TestGitRepositoryDeletionConsequences(TestCaseWithFactory): | 847 | class TestGitRepositoryDeletionConsequences(TestCaseWithFactory): |
402 | 823 | """Test determination and application of repository deletion | 848 | """Test determination and application of repository deletion |
403 | 824 | 849 | ||
404 | === modified file 'lib/lp/code/stories/branches/xx-branch-deletion.txt' | |||
405 | --- lib/lp/code/stories/branches/xx-branch-deletion.txt 2018-05-13 10:35:52 +0000 | |||
406 | +++ lib/lp/code/stories/branches/xx-branch-deletion.txt 2019-03-21 16:27:30 +0000 | |||
407 | @@ -52,6 +52,20 @@ | |||
408 | 52 | >>> print_feedback_messages(browser.contents) | 52 | >>> print_feedback_messages(browser.contents) |
409 | 53 | Branch ~alice/earthlynx/to-delete deleted... | 53 | Branch ~alice/earthlynx/to-delete deleted... |
410 | 54 | 54 | ||
411 | 55 | Branch deletion is asynchronous. If the user finds the branch again before | ||
412 | 56 | the deletion completes, then they see an indication that it is being | ||
413 | 57 | deleted, and there is no 'Delete branch' action. | ||
414 | 58 | |||
415 | 59 | >>> browser.open( | ||
416 | 60 | ... 'http://code.launchpad.dev/~alice/earthlynx/to-delete-deleting') | ||
417 | 61 | >>> print(extract_text(find_tag_by_id( | ||
418 | 62 | ... browser.contents, 'branch-deleting'))) | ||
419 | 63 | This branch is being deleted. | ||
420 | 64 | >>> browser.getLink('Delete branch') | ||
421 | 65 | Traceback (most recent call last): | ||
422 | 66 | ... | ||
423 | 67 | LinkNotFoundError | ||
424 | 68 | |||
425 | 55 | If the branch is junk, then the user is taken back to the code listing for | 69 | If the branch is junk, then the user is taken back to the code listing for |
426 | 56 | the deleted branch's owner. | 70 | the deleted branch's owner. |
427 | 57 | 71 | ||
428 | 58 | 72 | ||
429 | === modified file 'lib/lp/code/stories/webservice/xx-branch.txt' | |||
430 | --- lib/lp/code/stories/webservice/xx-branch.txt 2019-03-21 16:27:29 +0000 | |||
431 | +++ lib/lp/code/stories/webservice/xx-branch.txt 2019-03-21 16:27:30 +0000 | |||
432 | @@ -300,4 +300,17 @@ | |||
433 | 300 | 300 | ||
434 | 301 | >>> print_branches(webservice, '/widgets') | 301 | >>> print_branches(webservice, '/widgets') |
435 | 302 | ~eric/fooix/trunk - Development | 302 | ~eric/fooix/trunk - Development |
437 | 303 | 303 | ~eric/fooix/feature-branch-deleting - Experimental | |
438 | 304 | ~mary/blob/bar-deleting - Development | ||
439 | 305 | |||
440 | 306 | >>> from zope.component import getUtility | ||
441 | 307 | >>> from lp.code.interfaces.branchjob import IBranchDeleteJobSource | ||
442 | 308 | >>> from lp.services.config import config | ||
443 | 309 | >>> from lp.services.job.runner import JobRunner | ||
444 | 310 | |||
445 | 311 | >>> with permissive_security_policy(config.IBranchDeleteJobSource.dbuser): | ||
446 | 312 | ... job_source = getUtility(IBranchDeleteJobSource) | ||
447 | 313 | ... JobRunner.fromReady(job_source).runAll() | ||
448 | 314 | |||
449 | 315 | >>> print_branches(webservice, '/widgets') | ||
450 | 316 | ~eric/fooix/trunk - Development | ||
451 | 304 | 317 | ||
452 | === modified file 'lib/lp/code/templates/branch-index.pt' | |||
453 | --- lib/lp/code/templates/branch-index.pt 2019-01-23 17:52:34 +0000 | |||
454 | +++ lib/lp/code/templates/branch-index.pt 2019-03-21 16:27:30 +0000 | |||
455 | @@ -67,6 +67,10 @@ | |||
456 | 67 | <div metal:fill-slot="main"> | 67 | <div metal:fill-slot="main"> |
457 | 68 | 68 | ||
458 | 69 | <div class="yui-g first"> | 69 | <div class="yui-g first"> |
459 | 70 | <div tal:condition="python: context.deletion_status.name == 'DELETING'" | ||
460 | 71 | id="branch-deleting" class="warning message"> | ||
461 | 72 | This branch is being deleted. | ||
462 | 73 | </div> | ||
463 | 70 | <tal:branch-errors tal:replace="structure context/@@+messages" /> | 74 | <tal:branch-errors tal:replace="structure context/@@+messages" /> |
464 | 71 | </div> | 75 | </div> |
465 | 72 | 76 | ||
466 | 73 | 77 | ||
467 | === modified file 'lib/lp/code/templates/gitrepository-index.pt' | |||
468 | --- lib/lp/code/templates/gitrepository-index.pt 2019-01-28 15:36:56 +0000 | |||
469 | +++ lib/lp/code/templates/gitrepository-index.pt 2019-03-21 16:27:30 +0000 | |||
470 | @@ -33,6 +33,13 @@ | |||
471 | 33 | 33 | ||
472 | 34 | <div metal:fill-slot="main"> | 34 | <div metal:fill-slot="main"> |
473 | 35 | 35 | ||
474 | 36 | <div class="yui-g first"> | ||
475 | 37 | <div tal:condition="python: context.deletion_status.name == 'DELETING'" | ||
476 | 38 | id="repository-deleting" class="warning message"> | ||
477 | 39 | This repository is being deleted. | ||
478 | 40 | </div> | ||
479 | 41 | </div> | ||
480 | 42 | |||
481 | 36 | <div id="repository-description" tal:condition="context/description" | 43 | <div id="repository-description" tal:condition="context/description" |
482 | 37 | class="summary" | 44 | class="summary" |
483 | 38 | tal:content="structure context/description/fmt:text-to-html" /> | 45 | tal:content="structure context/description/fmt:text-to-html" /> |
484 | 39 | 46 | ||
485 | === modified file 'lib/lp/scripts/garbo.py' | |||
486 | --- lib/lp/scripts/garbo.py 2019-01-30 11:23:33 +0000 | |||
487 | +++ lib/lp/scripts/garbo.py 2019-03-21 16:27:30 +0000 | |||
488 | @@ -40,6 +40,7 @@ | |||
489 | 40 | Or, | 40 | Or, |
490 | 41 | Row, | 41 | Row, |
491 | 42 | SQL, | 42 | SQL, |
492 | 43 | Update, | ||
493 | 43 | ) | 44 | ) |
494 | 44 | from storm.info import ClassAlias | 45 | from storm.info import ClassAlias |
495 | 45 | from storm.store import EmptyResultSet | 46 | from storm.store import EmptyResultSet |
496 | @@ -57,13 +58,19 @@ | |||
497 | 57 | BugWatchScheduler, | 58 | BugWatchScheduler, |
498 | 58 | MAX_SAMPLE_SIZE, | 59 | MAX_SAMPLE_SIZE, |
499 | 59 | ) | 60 | ) |
500 | 61 | from lp.code.enums import ( | ||
501 | 62 | BranchDeletionStatus, | ||
502 | 63 | GitRepositoryDeletionStatus, | ||
503 | 64 | ) | ||
504 | 60 | from lp.code.interfaces.revision import IRevisionSet | 65 | from lp.code.interfaces.revision import IRevisionSet |
505 | 66 | from lp.code.model.branch import Branch | ||
506 | 61 | from lp.code.model.codeimportevent import CodeImportEvent | 67 | from lp.code.model.codeimportevent import CodeImportEvent |
507 | 62 | from lp.code.model.codeimportresult import CodeImportResult | 68 | from lp.code.model.codeimportresult import CodeImportResult |
508 | 63 | from lp.code.model.diff import ( | 69 | from lp.code.model.diff import ( |
509 | 64 | Diff, | 70 | Diff, |
510 | 65 | PreviewDiff, | 71 | PreviewDiff, |
511 | 66 | ) | 72 | ) |
512 | 73 | from lp.code.model.gitrepository import GitRepository | ||
513 | 67 | from lp.code.model.revision import ( | 74 | from lp.code.model.revision import ( |
514 | 68 | RevisionAuthor, | 75 | RevisionAuthor, |
515 | 69 | RevisionCache, | 76 | RevisionCache, |
516 | @@ -1611,6 +1618,62 @@ | |||
517 | 1611 | """ % (SnapBuildJobType.STORE_UPLOAD.value, JobStatus.COMPLETED.value) | 1618 | """ % (SnapBuildJobType.STORE_UPLOAD.value, JobStatus.COMPLETED.value) |
518 | 1612 | 1619 | ||
519 | 1613 | 1620 | ||
520 | 1621 | class BranchDeletionStatusPopulator(TunableLoop): | ||
521 | 1622 | """Populates Branch.deletion_status with ACTIVE.""" | ||
522 | 1623 | |||
523 | 1624 | maximum_chunk_size = 5000 | ||
524 | 1625 | |||
525 | 1626 | def __init__(self, log, abort_time=None): | ||
526 | 1627 | super(BranchDeletionStatusPopulator, self).__init__(log, abort_time) | ||
527 | 1628 | self.start_at = 1 | ||
528 | 1629 | self.store = IMasterStore(Branch) | ||
529 | 1630 | |||
530 | 1631 | def findBranches(self): | ||
531 | 1632 | return self.store.find( | ||
532 | 1633 | Branch, | ||
533 | 1634 | Branch.id >= self.start_at, | ||
534 | 1635 | Branch._deletion_status == None).order_by(Branch.id) | ||
535 | 1636 | |||
536 | 1637 | def isDone(self): | ||
537 | 1638 | return self.findBranches().is_empty() | ||
538 | 1639 | |||
539 | 1640 | def __call__(self, chunk_size): | ||
540 | 1641 | ids = [branch.id for branch in self.findBranches()] | ||
541 | 1642 | self.store.execute(Update( | ||
542 | 1643 | {Branch._deletion_status: BranchDeletionStatus.ACTIVE.value}, | ||
543 | 1644 | where=Branch.id.is_in(ids), table=Branch)) | ||
544 | 1645 | transaction.commit() | ||
545 | 1646 | |||
546 | 1647 | |||
547 | 1648 | class GitRepositoryDeletionStatusPopulator(TunableLoop): | ||
548 | 1649 | """Populates GitRepository.deletion_status with ACTIVE.""" | ||
549 | 1650 | |||
550 | 1651 | maximum_chunk_size = 5000 | ||
551 | 1652 | |||
552 | 1653 | def __init__(self, log, abort_time=None): | ||
553 | 1654 | super(GitRepositoryDeletionStatusPopulator, self).__init__( | ||
554 | 1655 | log, abort_time) | ||
555 | 1656 | self.start_at = 1 | ||
556 | 1657 | self.store = IMasterStore(GitRepository) | ||
557 | 1658 | |||
558 | 1659 | def findRepositories(self): | ||
559 | 1660 | return self.store.find( | ||
560 | 1661 | GitRepository, | ||
561 | 1662 | GitRepository.id >= self.start_at, | ||
562 | 1663 | GitRepository._deletion_status == None).order_by(GitRepository.id) | ||
563 | 1664 | |||
564 | 1665 | def isDone(self): | ||
565 | 1666 | return self.findRepositories().is_empty() | ||
566 | 1667 | |||
567 | 1668 | def __call__(self, chunk_size): | ||
568 | 1669 | ids = [branch.id for branch in self.findRepositories()] | ||
569 | 1670 | self.store.execute(Update( | ||
570 | 1671 | {GitRepository._deletion_status: | ||
571 | 1672 | GitRepositoryDeletionStatus.ACTIVE.value}, | ||
572 | 1673 | where=GitRepository.id.is_in(ids), table=GitRepository)) | ||
573 | 1674 | transaction.commit() | ||
574 | 1675 | |||
575 | 1676 | |||
576 | 1614 | class BaseDatabaseGarbageCollector(LaunchpadCronScript): | 1677 | class BaseDatabaseGarbageCollector(LaunchpadCronScript): |
577 | 1615 | """Abstract base class to run a collection of TunableLoops.""" | 1678 | """Abstract base class to run a collection of TunableLoops.""" |
578 | 1616 | script_name = None # Script name for locking and database user. Override. | 1679 | script_name = None # Script name for locking and database user. Override. |
579 | @@ -1884,6 +1947,7 @@ | |||
580 | 1884 | script_name = 'garbo-daily' | 1947 | script_name = 'garbo-daily' |
581 | 1885 | tunable_loops = [ | 1948 | tunable_loops = [ |
582 | 1886 | AnswerContactPruner, | 1949 | AnswerContactPruner, |
583 | 1950 | BranchDeletionStatusPopulator, | ||
584 | 1887 | BranchJobPruner, | 1951 | BranchJobPruner, |
585 | 1888 | BugNotificationPruner, | 1952 | BugNotificationPruner, |
586 | 1889 | BugWatchActivityPruner, | 1953 | BugWatchActivityPruner, |
587 | @@ -1891,6 +1955,7 @@ | |||
588 | 1891 | CodeImportResultPruner, | 1955 | CodeImportResultPruner, |
589 | 1892 | DiffPruner, | 1956 | DiffPruner, |
590 | 1893 | GitJobPruner, | 1957 | GitJobPruner, |
591 | 1958 | GitRepositoryDeletionStatusPopulator, | ||
592 | 1894 | HWSubmissionEmailLinker, | 1959 | HWSubmissionEmailLinker, |
593 | 1895 | LiveFSFilePruner, | 1960 | LiveFSFilePruner, |
594 | 1896 | LoginTokenPruner, | 1961 | LoginTokenPruner, |
595 | 1897 | 1962 | ||
596 | === modified file 'lib/lp/scripts/tests/test_garbo.py' | |||
597 | --- lib/lp/scripts/tests/test_garbo.py 2019-01-30 11:23:33 +0000 | |||
598 | +++ lib/lp/scripts/tests/test_garbo.py 2019-03-21 16:27:30 +0000 | |||
599 | @@ -15,6 +15,7 @@ | |||
600 | 15 | from StringIO import StringIO | 15 | from StringIO import StringIO |
601 | 16 | import time | 16 | import time |
602 | 17 | 17 | ||
603 | 18 | from psycopg2 import IntegrityError | ||
604 | 18 | from pytz import UTC | 19 | from pytz import UTC |
605 | 19 | from storm.exceptions import LostObjectError | 20 | from storm.exceptions import LostObjectError |
606 | 20 | from storm.expr import ( | 21 | from storm.expr import ( |
607 | @@ -54,7 +55,11 @@ | |||
608 | 54 | BranchFormat, | 55 | BranchFormat, |
609 | 55 | RepositoryFormat, | 56 | RepositoryFormat, |
610 | 56 | ) | 57 | ) |
612 | 57 | from lp.code.enums import CodeImportResultStatus | 58 | from lp.code.enums import ( |
613 | 59 | BranchDeletionStatus, | ||
614 | 60 | CodeImportResultStatus, | ||
615 | 61 | GitRepositoryDeletionStatus, | ||
616 | 62 | ) | ||
617 | 58 | from lp.code.interfaces.codeimportevent import ICodeImportEventSet | 63 | from lp.code.interfaces.codeimportevent import ICodeImportEventSet |
618 | 59 | from lp.code.interfaces.gitrepository import IGitRepositorySet | 64 | from lp.code.interfaces.gitrepository import IGitRepositorySet |
619 | 60 | from lp.code.model.branchjob import ( | 65 | from lp.code.model.branchjob import ( |
620 | @@ -1606,6 +1611,84 @@ | |||
621 | 1606 | # retained. | 1611 | # retained. |
622 | 1607 | self._test_SnapFilePruner('foo.snap', None, 30, expected_count=1) | 1612 | self._test_SnapFilePruner('foo.snap', None, 30, expected_count=1) |
623 | 1608 | 1613 | ||
624 | 1614 | def test_BranchDeletionStatusPopulator(self): | ||
625 | 1615 | switch_dbuser('testadmin') | ||
626 | 1616 | old_branches = [self.factory.makeAnyBranch() for _ in range(2)] | ||
627 | 1617 | for branch in old_branches: | ||
628 | 1618 | removeSecurityProxy(branch)._deletion_status = None | ||
629 | 1619 | try: | ||
630 | 1620 | Store.of(old_branches[0]).flush() | ||
631 | 1621 | except IntegrityError: | ||
632 | 1622 | # Now enforced by DB NOT NULL constraint; backfilling is no | ||
633 | 1623 | # longer necessary. | ||
634 | 1624 | return | ||
635 | 1625 | active_branches = [self.factory.makeAnyBranch() for _ in range(2)] | ||
636 | 1626 | for branch in active_branches: | ||
637 | 1627 | removeSecurityProxy(branch)._deletion_status = ( | ||
638 | 1628 | BranchDeletionStatus.ACTIVE) | ||
639 | 1629 | deleting_branches = [self.factory.makeAnyBranch() for _ in range(2)] | ||
640 | 1630 | for branch in deleting_branches: | ||
641 | 1631 | removeSecurityProxy(branch)._deletion_status = ( | ||
642 | 1632 | BranchDeletionStatus.DELETING) | ||
643 | 1633 | transaction.commit() | ||
644 | 1634 | |||
645 | 1635 | self.runDaily() | ||
646 | 1636 | |||
647 | 1637 | # Old branches are backfilled. | ||
648 | 1638 | for branch in old_branches: | ||
649 | 1639 | self.assertEqual( | ||
650 | 1640 | BranchDeletionStatus.ACTIVE, | ||
651 | 1641 | removeSecurityProxy(branch)._deletion_status) | ||
652 | 1642 | # Other branches are left alone. | ||
653 | 1643 | for branch in active_branches: | ||
654 | 1644 | self.assertEqual( | ||
655 | 1645 | BranchDeletionStatus.ACTIVE, | ||
656 | 1646 | removeSecurityProxy(branch)._deletion_status) | ||
657 | 1647 | for branch in deleting_branches: | ||
658 | 1648 | self.assertEqual( | ||
659 | 1649 | BranchDeletionStatus.DELETING, | ||
660 | 1650 | removeSecurityProxy(branch)._deletion_status) | ||
661 | 1651 | |||
662 | 1652 | def test_GitRepositoryDeletionStatusPopulator(self): | ||
663 | 1653 | switch_dbuser('testadmin') | ||
664 | 1654 | old_repositories = [self.factory.makeGitRepository() for _ in range(2)] | ||
665 | 1655 | for repository in old_repositories: | ||
666 | 1656 | removeSecurityProxy(repository)._deletion_status = None | ||
667 | 1657 | try: | ||
668 | 1658 | Store.of(old_repositories[0]).flush() | ||
669 | 1659 | except IntegrityError: | ||
670 | 1660 | # Now enforced by DB NOT NULL constraint; backfilling is no | ||
671 | 1661 | # longer necessary. | ||
672 | 1662 | return | ||
673 | 1663 | active_repositories = [ | ||
674 | 1664 | self.factory.makeGitRepository() for _ in range(2)] | ||
675 | 1665 | for repository in active_repositories: | ||
676 | 1666 | removeSecurityProxy(repository)._deletion_status = ( | ||
677 | 1667 | GitRepositoryDeletionStatus.ACTIVE) | ||
678 | 1668 | deleting_repositories = [ | ||
679 | 1669 | self.factory.makeGitRepository() for _ in range(2)] | ||
680 | 1670 | for repository in deleting_repositories: | ||
681 | 1671 | removeSecurityProxy(repository)._deletion_status = ( | ||
682 | 1672 | GitRepositoryDeletionStatus.DELETING) | ||
683 | 1673 | transaction.commit() | ||
684 | 1674 | |||
685 | 1675 | self.runDaily() | ||
686 | 1676 | |||
687 | 1677 | # Old repositories are backfilled. | ||
688 | 1678 | for repository in old_repositories: | ||
689 | 1679 | self.assertEqual( | ||
690 | 1680 | GitRepositoryDeletionStatus.ACTIVE, | ||
691 | 1681 | removeSecurityProxy(repository)._deletion_status) | ||
692 | 1682 | # Other repositories are left alone. | ||
693 | 1683 | for repository in active_repositories: | ||
694 | 1684 | self.assertEqual( | ||
695 | 1685 | GitRepositoryDeletionStatus.ACTIVE, | ||
696 | 1686 | removeSecurityProxy(repository)._deletion_status) | ||
697 | 1687 | for repository in deleting_repositories: | ||
698 | 1688 | self.assertEqual( | ||
699 | 1689 | GitRepositoryDeletionStatus.DELETING, | ||
700 | 1690 | removeSecurityProxy(repository)._deletion_status) | ||
701 | 1691 | |||
702 | 1609 | 1692 | ||
703 | 1610 | class TestGarboTasks(TestCaseWithFactory): | 1693 | class TestGarboTasks(TestCaseWithFactory): |
704 | 1611 | layer = LaunchpadZopelessLayer | 1694 | layer = LaunchpadZopelessLayer |
This seems unnecessary since adding some more indexes has made deletion fast enough, so withdrawing this until we have a clear need.