Merge lp:~cjwatson/launchpad/copy-packages-with-default-series into lp:launchpad

Proposed by Colin Watson on 2012-06-28
Status: Merged
Approved by: Colin Watson on 2012-06-28
Approved revision: no longer in the source branch.
Merged at revision: 15523
Proposed branch: lp:~cjwatson/launchpad/copy-packages-with-default-series
Merge into: lp:launchpad
Diff against target: 397 lines (+135/-53)
8 files modified
lib/lp/registry/browser/distroseries.py (+2/-2)
lib/lp/soyuz/browser/archive.py (+3/-5)
lib/lp/soyuz/browser/tests/test_package_copying_mixin.py (+29/-0)
lib/lp/soyuz/model/archive.py (+8/-18)
lib/lp/soyuz/model/packagecopyjob.py (+6/-6)
lib/lp/soyuz/scripts/packagecopier.py (+40/-19)
lib/lp/soyuz/tests/test_archive.py (+43/-0)
lib/lp/soyuz/tests/test_packagecopyjob.py (+4/-3)
To merge this branch: bzr merge lp:~cjwatson/launchpad/copy-packages-with-default-series
Reviewer Review Type Date Requested Status
Jelmer Vernooij (community) code 2012-06-28 Approve on 2012-06-28
Review via email: mp+112557@code.launchpad.net

Commit Message

Fix copying packages with the default target series (i.e. the source series).

Description of the Change

== Summary ==

In various places related to copying packages, Launchpad documents that passing a target series of None means that the target series will be the same as the source series (but in the target archive, of course). However, this doesn't actually work everywhere and OOPSes (bug 795005).

== Proposed fix ==

The OOPS itself is in check_copy_permissions when series is None. That function's current interface takes a sequence of SourcePackageNames, and with that interface this bug is impossible to fix because we only do one permissions check per SPN even if that corresponds to the same source package in multiple series. However, all the call sites have convenient access to the SourcePackagePublishingHistory, so I changed the interface to take a sequence of SPPHs instead, making this straightforward.

I moved the bulk loading down from Archive.copyPackages to check_copy_permissions (I don't think there was currently any bulk loading happening when copying via the UI, which can't have helped matters), and added a bit more for good measure.

Once this is fixed, it also becomes necessary to ensure that the PCJs have the correct target series in their metadata, rather than leaving it as None and deferring that decision to when the PCJ is run, since otherwise calls such as getPendingJobsForTargetSeries won't work. I therefore went through all sites where PCJs are created and ensured that they handle the target series being None where applicable.

== LOC Rationale ==

+59. This is part of allowing us to remove synchronous copying from the PPA UI, as well as eventually removing delayed copies. Besides, I have 2976 lines of credit.

== Tests ==

bin/test -vvct test_copyPackage -t test_packagecopyjob

I haven't explicitly added browser tests here. This is mainly because I actually ran into this bug while working on another branch to remove synchronous copying from the PPA UI, which caused a failure in lib/lp/soyuz/browser/tests/archive-views.txt attributable to this bug. So, while that isn't tested right now, it soon will be.

== Demo and Q/A ==

Find a PPA with packages in several series, create a new temporary PPA, and use Archive.copyPackages with the to_series argument omitted to copy everything from the first to the second (with include_binaries=True to avoid unnecessary rebuilds). It should work with all the series information copied over.

Get an admin to set the soyuz.derived_series.max_synchronous_syncs feature flag to something small like 5 on qastaging, and try the same thing, only this time using the +copy-packages web UI. You'll need to copy >5 (or whatever you set) packages for this to work.

== Lint ==

Just one false positive:

./lib/lp/soyuz/model/archive.py
     347: redefinition of function 'private' from line 343

To post a comment you must log in.
Jelmer Vernooij (jelmer) wrote :

