← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~jtv/launchpad/bug-800652 into lp:launchpad

 

Jeroen T. Vermeulen has proposed merging lp:~jtv/launchpad/bug-800652 into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #800634 in Launchpad itself: "+queue page doesn't show the overrides for syncs"
  https://bugs.launchpad.net/launchpad/+bug/800634
  Bug #800652 in Launchpad itself: "+queue page broken for accepting syncs"
  https://bugs.launchpad.net/launchpad/+bug/800652

For more details, see:
https://code.launchpad.net/~jtv/launchpad/bug-800652/+merge/65674

= Summary =

This makes the +queue page show overrides for sync uploads.  It also simplifies the handling of the overrides in various ways.


== Implementation details ==

You'll see a better separation of display logic and database logic for components and sections: the model knows that components and sections are only relevant for source and sync uploads, and the display logic merely cares about representing Nones as empty strings.

Since ComplegePackageUpload.package_sets is now the empty list when it does not apply, there's no need to special-case it now.

I split up both of the functions that accept an upload and add a source override, each into one function for sync uploads, one for non-sync uploads, plus a simple abstract master method to choose between the two.  I hope that makes it a little easier to follow the gist of things.

In the case of the source overrides, I hoisted the check for "is the new component an allowed one?" up into the master method.  If that check fails, the error message mentioned both the old component and the new component without saying which was the problem.  I hopefully made it a little clearer by stating just the new component in the case where that is the forbidden one.

The check for the old component went into the source-upload method, but I added a similar one for the sync-upload case.  It works slightly differently because the package copy job knows its component name but not its Component object.

In order to fix irrelevant test breakage, I fixed the factory method for creating sync uploads to add the override that the upload would normally have.  This also made it necessary for one test to change the way it defined the metadata it expected from its job: the newly-initialized override data was hiding the data that the test was trying to add!


== Tests ==

I ran:
{{{
./bin/test -vvc lp.soyuz -t queue -t packageupload -t packagecopyjob
}}}


== Demo and Q/A ==

