← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~cjwatson/launchpad/copy-explicit-pocket into lp:launchpad

 

Colin Watson has proposed merging lp:~cjwatson/launchpad/copy-explicit-pocket into lp:launchpad.

Commit message:
Allow specifying source distroseries and pocket in Archive.copyPackage and in PackageCopyJob metadata.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #1070324 in Launchpad itself: "Incremental copying of binaries doesn't work due to lack of explicit series/pocket selection"
  https://bugs.launchpad.net/launchpad/+bug/1070324

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/copy-explicit-pocket/+merge/131002

== Summary ==

Bug 1070324: Incremental copying of binaries doesn't work.  Fixing this would save us a lot of (variously) manual recovery work and careful attention to avoid manual recovery work, particularly now that we're about to start automatically copying things from PROPOSED to RELEASE.

== Proposed fix ==

Allow optionally passing a source distroseries and pocket to Archive.copyPackage, and pass it all the way down the chain.  I didn't bother with Archive.copyPackages as I don't think we need this sort of fine-grained control there, although most of the pieces are there if somebody wants to do that later for some reason.

The underlying copy script workflow is tested in lib/lp/soyuz/scripts/tests/test_copypackage.py:TestCopyBuildRecords.test_incremental_binary_copies.

== Implementation details ==

I moved PCJ's Archive.getPublishedSources call into its own method to make it more conveniently testable.

== LOC Rationale ==

+103.  I have 5754 lines of credit at the moment, and I think this is worth it to avoid occasional manual recovery work.

== Tests ==

bin/test -vvct lp.soyuz.tests.test_archive.TestCopyPackage -t lp.soyuz.tests.test_packagecopyjob

== Demo and Q/A ==

A lack of dogfood/staging builders makes it hard to QA anything involving real builds, never mind for multiple architectures; but given the tests and that this is a new optional facility it's probably reasonable to just check that copies without from_series/from_pocket still work, along with copies with from_series/from_pocket matching the SPPH to be copied.
-- 
https://code.launchpad.net/~cjwatson/launchpad/copy-explicit-pocket/+merge/131002
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/copy-explicit-pocket into lp:launchpad.
=== modified file 'lib/lp/soyuz/interfaces/archive.py'
--- lib/lp/soyuz/interfaces/archive.py	2012-09-21 07:02:26 +0000
+++ lib/lp/soyuz/interfaces/archive.py	2012-10-23 13:31:46 +0000
@@ -1318,8 +1318,9 @@
         version=TextLine(title=_("Version")),
         from_archive=Reference(schema=Interface),
         # Really IArchive, see below
-        to_pocket=TextLine(title=_("Pocket name")),
-        to_series=TextLine(title=_("Distroseries name"), required=False),
+        to_pocket=TextLine(title=_("Target pocket name")),
+        to_series=TextLine(
+            title=_("Target distroseries name"), required=False),
         include_binaries=Bool(
             title=_("Include Binaries"),
             description=_("Whether or not to copy binaries already built for"
@@ -1335,12 +1336,16 @@
             description=_("Automatically approve this copy (queue admins "
                           "only)."),
             required=False),
+        from_pocket=TextLine(title=_("Source pocket name"), required=False),
+        from_series=TextLine(
+            title=_("Source distroseries name"), required=False),
         )
     @export_write_operation()
     @operation_for_version('devel')
     def copyPackage(source_name, version, from_archive, to_pocket,
                     person, to_series=None, include_binaries=False,
-                    sponsored=None, unembargo=False, auto_approve=False):
+                    sponsored=None, unembargo=False, auto_approve=False,
+                    from_pocket=None, from_series=None):
         """Copy a single named source into this archive.
 
         Asynchronously copy a specific version of a named source to the
@@ -1367,6 +1372,10 @@
         :param auto_approve: if True and the `IPerson` requesting the sync
             has queue admin permissions on the target, accept the copy
             immediately rather than setting it to unapproved.
+        :param from_pocket: the source pocket (as a string). If omitted,
+            copy from any pocket with a matching version.
+        :param from_series: the source distroseries (as a string). If
+            omitted, copy from any series with a matching version.
 
         :raises NoSuchSourcePackageName: if the source name is invalid
         :raises PocketNotFound: if the pocket name is invalid

=== modified file 'lib/lp/soyuz/interfaces/packagecopyjob.py'
--- lib/lp/soyuz/interfaces/packagecopyjob.py	2012-07-30 16:48:37 +0000
+++ lib/lp/soyuz/interfaces/packagecopyjob.py	2012-10-23 13:31:46 +0000
@@ -128,7 +128,8 @@
                target_archive, target_distroseries, target_pocket,
                include_binaries=False, package_version=None,
                copy_policy=PackageCopyPolicy.INSECURE, requester=None,
-               sponsored=None, unembargo=False, auto_approve=False):
+               sponsored=None, unembargo=False, auto_approve=False,
+               source_distroseries=None, source_pocket=None):
         """Create a new `IPlainPackageCopyJob`.
 
         :param package_name: The name of the source package to copy.