It would be nice to add a test for the case where you're copying two packages with a different distroseries. Other than that, this looks good.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/registry/browser/distroseries.py'
2--- lib/lp/registry/browser/distroseries.py 2012-06-27 13:04:42 +0000
3+++ lib/lp/registry/browser/distroseries.py 2012-06-29 01:07:36 +0000
4@@ -1293,12 +1293,12 @@
5 dsd.parent_source_version,
6 dsd.parent_series.main_archive,
7 target_distroseries.main_archive,
8+ target_distroseries,
9 PackagePublishingPocket.RELEASE,
10 )
11 for dsd in self.getUpgrades()]
12 getUtility(IPlainPackageCopyJobSource).createMultiple(
13- target_distroseries, copies, self.user,
14- copy_policy=PackageCopyPolicy.MASS_SYNC)
15+ copies, self.user, copy_policy=PackageCopyPolicy.MASS_SYNC)
16
17 self.request.response.addInfoNotification(
18 (u"Upgrades of {context.displayname} packages have been "
19
20=== modified file 'lib/lp/soyuz/browser/archive.py'
21--- lib/lp/soyuz/browser/archive.py 2012-06-14 03:22:43 +0000
22+++ lib/lp/soyuz/browser/archive.py 2012-06-29 01:07:36 +0000
23@@ -1311,16 +1311,14 @@
24 not permitted.
25 """
26 if check_permissions:
27- spns = [
28- spph.sourcepackagerelease.sourcepackagename
29- for spph in source_pubs]
30 check_copy_permissions(
31- person, dest_archive, dest_series, dest_pocket, spns)
32+ person, dest_archive, dest_series, dest_pocket, source_pubs)
33
34 job_source = getUtility(IPlainPackageCopyJobSource)
35 for spph in source_pubs:
36 job_source.create(
37- spph.source_package_name, spph.archive, dest_archive, dest_series,
38+ spph.source_package_name, spph.archive, dest_archive,
39+ dest_series if dest_series is not None else spph.distroseries,
40 dest_pocket, include_binaries=include_binaries,
41 package_version=spph.sourcepackagerelease.version,
42 copy_policy=PackageCopyPolicy.INSECURE,
43
44=== modified file 'lib/lp/soyuz/browser/tests/test_package_copying_mixin.py'
45--- lib/lp/soyuz/browser/tests/test_package_copying_mixin.py 2012-01-01 02:58:52 +0000
46+++ lib/lp/soyuz/browser/tests/test_package_copying_mixin.py 2012-06-29 01:07:36 +0000
47@@ -251,6 +251,35 @@
48 spr = spph.sourcepackagerelease
49 self.assertEqual(spr.sourcepackagename.name, job.package_name)
50 self.assertEqual(spr.version, job.package_version)
51+ self.assertEqual(dest_series, job.target_distroseries)
52+
53+ def test_copy_asynchronously_handles_no_dest_series(self):
54+ # If dest_series is None, copy_asynchronously creates jobs that will
55+ # copy each source into the same distroseries in the target archive.
56+ distribution = self.makeDistribution()
57+ series_one = self.factory.makeDistroSeries(
58+ distribution=distribution, registrant=self.person)
59+ series_two = self.factory.makeDistroSeries(
60+ distribution=distribution, registrant=self.person)
61+ spph_one = self.factory.makeSourcePackagePublishingHistory(
62+ distroseries=series_one, sourcepackagename="one",
63+ maintainer=self.person, creator=self.person)
64+ spph_two = self.factory.makeSourcePackagePublishingHistory(
65+ distroseries=series_two, sourcepackagename="two",
66+ maintainer=self.person, creator=self.person)
67+ pocket = self.factory.getAnyPocket()
68+ target_archive = self.factory.makeArchive(
69+ owner=self.person, distribution=distribution)
70+ copy_asynchronously(
71+ [spph_one, spph_two], target_archive, None, pocket,
72+ include_binaries=False, check_permissions=False,
73+ person=self.person)
74+ jobs = list(getUtility(IPlainPackageCopyJobSource).getActiveJobs(
75+ target_archive))
76+ self.assertEqual(2, len(jobs))
77+ self.assertContentEqual(
78+ [("one", spph_one.distroseries), ("two", spph_two.distroseries)],
79+ [(job.package_name, job.target_distroseries) for job in jobs])
80
81 def test_do_copy_goes_async_if_canCopySynchronously_says_so(self):
82 # The view opts for asynchronous copying if canCopySynchronously
83
84=== modified file 'lib/lp/soyuz/model/archive.py'
85--- lib/lp/soyuz/model/archive.py 2012-06-19 23:49:20 +0000
86+++ lib/lp/soyuz/model/archive.py 2012-06-29 01:07:36 +0000
87@@ -72,7 +72,6 @@
88 from lp.registry.model.sourcepackagename import SourcePackageName
89 from lp.registry.model.teammembership import TeamParticipation
90 from lp.services.config import config
91-from lp.services.database.bulk import load_related
92 from lp.services.database.constants import UTC_NOW
93 from lp.services.database.datetimecol import UtcDateTimeCol
94 from lp.services.database.decoratedresultset import DecoratedResultSet
95@@ -1641,11 +1640,12 @@
96 series = self._text_to_series(to_series)
97 # Upload permission checks, this will raise CannotCopy as
98 # necessary.
99- sourcepackagename = getUtility(ISourcePackageNameSet)[source_name]
100- check_copy_permissions(
101- person, self, series, pocket, [sourcepackagename])
102+ source = self._validateAndFindSource(
103+ from_archive, source_name, version)
104+ if series is None:
105+ series = source.distroseries
106+ check_copy_permissions(person, self, series, pocket, [source])
107
108- self._validateAndFindSource(from_archive, source_name, version)
109 job_source = getUtility(IPlainPackageCopyJobSource)
110 job_source.create(
111 package_name=source_name, source_archive=from_archive,
112@@ -1667,20 +1667,10 @@
113 raise CannotCopy(
114 "None of the supplied package names are published")
115
116- # Bulk-load the sourcepackagereleases so that the list
117- # comprehension doesn't generate additional queries. The
118- # sourcepackagenames themselves will already have been loaded when
119- # generating the list of source publications in "sources".
120- load_related(
121- SourcePackageRelease, sources, ["sourcepackagereleaseID"])
122- sourcepackagenames = [source.sourcepackagerelease.sourcepackagename
123- for source in sources]
124-
125 # Now do a mass check of permissions.
126 pocket = self._text_to_pocket(to_pocket)
127 series = self._text_to_series(to_series)
128- check_copy_permissions(
129- person, self, series, pocket, sourcepackagenames)
130+ check_copy_permissions(person, self, series, pocket, sources)
131
132 # If we get this far then we can create the PackageCopyJob.
133 copy_tasks = []
134@@ -1690,14 +1680,14 @@
135 source.sourcepackagerelease.version,
136 from_archive,
137 self,
138+ series if series is not None else source.distroseries,
139 PackagePublishingPocket.RELEASE
140 )
141 copy_tasks.append(task)
142
143 job_source = getUtility(IPlainPackageCopyJobSource)
144 job_source.createMultiple(
145- series, copy_tasks, person,
146- copy_policy=PackageCopyPolicy.MASS_SYNC,
147+ copy_tasks, person, copy_policy=PackageCopyPolicy.MASS_SYNC,
148 include_binaries=include_binaries, sponsored=sponsored,
149 unembargo=unembargo)
150
151
152=== modified file 'lib/lp/soyuz/model/packagecopyjob.py'
153--- lib/lp/soyuz/model/packagecopyjob.py 2012-06-24 01:56:36 +0000
154+++ lib/lp/soyuz/model/packagecopyjob.py 2012-06-29 01:07:36 +0000
155@@ -287,9 +287,8 @@
156 return derived
157
158 @classmethod
159- def _composeJobInsertionTuple(cls, target_distroseries, copy_policy,
160- include_binaries, job_id, copy_task,
161- sponsored, unembargo):
162+ def _composeJobInsertionTuple(cls, copy_policy, include_binaries, job_id,
163+ copy_task, sponsored, unembargo):
164 """Create an SQL fragment for inserting a job into the database.
165
166 :return: A string representing an SQL tuple containing initializers
167@@ -301,6 +300,7 @@
168 package_version,
169 source_archive,
170 target_archive,
171+ target_distroseries,
172 target_pocket,
173 ) = copy_task
174 metadata = cls._makeMetadata(
175@@ -313,7 +313,7 @@
176 return data
177
178 @classmethod
179- def createMultiple(cls, target_distroseries, copy_tasks, requester,
180+ def createMultiple(cls, copy_tasks, requester,
181 copy_policy=PackageCopyPolicy.INSECURE,
182 include_binaries=False, sponsored=None,
183 unembargo=False):
184@@ -322,8 +322,8 @@
185 job_ids = Job.createMultiple(store, len(copy_tasks), requester)
186 job_contents = [
187 cls._composeJobInsertionTuple(
188- target_distroseries, copy_policy, include_binaries, job_id,
189- task, sponsored, unembargo)
190+ copy_policy, include_binaries, job_id, task, sponsored,
191+ unembargo)
192 for job_id, task in zip(job_ids, copy_tasks)]
193 return bulk.create(
194 (PackageCopyJob.job_type, PackageCopyJob.target_distroseries,
195
196=== modified file 'lib/lp/soyuz/scripts/packagecopier.py'
197--- lib/lp/soyuz/scripts/packagecopier.py 2012-06-27 17:20:45 +0000
198+++ lib/lp/soyuz/scripts/packagecopier.py 2012-06-29 01:07:36 +0000
199@@ -16,6 +16,7 @@
200 'update_files_privacy',
201 ]
202
203+from operator import attrgetter
204 import os
205 import tempfile
206
207@@ -26,6 +27,7 @@
208
209 from lp.app.errors import NotFoundError
210 from lp.buildmaster.enums import BuildStatus
211+from lp.services.database.bulk import load_related
212 from lp.services.librarian.interfaces import ILibraryFileAliasSet
213 from lp.services.librarian.utils import copy_and_close
214 from lp.soyuz.adapters.notification import notify
215@@ -204,39 +206,59 @@
216 return {'status': BuildSetStatus.NEEDSBUILD}
217
218
219-def check_copy_permissions(person, archive, series, pocket,
220- sourcepackagenames):
221+def check_copy_permissions(person, archive, series, pocket, sources):
222 """Check that `person` has permission to copy a package.
223
224 :param person: User attempting the upload.
225 :param archive: Destination `Archive`.
226 :param series: Destination `DistroSeries`.
227 :param pocket: Destination `Pocket`.
228- :param sourcepackagenames: Sequence of `SourcePackageName`s for the
229+ :param sources: Sequence of `SourcePackagePublishingHistory`s for the
230 packages to be copied.
231 :raises CannotCopy: If the copy is not allowed.
232 """
233+ # Circular import.
234+ from lp.soyuz.model.sourcepackagerelease import SourcePackageRelease
235+
236 if person is None:
237 raise CannotCopy("Cannot check copy permissions (no requester).")
238
239+ if len(sources) > 1:
240+ # Bulk-load the data we'll need from each source publication.
241+ load_related(SourcePackageRelease, sources, ["sourcepackagereleaseID"])
242+
243 # If there is a requester, check that he has upload permission into
244 # the destination (archive, component, pocket). This check is done
245 # here rather than in the security adapter because it requires more
246 # info than is available in the security adapter.
247- for spn in set(sourcepackagenames):
248- package = series.getSourcePackage(spn)
249- destination_component = package.latest_published_component
250-
251- # If destination_component is not None, make sure the person
252- # has upload permission for this component. Otherwise, any
253- # upload permission on this archive will do.
254- strict_component = destination_component is not None
255- reason = archive.checkUpload(
256- person, series, spn, destination_component, pocket,
257- strict_component=strict_component)
258-
259- if reason is not None:
260- raise CannotCopy(reason)
261+ if series is None:
262+ # Use each source's series as the destination for that source.
263+ for source in sources:
264+ reason = archive.checkUpload(
265+ person, source.distroseries,
266+ source.sourcepackagerelease.sourcepackagename,
267+ source.component, pocket, strict_component=True)
268+
269+ if reason is not None:
270+ raise CannotCopy(reason)
271+ else:
272+ sourcepackagenames = [
273+ source.sourcepackagerelease.sourcepackagename
274+ for source in sources]
275+ for spn in set(sourcepackagenames):
276+ package = series.getSourcePackage(spn)
277+ destination_component = package.latest_published_component
278+
279+ # If destination_component is not None, make sure the person
280+ # has upload permission for this component. Otherwise, any
281+ # upload permission on this archive will do.
282+ strict_component = destination_component is not None
283+ reason = archive.checkUpload(
284+ person, series, spn, destination_component, pocket,
285+ strict_component=strict_component)
286+
287+ if reason is not None:
288+ raise CannotCopy(reason)
289
290
291 class CopyChecker:
292@@ -463,8 +485,7 @@
293 """
294 if check_permissions:
295 check_copy_permissions(
296- person, self.archive, series, pocket,
297- [source.sourcepackagerelease.sourcepackagename])
298+ person, self.archive, series, pocket, [source])
299
300 if series not in self.archive.distribution.series:
301 raise CannotCopy(
302
303=== modified file 'lib/lp/soyuz/tests/test_archive.py'
304--- lib/lp/soyuz/tests/test_archive.py 2012-06-19 23:49:20 +0000
305+++ lib/lp/soyuz/tests/test_archive.py 2012-06-29 01:07:36 +0000
306@@ -2368,6 +2368,22 @@
307 self.assertEqual(target_archive, copy_job.target_archive)
308 self.assertTrue(copy_job.unembargo)
309
310+ def test_copyPackage_with_default_distroseries(self):
311+ # If to_series is None, copyPackage copies into the same series as
312+ # the source in the target archive.
313+ (source, source_archive, source_name, target_archive, to_pocket,
314+ to_series, version) = self._setup_copy_data()
315+ with person_logged_in(target_archive.owner):
316+ target_archive.copyPackage(
317+ source_name, version, source_archive, to_pocket.name,
318+ include_binaries=False, person=target_archive.owner)
319+
320+ # There should be one copy job with the correct target series.
321+ job_source = getUtility(IPlainPackageCopyJobSource)
322+ copy_jobs = job_source.getActiveJobs(target_archive)
323+ self.assertEqual(1, copy_jobs.count())
324+ self.assertEqual(source.distroseries, copy_jobs[0].target_distroseries)
325+
326 def test_copyPackages_with_single_package(self):
327 (source, source_archive, source_name, target_archive, to_pocket,
328 to_series, version) = self._setup_copy_data()
329@@ -2505,6 +2521,33 @@
330 self.assertEqual(copy_job, copy_jobs[0])
331 self.assertEqual(new_version, copy_jobs[1].package_version)
332
333+ def test_copyPackages_with_default_distroseries(self):
334+ # If to_series is None, copyPackages copies into the same series as
335+ # each source in the target archive.
336+ (source, source_archive, source_name, target_archive, to_pocket,
337+ to_series, version) = self._setup_copy_data()
338+ sources = [source]
339+ other_series = self.factory.makeDistroSeries(
340+ distribution=target_archive.distribution)
341+ sources.append(self.factory.makeSourcePackagePublishingHistory(
342+ distroseries=other_series, archive=source_archive,
343+ status=PackagePublishingStatus.PUBLISHED))
344+ names = [source.sourcepackagerelease.sourcepackagename.name
345+ for source in sources]
346+
347+ with person_logged_in(target_archive.owner):
348+ target_archive.copyPackages(
349+ names, source_archive, to_pocket.name, include_binaries=False,
350+ person=target_archive.owner)
351+
352+ # There should be two copy jobs with the correct target series.
353+ job_source = getUtility(IPlainPackageCopyJobSource)
354+ copy_jobs = job_source.getActiveJobs(target_archive)
355+ self.assertEqual(2, copy_jobs.count())
356+ self.assertContentEqual(
357+ [source.distroseries for source in sources],
358+ [copy_job.target_distroseries for copy_job in copy_jobs])
359+
360
361 class TestgetAllPublishedBinaries(TestCaseWithFactory):
362
363
364=== modified file 'lib/lp/soyuz/tests/test_packagecopyjob.py'
365--- lib/lp/soyuz/tests/test_packagecopyjob.py 2012-06-27 00:49:47 +0000
366+++ lib/lp/soyuz/tests/test_packagecopyjob.py 2012-06-29 01:07:36 +0000
367@@ -243,6 +243,7 @@
368 "1.5mother1",
369 mother.parent_series.main_archive,
370 derived_series.main_archive,
371+ derived_series,
372 PackagePublishingPocket.RELEASE,
373 ),
374 (
375@@ -250,12 +251,11 @@
376 "0.9father1",
377 father.parent_series.main_archive,
378 derived_series.main_archive,
379+ derived_series,
380 PackagePublishingPocket.UPDATES,
381 ),
382 ]
383- job_ids = list(
384- job_source.createMultiple(mother.derived_series, copy_tasks,
385- requester))
386+ job_ids = list(job_source.createMultiple(copy_tasks, requester))
387 jobs = list(job_source.getActiveJobs(derived_series.main_archive))
388 self.assertContentEqual(job_ids, [job.id for job in jobs])
389 self.assertEqual(len(copy_tasks), len(set([job.job for job in jobs])))
390@@ -269,6 +269,7 @@
391 job.package_version,
392 job.source_archive,
393 job.target_archive,
394+ job.target_distroseries,
395 job.target_pocket,
396 )
397 for job in jobs]