Merge ~cjwatson/launchpad:move-package into launchpad:master
- Git
- lp:~cjwatson/launchpad
- move-package
- Merge into master
Proposed by
Colin Watson
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Colin Watson | ||||
Approved revision: | 0ed27c6fcfba459ec61213fb6fd6a7a6b33c2c1a | ||||
Merge reported by: | Otto Co-Pilot | ||||
Merged at revision: | not available | ||||
Proposed branch: | ~cjwatson/launchpad:move-package | ||||
Merge into: | launchpad:master | ||||
Diff against target: |
808 lines (+312/-50) 11 files modified
lib/lp/code/vocabularies/sourcepackagerecipe.py (+2/-2) lib/lp/soyuz/browser/archive.py (+2/-11) lib/lp/soyuz/interfaces/archive.py (+3/-1) lib/lp/soyuz/interfaces/packagecopyjob.py (+7/-2) lib/lp/soyuz/model/archive.py (+9/-6) lib/lp/soyuz/model/packagecopyjob.py (+15/-9) lib/lp/soyuz/scripts/packagecopier.py (+63/-9) lib/lp/soyuz/scripts/tests/test_copypackage.py (+44/-0) lib/lp/soyuz/tests/test_archive.py (+124/-5) lib/lp/soyuz/tests/test_packagecopyjob.py (+28/-3) lib/lp/soyuz/vocabularies.py (+15/-2) |
||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Kristian Glass (community) | Approve | ||
Review via email: mp+373942@code.launchpad.net |
Commit message
Add an atomic "move package" operation
Archive.copyPackage and Archive.
argument, which causes the source publication to be deleted if the copy
succeeds.
This allows us to fix a long-standing problem with Ubuntu's
proposed-migration process: it needs to do a copy and delete when
migrating packages from devel-proposed to devel, but since the copy is
asynchronous it can fail without proposed-migration being aware of this,
leading to the package in question simply being removed. Moving the
deletion into the copier avoids this problem.
LP: #1329052
Description of the change
To post a comment you must log in.
Revision history for this message
Otto Co-Pilot (otto-copilot) wrote : | # |
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/lib/lp/code/vocabularies/sourcepackagerecipe.py b/lib/lp/code/vocabularies/sourcepackagerecipe.py | |||
2 | index 0c56101..31cf4bc 100644 | |||
3 | --- a/lib/lp/code/vocabularies/sourcepackagerecipe.py | |||
4 | +++ b/lib/lp/code/vocabularies/sourcepackagerecipe.py | |||
5 | @@ -1,4 +1,4 @@ | |||
7 | 1 | # Copyright 2010-2018 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2010-2019 Canonical Ltd. This software is licensed under the |
8 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
9 | 3 | 3 | ||
10 | 4 | """Source Package Recipe vocabularies used in the lp/code modules.""" | 4 | """Source Package Recipe vocabularies used in the lp/code modules.""" |
11 | @@ -22,8 +22,8 @@ from lp.services.webapp.vocabulary import ( | |||
12 | 22 | IHugeVocabulary, | 22 | IHugeVocabulary, |
13 | 23 | SQLObjectVocabularyBase, | 23 | SQLObjectVocabularyBase, |
14 | 24 | ) | 24 | ) |
15 | 25 | from lp.soyuz.browser.archive import make_archive_vocabulary | ||
16 | 26 | from lp.soyuz.interfaces.archive import IArchiveSet | 25 | from lp.soyuz.interfaces.archive import IArchiveSet |
17 | 26 | from lp.soyuz.vocabularies import make_archive_vocabulary | ||
18 | 27 | 27 | ||
19 | 28 | 28 | ||
20 | 29 | @implementer(IHugeVocabulary) | 29 | @implementer(IHugeVocabulary) |
21 | diff --git a/lib/lp/soyuz/browser/archive.py b/lib/lp/soyuz/browser/archive.py | |||
22 | index 6dd6cfe..3170ea0 100644 | |||
23 | --- a/lib/lp/soyuz/browser/archive.py | |||
24 | +++ b/lib/lp/soyuz/browser/archive.py | |||
25 | @@ -1,4 +1,4 @@ | |||
27 | 1 | # Copyright 2009-2018 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2009-2019 Canonical Ltd. This software is licensed under the |
28 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
29 | 3 | 3 | ||
30 | 4 | """Browser views for archive.""" | 4 | """Browser views for archive.""" |
31 | @@ -23,7 +23,6 @@ __all__ = [ | |||
32 | 23 | 'ArchiveView', | 23 | 'ArchiveView', |
33 | 24 | 'ArchiveViewBase', | 24 | 'ArchiveViewBase', |
34 | 25 | 'EnableProcessorsMixin', | 25 | 'EnableProcessorsMixin', |
35 | 26 | 'make_archive_vocabulary', | ||
36 | 27 | 'PackageCopyingMixin', | 26 | 'PackageCopyingMixin', |
37 | 28 | 'traverse_named_ppa', | 27 | 'traverse_named_ppa', |
38 | 29 | ] | 28 | ] |
39 | @@ -175,6 +174,7 @@ from lp.soyuz.model.archive import ( | |||
40 | 175 | ) | 174 | ) |
41 | 176 | from lp.soyuz.model.publishing import SourcePackagePublishingHistory | 175 | from lp.soyuz.model.publishing import SourcePackagePublishingHistory |
42 | 177 | from lp.soyuz.scripts.packagecopier import check_copy_permissions | 176 | from lp.soyuz.scripts.packagecopier import check_copy_permissions |
43 | 177 | from lp.soyuz.vocabularies import make_archive_vocabulary | ||
44 | 178 | 178 | ||
45 | 179 | 179 | ||
46 | 180 | class ArchiveBadges(HasBadgeBase): | 180 | class ArchiveBadges(HasBadgeBase): |
47 | @@ -1433,15 +1433,6 @@ class PackageCopyingMixin: | |||
48 | 1433 | return True | 1433 | return True |
49 | 1434 | 1434 | ||
50 | 1435 | 1435 | ||
51 | 1436 | def make_archive_vocabulary(archives): | ||
52 | 1437 | terms = [] | ||
53 | 1438 | for archive in archives: | ||
54 | 1439 | label = '%s [%s]' % (archive.displayname, archive.reference) | ||
55 | 1440 | terms.append(SimpleTerm(archive, archive.reference, label)) | ||
56 | 1441 | terms.sort(key=lambda x: x.value.reference) | ||
57 | 1442 | return SimpleVocabulary(terms) | ||
58 | 1443 | |||
59 | 1444 | |||
60 | 1445 | class ArchivePackageCopyingView(ArchiveSourceSelectionFormView, | 1436 | class ArchivePackageCopyingView(ArchiveSourceSelectionFormView, |
61 | 1446 | PackageCopyingMixin): | 1437 | PackageCopyingMixin): |
62 | 1447 | """Archive package copying view class. | 1438 | """Archive package copying view class. |
63 | diff --git a/lib/lp/soyuz/interfaces/archive.py b/lib/lp/soyuz/interfaces/archive.py | |||
64 | index e28f08b..2aae255 100644 | |||
65 | --- a/lib/lp/soyuz/interfaces/archive.py | |||
66 | +++ b/lib/lp/soyuz/interfaces/archive.py | |||
67 | @@ -1522,7 +1522,7 @@ class IArchiveView(IHasBuildRecords): | |||
68 | 1522 | person, to_series=None, include_binaries=False, | 1522 | person, to_series=None, include_binaries=False, |
69 | 1523 | sponsored=None, unembargo=False, auto_approve=False, | 1523 | sponsored=None, unembargo=False, auto_approve=False, |
70 | 1524 | silent=False, from_pocket=None, from_series=None, | 1524 | silent=False, from_pocket=None, from_series=None, |
72 | 1525 | phased_update_percentage=None): | 1525 | phased_update_percentage=None, move=False): |
73 | 1526 | """Copy a single named source into this archive. | 1526 | """Copy a single named source into this archive. |
74 | 1527 | 1527 | ||
75 | 1528 | Asynchronously copy a specific version of a named source to the | 1528 | Asynchronously copy a specific version of a named source to the |
76 | @@ -1563,6 +1563,8 @@ class IArchiveView(IHasBuildRecords): | |||
77 | 1563 | omitted, copy from any series with a matching version. | 1563 | omitted, copy from any series with a matching version. |
78 | 1564 | :param phased_update_percentage: the phased update percentage to | 1564 | :param phased_update_percentage: the phased update percentage to |
79 | 1565 | apply to the copied publication. | 1565 | apply to the copied publication. |
80 | 1566 | :param move: if True, delete the source publication after copying it | ||
81 | 1567 | to the destination. | ||
82 | 1566 | 1568 | ||
83 | 1567 | :raises NoSuchSourcePackageName: if the source name is invalid | 1569 | :raises NoSuchSourcePackageName: if the source name is invalid |
84 | 1568 | :raises PocketNotFound: if the pocket name is invalid | 1570 | :raises PocketNotFound: if the pocket name is invalid |
85 | diff --git a/lib/lp/soyuz/interfaces/packagecopyjob.py b/lib/lp/soyuz/interfaces/packagecopyjob.py | |||
86 | index 32ef2dc..043fff5 100644 | |||
87 | --- a/lib/lp/soyuz/interfaces/packagecopyjob.py | |||
88 | +++ b/lib/lp/soyuz/interfaces/packagecopyjob.py | |||
89 | @@ -1,4 +1,4 @@ | |||
91 | 1 | # Copyright 2010-2013 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2010-2019 Canonical Ltd. This software is licensed under the |
92 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
93 | 3 | 3 | ||
94 | 4 | __metaclass__ = type | 4 | __metaclass__ = type |
95 | @@ -130,7 +130,7 @@ class IPlainPackageCopyJobSource(IJobSource): | |||
96 | 130 | copy_policy=PackageCopyPolicy.INSECURE, requester=None, | 130 | copy_policy=PackageCopyPolicy.INSECURE, requester=None, |
97 | 131 | sponsored=None, unembargo=False, auto_approve=False, | 131 | sponsored=None, unembargo=False, auto_approve=False, |
98 | 132 | silent=False, source_distroseries=None, source_pocket=None, | 132 | silent=False, source_distroseries=None, source_pocket=None, |
100 | 133 | phased_update_percentage=None): | 133 | phased_update_percentage=None, move=False): |
101 | 134 | """Create a new `IPlainPackageCopyJob`. | 134 | """Create a new `IPlainPackageCopyJob`. |
102 | 135 | 135 | ||
103 | 136 | :param package_name: The name of the source package to copy. | 136 | :param package_name: The name of the source package to copy. |
104 | @@ -162,6 +162,8 @@ class IPlainPackageCopyJobSource(IJobSource): | |||
105 | 162 | from any pocket with a matching version. | 162 | from any pocket with a matching version. |
106 | 163 | :param phased_update_percentage: The phased update percentage to | 163 | :param phased_update_percentage: The phased update percentage to |
107 | 164 | apply to the copied publication. | 164 | apply to the copied publication. |
108 | 165 | :param move: If True, delete the source publication after copying it | ||
109 | 166 | to the destination. | ||
110 | 165 | """ | 167 | """ |
111 | 166 | 168 | ||
112 | 167 | def createMultiple(target_distroseries, copy_tasks, requester, | 169 | def createMultiple(target_distroseries, copy_tasks, requester, |
113 | @@ -254,6 +256,9 @@ class IPlainPackageCopyJob(IRunnableJob): | |||
114 | 254 | phased_update_percentage = Int( | 256 | phased_update_percentage = Int( |
115 | 255 | title=_("Phased update percentage"), required=False, readonly=True) | 257 | title=_("Phased update percentage"), required=False, readonly=True) |
116 | 256 | 258 | ||
117 | 259 | move = Bool( | ||
118 | 260 | title=_("Delete source after copy"), required=False, readonly=True) | ||
119 | 261 | |||
120 | 257 | def addSourceOverride(override): | 262 | def addSourceOverride(override): |
121 | 258 | """Add an `ISourceOverride` to the metadata.""" | 263 | """Add an `ISourceOverride` to the metadata.""" |
122 | 259 | 264 | ||
123 | diff --git a/lib/lp/soyuz/model/archive.py b/lib/lp/soyuz/model/archive.py | |||
124 | index acf8541..cb3f089 100644 | |||
125 | --- a/lib/lp/soyuz/model/archive.py | |||
126 | +++ b/lib/lp/soyuz/model/archive.py | |||
127 | @@ -1829,7 +1829,7 @@ class Archive(SQLBase): | |||
128 | 1829 | person, to_series=None, include_binaries=False, | 1829 | person, to_series=None, include_binaries=False, |
129 | 1830 | sponsored=None, unembargo=False, auto_approve=False, | 1830 | sponsored=None, unembargo=False, auto_approve=False, |
130 | 1831 | silent=False, from_pocket=None, from_series=None, | 1831 | silent=False, from_pocket=None, from_series=None, |
132 | 1832 | phased_update_percentage=None): | 1832 | phased_update_percentage=None, move=False): |
133 | 1833 | """See `IArchive`.""" | 1833 | """See `IArchive`.""" |
134 | 1834 | # Asynchronously copy a package using the job system. | 1834 | # Asynchronously copy a package using the job system. |
135 | 1835 | from lp.soyuz.scripts.packagecopier import check_copy_permissions | 1835 | from lp.soyuz.scripts.packagecopier import check_copy_permissions |
136 | @@ -1854,7 +1854,8 @@ class Archive(SQLBase): | |||
137 | 1854 | from_pocket=from_pocket) | 1854 | from_pocket=from_pocket) |
138 | 1855 | if series is None: | 1855 | if series is None: |
139 | 1856 | series = source.distroseries | 1856 | series = source.distroseries |
141 | 1857 | check_copy_permissions(person, self, series, pocket, [source]) | 1857 | check_copy_permissions( |
142 | 1858 | person, self, series, pocket, [source], move=move) | ||
143 | 1858 | 1859 | ||
144 | 1859 | job_source = getUtility(IPlainPackageCopyJobSource) | 1860 | job_source = getUtility(IPlainPackageCopyJobSource) |
145 | 1860 | job_source.create( | 1861 | job_source.create( |
146 | @@ -1866,12 +1867,12 @@ class Archive(SQLBase): | |||
147 | 1866 | sponsored=sponsored, unembargo=unembargo, | 1867 | sponsored=sponsored, unembargo=unembargo, |
148 | 1867 | auto_approve=auto_approve, silent=silent, | 1868 | auto_approve=auto_approve, silent=silent, |
149 | 1868 | source_distroseries=from_series, source_pocket=from_pocket, | 1869 | source_distroseries=from_series, source_pocket=from_pocket, |
151 | 1869 | phased_update_percentage=phased_update_percentage) | 1870 | phased_update_percentage=phased_update_percentage, move=move) |
152 | 1870 | 1871 | ||
153 | 1871 | def copyPackages(self, source_names, from_archive, to_pocket, | 1872 | def copyPackages(self, source_names, from_archive, to_pocket, |
154 | 1872 | person, to_series=None, from_series=None, | 1873 | person, to_series=None, from_series=None, |
155 | 1873 | include_binaries=None, sponsored=None, unembargo=False, | 1874 | include_binaries=None, sponsored=None, unembargo=False, |
157 | 1874 | auto_approve=False, silent=False): | 1875 | auto_approve=False, silent=False, move=False): |
158 | 1875 | """See `IArchive`.""" | 1876 | """See `IArchive`.""" |
159 | 1876 | from lp.soyuz.scripts.packagecopier import check_copy_permissions | 1877 | from lp.soyuz.scripts.packagecopier import check_copy_permissions |
160 | 1877 | sources = self._collectLatestPublishedSources( | 1878 | sources = self._collectLatestPublishedSources( |
161 | @@ -1880,7 +1881,8 @@ class Archive(SQLBase): | |||
162 | 1880 | # Now do a mass check of permissions. | 1881 | # Now do a mass check of permissions. |
163 | 1881 | pocket = self._text_to_pocket(to_pocket) | 1882 | pocket = self._text_to_pocket(to_pocket) |
164 | 1882 | series = self._text_to_series(to_series) | 1883 | series = self._text_to_series(to_series) |
166 | 1883 | check_copy_permissions(person, self, series, pocket, sources) | 1884 | check_copy_permissions( |
167 | 1885 | person, self, series, pocket, sources, move=move) | ||
168 | 1884 | 1886 | ||
169 | 1885 | # If we get this far then we can create the PackageCopyJob. | 1887 | # If we get this far then we can create the PackageCopyJob. |
170 | 1886 | copy_tasks = [] | 1888 | copy_tasks = [] |
171 | @@ -1899,7 +1901,8 @@ class Archive(SQLBase): | |||
172 | 1899 | job_source.createMultiple( | 1901 | job_source.createMultiple( |
173 | 1900 | copy_tasks, person, copy_policy=PackageCopyPolicy.MASS_SYNC, | 1902 | copy_tasks, person, copy_policy=PackageCopyPolicy.MASS_SYNC, |
174 | 1901 | include_binaries=include_binaries, sponsored=sponsored, | 1903 | include_binaries=include_binaries, sponsored=sponsored, |
176 | 1902 | unembargo=unembargo, auto_approve=auto_approve, silent=silent) | 1904 | unembargo=unembargo, auto_approve=auto_approve, silent=silent, |
177 | 1905 | move=move) | ||
178 | 1903 | 1906 | ||
179 | 1904 | def _collectLatestPublishedSources(self, from_archive, from_series, | 1907 | def _collectLatestPublishedSources(self, from_archive, from_series, |
180 | 1905 | source_names): | 1908 | source_names): |
181 | diff --git a/lib/lp/soyuz/model/packagecopyjob.py b/lib/lp/soyuz/model/packagecopyjob.py | |||
182 | index b4d92b2..90389e9 100644 | |||
183 | --- a/lib/lp/soyuz/model/packagecopyjob.py | |||
184 | +++ b/lib/lp/soyuz/model/packagecopyjob.py | |||
185 | @@ -1,4 +1,4 @@ | |||
187 | 1 | # Copyright 2010-2016 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2010-2019 Canonical Ltd. This software is licensed under the |
188 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
189 | 3 | 3 | ||
190 | 4 | __metaclass__ = type | 4 | __metaclass__ = type |
191 | @@ -275,7 +275,7 @@ class PlainPackageCopyJob(PackageCopyJobDerived): | |||
192 | 275 | include_binaries, sponsored=None, unembargo=False, | 275 | include_binaries, sponsored=None, unembargo=False, |
193 | 276 | auto_approve=False, silent=False, | 276 | auto_approve=False, silent=False, |
194 | 277 | source_distroseries=None, source_pocket=None, | 277 | source_distroseries=None, source_pocket=None, |
196 | 278 | phased_update_percentage=None): | 278 | phased_update_percentage=None, move=False): |
197 | 279 | """Produce a metadata dict for this job.""" | 279 | """Produce a metadata dict for this job.""" |
198 | 280 | return { | 280 | return { |
199 | 281 | 'target_pocket': target_pocket.value, | 281 | 'target_pocket': target_pocket.value, |
200 | @@ -289,6 +289,7 @@ class PlainPackageCopyJob(PackageCopyJobDerived): | |||
201 | 289 | source_distroseries.name if source_distroseries else None, | 289 | source_distroseries.name if source_distroseries else None, |
202 | 290 | 'source_pocket': source_pocket.value if source_pocket else None, | 290 | 'source_pocket': source_pocket.value if source_pocket else None, |
203 | 291 | 'phased_update_percentage': phased_update_percentage, | 291 | 'phased_update_percentage': phased_update_percentage, |
204 | 292 | 'move': move, | ||
205 | 292 | } | 293 | } |
206 | 293 | 294 | ||
207 | 294 | @classmethod | 295 | @classmethod |
208 | @@ -298,14 +299,14 @@ class PlainPackageCopyJob(PackageCopyJobDerived): | |||
209 | 298 | copy_policy=PackageCopyPolicy.INSECURE, requester=None, | 299 | copy_policy=PackageCopyPolicy.INSECURE, requester=None, |
210 | 299 | sponsored=None, unembargo=False, auto_approve=False, | 300 | sponsored=None, unembargo=False, auto_approve=False, |
211 | 300 | silent=False, source_distroseries=None, source_pocket=None, | 301 | silent=False, source_distroseries=None, source_pocket=None, |
213 | 301 | phased_update_percentage=None): | 302 | phased_update_percentage=None, move=False): |
214 | 302 | """See `IPlainPackageCopyJobSource`.""" | 303 | """See `IPlainPackageCopyJobSource`.""" |
215 | 303 | assert package_version is not None, "No package version specified." | 304 | assert package_version is not None, "No package version specified." |
216 | 304 | assert requester is not None, "No requester specified." | 305 | assert requester is not None, "No requester specified." |
217 | 305 | metadata = cls._makeMetadata( | 306 | metadata = cls._makeMetadata( |
218 | 306 | target_pocket, package_version, include_binaries, sponsored, | 307 | target_pocket, package_version, include_binaries, sponsored, |
219 | 307 | unembargo, auto_approve, silent, source_distroseries, | 308 | unembargo, auto_approve, silent, source_distroseries, |
221 | 308 | source_pocket, phased_update_percentage) | 309 | source_pocket, phased_update_percentage, move) |
222 | 309 | job = PackageCopyJob( | 310 | job = PackageCopyJob( |
223 | 310 | job_type=cls.class_job_type, | 311 | job_type=cls.class_job_type, |
224 | 311 | source_archive=source_archive, | 312 | source_archive=source_archive, |
225 | @@ -323,7 +324,7 @@ class PlainPackageCopyJob(PackageCopyJobDerived): | |||
226 | 323 | @classmethod | 324 | @classmethod |
227 | 324 | def _composeJobInsertionTuple(cls, copy_policy, include_binaries, job_id, | 325 | def _composeJobInsertionTuple(cls, copy_policy, include_binaries, job_id, |
228 | 325 | copy_task, sponsored, unembargo, | 326 | copy_task, sponsored, unembargo, |
230 | 326 | auto_approve, silent): | 327 | auto_approve, silent, move): |
231 | 327 | """Create an SQL fragment for inserting a job into the database. | 328 | """Create an SQL fragment for inserting a job into the database. |
232 | 328 | 329 | ||
233 | 329 | :return: A string representing an SQL tuple containing initializers | 330 | :return: A string representing an SQL tuple containing initializers |
234 | @@ -340,7 +341,7 @@ class PlainPackageCopyJob(PackageCopyJobDerived): | |||
235 | 340 | ) = copy_task | 341 | ) = copy_task |
236 | 341 | metadata = cls._makeMetadata( | 342 | metadata = cls._makeMetadata( |
237 | 342 | target_pocket, package_version, include_binaries, sponsored, | 343 | target_pocket, package_version, include_binaries, sponsored, |
239 | 343 | unembargo, auto_approve, silent) | 344 | unembargo, auto_approve, silent, move=move) |
240 | 344 | data = ( | 345 | data = ( |
241 | 345 | cls.class_job_type, target_distroseries, copy_policy, | 346 | cls.class_job_type, target_distroseries, copy_policy, |
242 | 346 | source_archive, target_archive, package_name, job_id, | 347 | source_archive, target_archive, package_name, job_id, |
243 | @@ -351,14 +352,15 @@ class PlainPackageCopyJob(PackageCopyJobDerived): | |||
244 | 351 | def createMultiple(cls, copy_tasks, requester, | 352 | def createMultiple(cls, copy_tasks, requester, |
245 | 352 | copy_policy=PackageCopyPolicy.INSECURE, | 353 | copy_policy=PackageCopyPolicy.INSECURE, |
246 | 353 | include_binaries=False, sponsored=None, | 354 | include_binaries=False, sponsored=None, |
248 | 354 | unembargo=False, auto_approve=False, silent=False): | 355 | unembargo=False, auto_approve=False, silent=False, |
249 | 356 | move=False): | ||
250 | 355 | """See `IPlainPackageCopyJobSource`.""" | 357 | """See `IPlainPackageCopyJobSource`.""" |
251 | 356 | store = IMasterStore(Job) | 358 | store = IMasterStore(Job) |
252 | 357 | job_ids = Job.createMultiple(store, len(copy_tasks), requester) | 359 | job_ids = Job.createMultiple(store, len(copy_tasks), requester) |
253 | 358 | job_contents = [ | 360 | job_contents = [ |
254 | 359 | cls._composeJobInsertionTuple( | 361 | cls._composeJobInsertionTuple( |
255 | 360 | copy_policy, include_binaries, job_id, task, sponsored, | 362 | copy_policy, include_binaries, job_id, task, sponsored, |
257 | 361 | unembargo, auto_approve, silent) | 363 | unembargo, auto_approve, silent, move) |
258 | 362 | for job_id, task in zip(job_ids, copy_tasks)] | 364 | for job_id, task in zip(job_ids, copy_tasks)] |
259 | 363 | return bulk.create( | 365 | return bulk.create( |
260 | 364 | (PackageCopyJob.job_type, PackageCopyJob.target_distroseries, | 366 | (PackageCopyJob.job_type, PackageCopyJob.target_distroseries, |
261 | @@ -467,6 +469,10 @@ class PlainPackageCopyJob(PackageCopyJobDerived): | |||
262 | 467 | return self.metadata.get('phased_update_percentage') | 469 | return self.metadata.get('phased_update_percentage') |
263 | 468 | 470 | ||
264 | 469 | @property | 471 | @property |
265 | 472 | def move(self): | ||
266 | 473 | return self.metadata.get('move', False) | ||
267 | 474 | |||
268 | 475 | @property | ||
269 | 470 | def requester_can_admin_target(self): | 476 | def requester_can_admin_target(self): |
270 | 471 | return self.target_archive.canAdministerQueue( | 477 | return self.target_archive.canAdministerQueue( |
271 | 472 | self.requester, self.getSourceOverride().component, | 478 | self.requester, self.getSourceOverride().component, |
272 | @@ -659,7 +665,7 @@ class PlainPackageCopyJob(PackageCopyJobDerived): | |||
273 | 659 | sponsored=self.sponsored, packageupload=pu, | 665 | sponsored=self.sponsored, packageupload=pu, |
274 | 660 | unembargo=self.unembargo, | 666 | unembargo=self.unembargo, |
275 | 661 | phased_update_percentage=self.phased_update_percentage, | 667 | phased_update_percentage=self.phased_update_percentage, |
277 | 662 | logger=self.logger) | 668 | move=self.move, logger=self.logger) |
278 | 663 | 669 | ||
279 | 664 | # Add a PackageDiff for this new upload if it has ancestry. | 670 | # Add a PackageDiff for this new upload if it has ancestry. |
280 | 665 | if copied_publications and not ancestry.is_empty(): | 671 | if copied_publications and not ancestry.is_empty(): |
281 | diff --git a/lib/lp/soyuz/scripts/packagecopier.py b/lib/lp/soyuz/scripts/packagecopier.py | |||
282 | index 3891af5..2e4a767 100644 | |||
283 | --- a/lib/lp/soyuz/scripts/packagecopier.py | |||
284 | +++ b/lib/lp/soyuz/scripts/packagecopier.py | |||
285 | @@ -1,4 +1,4 @@ | |||
287 | 1 | # Copyright 2009-2015 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2009-2019 Canonical Ltd. This software is licensed under the |
288 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
289 | 3 | 3 | ||
290 | 4 | """Package copying utilities.""" | 4 | """Package copying utilities.""" |
291 | @@ -15,9 +15,15 @@ __all__ = [ | |||
292 | 15 | 15 | ||
293 | 16 | import apt_pkg | 16 | import apt_pkg |
294 | 17 | from lazr.delegates import delegate_to | 17 | from lazr.delegates import delegate_to |
296 | 18 | from zope.component import getUtility | 18 | from zope.component import ( |
297 | 19 | getAdapter, | ||
298 | 20 | getUtility, | ||
299 | 21 | ) | ||
300 | 19 | from zope.security.proxy import removeSecurityProxy | 22 | from zope.security.proxy import removeSecurityProxy |
301 | 20 | 23 | ||
302 | 24 | from lp.app.interfaces.security import IAuthorization | ||
303 | 25 | from lp.registry.interfaces.role import IPersonRoles | ||
304 | 26 | from lp.registry.model.person import Person | ||
305 | 21 | from lp.services.database.bulk import load_related | 27 | from lp.services.database.bulk import load_related |
306 | 22 | from lp.soyuz.adapters.overrides import SourceOverride | 28 | from lp.soyuz.adapters.overrides import SourceOverride |
307 | 23 | from lp.soyuz.enums import SourcePackageFormat | 29 | from lp.soyuz.enums import SourcePackageFormat |
308 | @@ -141,7 +147,8 @@ class CheckedCopy: | |||
309 | 141 | return {'status': BuildSetStatus.NEEDSBUILD} | 147 | return {'status': BuildSetStatus.NEEDSBUILD} |
310 | 142 | 148 | ||
311 | 143 | 149 | ||
313 | 144 | def check_copy_permissions(person, archive, series, pocket, sources): | 150 | def check_copy_permissions(person, archive, series, pocket, sources, |
314 | 151 | move=False): | ||
315 | 145 | """Check that `person` has permission to copy a package. | 152 | """Check that `person` has permission to copy a package. |
316 | 146 | 153 | ||
317 | 147 | :param person: User attempting the upload. | 154 | :param person: User attempting the upload. |
318 | @@ -150,9 +157,12 @@ def check_copy_permissions(person, archive, series, pocket, sources): | |||
319 | 150 | :param pocket: Destination `Pocket`. | 157 | :param pocket: Destination `Pocket`. |
320 | 151 | :param sources: Sequence of `SourcePackagePublishingHistory`s for the | 158 | :param sources: Sequence of `SourcePackagePublishingHistory`s for the |
321 | 152 | packages to be copied. | 159 | packages to be copied. |
322 | 160 | :param move: If True, also check whether `person` has permission to | ||
323 | 161 | delete the sources. | ||
324 | 153 | :raises CannotCopy: If the copy is not allowed. | 162 | :raises CannotCopy: If the copy is not allowed. |
325 | 154 | """ | 163 | """ |
326 | 155 | # Circular import. | 164 | # Circular import. |
327 | 165 | from lp.soyuz.model.archive import Archive | ||
328 | 156 | from lp.soyuz.model.sourcepackagerelease import SourcePackageRelease | 166 | from lp.soyuz.model.sourcepackagerelease import SourcePackageRelease |
329 | 157 | 167 | ||
330 | 158 | if person is None: | 168 | if person is None: |
331 | @@ -161,6 +171,12 @@ def check_copy_permissions(person, archive, series, pocket, sources): | |||
332 | 161 | if len(sources) > 1: | 171 | if len(sources) > 1: |
333 | 162 | # Bulk-load the data we'll need from each source publication. | 172 | # Bulk-load the data we'll need from each source publication. |
334 | 163 | load_related(SourcePackageRelease, sources, ["sourcepackagereleaseID"]) | 173 | load_related(SourcePackageRelease, sources, ["sourcepackagereleaseID"]) |
335 | 174 | if move: | ||
336 | 175 | # Bulk-load at least some of the data we'll need for permission | ||
337 | 176 | # checks on each source archive. Not all of this is currently | ||
338 | 177 | # preloadable. | ||
339 | 178 | archives = load_related(Archive, sources, ["archiveID"]) | ||
340 | 179 | load_related(Person, archives, ["ownerID"]) | ||
341 | 164 | 180 | ||
342 | 165 | # If there is a requester, check that they have upload permission into | 181 | # If there is a requester, check that they have upload permission into |
343 | 166 | # the destination (archive, component, pocket). This check is done | 182 | # the destination (archive, component, pocket). This check is done |
344 | @@ -191,6 +207,29 @@ def check_copy_permissions(person, archive, series, pocket, sources): | |||
345 | 191 | person, override.component, pocket, dest_series): | 207 | person, override.component, pocket, dest_series): |
346 | 192 | raise CannotCopy(reason) | 208 | raise CannotCopy(reason) |
347 | 193 | 209 | ||
348 | 210 | if move: | ||
349 | 211 | roles = IPersonRoles(person) | ||
350 | 212 | for source_archive in set(source.archive for source in sources): | ||
351 | 213 | # XXX cjwatson 2019-10-09: Checking the archive rather than the | ||
352 | 214 | # SPPH duplicates security adapter logic, which is unfortunate; | ||
353 | 215 | # but too much of the logic required to use | ||
354 | 216 | # DelegatedAuthorization-based adapters such as the one used for | ||
355 | 217 | # launchpad.Edit on SPPH lives in | ||
356 | 218 | # lp.services.webapp.authorization and is hard to use without a | ||
357 | 219 | # full Zope interaction. | ||
358 | 220 | source_archive_authz = getAdapter( | ||
359 | 221 | source_archive, IAuthorization, "launchpad.Append") | ||
360 | 222 | if not source_archive_authz.checkAuthenticated(roles): | ||
361 | 223 | raise CannotCopy( | ||
362 | 224 | "%s is not permitted to delete publications from %s." % | ||
363 | 225 | (person.display_name, source_archive.displayname)) | ||
364 | 226 | for source in sources: | ||
365 | 227 | if not source.archive.canModifySuite( | ||
366 | 228 | source.distroseries, source.pocket): | ||
367 | 229 | raise CannotCopy( | ||
368 | 230 | "Cannot delete publications from suite '%s'" % | ||
369 | 231 | source.distroseries.getSuite(source.pocket)) | ||
370 | 232 | |||
371 | 194 | 233 | ||
372 | 195 | class CopyChecker: | 234 | class CopyChecker: |
373 | 196 | """Check copy candiates. | 235 | """Check copy candiates. |
374 | @@ -388,7 +427,7 @@ class CopyChecker: | |||
375 | 388 | "different contents." % lf.libraryfile.filename) | 427 | "different contents." % lf.libraryfile.filename) |
376 | 389 | 428 | ||
377 | 390 | def checkCopy(self, source, series, pocket, person=None, | 429 | def checkCopy(self, source, series, pocket, person=None, |
379 | 391 | check_permissions=True): | 430 | check_permissions=True, move=False): |
380 | 392 | """Check if the source can be copied to the given location. | 431 | """Check if the source can be copied to the given location. |
381 | 393 | 432 | ||
382 | 394 | Check possible conflicting publications in the destination archive. | 433 | Check possible conflicting publications in the destination archive. |
383 | @@ -407,13 +446,15 @@ class CopyChecker: | |||
384 | 407 | :param person: requester `IPerson`. | 446 | :param person: requester `IPerson`. |
385 | 408 | :param check_permissions: boolean indicating whether or not the | 447 | :param check_permissions: boolean indicating whether or not the |
386 | 409 | requester's permissions to copy should be checked. | 448 | requester's permissions to copy should be checked. |
387 | 449 | :param move: if True, also check whether `person` has permission to | ||
388 | 450 | delete the source. | ||
389 | 410 | 451 | ||
390 | 411 | :raise CannotCopy when a copy is not allowed to be performed | 452 | :raise CannotCopy when a copy is not allowed to be performed |
391 | 412 | containing the reason of the error. | 453 | containing the reason of the error. |
392 | 413 | """ | 454 | """ |
393 | 414 | if check_permissions: | 455 | if check_permissions: |
394 | 415 | check_copy_permissions( | 456 | check_copy_permissions( |
396 | 416 | person, self.archive, series, pocket, [source]) | 457 | person, self.archive, series, pocket, [source], move=move) |
397 | 417 | 458 | ||
398 | 418 | if series.distribution != self.archive.distribution: | 459 | if series.distribution != self.archive.distribution: |
399 | 419 | raise CannotCopy( | 460 | raise CannotCopy( |
400 | @@ -483,7 +524,7 @@ def do_copy(sources, archive, series, pocket, include_binaries=False, | |||
401 | 483 | send_email=False, strict_binaries=True, close_bugs=True, | 524 | send_email=False, strict_binaries=True, close_bugs=True, |
402 | 484 | create_dsd_job=True, announce_from_person=None, sponsored=None, | 525 | create_dsd_job=True, announce_from_person=None, sponsored=None, |
403 | 485 | packageupload=None, unembargo=False, phased_update_percentage=None, | 526 | packageupload=None, unembargo=False, phased_update_percentage=None, |
405 | 486 | logger=None): | 527 | move=False, logger=None): |
406 | 487 | """Perform the complete copy of the given sources incrementally. | 528 | """Perform the complete copy of the given sources incrementally. |
407 | 488 | 529 | ||
408 | 489 | Verifies if each copy can be performed using `CopyChecker` and | 530 | Verifies if each copy can be performed using `CopyChecker` and |
409 | @@ -532,6 +573,8 @@ def do_copy(sources, archive, series, pocket, include_binaries=False, | |||
410 | 532 | doing so. | 573 | doing so. |
411 | 533 | :param phased_update_percentage: The phased update percentage to apply | 574 | :param phased_update_percentage: The phased update percentage to apply |
412 | 534 | to the copied publication. | 575 | to the copied publication. |
413 | 576 | :param move: If True, delete the source publication after copying to the | ||
414 | 577 | destination. | ||
415 | 535 | :param logger: An optional logger. | 578 | :param logger: An optional logger. |
416 | 536 | 579 | ||
417 | 537 | :raise CannotCopy when one or more copies were not allowed. The error | 580 | :raise CannotCopy when one or more copies were not allowed. The error |
418 | @@ -554,7 +597,8 @@ def do_copy(sources, archive, series, pocket, include_binaries=False, | |||
419 | 554 | destination_series = series | 597 | destination_series = series |
420 | 555 | try: | 598 | try: |
421 | 556 | copy_checker.checkCopy( | 599 | copy_checker.checkCopy( |
423 | 557 | source, destination_series, pocket, person, check_permissions) | 600 | source, destination_series, pocket, person, check_permissions, |
424 | 601 | move=move) | ||
425 | 558 | except CannotCopy as reason: | 602 | except CannotCopy as reason: |
426 | 559 | errors.append("%s (%s)" % (source.displayname, reason)) | 603 | errors.append("%s (%s)" % (source.displayname, reason)) |
427 | 560 | continue | 604 | continue |
428 | @@ -610,7 +654,8 @@ def do_copy(sources, archive, series, pocket, include_binaries=False, | |||
429 | 610 | override, close_bugs=close_bugs, create_dsd_job=create_dsd_job, | 654 | override, close_bugs=close_bugs, create_dsd_job=create_dsd_job, |
430 | 611 | close_bugs_since_version=old_version, creator=creator, | 655 | close_bugs_since_version=old_version, creator=creator, |
431 | 612 | sponsor=sponsor, packageupload=packageupload, | 656 | sponsor=sponsor, packageupload=packageupload, |
433 | 613 | phased_update_percentage=phased_update_percentage, logger=logger) | 657 | phased_update_percentage=phased_update_percentage, move=move, |
434 | 658 | logger=logger) | ||
435 | 614 | if send_email and sub_copies: | 659 | if send_email and sub_copies: |
436 | 615 | mailer = PackageUploadMailer.forAction( | 660 | mailer = PackageUploadMailer.forAction( |
437 | 616 | 'accepted', person, source.sourcepackagerelease, [], [], | 661 | 'accepted', person, source.sourcepackagerelease, [], [], |
438 | @@ -639,7 +684,7 @@ def _do_direct_copy(source, archive, series, pocket, include_binaries, | |||
439 | 639 | override=None, close_bugs=True, create_dsd_job=True, | 684 | override=None, close_bugs=True, create_dsd_job=True, |
440 | 640 | close_bugs_since_version=None, creator=None, | 685 | close_bugs_since_version=None, creator=None, |
441 | 641 | sponsor=None, packageupload=None, | 686 | sponsor=None, packageupload=None, |
443 | 642 | phased_update_percentage=None, logger=None): | 687 | phased_update_percentage=None, move=False, logger=None): |
444 | 643 | """Copy publishing records to another location. | 688 | """Copy publishing records to another location. |
445 | 644 | 689 | ||
446 | 645 | Copy each item of the given list of `SourcePackagePublishingHistory` | 690 | Copy each item of the given list of `SourcePackagePublishingHistory` |
447 | @@ -671,6 +716,8 @@ def _do_direct_copy(source, archive, series, pocket, include_binaries, | |||
448 | 671 | to be created. | 716 | to be created. |
449 | 672 | :param phased_update_percentage: The phased update percentage to apply | 717 | :param phased_update_percentage: The phased update percentage to apply |
450 | 673 | to the copied publication. | 718 | to the copied publication. |
451 | 719 | :param move: If True, delete the source publication after copying to the | ||
452 | 720 | destination. | ||
453 | 674 | :param logger: An optional logger. | 721 | :param logger: An optional logger. |
454 | 675 | 722 | ||
455 | 676 | :return: a list of `ISourcePackagePublishingHistory` and | 723 | :return: a list of `ISourcePackagePublishingHistory` and |
456 | @@ -742,4 +789,11 @@ def _do_direct_copy(source, archive, series, pocket, include_binaries, | |||
457 | 742 | # XXX cjwatson 2012-06-22 bug=869308: Fails to honour P-a-s. | 789 | # XXX cjwatson 2012-06-22 bug=869308: Fails to honour P-a-s. |
458 | 743 | source_copy.createMissingBuilds(logger=logger) | 790 | source_copy.createMissingBuilds(logger=logger) |
459 | 744 | 791 | ||
460 | 792 | if move: | ||
461 | 793 | removal_comment = "Moved to %s" % series.getSuite(pocket) | ||
462 | 794 | if archive != source.archive: | ||
463 | 795 | removal_comment += " in %s" % archive.reference | ||
464 | 796 | getUtility(IPublishingSet).requestDeletion( | ||
465 | 797 | [source], creator, removal_comment=removal_comment) | ||
466 | 798 | |||
467 | 745 | return copies | 799 | return copies |
468 | diff --git a/lib/lp/soyuz/scripts/tests/test_copypackage.py b/lib/lp/soyuz/scripts/tests/test_copypackage.py | |||
469 | index 388fcf7..eee530a 100644 | |||
470 | --- a/lib/lp/soyuz/scripts/tests/test_copypackage.py | |||
471 | +++ b/lib/lp/soyuz/scripts/tests/test_copypackage.py | |||
472 | @@ -1732,6 +1732,50 @@ class TestDoDirectCopy(BaseDoCopyTests, TestCaseWithFactory): | |||
473 | 1732 | self.assertIsInstance(copies[1], BinaryPackagePublishingHistory) | 1732 | self.assertIsInstance(copies[1], BinaryPackagePublishingHistory) |
474 | 1733 | self.assertEqual("i386", copies[1].distroarchseries.architecturetag) | 1733 | self.assertEqual("i386", copies[1].distroarchseries.architecturetag) |
475 | 1734 | 1734 | ||
476 | 1735 | def test_copy_without_move(self): | ||
477 | 1736 | # A copy with move=False (the default) leaves the source publication | ||
478 | 1737 | # intact. | ||
479 | 1738 | nobby, archive, source = self._setup_archive() | ||
480 | 1739 | target_archive = self.factory.makeArchive( | ||
481 | 1740 | distribution=self.test_publisher.ubuntutest) | ||
482 | 1741 | [copied_source] = do_copy( | ||
483 | 1742 | [source], target_archive, nobby, source.pocket, | ||
484 | 1743 | include_binaries=False, person=target_archive.owner, | ||
485 | 1744 | check_permissions=False, send_email=False) | ||
486 | 1745 | self.assertEqual(PackagePublishingStatus.PENDING, copied_source.status) | ||
487 | 1746 | self.assertEqual(PackagePublishingStatus.PENDING, source.status) | ||
488 | 1747 | |||
489 | 1748 | def test_copy_with_move(self): | ||
490 | 1749 | # A copy with move=True deletes the source publication. | ||
491 | 1750 | nobby, archive, source = self._setup_archive() | ||
492 | 1751 | target_archive = self.factory.makeArchive( | ||
493 | 1752 | distribution=self.test_publisher.ubuntutest) | ||
494 | 1753 | [copied_source] = do_copy( | ||
495 | 1754 | [source], target_archive, nobby, source.pocket, | ||
496 | 1755 | include_binaries=False, person=target_archive.owner, | ||
497 | 1756 | check_permissions=False, send_email=False, move=True) | ||
498 | 1757 | self.assertEqual(PackagePublishingStatus.PENDING, copied_source.status) | ||
499 | 1758 | self.assertEqual(PackagePublishingStatus.DELETED, source.status) | ||
500 | 1759 | self.assertEqual( | ||
501 | 1760 | "Moved to %s in %s" % ( | ||
502 | 1761 | nobby.getSuite(source.pocket), target_archive.reference), | ||
503 | 1762 | source.removal_comment) | ||
504 | 1763 | |||
505 | 1764 | def test_copy_with_move_failure(self): | ||
506 | 1765 | # If a copy with move=True fails, then the source publication is | ||
507 | 1766 | # left intact. | ||
508 | 1767 | nobby, archive, source = self._setup_archive() | ||
509 | 1768 | self.test_publisher.getPubSource( | ||
510 | 1769 | sourcename=source.source_package_name, | ||
511 | 1770 | archive=nobby.main_archive, version="1.0-2", | ||
512 | 1771 | architecturehintlist="any") | ||
513 | 1772 | self.assertRaises( | ||
514 | 1773 | CannotCopy, do_copy, | ||
515 | 1774 | [source], archive, nobby, source.pocket, | ||
516 | 1775 | include_binaries=False, person=source.sourcepackagerelease.creator, | ||
517 | 1776 | check_permissions=False, send_email=False, move=True) | ||
518 | 1777 | self.assertEqual(PackagePublishingStatus.PENDING, source.status) | ||
519 | 1778 | |||
520 | 1735 | 1779 | ||
521 | 1736 | class TestCopyBuildRecords(TestCaseWithFactory): | 1780 | class TestCopyBuildRecords(TestCaseWithFactory): |
522 | 1737 | """Test handling of binaries and their build records when copying.""" | 1781 | """Test handling of binaries and their build records when copying.""" |
523 | diff --git a/lib/lp/soyuz/tests/test_archive.py b/lib/lp/soyuz/tests/test_archive.py | |||
524 | index 09f0141..b015953 100644 | |||
525 | --- a/lib/lp/soyuz/tests/test_archive.py | |||
526 | +++ b/lib/lp/soyuz/tests/test_archive.py | |||
527 | @@ -2798,20 +2798,23 @@ class TestCopyPackage(TestCaseWithFactory): | |||
528 | 2798 | 2798 | ||
529 | 2799 | layer = DatabaseFunctionalLayer | 2799 | layer = DatabaseFunctionalLayer |
530 | 2800 | 2800 | ||
532 | 2801 | def _setup_copy_data(self, source_distribution=None, source_private=False, | 2801 | def _setup_copy_data(self, source_distribution=None, source_purpose=None, |
533 | 2802 | source_private=False, source_pocket=None, | ||
534 | 2802 | target_purpose=None, | 2803 | target_purpose=None, |
535 | 2803 | target_status=SeriesStatus.DEVELOPMENT, | 2804 | target_status=SeriesStatus.DEVELOPMENT, |
536 | 2804 | same_distribution=False): | 2805 | same_distribution=False): |
537 | 2805 | if target_purpose is None: | 2806 | if target_purpose is None: |
538 | 2806 | target_purpose = ArchivePurpose.PPA | 2807 | target_purpose = ArchivePurpose.PPA |
539 | 2807 | source_archive = self.factory.makeArchive( | 2808 | source_archive = self.factory.makeArchive( |
541 | 2808 | distribution=source_distribution, private=source_private) | 2809 | distribution=source_distribution, purpose=source_purpose, |
542 | 2810 | private=source_private) | ||
543 | 2809 | target_distribution = ( | 2811 | target_distribution = ( |
544 | 2810 | source_archive.distribution if same_distribution else None) | 2812 | source_archive.distribution if same_distribution else None) |
545 | 2811 | target_archive = self.factory.makeArchive( | 2813 | target_archive = self.factory.makeArchive( |
546 | 2812 | distribution=target_distribution, purpose=target_purpose) | 2814 | distribution=target_distribution, purpose=target_purpose) |
547 | 2813 | source = self.factory.makeSourcePackagePublishingHistory( | 2815 | source = self.factory.makeSourcePackagePublishingHistory( |
549 | 2814 | archive=source_archive, status=PackagePublishingStatus.PUBLISHED) | 2816 | archive=source_archive, pocket=source_pocket, |
550 | 2817 | status=PackagePublishingStatus.PUBLISHED) | ||
551 | 2815 | with person_logged_in(source_archive.owner): | 2818 | with person_logged_in(source_archive.owner): |
552 | 2816 | source_name = source.source_package_name | 2819 | source_name = source.source_package_name |
553 | 2817 | version = source.source_package_version | 2820 | version = source.source_package_version |
554 | @@ -2854,7 +2857,8 @@ class TestCopyPackage(TestCaseWithFactory): | |||
555 | 2854 | include_binaries=False, | 2857 | include_binaries=False, |
556 | 2855 | sponsored=sponsored, | 2858 | sponsored=sponsored, |
557 | 2856 | copy_policy=PackageCopyPolicy.INSECURE, | 2859 | copy_policy=PackageCopyPolicy.INSECURE, |
559 | 2857 | phased_update_percentage=30)) | 2860 | phased_update_percentage=30, |
560 | 2861 | move=False)) | ||
561 | 2858 | 2862 | ||
562 | 2859 | def test_copyPackage_disallows_non_primary_archive_uploaders(self): | 2863 | def test_copyPackage_disallows_non_primary_archive_uploaders(self): |
563 | 2860 | # If copying to a primary archive and you're not an uploader for | 2864 | # If copying to a primary archive and you're not an uploader for |
564 | @@ -3052,6 +3056,63 @@ class TestCopyPackage(TestCaseWithFactory): | |||
565 | 3052 | self.assertEqual(source.distroseries, copy_job.source_distroseries) | 3056 | self.assertEqual(source.distroseries, copy_job.source_distroseries) |
566 | 3053 | self.assertEqual(source.pocket, copy_job.source_pocket) | 3057 | self.assertEqual(source.pocket, copy_job.source_pocket) |
567 | 3054 | 3058 | ||
568 | 3059 | def test_copyPackage_move(self): | ||
569 | 3060 | # Passing move=True causes copyPackage to create a copy job that | ||
570 | 3061 | # will delete the source publication after copying. | ||
571 | 3062 | (source, source_archive, source_name, target_archive, to_pocket, | ||
572 | 3063 | to_series, version) = self._setup_copy_data( | ||
573 | 3064 | source_distribution=self.factory.makeDistribution()) | ||
574 | 3065 | with person_logged_in(target_archive.owner): | ||
575 | 3066 | target_archive.newComponentUploader(source_archive.owner, "main") | ||
576 | 3067 | with person_logged_in(source_archive.owner): | ||
577 | 3068 | target_archive.copyPackage( | ||
578 | 3069 | source_name, version, source_archive, to_pocket.name, | ||
579 | 3070 | to_series=to_series.name, include_binaries=True, | ||
580 | 3071 | person=source_archive.owner, move=True) | ||
581 | 3072 | |||
582 | 3073 | # There should be one copy job, with move=True set. | ||
583 | 3074 | job_source = getUtility(IPlainPackageCopyJobSource) | ||
584 | 3075 | copy_job = job_source.getActiveJobs(target_archive).one() | ||
585 | 3076 | self.assertTrue(copy_job.move) | ||
586 | 3077 | |||
587 | 3078 | def test_copyPackage_move_without_permission(self): | ||
588 | 3079 | # Passing move=True checks that the user is permitted to delete the | ||
589 | 3080 | # source publication. | ||
590 | 3081 | (source, source_archive, source_name, target_archive, to_pocket, | ||
591 | 3082 | to_series, version) = self._setup_copy_data( | ||
592 | 3083 | source_distribution=self.factory.makeDistribution()) | ||
593 | 3084 | with person_logged_in(target_archive.owner): | ||
594 | 3085 | expected_error = ( | ||
595 | 3086 | "%s is not permitted to delete publications from %s." % ( | ||
596 | 3087 | target_archive.owner.display_name, | ||
597 | 3088 | source_archive.displayname)) | ||
598 | 3089 | self.assertRaisesWithContent( | ||
599 | 3090 | CannotCopy, expected_error, target_archive.copyPackage, | ||
600 | 3091 | source_name, version, source_archive, to_pocket.name, | ||
601 | 3092 | to_series=to_series.name, include_binaries=True, | ||
602 | 3093 | person=target_archive.owner, move=True) | ||
603 | 3094 | |||
604 | 3095 | def test_copyPackage_move_from_immutable_suite(self): | ||
605 | 3096 | # Passing move=True checks that the source suite can be modified. | ||
606 | 3097 | (source, source_archive, source_name, target_archive, to_pocket, | ||
607 | 3098 | to_series, version) = self._setup_copy_data( | ||
608 | 3099 | source_distribution=self.factory.makeDistribution(), | ||
609 | 3100 | source_purpose=ArchivePurpose.PRIMARY, | ||
610 | 3101 | source_pocket=PackagePublishingPocket.RELEASE) | ||
611 | 3102 | with person_logged_in(target_archive.owner): | ||
612 | 3103 | target_archive.newComponentUploader(source_archive.owner, "main") | ||
613 | 3104 | removeSecurityProxy(source.distroseries).status = ( | ||
614 | 3105 | SeriesStatus.SUPPORTED) | ||
615 | 3106 | with person_logged_in(source_archive.owner): | ||
616 | 3107 | expected_error = ( | ||
617 | 3108 | "Cannot delete publications from suite '%s'" % ( | ||
618 | 3109 | source.distroseries.getSuite(source.pocket))) | ||
619 | 3110 | self.assertRaisesWithContent( | ||
620 | 3111 | CannotCopy, expected_error, target_archive.copyPackage, | ||
621 | 3112 | source_name, version, source_archive, to_pocket.name, | ||
622 | 3113 | to_series=to_series.name, include_binaries=True, | ||
623 | 3114 | person=source_archive.owner, move=True) | ||
624 | 3115 | |||
625 | 3055 | def test_copyPackages_with_single_package(self): | 3116 | def test_copyPackages_with_single_package(self): |
626 | 3056 | (source, source_archive, source_name, target_archive, to_pocket, | 3117 | (source, source_archive, source_name, target_archive, to_pocket, |
627 | 3057 | to_series, version) = self._setup_copy_data() | 3118 | to_series, version) = self._setup_copy_data() |
628 | @@ -3080,7 +3141,8 @@ class TestCopyPackage(TestCaseWithFactory): | |||
629 | 3080 | target_pocket=to_pocket, | 3141 | target_pocket=to_pocket, |
630 | 3081 | include_binaries=False, | 3142 | include_binaries=False, |
631 | 3082 | sponsored=sponsored, | 3143 | sponsored=sponsored, |
633 | 3083 | copy_policy=PackageCopyPolicy.MASS_SYNC)) | 3144 | copy_policy=PackageCopyPolicy.MASS_SYNC, |
634 | 3145 | move=False)) | ||
635 | 3084 | 3146 | ||
636 | 3085 | def test_copyPackages_with_multiple_packages(self): | 3147 | def test_copyPackages_with_multiple_packages(self): |
637 | 3086 | # PENDING and PUBLISHED packages should both be copied. | 3148 | # PENDING and PUBLISHED packages should both be copied. |
638 | @@ -3297,6 +3359,63 @@ class TestCopyPackage(TestCaseWithFactory): | |||
639 | 3297 | copy_job = job_source.getActiveJobs(target_archive).one() | 3359 | copy_job = job_source.getActiveJobs(target_archive).one() |
640 | 3298 | self.assertEqual(to_pocket, copy_job.target_pocket) | 3360 | self.assertEqual(to_pocket, copy_job.target_pocket) |
641 | 3299 | 3361 | ||
642 | 3362 | def test_copyPackages_move(self): | ||
643 | 3363 | # Passing move=True causes copyPackages to create copy jobs that | ||
644 | 3364 | # will delete the source publication after copying. | ||
645 | 3365 | (source, source_archive, source_name, target_archive, to_pocket, | ||
646 | 3366 | to_series, version) = self._setup_copy_data( | ||
647 | 3367 | source_distribution=self.factory.makeDistribution()) | ||
648 | 3368 | with person_logged_in(target_archive.owner): | ||
649 | 3369 | target_archive.newComponentUploader(source_archive.owner, "main") | ||
650 | 3370 | with person_logged_in(source_archive.owner): | ||
651 | 3371 | target_archive.copyPackages( | ||
652 | 3372 | [source_name], source_archive, to_pocket.name, | ||
653 | 3373 | to_series=to_series.name, include_binaries=True, | ||
654 | 3374 | person=source_archive.owner, move=True) | ||
655 | 3375 | |||
656 | 3376 | # There should be one copy job, with move=True set. | ||
657 | 3377 | job_source = getUtility(IPlainPackageCopyJobSource) | ||
658 | 3378 | copy_job = job_source.getActiveJobs(target_archive).one() | ||
659 | 3379 | self.assertTrue(copy_job.move) | ||
660 | 3380 | |||
661 | 3381 | def test_copyPackages_move_without_permission(self): | ||
662 | 3382 | # Passing move=True checks that the user is permitted to delete the | ||
663 | 3383 | # source publication. | ||
664 | 3384 | (source, source_archive, source_name, target_archive, to_pocket, | ||
665 | 3385 | to_series, version) = self._setup_copy_data( | ||
666 | 3386 | source_distribution=self.factory.makeDistribution()) | ||
667 | 3387 | with person_logged_in(target_archive.owner): | ||
668 | 3388 | expected_error = ( | ||
669 | 3389 | "%s is not permitted to delete publications from %s." % ( | ||
670 | 3390 | target_archive.owner.display_name, | ||
671 | 3391 | source_archive.displayname)) | ||
672 | 3392 | self.assertRaisesWithContent( | ||
673 | 3393 | CannotCopy, expected_error, target_archive.copyPackages, | ||
674 | 3394 | [source_name], source_archive, to_pocket.name, | ||
675 | 3395 | to_series=to_series.name, include_binaries=True, | ||
676 | 3396 | person=target_archive.owner, move=True) | ||
677 | 3397 | |||
678 | 3398 | def test_copyPackages_move_from_immutable_suite(self): | ||
679 | 3399 | # Passing move=True checks that the source suite can be modified. | ||
680 | 3400 | (source, source_archive, source_name, target_archive, to_pocket, | ||
681 | 3401 | to_series, version) = self._setup_copy_data( | ||
682 | 3402 | source_distribution=self.factory.makeDistribution(), | ||
683 | 3403 | source_purpose=ArchivePurpose.PRIMARY, | ||
684 | 3404 | source_pocket=PackagePublishingPocket.RELEASE) | ||
685 | 3405 | with person_logged_in(target_archive.owner): | ||
686 | 3406 | target_archive.newComponentUploader(source_archive.owner, "main") | ||
687 | 3407 | removeSecurityProxy(source.distroseries).status = ( | ||
688 | 3408 | SeriesStatus.SUPPORTED) | ||
689 | 3409 | with person_logged_in(source_archive.owner): | ||
690 | 3410 | expected_error = ( | ||
691 | 3411 | "Cannot delete publications from suite '%s'" % ( | ||
692 | 3412 | source.distroseries.getSuite(source.pocket))) | ||
693 | 3413 | self.assertRaisesWithContent( | ||
694 | 3414 | CannotCopy, expected_error, target_archive.copyPackages, | ||
695 | 3415 | [source_name], source_archive, to_pocket.name, | ||
696 | 3416 | to_series=to_series.name, include_binaries=True, | ||
697 | 3417 | person=source_archive.owner, move=True) | ||
698 | 3418 | |||
699 | 3300 | 3419 | ||
700 | 3301 | class TestgetAllPublishedBinaries(TestCaseWithFactory): | 3420 | class TestgetAllPublishedBinaries(TestCaseWithFactory): |
701 | 3302 | 3421 | ||
702 | diff --git a/lib/lp/soyuz/tests/test_packagecopyjob.py b/lib/lp/soyuz/tests/test_packagecopyjob.py | |||
703 | index 68c1f95..6817c10 100644 | |||
704 | --- a/lib/lp/soyuz/tests/test_packagecopyjob.py | |||
705 | +++ b/lib/lp/soyuz/tests/test_packagecopyjob.py | |||
706 | @@ -1,4 +1,4 @@ | |||
708 | 1 | # Copyright 2010-2018 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2010-2019 Canonical Ltd. This software is licensed under the |
709 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
710 | 3 | 3 | ||
711 | 4 | """Tests for sync package jobs.""" | 4 | """Tests for sync package jobs.""" |
712 | @@ -220,7 +220,7 @@ class PlainPackageCopyJobTests(TestCaseWithFactory, LocalTestHelper): | |||
713 | 220 | package_version="1.0-1", include_binaries=False, | 220 | package_version="1.0-1", include_binaries=False, |
714 | 221 | copy_policy=PackageCopyPolicy.MASS_SYNC, | 221 | copy_policy=PackageCopyPolicy.MASS_SYNC, |
715 | 222 | requester=requester, sponsored=sponsored, | 222 | requester=requester, sponsored=sponsored, |
717 | 223 | phased_update_percentage=20) | 223 | phased_update_percentage=20, move=True) |
718 | 224 | self.assertProvides(job, IPackageCopyJob) | 224 | self.assertProvides(job, IPackageCopyJob) |
719 | 225 | self.assertEqual(archive1.id, job.source_archive_id) | 225 | self.assertEqual(archive1.id, job.source_archive_id) |
720 | 226 | self.assertEqual(archive1, job.source_archive) | 226 | self.assertEqual(archive1, job.source_archive) |
721 | @@ -230,11 +230,12 @@ class PlainPackageCopyJobTests(TestCaseWithFactory, LocalTestHelper): | |||
722 | 230 | self.assertEqual(PackagePublishingPocket.RELEASE, job.target_pocket) | 230 | self.assertEqual(PackagePublishingPocket.RELEASE, job.target_pocket) |
723 | 231 | self.assertEqual("foo", job.package_name) | 231 | self.assertEqual("foo", job.package_name) |
724 | 232 | self.assertEqual("1.0-1", job.package_version) | 232 | self.assertEqual("1.0-1", job.package_version) |
726 | 233 | self.assertEqual(False, job.include_binaries) | 233 | self.assertFalse(job.include_binaries) |
727 | 234 | self.assertEqual(PackageCopyPolicy.MASS_SYNC, job.copy_policy) | 234 | self.assertEqual(PackageCopyPolicy.MASS_SYNC, job.copy_policy) |
728 | 235 | self.assertEqual(requester, job.requester) | 235 | self.assertEqual(requester, job.requester) |
729 | 236 | self.assertEqual(sponsored, job.sponsored) | 236 | self.assertEqual(sponsored, job.sponsored) |
730 | 237 | self.assertEqual(20, job.phased_update_percentage) | 237 | self.assertEqual(20, job.phased_update_percentage) |
731 | 238 | self.assertTrue(job.move) | ||
732 | 238 | 239 | ||
733 | 239 | def test_createMultiple_creates_one_job_per_copy(self): | 240 | def test_createMultiple_creates_one_job_per_copy(self): |
734 | 240 | mother = self.factory.makeDistroSeriesParent() | 241 | mother = self.factory.makeDistroSeriesParent() |
735 | @@ -1726,6 +1727,30 @@ class PlainPackageCopyJobTests(TestCaseWithFactory, LocalTestHelper): | |||
736 | 1726 | 1, archive.getPublishedOnDiskBinaries( | 1727 | 1, archive.getPublishedOnDiskBinaries( |
737 | 1727 | status=PackagePublishingStatus.PENDING).count()) | 1728 | status=PackagePublishingStatus.PENDING).count()) |
738 | 1728 | 1729 | ||
739 | 1730 | def test_move(self): | ||
740 | 1731 | # A job with move=True deletes the old publication after copying it. | ||
741 | 1732 | source_archive = self.factory.makeArchive( | ||
742 | 1733 | self.distroseries.distribution) | ||
743 | 1734 | target_archive = self.factory.makeArchive( | ||
744 | 1735 | self.distroseries.distribution) | ||
745 | 1736 | spph = self.publisher.getPubSource( | ||
746 | 1737 | distroseries=self.distroseries, sourcename="moveme", | ||
747 | 1738 | archive=source_archive) | ||
748 | 1739 | with person_logged_in(target_archive.owner): | ||
749 | 1740 | target_archive.newComponentUploader(source_archive.owner, "main") | ||
750 | 1741 | job = self.createCopyJobForSPPH( | ||
751 | 1742 | spph, source_archive, target_archive, | ||
752 | 1743 | requester=source_archive.owner, move=True) | ||
753 | 1744 | self.runJob(job) | ||
754 | 1745 | self.assertEqual(JobStatus.COMPLETED, job.status) | ||
755 | 1746 | new_spph = target_archive.getPublishedSources(name="moveme").one() | ||
756 | 1747 | self.assertEqual(PackagePublishingStatus.PENDING, new_spph.status) | ||
757 | 1748 | self.assertEqual(PackagePublishingStatus.DELETED, spph.status) | ||
758 | 1749 | self.assertEqual( | ||
759 | 1750 | "Moved to %s in %s" % ( | ||
760 | 1751 | self.distroseries.name, target_archive.reference), | ||
761 | 1752 | spph.removal_comment) | ||
762 | 1753 | |||
763 | 1729 | 1754 | ||
764 | 1730 | class TestViaCelery(TestCaseWithFactory): | 1755 | class TestViaCelery(TestCaseWithFactory): |
765 | 1731 | """PackageCopyJob runs under Celery.""" | 1756 | """PackageCopyJob runs under Celery.""" |
766 | diff --git a/lib/lp/soyuz/vocabularies.py b/lib/lp/soyuz/vocabularies.py | |||
767 | index b4f40b5..bcaf16e 100644 | |||
768 | --- a/lib/lp/soyuz/vocabularies.py | |||
769 | +++ b/lib/lp/soyuz/vocabularies.py | |||
770 | @@ -1,4 +1,4 @@ | |||
772 | 1 | # Copyright 2009-2018 Canonical Ltd. This software is licensed under the GNU | 1 | # Copyright 2009-2019 Canonical Ltd. This software is licensed under the GNU |
773 | 2 | # Affero General Public License version 3 (see the file LICENSE). | 2 | # Affero General Public License version 3 (see the file LICENSE). |
774 | 3 | 3 | ||
775 | 4 | """Soyuz vocabularies.""" | 4 | """Soyuz vocabularies.""" |
776 | @@ -8,6 +8,7 @@ __metaclass__ = type | |||
777 | 8 | __all__ = [ | 8 | __all__ = [ |
778 | 9 | 'ComponentVocabulary', | 9 | 'ComponentVocabulary', |
779 | 10 | 'FilteredDistroArchSeriesVocabulary', | 10 | 'FilteredDistroArchSeriesVocabulary', |
780 | 11 | 'make_archive_vocabulary', | ||
781 | 11 | 'PackageReleaseVocabulary', | 12 | 'PackageReleaseVocabulary', |
782 | 12 | 'PPAVocabulary', | 13 | 'PPAVocabulary', |
783 | 13 | ] | 14 | ] |
784 | @@ -18,7 +19,10 @@ from storm.locals import ( | |||
785 | 18 | ) | 19 | ) |
786 | 19 | from zope.component import getUtility | 20 | from zope.component import getUtility |
787 | 20 | from zope.interface import implementer | 21 | from zope.interface import implementer |
789 | 21 | from zope.schema.vocabulary import SimpleTerm | 22 | from zope.schema.vocabulary import ( |
790 | 23 | SimpleTerm, | ||
791 | 24 | SimpleVocabulary, | ||
792 | 25 | ) | ||
793 | 22 | from zope.security.interfaces import Unauthorized | 26 | from zope.security.interfaces import Unauthorized |
794 | 23 | 27 | ||
795 | 24 | from lp.registry.model.distroseries import DistroSeries | 28 | from lp.registry.model.distroseries import DistroSeries |
796 | @@ -150,3 +154,12 @@ class PPAVocabulary(SQLObjectVocabularyBase): | |||
797 | 150 | search_clause) | 154 | search_clause) |
798 | 151 | return self._table.select( | 155 | return self._table.select( |
799 | 152 | clause, orderBy=self._orderBy, clauseTables=self._clauseTables) | 156 | clause, orderBy=self._orderBy, clauseTables=self._clauseTables) |
800 | 157 | |||
801 | 158 | |||
802 | 159 | def make_archive_vocabulary(archives): | ||
803 | 160 | terms = [] | ||
804 | 161 | for archive in archives: | ||
805 | 162 | label = '%s [%s]' % (archive.displayname, archive.reference) | ||
806 | 163 | terms.append(SimpleTerm(archive, archive.reference, label)) | ||
807 | 164 | terms.sort(key=lambda x: x.value.reference) | ||
808 | 165 | return SimpleVocabulary(terms) |
> "[this] is ~800 lines of code motion [and test] and passing parameters down through multiple layers and 6 lines of actually doing the removal after the copy if instructed to do so
Yes, yes it is!
And it looks good to me