Create a PCJ upload.  View it on the +queue page, without folding open its details (since there won't be any, actually).  Its component and section will be visible.


= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/soyuz/browser/tests/test_queue.py
  lib/lp/soyuz/model/queue.py
  lib/lp/soyuz/stories/soyuz/xx-queue-pages-motu.txt
  lib/lp/soyuz/interfaces/queue.py
  lib/lp/soyuz/browser/queue.py
  lib/lp/soyuz/model/packagecopyjob.py
  lib/lp/soyuz/configure.zcml
  lib/lp/soyuz/interfaces/packagecopyjob.py
  lib/lp/testing/factory.py
  lib/lp/soyuz/tests/test_packagecopyjob.py
  lib/lp/soyuz/tests/test_packageupload.py
  lib/lp/soyuz/adapters/overrides.py
  lib/lp/soyuz/doc/distroseriesqueue.txt

./lib/lp/soyuz/stories/soyuz/xx-queue-pages-motu.txt
       1: narrative uses a moin header.
      20: narrative uses a moin header.
      41: want exceeds 78 characters.
      42: want exceeds 78 characters.
      43: want exceeds 78 characters.
      44: want exceeds 78 characters.
      45: want exceeds 78 characters.
      46: want exceeds 78 characters.
      47: want exceeds 78 characters.
      73: source exceeds 78 characters.
      74: source exceeds 78 characters.
-- 
https://code.launchpad.net/~jtv/launchpad/bug-800652/+merge/65674
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~jtv/launchpad/bug-800652 into lp:launchpad.
=== modified file 'lib/lp/soyuz/adapters/overrides.py'
--- lib/lp/soyuz/adapters/overrides.py	2011-06-05 08:17:40 +0000
+++ lib/lp/soyuz/adapters/overrides.py	2011-06-23 14:17:28 +0000
@@ -40,13 +40,7 @@
 from lp.soyuz.model.binarypackagename import BinaryPackageName
 from lp.soyuz.model.binarypackagerelease import BinaryPackageRelease
 from lp.soyuz.model.component import Component
-from lp.soyuz.model.distroarchseries import DistroArchSeries
-from lp.soyuz.model.publishing import (
-    BinaryPackagePublishingHistory,
-    SourcePackagePublishingHistory,
-    )
 from lp.soyuz.model.section import Section
-from lp.soyuz.model.sourcepackagerelease import SourcePackageRelease
 
 
 class IOverride(Interface):
@@ -130,7 +124,7 @@
 
     def __repr__(self):
         return ("<BinaryOverride at %x component=%r section=%r "
-            "binary_package_name=%r distro_arch_series=%r priority=%r>" % 
+            "binary_package_name=%r distro_arch_series=%r priority=%r>" %
             (id(self), self.component, self.section, self.binary_package_name,
              self.distro_arch_series, self.priority))
 
@@ -187,7 +181,7 @@
 
 class FromExistingOverridePolicy(BaseOverridePolicy):
     """Override policy that only searches for existing publications.
-    
+
     Override policy that returns the SourcePackageName, component and
     section for the latest published source publication, or the
     BinaryPackageName, DistroArchSeries, component, section and priority
@@ -195,10 +189,15 @@
     """
 
     def calculateSourceOverrides(self, archive, distroseries, pocket, spns):
-        store = IStore(SourcePackagePublishingHistory)
+        # Avoid circular imports.
+        from lp.soyuz.model.sourcepackagerelease import SourcePackageRelease
+        from lp.soyuz.model.publishing import SourcePackagePublishingHistory
+
         def eager_load(rows):
             bulk.load(Component, (row[1] for row in rows))
             bulk.load(Section, (row[2] for row in rows))
+
+        store = IStore(SourcePackagePublishingHistory)
         already_published = DecoratedResultSet(
             store.find(
                 (SourcePackageRelease.sourcepackagenameID,
@@ -226,10 +225,15 @@
 
     def calculateBinaryOverrides(self, archive, distroseries, pocket,
                                  binaries):
-        store = IStore(BinaryPackagePublishingHistory)
+        # Avoid circular imports.
+        from lp.soyuz.model.distroarchseries import DistroArchSeries
+        from lp.soyuz.model.publishing import BinaryPackagePublishingHistory
+
         def eager_load(rows):
             bulk.load(Component, (row[2] for row in rows))
             bulk.load(Section, (row[3] for row in rows))
+
+        store = IStore(BinaryPackagePublishingHistory)
         expanded = calculate_target_das(distroseries, binaries)
         candidates = (
             make_package_condition(archive, das, bpn)
@@ -266,11 +270,11 @@
 
 class UnknownOverridePolicy(BaseOverridePolicy):
     """Override policy that returns defaults.
-    
+
     Override policy that assumes everything passed in doesn't exist, so
     returns the defaults.
     """
-    
+
     def calculateSourceOverrides(self, archive, distroseries, pocket,
                                  sources):
         default_component = archive.default_component or getUtility(
@@ -291,9 +295,9 @@
 class UbuntuOverridePolicy(FromExistingOverridePolicy,
                            UnknownOverridePolicy):
     """Override policy for Ubuntu.
-    
-    An override policy that incorporates both the from existing policy 
-    and the unknown policy.
+
+    An override policy that incorporates both the existing policy and the
+    unknown policy.
     """
 
     def calculateSourceOverrides(self, archive, distroseries, pocket,
@@ -314,9 +318,12 @@
         total = set(binaries)
         overrides = FromExistingOverridePolicy.calculateBinaryOverrides(
             self, archive, distroseries, pocket, binaries)
-        existing = set((
-            overide.binary_package_name, overide.distro_arch_series.architecturetag)
-                for overide in overrides)
+        existing = set(
+            (
+                overide.binary_package_name,
+                overide.distro_arch_series.architecturetag,
+            )
+            for overide in overrides)
         missing = total.difference(existing)
         if missing:
             unknown = UnknownOverridePolicy.calculateBinaryOverrides(
@@ -340,6 +347,8 @@
 
 
 def make_package_condition(archive, das, bpn):
+    # Avoid circular imports.
+    from lp.soyuz.model.publishing import BinaryPackagePublishingHistory
     return And(
         BinaryPackagePublishingHistory.archiveID == archive.id,
         BinaryPackagePublishingHistory.distroarchseriesID == das.id,
@@ -347,9 +356,13 @@
 
 
 def id_resolver(lookups):
+    # Avoid circular imports.
+    from lp.soyuz.model.publishing import SourcePackagePublishingHistory
+
     def _resolve(row):
         store = IStore(SourcePackagePublishingHistory)
         return tuple(
             (value if cls is None else store.get(cls, value))
             for value, cls in zip(row, lookups))
+
     return _resolve

=== modified file 'lib/lp/soyuz/browser/queue.py'
--- lib/lp/soyuz/browser/queue.py	2011-06-22 07:13:29 +0000
+++ lib/lp/soyuz/browser/queue.py	2011-06-23 14:17:28 +0000
@@ -46,7 +46,6 @@
     QueueInconsistentStateError,
     )
 from lp.soyuz.interfaces.section import ISectionSet
-from lp.soyuz.model.queue import PackageUploadSource
 
 
 QUEUE_SIZE = 30
@@ -199,6 +198,7 @@
         in the +queue template.
         """
         # Avoid circular imports.
+        from lp.soyuz.model.queue import PackageUploadSource
         from lp.soyuz.model.sourcepackagerelease import SourcePackageRelease
 
         uploads = list(self.batchnav.currentBatch())
@@ -396,14 +396,13 @@
                                (queue_item.displayname, info))
             else:
                 if source_overridden:
-                    success.append("OK: %(name)s(%(component)s/%(section)s)" %
-                                   feedback_interpolations)
+                    desc = "%(name)s(%(component)s/%(section)s)"
                 elif binary_overridden:
-                    success.append(
-                        "OK: %(name)s(%(component)s/%(section)s/%(priority)s)"
-                            % feedback_interpolations)
+                    desc = "%(name)s(%(component)s/%(section)s/%(priority)s)"
                 else:
-                    success.append('OK: %s' % queue_item.displayname)
+                    desc = "%(name)s"
+                success.append(
+                    "OK: " + desc % feedback_interpolations)
 
         for message in success:
             self.request.response.addInfoNotification(message)
@@ -480,11 +479,8 @@
         # Create a list of source files if this is a source upload.
         self.source_files = source_upload_files.get(self.id, None)
 
-        # Pre-fetch the sourcepackagerelease if it exists.
         if self.contains_source:
             self.sourcepackagerelease = self.sources[0].sourcepackagerelease
-        else:
-            self.sourcepackagerelease = None
 
         if self.contains_source:
             self.package_sets = package_sets.get(
@@ -513,27 +509,26 @@
     @property
     def display_package_sets(self):
         """Package sets, if any, for display on the +queue page."""
-        if self.contains_source:
-            return ' '.join(sorted(
-                packageset.name for packageset in self.package_sets))
-        else:
-            return ""
+        return ' '.join(sorted(
+            packageset.name for packageset in self.package_sets))
 
     @property
     def display_component(self):
         """Component name, if any, for display on the +queue page."""
-        if self.contains_source:
-            return self.component_name.lower()
+        component_name = self.component_name
+        if component_name is None:
+            return ""
         else:
-            return ""
+            return component_name.lower()
 
     @property
     def display_section(self):
         """Section name, if any, for display on the +queue page."""
-        if self.contains_source:
-            return self.section_name.lower()
+        section_name = self.section_name
+        if section_name is None:
+            return ""
         else:
-            return ""
+            return section_name.lower()
 
     @property
     def display_priority(self):

=== modified file 'lib/lp/soyuz/browser/tests/test_queue.py'
--- lib/lp/soyuz/browser/tests/test_queue.py	2011-06-21 05:30:19 +0000
+++ lib/lp/soyuz/browser/tests/test_queue.py	2011-06-23 14:17:28 +0000
@@ -272,7 +272,7 @@
             package_sets.values()[0][0].name,
             complete_upload.display_package_sets)
 
-    def test_display_package_sets_returns_empty_for_non_source_upload(self):
+    def test_display_package_sets_returns_empty_for_other_upload(self):
         upload = self.factory.makeBuildPackageUpload()
         complete_upload = self.makeCompletePackageUpload(
             upload, package_sets=self.mapPackageSets(upload))
@@ -287,34 +287,51 @@
         self.assertEqual("aaa bbb ccc", complete_upload.display_package_sets)
 
     def test_display_component_returns_source_upload_component_name(self):
-        complete_upload = self.makeCompletePackageUpload()
-        self.assertEqual(
-            complete_upload.sourcepackagerelease.component.name.lower(),
-            complete_upload.display_component)
-
-    def test_display_component_returns_empty_for_non_source_upload(self):
+        upload = self.factory.makeSourcePackageUpload()
+        complete_upload = self.makeCompletePackageUpload(upload)
+        self.assertEqual(
+            upload.sourcepackagerelease.component.name.lower(),
+            complete_upload.display_component)
+
+    def test_display_component_returns_copy_job_upload_component_name(self):
+        copy_job_upload = self.factory.makeCopyJobPackageUpload()
+        complete_upload = self.makeCompletePackageUpload(copy_job_upload)
+        self.assertEqual(
+            copy_job_upload.component_name.lower(),
+            complete_upload.display_component)
+
+    def test_display_component_returns_empty_for_other_upload(self):
         complete_upload = self.makeCompletePackageUpload(
             self.factory.makeBuildPackageUpload())
         self.assertEqual('', complete_upload.display_component)
 
     def test_display_section_returns_source_upload_section_name(self):
-        complete_upload = self.makeCompletePackageUpload()
-        self.assertEqual(
-            complete_upload.sourcepackagerelease.section.name.lower(),
-            complete_upload.display_section)
-
-    def test_display_section_returns_empty_for_non_source_upload(self):
+        upload = self.factory.makeSourcePackageUpload()
+        complete_upload = self.makeCompletePackageUpload(upload)
+        self.assertEqual(
+            upload.sourcepackagerelease.section.name.lower(),
+            complete_upload.display_section)
+
+    def test_display_section_returns_copy_job_upload_section_name(self):
+        copy_job_upload = self.factory.makeCopyJobPackageUpload()
+        complete_upload = self.makeCompletePackageUpload(copy_job_upload)
+        self.assertEqual(
+            copy_job_upload.section_name.lower(),
+            complete_upload.display_section)
+
+    def test_display_section_returns_empty_for_other_upload(self):
         complete_upload = self.makeCompletePackageUpload(
             self.factory.makeBuildPackageUpload())
         self.assertEqual('', complete_upload.display_section)
 
     def test_display_priority_returns_source_upload_priority(self):
-        complete_upload = self.makeCompletePackageUpload()
+        upload = self.factory.makeSourcePackageUpload()
+        complete_upload = self.makeCompletePackageUpload(upload)
         self.assertEqual(
-            complete_upload.sourcepackagerelease.urgency.name.lower(),
+            upload.sourcepackagerelease.urgency.name.lower(),
             complete_upload.display_priority)
 
-    def test_display_priority_returns_empty_for_non_source_upload(self):
+    def test_display_priority_returns_empty_for_other_upload(self):
         complete_upload = self.makeCompletePackageUpload(
             self.factory.makeBuildPackageUpload())
         self.assertEqual('', complete_upload.display_priority)

=== modified file 'lib/lp/soyuz/configure.zcml'
--- lib/lp/soyuz/configure.zcml	2011-06-15 02:41:34 +0000
+++ lib/lp/soyuz/configure.zcml	2011-06-23 14:17:28 +0000
@@ -914,6 +914,11 @@
 
     <!-- PackageCopyJobSource -->
     <securedutility
+        component=".model.packagecopyjob.PackageCopyJob"
+        provides=".interfaces.packagecopyjob.IPackageCopyJobSource">
+      <allow interface=".interfaces.packagecopyjob.IPackageCopyJobSource"/>
+    </securedutility>
+    <securedutility
         component=".model.packagecopyjob.PlainPackageCopyJob"
         provides=".interfaces.packagecopyjob.IPlainPackageCopyJobSource">
       <allow interface=".interfaces.packagecopyjob.IPlainPackageCopyJobSource"/>

=== modified file 'lib/lp/soyuz/doc/distroseriesqueue.txt'
--- lib/lp/soyuz/doc/distroseriesqueue.txt	2011-06-16 09:38:04 +0000
+++ lib/lp/soyuz/doc/distroseriesqueue.txt	2011-06-23 14:17:28 +0000
@@ -676,7 +676,7 @@
     ...     allowed_components=(universe,))
     Traceback (most recent call last):
     ...
-    QueueInconsistentStateError: No rights to override from main to restricted
+    QueueInconsistentStateError: No rights to override to restricted
 
 Allowing "restricted" still won't work because the original component
 is "main":

=== modified file 'lib/lp/soyuz/interfaces/packagecopyjob.py'
--- lib/lp/soyuz/interfaces/packagecopyjob.py	2011-06-14 06:01:14 +0000
+++ lib/lp/soyuz/interfaces/packagecopyjob.py	2011-06-23 14:17:28 +0000
@@ -5,6 +5,7 @@
 
 __all__ = [
     "IPackageCopyJob",
+    "IPackageCopyJobSource",
     "IPlainPackageCopyJob",
     "IPlainPackageCopyJobSource",
     "PackageCopyJobType",
@@ -37,6 +38,21 @@
 from lp.soyuz.interfaces.archive import IArchive
 
 
+class IPackageCopyJobSource(Interface):
+    """Utility for `IPackageCopyJob`-implementing types."""
+
+    def wrap(package_copy_job):
+        """Wrap a `PackageCopyJob` in its concrete implementation type.
+
+        As a special case, `None` produces `None`.
+
+        :param package_copy_job: A `PackageCopyJob`.
+        :return: An `IPackageCopyJob` implementation based on
+            `package_copy_job`, but of the job's specific concrete type
+            (such as `PlainPackageCopyJob`).
+        """
+
+
 class IPackageCopyJob(Interface):
     """A job that copies packages between `IArchive`s."""
 
@@ -74,6 +90,12 @@
         schema=IJob, title=_('The common Job attributes'),
         required=True, readonly=True)
 
+    component_name = TextLine(
+        title=_("Component override name"), required=False, readonly=True)
+
+    section_name = TextLine(
+        title=_("Section override name"), required=False, readonly=True)
+
     metadata = Attribute('A dict of data about the job.')
 
 

=== modified file 'lib/lp/soyuz/interfaces/queue.py'
--- lib/lp/soyuz/interfaces/queue.py	2011-06-17 05:23:56 +0000
+++ lib/lp/soyuz/interfaces/queue.py	2011-06-23 14:17:28 +0000
@@ -140,6 +140,11 @@
     package_copy_job = Reference(
         schema=IPackageCopyJob,
         description=_("The PackageCopyJob for this upload, if it has one."),
+        title=_("Raw Package Copy Job"), required=False, readonly=True)
+
+    concrete_package_copy_job = Reference(
+        schema=IPackageCopyJob,
+        description=_("Concrete IPackageCopyJob implementation, if any."),
         title=_("Package Copy Job"), required=False, readonly=True)
 
     archive = exported(

=== modified file 'lib/lp/soyuz/model/packagecopyjob.py'
--- lib/lp/soyuz/model/packagecopyjob.py	2011-06-16 13:21:14 +0000
+++ lib/lp/soyuz/model/packagecopyjob.py	2011-06-23 14:17:28 +0000
@@ -22,6 +22,7 @@
     classProvides,
     implements,
     )
+from zope.security.proxy import removeSecurityProxy
 
 from canonical.database.enumcol import EnumCol
 from canonical.database.sqlbase import sqlvalues
@@ -61,6 +62,7 @@
 from lp.soyuz.interfaces.copypolicy import ICopyPolicy
 from lp.soyuz.interfaces.packagecopyjob import (
     IPackageCopyJob,
+    IPackageCopyJobSource,
     IPlainPackageCopyJob,
     IPlainPackageCopyJobSource,
     PackageCopyJobType,
@@ -75,6 +77,7 @@
     """Base class for package copying jobs."""
 
     implements(IPackageCopyJob)
+    classProvides(IPackageCopyJobSource)
 
     __storm_table__ = 'PackageCopyJob'
 
@@ -99,6 +102,31 @@
 
     _json_data = Unicode('json_data')
 
+    # Derived concrete classes.  The entire class gets one dict for
+    # this; it's not meant to be on an instance.
+    concrete_classes = {}
+
+    @classmethod
+    def registerConcreteClass(cls, new_class):
+        """Register a concrete `IPackageCopyJob`-implementing class."""
+        assert new_class.class_job_type not in cls.concrete_classes, (
+            "Class %s is already registered." % new_class)
+        cls.concrete_classes[new_class.class_job_type] = new_class
+
+    @classmethod
+    def wrap(cls, package_copy_job):
+        """See `IPackageCopyJobSource`."""
+        if package_copy_job is None:
+            return None
+        job_type = removeSecurityProxy(package_copy_job).job_type
+        concrete_class = cls.concrete_classes[job_type]
+        return concrete_class(package_copy_job)
+
+    @classmethod
+    def getByID(cls, pcj_id):
+        """See `IPackageCopyJobSource`."""
+        return cls.wrap(IStore(PackageCopyJob).get(PackageCopyJob, pcj_id))
+
     def __init__(self, source_archive, target_archive, target_distroseries,
                  job_type, metadata, package_name=None, copy_policy=None):
         super(PackageCopyJob, self).__init__()
@@ -130,6 +158,16 @@
         existing.update(metadata_dict)
         self._json_data = self.serializeMetadata(existing)
 
+    @property
+    def component_name(self):
+        """See `IPackageCopyJob`."""
+        return self.metadata.get("component_override")
+
+    @property
+    def section_name(self):
+        """See `IPackageCopyJob`."""
+        return self.metadata.get("section_override")
+
 
 class PackageCopyJobDerived(BaseRunnableJob):
     """Abstract class for deriving from PackageCopyJob."""
@@ -339,13 +377,13 @@
         metadata_dict = dict(
             component_override=component,
             section_override=section)
-        self.context.extendMetadata(metadata_dict)
+        removeSecurityProxy(self.context).extendMetadata(metadata_dict)
 
     def getSourceOverride(self):
         """Fetch an `ISourceOverride` from the metadata."""
         name = self.package_name
-        component_name = self.metadata.get("component_override")
-        section_name = self.metadata.get("section_override")
+        component_name = self.component_name
+        section_name = self.section_name
         source_package_name = getUtility(ISourcePackageNameSet)[name]
         try:
             component = getUtility(IComponentSet)[component_name]
@@ -508,3 +546,6 @@
     def getPolicyImplementation(self):
         """Return the `ICopyPolicy` applicable to this job."""
         return ICopyPolicy(self.copy_policy)
+
+
+PackageCopyJob.registerConcreteClass(PlainPackageCopyJob)

=== modified file 'lib/lp/soyuz/model/queue.py'
--- lib/lp/soyuz/model/queue.py	2011-06-22 12:03:31 +0000
+++ lib/lp/soyuz/model/queue.py	2011-06-23 14:17:28 +0000
@@ -66,11 +66,13 @@
 from lp.services.mail.signedmessage import strip_pgp_signature
 from lp.services.propertycache import cachedproperty
 from lp.soyuz.adapters.notification import notify
+from lp.soyuz.adapters.overrides import SourceOverride
 from lp.soyuz.enums import (
     PackageUploadCustomFormat,
     PackageUploadStatus,
     )
 from lp.soyuz.interfaces.archive import MAIN_ARCHIVE_PURPOSES
+from lp.soyuz.interfaces.packagecopyjob import IPackageCopyJobSource
 from lp.soyuz.interfaces.publishing import (
     IPublishingSet,
     ISourcePackagePublishingHistory,
@@ -467,25 +469,39 @@
         self._closeBugs(changesfile_path, logger)
         self._giveKarma()
 
-    def acceptFromQueue(self, logger=None, dry_run=False):
-        """See `IPackageUpload`."""
-        assert not self.is_delayed_copy, 'Cannot process delayed copies.'
-
-        if self.package_copy_job is not None:
-            if self.status == PackageUploadStatus.REJECTED:
-                raise QueueInconsistentStateError(
-                    "Can't resurrect rejected syncs")
-            # Circular imports :(
-            from lp.soyuz.model.packagecopyjob import PlainPackageCopyJob
-            # Release the job hounds, Smithers.
-            self.setAccepted()
-            job = PlainPackageCopyJob.get(self.package_copy_job_id)
-            job.resume()
-            # The copy job will send emails as appropriate.  We don't
-            # need to worry about closing bugs from syncs, although we
-            # should probably give karma but that needs more work to
-            # fix here.
-            return
+    def _acceptSyncFromQueue(self):
+        """Accept a sync upload from the queue."""
+        # Circular imports :(
+        from lp.soyuz.model.packagecopyjob import PlainPackageCopyJob
+
+        assert self.package_copy_job is not None, (
+            "This method is for copy-job uploads only.")
+        assert not self.is_delayed_copy, (
+            "This method is not for delayed copies.")
+
+        if self.status == PackageUploadStatus.REJECTED:
+            raise QueueInconsistentStateError(
+                "Can't resurrect rejected syncs")
+
+        # Release the job hounds, Smithers.
+        self.setAccepted()
+        job = PlainPackageCopyJob.get(self.package_copy_job_id)
+        job.resume()
+        # The copy job will send emails as appropriate.  We don't
+        # need to worry about closing bugs from syncs, although we
+        # should probably give karma but that needs more work to
+        # fix here.
+
+    def _acceptNonSyncFromQueue(self, logger=None, dry_run=False):
+        """Accept a "regular" upload from the queue.
+
+        This is the normal case, for uploads that are not delayed and are not
+        attached to package copy jobs.
+        """
+        assert self.package_copy_job is None, (
+            "This method is not for copy-job uploads.")
+        assert not self.is_delayed_copy, (
+            "This method is not for delayed copies.")
 
         self.setAccepted()
 
@@ -515,6 +531,15 @@
         # Give some karma!
         self._giveKarma()
 
+    def acceptFromQueue(self, logger=None, dry_run=False):
+        """See `IPackageUpload`."""
+        assert not self.is_delayed_copy, 'Cannot process delayed copies.'
+
+        if self.package_copy_job is None:
+            self._acceptNonSyncFromQueue(logger, dry_run)
+        else:
+            self._acceptSyncFromQueue()
+
     def acceptFromCopy(self):
         """See `IPackageUpload`."""
         assert self.is_delayed_copy, 'Can only process delayed-copies.'
@@ -615,7 +640,7 @@
     def package_version(self):
         """See `IPackageUpload`."""
         if self.package_copy_job_id is not None:
-            return self.package_copy_job.metadata["package_version"]
+            return self.package_copy_job.package_version
         elif self.sourcepackagerelease is not None:
             return self.sourcepackagerelease.version
         else:
@@ -625,8 +650,8 @@
     def component_name(self):
         """See `IPackageUpload`."""
         if self.package_copy_job_id is not None:
-            return self.package_copy_job.metadata["component_override"]
-        elif self.sourcepackagerelease is not None:
+            return self.package_copy_job.component_name
+        elif self.contains_source:
             return self.sourcepackagerelease.component.name
         else:
             return None
@@ -635,8 +660,8 @@
     def section_name(self):
         """See `IPackageUpload`."""
         if self.package_copy_job_id is not None:
-            return self.package_copy_job.metadata["section_override"]
-        elif self.sourcepackagerelease is not None:
+            return self.package_copy_job.section_name
+        elif self.contains_source:
             return self.sourcepackagerelease.section.name
         else:
             return None
@@ -853,45 +878,68 @@
                     existing_components.add(binary.component)
         return existing_components
 
-    def overrideSource(self, new_component, new_section, allowed_components):
+    @cachedproperty
+    def concrete_package_copy_job(self):
         """See `IPackageUpload`."""
-        if new_component is None and new_section is None:
-            # Nothing needs overriding, bail out.
-            return False
-
-        if self.package_copy_job is not None:
-            # We just need to add the required component/section to the
-            # job metadata.
-            extra_data = {}
-            if new_component is not None:
-                extra_data['component_override'] = new_component.name
-            if new_section is not None:
-                extra_data['section_override'] = new_section.name
-            self.package_copy_job.extendMetadata(extra_data)
-            return
-
-        if not self.contains_source:
-            return False
+        return getUtility(IPackageCopyJobSource).wrap(self.package_copy_job)
+
+    def _overrideSyncSource(self, new_component, new_section,
+                            allowed_components):
+        """Override source on the upload's `PackageCopyJob`, if any."""
+        if self.package_copy_job is None:
+            return False
+
+        copy_job = self.concrete_package_copy_job
+        allowed_component_names = [
+            component.name for component in allowed_components]
+        if copy_job.component_name not in allowed_component_names:
+            raise QueueInconsistentStateError(
+                "No rights to override from %s to %s" % (
+                    copy_job.component_name, new_component.name))
+        copy_job.addSourceOverride(SourceOverride(
+            copy_job.package_name, new_component, new_section))
+
+        return True
+
+    def _overrideNonSyncSource(self, new_component, new_section,
+                               allowed_components):
+        """Override sources on a source upload."""
+        made_changes = False
 
         for source in self.sources:
-            if (new_component not in allowed_components or
-                source.sourcepackagerelease.component not in
-                    allowed_components):
-                # The old or the new component is not in the list of
-                # allowed components to override.
+            old_component = source.sourcepackagerelease.component
+            if old_component not in allowed_components:
+                # The old component is not in the list of allowed components
+                # to override.
                 raise QueueInconsistentStateError(
                     "No rights to override from %s to %s" % (
-                        source.sourcepackagerelease.component.name,
-                        new_component.name))
+                        old_component.name, new_component.name))
             source.sourcepackagerelease.override(
                 component=new_component, section=new_section)
+            made_changes = True
 
         # We override our own archive too, as it is used to create
         # the SPPH during publish().
         self.archive = self.distroseries.distribution.getArchiveByComponent(
             new_component.name)
 
-        return True
+        return made_changes
+
+    def overrideSource(self, new_component, new_section, allowed_components):
+        """See `IPackageUpload`."""
+        if new_component is None and new_section is None:
+            # Nothing needs overriding, bail out.
+            return False
+
+        if new_component not in allowed_components:
+            raise QueueInconsistentStateError(
+                "No rights to override to %s" % new_component.name)
+
+        return (
+            self._overrideSyncSource(
+                new_component, new_section, allowed_components) or
+            self._overrideNonSyncSource(
+                new_component, new_section, allowed_components))
 
     def overrideBinaries(self, new_component, new_section, new_priority,
                          allowed_components):

=== modified file 'lib/lp/soyuz/stories/soyuz/xx-queue-pages-motu.txt'
--- lib/lp/soyuz/stories/soyuz/xx-queue-pages-motu.txt	2010-11-06 12:45:26 +0000
+++ lib/lp/soyuz/stories/soyuz/xx-queue-pages-motu.txt	2011-06-23 14:17:28 +0000
@@ -110,7 +110,7 @@
     >>> motu_browser.getControl(name="Accept").click()
     >>> for message in get_feedback_messages(motu_browser.contents):
     ...     print message
-    FAILED: alsa-utils (No rights to override from universe to main)
+    FAILED: alsa-utils (No rights to override to main)
 
 The same applies to the binary:
 
@@ -120,7 +120,7 @@
     >>> motu_browser.getControl(name="Accept").click()
     >>> for message in get_feedback_messages(motu_browser.contents):
     ...     print message
-    FAILED: pmount (No rights to override from universe to main)
+    FAILED: pmount (No rights to override to main)
 
 Our user is able to override to multiverse, however.  Let's do that
 with pmount:

=== modified file 'lib/lp/soyuz/tests/test_packagecopyjob.py'
--- lib/lp/soyuz/tests/test_packagecopyjob.py	2011-06-10 11:17:39 +0000
+++ lib/lp/soyuz/tests/test_packagecopyjob.py	2011-06-23 14:17:28 +0000
@@ -17,7 +17,10 @@
 from canonical.config import config
 from canonical.launchpad.interfaces.lpstorm import IStore
 from canonical.launchpad.webapp.testing import verifyObject
-from canonical.testing import LaunchpadZopelessLayer
+from canonical.testing import (
+    LaunchpadZopelessLayer,
+    ZopelessDatabaseLayer,
+    )
 from lp.registry.model.distroseriesdifferencecomment import (
     DistroSeriesDifferenceComment,
     )
@@ -43,6 +46,7 @@
 from lp.soyuz.interfaces.component import IComponentSet
 from lp.soyuz.interfaces.packagecopyjob import (
     IPackageCopyJob,
+    IPackageCopyJobSource,
     IPlainPackageCopyJob,
     IPlainPackageCopyJobSource,
     )
@@ -52,12 +56,14 @@
 from lp.soyuz.interfaces.sourcepackageformat import (
     ISourcePackageFormatSelectionSet,
     )
+from lp.soyuz.model.packagecopyjob import PackageCopyJob
 from lp.soyuz.tests.test_publishing import SoyuzTestPublisher
 from lp.testing import (
     run_script,
     TestCaseWithFactory,
     )
 from lp.testing.mail_helpers import pop_notifications
+from lp.testing.matchers import Provides
 from lp.testing.fakemethod import FakeMethod
 
 
@@ -298,7 +304,7 @@
             archive=breezy_archive)
         # The target archive needs ancestry so the package is
         # auto-accepted.
-        ancestry = publisher.getPubSource(
+        publisher.getPubSource(
             distroseries=target_series, sourcename="libc",
             version="2.8-0", status=PackagePublishingStatus.PUBLISHED,
             archive=target_archive)
@@ -489,7 +495,7 @@
 
         # Publish a package in the source archive with some overridable
         # properties set to known values.
-        source_package = publisher.getPubSource(
+        publisher.getPubSource(
             distroseries=distroseries, sourcename="libc",
             component='universe', section='web',
             version="2.8-1", status=PackagePublishingStatus.PUBLISHED,
@@ -497,7 +503,7 @@
 
         # Now put the same named package in the target archive with
         # different override values.
-        ancestry_package = publisher.getPubSource(
+        publisher.getPubSource(
             distroseries=distroseries, sourcename="libc",
             component='restricted', section='games',
             version="2.8-0", status=PackagePublishingStatus.PUBLISHED,
@@ -594,7 +600,7 @@
 
         # Publish a package in the source archive with some overridable
         # properties set to known values.
-        source_package = publisher.getPubSource(
+        publisher.getPubSource(
             distroseries=distroseries, sourcename="copyme",
             component='multiverse', section='web',
             version="2.8-1", status=PackagePublishingStatus.PUBLISHED,
@@ -645,7 +651,7 @@
         source_archive = self.factory.makeArchive()
 
         # Publish a package in the source archive.
-        source_package = publisher.getPubSource(
+        publisher.getPubSource(
             distroseries=distroseries, sourcename="copyme",
             version="2.8-1", status=PackagePublishingStatus.PUBLISHED,
             component='multiverse', section='web',
@@ -653,7 +659,7 @@
 
         # Now put the same named package in the target archive so it has
         # ancestry.
-        ancestry_package = publisher.getPubSource(
+        publisher.getPubSource(
             distroseries=distroseries, sourcename="copyme",
             version="2.8-0", status=PackagePublishingStatus.PUBLISHED,
             component='main', section='games',
@@ -698,7 +704,7 @@
         source_archive = self.factory.makeArchive()
 
         # Publish a package in the source archive.
-        source_package = publisher.getPubSource(
+        publisher.getPubSource(
             distroseries=distroseries, sourcename="copyme",
             version="2.8-1", status=PackagePublishingStatus.PUBLISHED,
             archive=source_archive)
@@ -838,3 +844,25 @@
         transaction.commit()
         self.layer.switchDbUser(self.dbuser)
         removeSecurityProxy(job).reportFailure(CannotCopy("Mommy it hurts"))
+
+
+class TestPackageCopyJobSource(TestCaseWithFactory):
+    """Test the `IPackageCopyJob` utility."""
+
+    layer = ZopelessDatabaseLayer
+
+    def test_implements_interface(self):
+        job_source = getUtility(IPackageCopyJobSource)
+        self.assertThat(job_source, Provides(IPackageCopyJobSource))
+
+    def test_wrap_accepts_None(self):
+        job_source = getUtility(IPackageCopyJobSource)
+        self.assertIs(None, job_source.wrap(None))
+
+    def test_wrap_wraps_PlainPackageCopyJob(self):
+        original_ppcj = self.factory.makePlainPackageCopyJob()
+        IStore(PackageCopyJob).flush()
+        pcj = IStore(PackageCopyJob).get(PackageCopyJob, original_ppcj.id)
+        self.assertNotEqual(None, pcj)
+        job_source = getUtility(IPackageCopyJobSource)
+        self.assertEqual(original_ppcj, job_source.wrap(pcj))

=== modified file 'lib/lp/soyuz/tests/test_packageupload.py'
--- lib/lp/soyuz/tests/test_packageupload.py	2011-06-22 12:03:31 +0000
+++ lib/lp/soyuz/tests/test_packageupload.py	2011-06-23 14:17:28 +0000
@@ -370,14 +370,11 @@
 
     def makeUploadWithPackageCopyJob(self, sourcepackagename=None):
         """Create a `PackageUpload` plus attached `PlainPackageCopyJob`."""
-        job_factory_args = {}
-        if sourcepackagename is not None:
-            job_factory_args['package_name'] = sourcepackagename.name
-            job_factory_args['package_version'] = '1.0'
-        job = self.factory.makePlainPackageCopyJob(**job_factory_args)
-        naked_job = removeSecurityProxy(job).context
-        upload = self.factory.makePackageUpload(package_copy_job=naked_job)
-        return upload, job
+        from lp.soyuz.model.packagecopyjob import IPackageCopyJobSource
+        upload = self.factory.makeCopyJobPackageUpload(
+            sourcepackagename=sourcepackagename)
+        return upload, getUtility(IPackageCopyJobSource).wrap(
+            upload.package_copy_job)
 
     def test_package_copy_job_property(self):
         # Test that we can set and get package_copy_job.
@@ -395,25 +392,48 @@
     def test_overrideSource_with_copy_job(self):
         # The overrides should be stored in the job's metadata.
         pu, pcj = self.makeUploadWithPackageCopyJob()
+        old_component = getUtility(IComponentSet)[pcj.component_name]
         component = getUtility(IComponentSet)['restricted']
         section = getUtility(ISectionSet)['games']
 
-        expected_metadata = {
+        expected_metadata = {}
+        expected_metadata.update(pcj.metadata)
+        expected_metadata.update({
             'component_override': component.name,
             'section_override': section.name,
-        }
-        expected_metadata.update(pcj.metadata)
-
-        pu.overrideSource(component, section, allowed_components=[component])
-
+            })
+
+        result = pu.overrideSource(
+            component, section, allowed_components=[component, old_component])
+
+        self.assertTrue(result)
         self.assertEqual(expected_metadata, pcj.metadata)
 
+    def test_overrideSource_checks_permission_for_old_component(self):
+        pu = self.factory.makeCopyJobPackageUpload()
+        only_allowed_component = self.factory.makeComponent()
+        section = self.factory.makeSection()
+        self.assertRaises(
+            QueueInconsistentStateError,
+            pu.overrideSource,
+            only_allowed_component, section, [only_allowed_component])
+
+    def test_overrideSource_checks_permission_for_new_component(self):
+        pu, pcj = self.makeUploadWithPackageCopyJob()
+        current_component = getUtility(IComponentSet)[pcj.component_name]
+        disallowed_component = self.factory.makeComponent()
+        section = self.factory.makeSection()
+        self.assertRaises(
+            QueueInconsistentStateError,
+            pu.overrideSource,
+            disallowed_component, section, [current_component])
+
     def test_acceptFromQueue_with_copy_job(self):
         # acceptFromQueue should accept the upload and resume the copy
         # job.
         pu, pcj = self.makeUploadWithPackageCopyJob()
+        pu.pocket = PackagePublishingPocket.RELEASE
         self.assertEqual(PackageUploadStatus.NEW, pu.status)
-        pcj.suspend()
 
         pu.acceptFromQueue()
 
@@ -423,7 +443,6 @@
     def test_rejectFromQueue_with_copy_job(self):
         # rejectFromQueue will reject the upload and fail the copy job.
         pu, pcj = self.makeUploadWithPackageCopyJob()
-        pcj.suspend()
 
         pu.rejectFromQueue()
 
@@ -451,11 +470,10 @@
         # An upload with a copy job takes its component and section
         # names from the job.
         spn = self.factory.makeSourcePackageName()
-        upload, job = self.makeUploadWithPackageCopyJob(sourcepackagename=spn)
+        upload, pcj = self.makeUploadWithPackageCopyJob(sourcepackagename=spn)
         component = self.factory.makeComponent()
         section = self.factory.makeSection()
-        job.addSourceOverride(SourceOverride(
-            source_package_name=spn, component=component, section=section))
+        pcj.addSourceOverride(SourceOverride(spn, component, section))
         self.assertEqual(component.name, upload.component_name)
 
     def test_displayname_is_package_name(self):

=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py	2011-06-14 20:43:01 +0000
+++ lib/lp/testing/factory.py	2011-06-23 14:17:28 +0000
@@ -243,6 +243,7 @@
 from lp.services.utils import AutoDecorate
 from lp.services.worlddata.interfaces.country import ICountrySet
 from lp.services.worlddata.interfaces.language import ILanguageSet
+from lp.soyuz.adapters.overrides import SourceOverride
 from lp.soyuz.adapters.packagelocation import PackageLocation
 from lp.soyuz.enums import (
     ArchivePurpose,
@@ -3442,6 +3443,8 @@
             source_archive=spph.archive,
             target_archive=distroseries.main_archive,
             target_distroseries=distroseries)
+        job.addSourceOverride(SourceOverride(
+            spr.sourcepackagename, spr.component, spr.section))
         try:
             job.run()
         except SuspendJobException: