← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~pappacena/launchpad:package-publishing-copied-from-archive into launchpad:master

 

Thiago F. Pappacena has proposed merging ~pappacena/launchpad:package-publishing-copied-from-archive into launchpad:master.

Commit message:
Keeping track of the original archives of source and binary package copies on [Source|Binary]PackagePublishingHistory.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~pappacena/launchpad/+git/launchpad/+merge/380382

The tracking of "copied_from_archive" seems to be working fine, but it's missing showing this information on S/BPackagePublishingHistory page.

On the page, it's shown as "Previous archive was <archive>" text, right below where we show that the package was copied today. I'm not sure this is the right text to show to the user, but I couldn't think of anything different.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~pappacena/launchpad:package-publishing-copied-from-archive into launchpad:master.
diff --git a/lib/lp/_schema_circular_imports.py b/lib/lp/_schema_circular_imports.py
index 70c5b72..1bc1a06 100644
--- a/lib/lp/_schema_circular_imports.py
+++ b/lib/lp/_schema_circular_imports.py
@@ -361,6 +361,10 @@ patch_reference_property(
 patch_reference_property(
     ISourcePackagePublishingHistory, 'archive', IArchive)
 patch_reference_property(
+    IBinaryPackagePublishingHistory, 'copied_from_archive', IArchive)
+patch_reference_property(
+    ISourcePackagePublishingHistory, 'copied_from_archive', IArchive)
+patch_reference_property(
     ISourcePackagePublishingHistory, 'ancestor',
     ISourcePackagePublishingHistory)
 patch_reference_property(
diff --git a/lib/lp/soyuz/browser/publishing.py b/lib/lp/soyuz/browser/publishing.py
index 1a23bc3..9547600 100644
--- a/lib/lp/soyuz/browser/publishing.py
+++ b/lib/lp/soyuz/browser/publishing.py
@@ -1,4 +1,4 @@
-# Copyright 2009-2018 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2020 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Browser views for Soyuz publishing records."""
@@ -265,6 +265,14 @@ class SourcePublishingRecordView(BasePublishingRecordView):
 
         return False
 
+    def rootOriginArchive(self):
+        """Get the original archive from this binary build if this was a
+        copied publishing.
+        """
+        if not self.wasCopied():
+            return None
+        return self.context.sourcepackagerelease.upload_archive
+
     @property
     def allow_selection(self):
         """Do not render the checkbox corresponding to this record."""
@@ -406,3 +414,11 @@ class BinaryPublishingRecordView(BasePublishingRecordView):
             return True
 
         return False
+
+    def rootOriginArchive(self):
+        """Get the original archive from this binary build if this was a
+        copied publishing.
+        """
+        if not self.wasCopied():
+            return None
+        return self.context.binarypackagerelease.build.archive
diff --git a/lib/lp/soyuz/interfaces/publishing.py b/lib/lp/soyuz/interfaces/publishing.py
index 02c3615..fab8a76 100644
--- a/lib/lp/soyuz/interfaces/publishing.py
+++ b/lib/lp/soyuz/interfaces/publishing.py
@@ -1,4 +1,4 @@
-# Copyright 2009-2019 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2020 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Publishing interfaces."""
@@ -269,6 +269,13 @@ class ISourcePackagePublishingHistoryPublic(IPublishingView):
             Interface,
             title=_('Archive ID'), required=True, readonly=True,
             ))
+    copied_from_archive = exported(
+        Reference(
+            # Really IArchive (fixed in _schema_circular_imports.py).
+            Interface,
+            title=_('Original archive ID where this package was copied from.'),
+            required=False, readonly=True,
+            ))
     supersededby = Int(
             title=_('The sourcepackagerelease which superseded this one'),
             required=False, readonly=False,
@@ -703,6 +710,13 @@ class IBinaryPackagePublishingHistoryPublic(IPublishingView):
             description=_("The context archive for this publication."),
             required=True, readonly=True,
             ))