@@ -150,6 +151,12 @@
         :param auto_approve: if True and the user requesting the sync has
             queue admin permissions on the target, accept the copy
             immediately rather than setting it to unapproved.
+        :param source_distroseries: The `IDistroSeries` from which to copy
+            the packages. If omitted, copy from any series with a matching
+            version.
+        :param source_pocket: The pocket from which to copy the packages.
+            Must be a member of `PackagePublishingPocket`. If omitted, copy
+            from any pocket with a matching version.
         """
 
     def createMultiple(target_distroseries, copy_tasks, requester,
@@ -227,6 +234,14 @@
         title=_("Automatic approval"),
         required=False, readonly=True)
 
+    source_distroseries = Reference(
+        schema=IDistroSeries, title=_('Source DistroSeries.'),
+        required=False, readonly=True)
+
+    source_pocket = Int(
+        title=_("Source package publishing pocket"), required=False,
+        readonly=True)
+
     def addSourceOverride(override):
         """Add an `ISourceOverride` to the metadata."""
 
@@ -236,6 +251,9 @@
     def getSourceOverride():
         """Get an `ISourceOverride` from the metadata."""
 
+    def findSourcePublication():
+        """Find the appropriate origin `ISourcePackagePublishingHistory`."""
+
     copy_policy = Choice(
         title=_("Applicable copy policy"),
         values=PackageCopyPolicy, required=True, readonly=True)

=== modified file 'lib/lp/soyuz/model/archive.py'
--- lib/lp/soyuz/model/archive.py	2012-10-19 13:11:51 +0000
+++ lib/lp/soyuz/model/archive.py	2012-10-23 13:31:46 +0000
@@ -1639,13 +1639,15 @@
             sources, to_pocket, to_series, include_binaries,
             person=person)
 
-    def _validateAndFindSource(self, from_archive, source_name, version):
+    def _validateAndFindSource(self, from_archive, source_name, version,
+                               from_series=None, from_pocket=None):
         # Check to see if the source package exists, and raise a useful error
         # if it doesn't.
         getUtility(ISourcePackageNameSet)[source_name]
         # Find and validate the source package version required.
         source = from_archive.getPublishedSources(
-            name=source_name, version=version, exact_match=True).first()
+            name=source_name, version=version, exact_match=True,
+            distroseries=from_series, pocket=from_pocket).first()
         if source is None:
             raise CannotCopy(
                 "%s is not published in %s." %
@@ -1664,15 +1666,21 @@
 
     def copyPackage(self, source_name, version, from_archive, to_pocket,
                     person, to_series=None, include_binaries=False,
-                    sponsored=None, unembargo=False, auto_approve=False):
+                    sponsored=None, unembargo=False, auto_approve=False,
+                    from_pocket=None, from_series=None):
         """See `IArchive`."""
         # Asynchronously copy a package using the job system.
         pocket = self._text_to_pocket(to_pocket)
         series = self._text_to_series(to_series)
+        if from_pocket:
+            from_pocket = self._text_to_pocket(from_pocket)
+        if from_series:
+            from_series = self._text_to_series(from_series)
         # Upload permission checks, this will raise CannotCopy as
         # necessary.
         source = self._validateAndFindSource(
-            from_archive, source_name, version)
+            from_archive, source_name, version, from_series=from_series,
+            from_pocket=from_pocket)
         if series is None:
             series = source.distroseries
         check_copy_permissions(person, self, series, pocket, [source])
@@ -1685,7 +1693,8 @@
             package_version=version, include_binaries=include_binaries,
             copy_policy=PackageCopyPolicy.INSECURE, requester=person,
             sponsored=sponsored, unembargo=unembargo,
-            auto_approve=auto_approve)
+            auto_approve=auto_approve, source_distroseries=from_series,
+            source_pocket=from_pocket)
 
     def copyPackages(self, source_names, from_archive, to_pocket,
                      person, to_series=None, from_series=None,

=== modified file 'lib/lp/soyuz/model/packagecopyjob.py'
--- lib/lp/soyuz/model/packagecopyjob.py	2012-10-17 16:05:07 +0000
+++ lib/lp/soyuz/model/packagecopyjob.py	2012-10-23 13:31:46 +0000
@@ -270,19 +270,19 @@
     @classmethod
     def _makeMetadata(cls, target_pocket, package_version,
                       include_binaries, sponsored=None, unembargo=False,
-                      auto_approve=False):
+                      auto_approve=False, source_distroseries=None,
+                      source_pocket=None):
         """Produce a metadata dict for this job."""
-        if sponsored:
-            sponsored_name = sponsored.name
-        else:
-            sponsored_name = None
         return {
             'target_pocket': target_pocket.value,
             'package_version': package_version,
             'include_binaries': bool(include_binaries),
-            'sponsored': sponsored_name,
+            'sponsored': sponsored.name if sponsored else None,
             'unembargo': unembargo,
             'auto_approve': auto_approve,
+            'source_distroseries':
+                source_distroseries.name if source_distroseries else None,
+            'source_pocket': source_pocket.value if source_pocket else None,
         }
 
     @classmethod
@@ -290,13 +290,14 @@
                target_archive, target_distroseries, target_pocket,
                include_binaries=False, package_version=None,
                copy_policy=PackageCopyPolicy.INSECURE, requester=None,
-               sponsored=None, unembargo=False, auto_approve=False):
+               sponsored=None, unembargo=False, auto_approve=False,
+               source_distroseries=None, source_pocket=None):
         """See `IPlainPackageCopyJobSource`."""
         assert package_version is not None, "No package version specified."
         assert requester is not None, "No requester specified."
         metadata = cls._makeMetadata(
             target_pocket, package_version, include_binaries, sponsored,
-            unembargo, auto_approve)
+            unembargo, auto_approve, source_distroseries, source_pocket)
         job = PackageCopyJob(
             job_type=cls.class_job_type,
             source_archive=source_archive,
@@ -435,6 +436,20 @@
     def auto_approve(self):
         return self.metadata.get('auto_approve', False)
 
+    @property
+    def source_distroseries(self):
+        name = self.metadata.get('source_distroseries')
+        if name is None:
+            return None
+        return self.source_archive.distribution[name]
+
+    @property
+    def source_pocket(self):
+        name = self.metadata.get('source_pocket')
+        if name is None:
+            return None
+        return PackagePublishingPocket.items[name]
+
     def _createPackageUpload(self, unapproved=False):
         pu = self.target_distroseries.createQueueEntry(
             pocket=self.target_pocket, archive=self.target_archive,
@@ -472,6 +487,18 @@
 
         return SourceOverride(source_package_name, component, section)
 
+    def findSourcePublication(self):
+        """Find the appropriate origin `ISourcePackagePublishingHistory`."""
+        name = self.package_name
+        version = self.package_version
+        source_package = self.source_archive.getPublishedSources(
+            name=name, version=version, exact_match=True,
+            distroseries=self.source_distroseries,
+            pocket=self.source_pocket).first()
+        if source_package is None:
+            raise CannotCopy("Package %r %r not found." % (name, version))
+        return source_package
+
     def _checkPolicies(self, source_name, source_component=None,
                        auto_approve=False):
         # This helper will only return if it's safe to carry on with the
@@ -589,13 +616,7 @@
                 raise CannotCopy(
                     "Destination pocket must be 'release' for a PPA.")
 
-        name = self.package_name
-        version = self.package_version
-        source_package = self.source_archive.getPublishedSources(
-            name=name, version=version, exact_match=True).first()
-        if source_package is None:
-            raise CannotCopy("Package %r %r not found." % (name, version))
-        source_name = getUtility(ISourcePackageNameSet)[name]
+        source_package = self.findSourcePublication()
 
         # If there's a PackageUpload associated with this job then this
         # job has just been released by an archive admin from the queue.
@@ -604,13 +625,14 @@
         pu = getUtility(IPackageUploadSet).getByPackageCopyJobIDs(
             [self.context.id]).any()
         if pu is None:
+            source_name = getUtility(ISourcePackageNameSet)[self.package_name]
             self._checkPolicies(
                 source_name, source_package.sourcepackagerelease.component,
                 self.auto_approve)
 
         # The package is free to go right in, so just copy it now.
         ancestry = self.target_archive.getPublishedSources(
-            name=name, distroseries=self.target_distroseries,
+            name=self.package_name, distroseries=self.target_distroseries,
             pocket=self.target_pocket, exact_match=True)
         override = self.getSourceOverride()
         copy_policy = self.getPolicyImplementation()
@@ -698,12 +720,15 @@
             " from %s/%s" % (
                 self.source_archive.distribution.name,
                 self.source_archive.name))
+        if self.source_pocket is not None:
+            parts.append(", %s pocket," % self.source_pocket.name)
+        if self.source_distroseries is not None:
+            parts.append(" in %s" % self.source_distroseries)
         parts.append(
             " to %s/%s" % (
                 self.target_archive.distribution.name,
                 self.target_archive.name))
-        parts.append(
-            ", %s pocket," % self.target_pocket.name)
+        parts.append(", %s pocket," % self.target_pocket.name)
         if self.target_distroseries is not None:
             parts.append(" in %s" % self.target_distroseries)
         if self.include_binaries:

=== modified file 'lib/lp/soyuz/tests/test_archive.py'
--- lib/lp/soyuz/tests/test_archive.py	2012-10-19 13:11:51 +0000
+++ lib/lp/soyuz/tests/test_archive.py	2012-10-23 13:31:46 +0000
@@ -2374,6 +2374,32 @@
                 source_name, version, target_archive, to_pocket.name,
                 target_archive.owner)
 
+    def test_copyPackage_with_source_series_and_pocket(self):
+        # The from_series and from_pocket parameters cause copyPackage to
+        # select a matching source publication.
+        (source, source_archive, source_name, target_archive, to_pocket,
+         to_series, version) = self._setup_copy_data()
+        other_series = self.factory.makeDistroSeries(
+            distribution=source_archive.distribution,
+            status=SeriesStatus.DEVELOPMENT)
+        with person_logged_in(source_archive.owner):
+            source.copyTo(
+                other_series, PackagePublishingPocket.UPDATES, source_archive)
+            source.requestDeletion(source_archive.owner)
+        with person_logged_in(target_archive.owner):
+            target_archive.copyPackage(
+                source_name, version, source_archive, to_pocket.name,
+                include_binaries=False, person=target_archive.owner,
+                from_series=source.distroseries.name,
+                from_pocket=source.pocket.name)
+
+        # There should be one copy job, with the source distroseries and
+        # pocket set.
+        job_source = getUtility(IPlainPackageCopyJobSource)
+        copy_job = job_source.getActiveJobs(target_archive).one()
+        self.assertEqual(source.distroseries, copy_job.source_distroseries)
+        self.assertEqual(source.pocket, copy_job.source_pocket)
+
     def test_copyPackages_with_single_package(self):
         (source, source_archive, source_name, target_archive, to_pocket,
          to_series, version) = self._setup_copy_data()

=== modified file 'lib/lp/soyuz/tests/test_packagecopyjob.py'
--- lib/lp/soyuz/tests/test_packagecopyjob.py	2012-10-17 12:33:21 +0000
+++ lib/lp/soyuz/tests/test_packagecopyjob.py	2012-10-23 13:31:46 +0000
@@ -1478,6 +1478,22 @@
 
         self.assertEqual(override, pcj.getSourceOverride())
 
+    def test_findSourcePublication_with_source_series_and_pocket(self):
+        # The source_distroseries and source_pocket parameters cause
+        # findSourcePublication to select a matching source publication.
+        spph = self.publisher.getPubSource()
+        other_series = self.factory.makeDistroSeries(
+            distribution=spph.distroseries.distribution,
+            status=SeriesStatus.DEVELOPMENT)
+        spph.copyTo(
+            other_series, PackagePublishingPocket.PROPOSED, spph.archive)
+        spph.requestDeletion(spph.archive.owner)
+        job = self.createCopyJobForSPPH(
+            spph, spph.archive, spph.archive,
+            target_pocket=PackagePublishingPocket.UPDATES,
+            source_distroseries=spph.distroseries, source_pocket=spph.pocket)
+        self.assertEqual(spph, job.findSourcePublication())
+
     def test_getPolicyImplementation_returns_policy(self):
         # getPolicyImplementation returns the ICopyPolicy that was
         # chosen for the job.


Follow ups