+    copied_from_archive = exported(
+        Reference(
+            # Really IArchive (fixed in _schema_circular_imports.py).
+            Interface,
+            title=_('Original archive ID where this package was copied from.'),
+            required=False, readonly=True,
+        ))
     removed_by = exported(
         Reference(
             IPerson,
@@ -865,7 +879,8 @@ class IBinaryPackagePublishingHistory(IBinaryPackagePublishingHistoryPublic,
 class IPublishingSet(Interface):
     """Auxiliary methods for dealing with sets of publications."""
 
-    def publishBinaries(archive, distroseries, pocket, binaries):
+    def publishBinaries(archive, distroseries, pocket, binaries,
+                        copied_from_archives=None):
         """Efficiently publish multiple BinaryPackageReleases in an Archive.
 
         Creates `IBinaryPackagePublishingHistory` records for each
@@ -879,6 +894,8 @@ class IPublishingSet(Interface):
         :param binaries: A dict mapping `BinaryPackageReleases` to their
             desired overrides as (`Component`, `Section`,
             `PackagePublishingPriority`, `phased_update_percentage`) tuples.
+        :param copied_from_archives: A dict mapping `BinaryPackageReleases`
+            to their original archives (for copy operations).
 
         :return: A list of new `IBinaryPackagePublishingHistory` records.
         """
@@ -903,7 +920,8 @@ class IPublishingSet(Interface):
 
     def newSourcePublication(archive, sourcepackagerelease, distroseries,
                              component, section, pocket, ancestor,
-                             create_dsd_job=True):
+                             create_dsd_job=True, copied_from_archive=None,
+                             creator=None, sponsor=None, packageupload=None):
         """Create a new `SourcePackagePublishingHistory`.
 
         :param archive: An `IArchive`
@@ -916,6 +934,8 @@ class IPublishingSet(Interface):
             version of this publishing record
         :param create_dsd_job: A boolean indicating whether or not a dsd job
              should be created for the new source publication.
+        :param copied_from_archive: For copy operations, this should be the
+            source archive (from where this new publication is comming from).
         :param creator: An optional `IPerson`. If this is None, the
             sourcepackagerelease's creator will be used.
         :param sponsor: An optional `IPerson` indicating the sponsor of this
diff --git a/lib/lp/soyuz/model/publishing.py b/lib/lp/soyuz/model/publishing.py
index 3e73e98..70007bb 100644
--- a/lib/lp/soyuz/model/publishing.py
+++ b/lib/lp/soyuz/model/publishing.py
@@ -1,4 +1,4 @@
-# Copyright 2009-2019 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2020 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 __metaclass__ = type
@@ -258,6 +258,8 @@ class SourcePackagePublishingHistory(SQLBase, ArchivePublisherBase):
                      default=PackagePublishingPocket.RELEASE,
                      notNull=True)
     archive = ForeignKey(dbName="archive", foreignKey="Archive", notNull=True)
+    copied_from_archive = ForeignKey(
+        dbName="copied_from_archive", foreignKey="Archive", notNull=False)
     removed_by = ForeignKey(
         dbName="removed_by", foreignKey="Person",
         storm_validator=validate_public_person, default=None)
@@ -529,6 +531,9 @@ class SourcePackagePublishingHistory(SQLBase, ArchivePublisherBase):
                 component = override.component
             if override.section is not None:
                 section = override.section
+
+        copied_from_archive = self.archive if archive != self.archive else None
+
         return getUtility(IPublishingSet).newSourcePublication(
             archive,
             self.sourcepackagerelease,
@@ -540,6 +545,7 @@ class SourcePackagePublishingHistory(SQLBase, ArchivePublisherBase):
             create_dsd_job=create_dsd_job,
             creator=creator,
             sponsor=sponsor,
+            copied_from_archive=copied_from_archive,
             packageupload=packageupload)
 
     def getStatusSummaryForBuilds(self):
@@ -638,6 +644,8 @@ class BinaryPackagePublishingHistory(SQLBase, ArchivePublisherBase):
     dateremoved = UtcDateTimeCol(default=None)
     pocket = EnumCol(dbName='pocket', schema=PackagePublishingPocket)
     archive = ForeignKey(dbName="archive", foreignKey="Archive", notNull=True)
+    copied_from_archive = ForeignKey(
+        dbName="copied_from_archive", foreignKey="Archive", notNull=False)
     removed_by = ForeignKey(
         dbName="removed_by", foreignKey="Person",
         storm_validator=validate_public_person, default=None)
@@ -1016,8 +1024,11 @@ def expand_binary_requests(distroseries, binaries):
 class PublishingSet:
     """Utilities for manipulating publications in batches."""
 
-    def publishBinaries(self, archive, distroseries, pocket, binaries):
+    def publishBinaries(self, archive, distroseries, pocket, binaries,
+                        copied_from_archives=None):
         """See `IPublishingSet`."""
+        if copied_from_archives is None:
+            copied_from_archives = {}
         # Expand the dict of binaries into a list of tuples including the
         # architecture.
         if distroseries.distribution != archive.distribution:
@@ -1066,13 +1077,24 @@ class PublishingSet:
         if not needed:
             return []
 
+        def get_origin_archive(bpr):
+            """Returns the original archive of a BinaryPackageRelease
+            to be set on the newly created BinaryPackagePublishingHistory.
+            """
+            original_archive = copied_from_archives.get(bpr)
+            if original_archive == archive:
+                return None
+            return original_archive
+
         BPPH = BinaryPackagePublishingHistory
         return bulk.create(
-            (BPPH.archive, BPPH.distroarchseries, BPPH.pocket,
+            (BPPH.archive, BPPH.copied_from_archive,
+             BPPH.distroarchseries, BPPH.pocket,
              BPPH.binarypackagerelease, BPPH.binarypackagename,
              BPPH.component, BPPH.section, BPPH.priority,
              BPPH.phased_update_percentage, BPPH.status, BPPH.datecreated),
-            [(archive, das, pocket, bpr, bpr.binarypackagename,
+            [(archive, get_origin_archive(bpr), das, pocket, bpr,
+              bpr.binarypackagename,
               get_component(archive, das.distroseries, component),
               section, priority, phased_update_percentage,
               PackagePublishingStatus.PENDING, UTC_NOW)
@@ -1150,12 +1172,16 @@ class PublishingSet:
                  bpph.priority, None)) for bpph in bpphs)
         if not with_overrides:
             return list()
+        copied_from_archives = {
+            bpph.binarypackagerelease: bpph.archive for bpph in bpphs}
         return self.publishBinaries(
-            archive, distroseries, pocket, with_overrides)
+            archive, distroseries, pocket, with_overrides,
+            copied_from_archives)
 
     def newSourcePublication(self, archive, sourcepackagerelease,
                              distroseries, component, section, pocket,
                              ancestor=None, create_dsd_job=True,
+                             copied_from_archive=None,
                              creator=None, sponsor=None, packageupload=None):
         """See `IPublishingSet`."""
         # Circular import.
@@ -1171,6 +1197,7 @@ class PublishingSet:
         pub = SourcePackagePublishingHistory(
             distroseries=distroseries,
             pocket=pocket,
+            copied_from_archive=copied_from_archive,
             archive=archive,
             sourcepackagename=sourcepackagerelease.sourcepackagename,
             sourcepackagerelease=sourcepackagerelease,
diff --git a/lib/lp/soyuz/scripts/tests/test_copypackage.py b/lib/lp/soyuz/scripts/tests/test_copypackage.py
index b5bd4c6..4406a96 100644
--- a/lib/lp/soyuz/scripts/tests/test_copypackage.py
+++ b/lib/lp/soyuz/scripts/tests/test_copypackage.py
@@ -1,4 +1,4 @@
-# Copyright 2009-2018 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2020 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 __metaclass__ = type
@@ -11,6 +11,7 @@ from testtools.content import text_content
 from testtools.matchers import (
     Equals,
     LessThan,
+    MatchesListwise,
     MatchesStructure,
     )
 import transaction
@@ -1015,6 +1016,38 @@ class CopyCheckerTestCase(TestCaseWithFactory):
             copied_source, copied_source.distroseries, copied_source.pocket,
             None, False)
 
+    def test_copy_to_another_archive_tracks_source_archive(self):
+        """Checks that creating a copy of a package publishing keeps track
+        of origin archive.
+        """
+        # Create a source with binaries in ubuntutest/breezy-autotest.
+        source = self.test_publisher.getPubSource(architecturehintlist='i386')
+        binary = self.test_publisher.getPubBinaries(pub_source=source)[0]
+
+        hoary = self.test_publisher.ubuntutest.getSeries('hoary-test')
+        self.test_publisher.addFakeChroots(hoary)
+
+        target_archive = self.factory.makeArchive(
+            distribution=self.test_publisher.ubuntutest,
+            purpose=ArchivePurpose.PPA)
+
+        copied_source = source.copyTo(hoary, source.pocket, target_archive)
+        copied_source = removeSecurityProxy(copied_source)
+        self.assertThat(
+            copied_source, MatchesStructure.byEquality(
+                    archive=target_archive,
+                    copied_from_archive=source.archive))
+
+        copied_binaries = binary.copyTo(hoary, source.pocket, target_archive)
+        copied_binaries = removeSecurityProxy(copied_binaries)
+        self.assertEqual(1, len(copied_binaries))
+        self.assertThat(
+            copied_binaries, MatchesListwise([
+                MatchesStructure.byEquality(
+                    archive=target_archive,
+                    copied_from_archive=binary.archive),
+            ]))
+
 
 class BaseDoCopyTests:
 
diff --git a/lib/lp/soyuz/stories/ppa/xx-copy-packages.txt b/lib/lp/soyuz/stories/ppa/xx-copy-packages.txt
index 8159d2d..95bec1a 100644
--- a/lib/lp/soyuz/stories/ppa/xx-copy-packages.txt
+++ b/lib/lp/soyuz/stories/ppa/xx-copy-packages.txt
@@ -289,6 +289,7 @@ context.
     Publishing details
       Copied from ubuntu hoary in Primary Archive for Ubuntu Linux by James
       Blackwell
+      Previous archive was PPA for Celso Providelo
     Changelog
       pmount (0.1-1) hoary; urgency=low
       * Fix description (Malone #1)
diff --git a/lib/lp/soyuz/stories/soyuz/xx-packagepublishinghistory.txt b/lib/lp/soyuz/stories/soyuz/xx-packagepublishinghistory.txt
index 31e98b7..e62ede7 100644
--- a/lib/lp/soyuz/stories/soyuz/xx-packagepublishinghistory.txt
+++ b/lib/lp/soyuz/stories/soyuz/xx-packagepublishinghistory.txt
@@ -27,6 +27,61 @@ shows the complete history of a package in all series.
     >>> print(table.findAll("tr")[2].td["colspan"])
     8
 
+Copy the package to a new distribution named "foo-distro". The publishing
+history page of Foo-distro should show that the package was copied, but no
+intermediate archive information.
+
+    >>> login('foo.bar@xxxxxxxxxxxxx')
+    >>> from lp.soyuz.model.archive import ArchivePurpose
+    >>> new_distro = stp.factory.makeDistribution(name='foo-distro')
+    >>> new_distroseries = stp.factory.makeDistroSeries(
+    ...     name='foo-series', distribution=new_distro)
+    >>> new_archive = stp.factory.makeArchive(
+    ...     distribution=new_distro,
+    ...     purpose=ArchivePurpose.PRIMARY)
+    >>> new_pub1 = source_pub.copyTo(
+    ...     new_distroseries, source_pub.pocket, new_archive)
+    >>> logout()
+
+    >>> anon_browser.open(
+    ...     'http://launchpad.test/%s/+source/test-history/'
+    ...     '+publishinghistory' % new_distro.name)
+
+    >>> table = find_tag_by_id(anon_browser.contents, 'publishing-summary')
+    >>> print(extract_text(table))
+    Date    Status    Target          Pocket   Component Section Version
+    ... UTC Pending   Foo-series      release  main      base    666
+    Copied from ubuntutest breezy-autotest in Primary Archive for Ubuntu Test
+
+
+Copying from "Foo-distro" to a new distribution "Another-distro". It should
+show the intermediate archive Foo-distro on Another-distro's history page,
+since we copied the package from there. It should also show that the
+original distribution was Ubuntutest breezy-autotest.
+
+    >>> login('foo.bar@xxxxxxxxxxxxx')
+    >>> from lp.soyuz.model.archive import ArchivePurpose
+    >>> new_distro = stp.factory.makeDistribution(name='another-distro')
+    >>> new_distroseries = stp.factory.makeDistroSeries(
+    ...     name='another-series', distribution=new_distro)
+    >>> new_archive = stp.factory.makeArchive(
+    ...     distribution=new_distro,
+    ...     purpose=ArchivePurpose.PRIMARY)
+    >>> new_pub2 = new_pub1.copyTo(
+    ...     new_distroseries, source_pub.pocket, new_archive)
+    >>> logout()
+
+    >>> anon_browser.open(
+    ...     'http://launchpad.test/%s/+source/test-history/'
+    ...     '+publishinghistory' % new_distro.name)
+
+    >>> table = find_tag_by_id(anon_browser.contents, 'publishing-summary')
+    >>> print(extract_text(table))
+    Date    Status    Target          Pocket   Component Section Version
+    ... UTC Pending   Another-series  release  main      base    666
+    Copied from ubuntutest breezy-autotest in Primary Archive for Ubuntu Test
+    Previous archive was Primary Archive for Foo-distro
+
 A publishing record will be shown as deleted in the publishing history after a
 request for deletion by a user.
 
diff --git a/lib/lp/soyuz/stories/webservice/xx-binary-package-publishing.txt b/lib/lp/soyuz/stories/webservice/xx-binary-package-publishing.txt
index 81482f2..df1a485 100644
--- a/lib/lp/soyuz/stories/webservice/xx-binary-package-publishing.txt
+++ b/lib/lp/soyuz/stories/webservice/xx-binary-package-publishing.txt
@@ -71,6 +71,7 @@ Each binary publication exposes a number of properties:
     binary_package_version: u'1.0'
     build_link: u'http://.../~cprov/+archive/ubuntu/ppa/+build/28'
     component_name: u'main'
+    copied_from_archive_link: None
     date_created: u'2007-08-10T13:00:00+00:00'
     date_made_pending: None
     date_published: u'2007-08-10T13:00:01+00:00'
diff --git a/lib/lp/soyuz/templates/packagepublishing-details.pt b/lib/lp/soyuz/templates/packagepublishing-details.pt
index 0b0102c..ca90dc6 100644
--- a/lib/lp/soyuz/templates/packagepublishing-details.pt
+++ b/lib/lp/soyuz/templates/packagepublishing-details.pt
@@ -34,7 +34,16 @@
       <span tal:attributes="title context/datepublished/fmt:datetime"
             tal:content="context/datepublished/fmt:displaydate" />
     </li>
-    <li tal:condition="view/wasCopied">
+
+    <tal:comment condition="nothing">
+           For package copies, we have a distinction between what is the
+           "previous_archive" (the archive from where we directly copied a
+           publishing) and the "root_archive" (the archive where the package
+           was originally uploaded to).
+    </tal:comment>
+    <li tal:define="previous_archive context/copied_from_archive;
+                    root_archive view/rootOriginArchive"
+        tal:condition="view/wasCopied">
       Copied from
       <tal:source_original_location condition="view/is_source">
         <tal:define
@@ -46,12 +55,11 @@
                     message string:${distro/name} ${series/name} in "
             replace="message" />
           <a tal:condition="linkify_archive"
-             tal:attributes="href source/upload_archive/fmt:url"
-             tal:content="source/upload_archive/displayname" />
+             tal:attributes="href root_archive/fmt:url"
+             tal:content="root_archive/displayname" />
           <tal:message
             condition="not:linkify_archive"
-            define="archive source/upload_archive;
-                    message string:${archive/displayname}"
+            define="message string:${root_archive/displayname}"
             replace="message" />
         </tal:define>
         <tal:source_creator condition="context/creator">
@@ -64,15 +72,20 @@
       <tal:binary_build_location condition="view/is_binary">
         <tal:message
           define="build context/binarypackagerelease/build;
-                  archive build/archive;
                   pocket build/pocket;
                   arch build/distro_arch_series;
                   series arch/distroseries;
                   distro series/distribution;
-                  message string:${distro/name} ${series/name}-${pocket/name/fmt:lower} ${arch/architecturetag} in ${archive/displayname}"
+                  message string:${distro/name} ${series/name}-${pocket/name/fmt:lower} ${arch/architecturetag} in ${root_archive/displayname}"
           replace="message" />
       </tal:binary_build_location>
-    </li>
 
+      <p tal:condition="python: previous_archive
+                                and previous_archive != root_archive">
+          Previous archive was
+          <a tal:attributes="href previous_archive/fmt:url"
+                  tal:content="previous_archive/displayname" />
+      </p>
+    </li>
   </ul>
 </tal:root>