← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~flacoste/launchpad/bug-781993-2 into lp:launchpad

 

Francis J. Lacoste has proposed merging lp:~flacoste/launchpad/bug-781993-2 into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~flacoste/launchpad/bug-781993-2/+merge/65057

= Summary =

We want to be able to file bugs in the Ensemble Principia distribution
(https://launchpad.net/principia). That distribution doesn't have published
source package, but they have official source package branch.

It builds on the previous guessPublishedSourcePackageName refactoring.

== Proposed fix ==

Allow filing a bug on a source package if it has official source package
branch.

== Pre-implementation notes ==

== Implementation details ==

 * If no published source package is found, it looks for official source
 package branches for it.

== Tests ==

In addition to the unit test for guessPublishedSourcePackageName, I added an
integration test just to make sure that we don't regress that functionality.
(By changing the vocabulary for example, they are currently very lax and will
allow any valid source or binary package  names. If we were to make them more
context sensitive, it might be a problem if official package branches weren't
considered.)


 ./bin/test -vvt 'test_guessPublished|TestFileBugSourcePackage'

== Demo and Q/A ==

Try filing a sourcepackage bug on qastaging on the the principia distribution.

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/bugs/stories/guided-filebug/xx-bug-reporting-tools.txt
  lib/lp/bugs/browser/bugtarget.py
  lib/lp/bugs/doc/bugzilla-import.txt
  lib/lp/bugs/browser/widgets/bugtask.py
  lib/lp/bugs/browser/tests/bug-views.txt
  lib/canonical/launchpad/interfaces/validation.py
  lib/lp/testing/factory.py
  lib/lp/bugs/browser/tests/test_bugtarget_filebug.py
  lib/lp/bugs/model/bug.py
  lib/lp/soyuz/doc/distribution.txt
  cronscripts/update-debwatches.py
  lib/lp/bugs/doc/bugtask-package-widget.txt
  lib/lp/registry/interfaces/distribution.py
  lib/lp/app/widgets/launchpadtarget.py
  lib/canonical/launchpad/scripts/debsync.py
  lib/lp/registry/tests/test_distribution.py
  lib/lp/bugs/scripts/bugzilla.py
  lib/lp/registry/model/distribution.py
  lib/lp/bugs/xmlrpc/bug.py
  lib/lp/bugs/interfaces/bug.py

./lib/lp/bugs/browser/tests/bug-views.txt
     195: source has trailing whitespace.
./lib/lp/bugs/model/bug.py
      52: 'SQLRaw' imported but unused
      52: 'Join' imported but unused
      52: 'Exists' imported but unused
     171: 'get_bug_privacy_filter' imported but unused
      52: 'Count' imported but unused
     304: E301 expected 1 blank line, found 0
    2590: E225 missing whitespace around operator
./cronscripts/update-debwatches.py
       9: '_pythonpath' imported but unused
./lib/lp/bugs/scripts/bugzilla.py
       8: Line exceeds 78 characters.

I'll fix this post-review.
-- 
https://code.launchpad.net/~flacoste/launchpad/bug-781993-2/+merge/65057
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~flacoste/launchpad/bug-781993-2 into lp:launchpad.
=== modified file 'cronscripts/update-debwatches.py'
--- cronscripts/update-debwatches.py	2011-05-29 01:36:05 +0000
+++ cronscripts/update-debwatches.py	2011-06-17 19:56:04 +0000
@@ -16,6 +16,7 @@
 from zope.component import getUtility
 
 from canonical.database.constants import UTC_NOW
+from lp.app.errors import NotFoundError
 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
 from lp.bugs.interfaces.bug import IBugSet
 from lp.bugs.interfaces.bugtask import (
@@ -41,7 +42,8 @@
     loglevel = logging.WARNING
 
     def add_my_options(self):
-        self.parser.add_option('--max', action='store', type='int', dest='max',
+        self.parser.add_option(
+            '--max', action='store', type='int', dest='max',
             default=None, help="The maximum number of bugs to synchronise.")
         self.parser.add_option('--debbugs', action='store', type='string',
             dest='debbugs',
@@ -49,7 +51,8 @@
             help="The location of your debbugs database.")
 
     def main(self):
-        if not os.path.exists(os.path.join(self.options.debbugs, 'index/index.db')):
+        if not os.path.exists(
+            os.path.join(self.options.debbugs, 'index/index.db')):
             raise LaunchpadScriptFailure('%s is not a debbugs db.'
                                          % self.options.debbugs)
 
@@ -68,10 +71,12 @@
         debwatches = debbugs_tracker.watches
 
         previousimportset = set([b.remotebug for b in debwatches])
-        self.logger.info('%d debbugs previously imported.' % len(previousimportset))
+        self.logger.info(
+            '%d debbugs previously imported.' % len(previousimportset))
 
         target_watches = [watch for watch in debwatches if watch.needscheck]
-        self.logger.info('%d debbugs watches to syncronise.' % len(target_watches))
+        self.logger.info(
+            '%d debbugs watches to syncronise.' % len(target_watches))
 
         self.logger.info('Sorting bug watches...')
         target_watches.sort(key=lambda a: a.remotebug)
@@ -100,12 +105,14 @@
         debian = getUtility(ILaunchpadCelebrities).debian
         debbugs_tracker = getUtility(ILaunchpadCelebrities).debbugs
 
-        # make sure we have tasks for all the debian package linkages, and also
-        # make sure we have updated their status and severity appropriately
+        # make sure we have tasks for all the debian package linkages, and
+        # also make sure we have updated their status and severity
+        # appropriately.
         for packagename in debian_bug.packagelist():
             try:
-                srcpkgname, binpkgname = debian.guessPackageNames(packagename)
-            except ValueError:
+                srcpkgname = debian.guessPublishedSourcePackageName(
+                    packagename)
+            except NotFoundError:
                 self.logger.error(sys.exc_value)
                 continue
             search_params = BugTaskSearchParams(user=None, bug=malone_bug,
@@ -114,8 +121,8 @@
             bugtasks = bugtaskset.search(search_params)
             if len(bugtasks) == 0:
                 # we need a new task to link the bug to the debian package
-                self.logger.info('Linking %d and debian %s/%s' % (
-                    malone_bug.id, srcpkgname.name, binpkgname.name))
+                self.logger.info('Linking %d and debian %s' % (
+                    malone_bug.id, srcpkgname.name))
                 # XXX: kiko 2007-02-03:
                 # This code is completely untested and broken.
                 bugtask = malone_bug.addTask(
@@ -127,10 +134,11 @@
                 assert len(bugtasks) == 1, 'Should only find a single task'
                 bugtask = bugtasks[0]
             status = bugtask.status
-            if status <> bugtask.setStatusFromDebbugs(debian_bug.status):
+            if status != bugtask.setStatusFromDebbugs(debian_bug.status):
                 waschanged = True
             severity = bugtask.severity
-            if severity <> bugtask.setSeverityFromDebbugs(debian_bug.severity):
+            if severity != bugtask.setSeverityFromDebbugs(
+                debian_bug.severity):
                 waschanged = True
 
         known_msg_ids = set([msg.rfc822msgid for msg in malone_bug.messages])
@@ -157,17 +165,18 @@
             if msg is None:
                 continue
 
-            # create the link between the bug and this message
-            bugmsg = malone_bug.linkMessage(msg)
+            # Create the link between the bug and this message.
+            malone_bug.linkMessage(msg)
 
-            # ok, this is a new message for this bug, so in effect something has
-            # changed
+            # ok, this is a new message for this bug, so in effect something
+            # has changed
             waschanged = True
 
             # now we need to analyse the message for useful data
             watches = bugwatchset.fromMessage(msg, malone_bug)
             for watch in watches:
-                self.logger.info('New watch for #%s on %s' % (watch.bug.id, watch.url))
+                self.logger.info(
+                    'New watch for #%s on %s' % (watch.bug.id, watch.url))
                 waschanged = True
 
             # and also for CVE ref clues
@@ -191,7 +200,8 @@
         if (len(debian_bug.mergedwith) > 0 and
             min(debian_bug.mergedwith) > debian_bug.id):
             for merged_id in debian_bug.mergedwith:
-                merged_bug = bugset.queryByRemoteBug(debbugs_tracker, merged_id)
+                merged_bug = bugset.queryByRemoteBug(
+                    debbugs_tracker, merged_id)
                 if merged_bug is not None:
                     # Bug has been imported already
                     if merged_bug.duplicateof == malone_bug:
@@ -199,11 +209,13 @@
                         continue
                     elif merged_bug.duplicateof is not None:
                         # Interesting, we think it's a dup of something else
-                        self.logger.warning('Debbugs thinks #%d is a dup of #%d' % (
+                        self.logger.warning(
+                            'Debbugs thinks #%d is a dup of #%d' % (
                             merged_bug.id, merged_bug.duplicateof))
                         continue
                     # Go ahead and merge it
-                    self.logger.info("Malone #%d is a duplicate of Malone #%d" % (
+                    self.logger.info(
+                        "Malone #%d is a duplicate of Malone #%d" % (
                         merged_bug.id, malone_bug.id))
                     merged_bug.duplicateof = malone_bug.id
 
@@ -211,7 +223,7 @@
                     waschanged = True
 
         # make a note of the remote watch status, if it has changed
-        if watch.remotestatus <> debian_bug.status:
+        if watch.remotestatus != debian_bug.status:
             watch.remotestatus = debian_bug.status
             waschanged = True
 
@@ -226,4 +238,3 @@
 if __name__ == '__main__':
     script = DebWatchUpdater('launchpad-debbugs-sync')
     script.lock_and_run()
-

=== modified file 'lib/canonical/launchpad/interfaces/validation.py'
--- lib/canonical/launchpad/interfaces/validation.py	2011-04-11 02:32:50 +0000
+++ lib/canonical/launchpad/interfaces/validation.py	2011-06-17 19:56:04 +0000
@@ -95,6 +95,7 @@
             "${email} isn't a valid email address.",
             mapping={'email': email}))
 
+
 def _check_email_availability(email):
     email_address = getUtility(IEmailAddressSet).getByEmail(email)
     if email_address is not None:
@@ -198,7 +199,8 @@
         # If the distribution has at least one series, check that the
         # source package has been published in the distribution.
         try:
-            distribution.guessPackageNames(sourcepackagename.name)
+            distribution.guessPublishedSourcePackageName(
+                sourcepackagename.name)
         except NotFoundError, e:
             raise LaunchpadValidationError(e)
     new_source_package = distribution.getSourcePackage(sourcepackagename)
@@ -232,9 +234,10 @@
     user = getUtility(ILaunchBag).user
     params = BugTaskSearchParams(user, bug=bug)
     if not bug_target.searchTasks(params).is_empty():
-        errors.append(LaunchpadValidationError(_(
-                    'A fix for this bug has already been requested for ${target}',
-                    mapping={'target': bug_target.displayname})))
+        errors.append(
+            LaunchpadValidationError(_(
+                'A fix for this bug has already been requested for ${target}',
+                mapping={'target': bug_target.displayname})))
 
     if len(errors) > 0:
         raise expose(WidgetsError(errors), 400)

=== modified file 'lib/canonical/launchpad/scripts/debsync.py'
--- lib/canonical/launchpad/scripts/debsync.py	2011-05-27 21:12:25 +0000
+++ lib/canonical/launchpad/scripts/debsync.py	2011-06-17 19:56:04 +0000
@@ -17,6 +17,7 @@
 from zope.component import getUtility
 
 from canonical.database.sqlbase import flush_database_updates
+from lp.app.errors import NotFoundError
 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
 from lp.bugs.interfaces.bug import (
     CreateBugParams,
@@ -60,7 +61,7 @@
         return False
     # and we won't import any bug that is newer than one week, to give
     # debian some time to find dups
-    if bug.date > datetime.datetime.now()-datetime.timedelta(minimum_age):
+    if bug.date > datetime.datetime.now() - datetime.timedelta(minimum_age):
         return False
     return True
 
@@ -103,7 +104,6 @@
 
 def import_bug(debian_bug, logger):
     """Consider importing a debian bug, return True if you did."""
-    packagelist = debian_bug.packagelist()
     bugset = getUtility(IBugSet)
     debbugs_tracker = getUtility(ILaunchpadCelebrities).debbugs
     malone_bug = bugset.queryByRemoteBug(debbugs_tracker, debian_bug.id)
@@ -143,11 +143,11 @@
     # debian_bug.packagelist[0] is going to be a single package name for
     # sure. we work through the package list, try to find one we can
     # work with, otherwise give up
-    srcpkg = binpkg = pkgname = None
+    srcpkg = pkgname = None
     for pkgname in debian_bug.packagelist():
         try:
-            srcpkg, binpkg = ubuntu.guessPackageNames(pkgname)
-        except ValueError:
+            srcpkg = ubuntu.guessPublishedSourcePackageName(pkgname)
+        except NotFoundError:
             logger.error(sys.exc_value)
     if srcpkg is None:
         # none of the package names gave us a source package we can use
@@ -158,8 +158,8 @@
         return False
     # sometimes debbugs has initial emails that contain the package name, we
     # can remove that
-    if title.startswith(pkgname+':'):
-        title = title[len(pkgname)+2:].strip()
+    if title.startswith(pkgname + ':'):
+        title = title[len(pkgname) + 2:].strip()
     params = CreateBugParams(
         title=title, msg=msg, owner=msg.owner,
         datecreated=msg.datecreated)
@@ -199,5 +199,3 @@
 
     flush_database_updates()
     return True
-
-

=== modified file 'lib/lp/app/widgets/launchpadtarget.py'
--- lib/lp/app/widgets/launchpadtarget.py	2011-05-27 21:12:25 +0000
+++ lib/lp/app/widgets/launchpadtarget.py	2011-06-17 19:56:04 +0000
@@ -135,8 +135,9 @@
                 if package_name is None:
                     return distribution
                 try:
-                    source_name, binary_name = distribution.guessPackageNames(
-                        package_name.name)
+                    source_name = (
+                        distribution.guessPublishedSourcePackageName(
+                            package_name.name))
                 except NotFoundError:
                     raise LaunchpadValidationError(
                         "There is no package name '%s' published in %s"

=== modified file 'lib/lp/bugs/browser/bugtarget.py'
--- lib/lp/bugs/browser/bugtarget.py	2011-06-07 23:20:43 +0000
+++ lib/lp/bugs/browser/bugtarget.py	2011-06-17 19:56:04 +0000
@@ -329,6 +329,17 @@
         self.extra_data = FileBugData()
 
     def initialize(self):
+        # redirect_ubuntu_filebug is a cached_property.
+        # Access it first just to compute its value. Because it
+        # makes a DB access to get the bug supervisor, it causes
+        # trouble in tests when form validation errors occur. Because the
+        # transaction is doomed, the storm cache is invalidated and accessing
+        # the property will result in a a LostObjectError, because
+        # the created objects disappeared. Not likely a problem in production
+        # since the objects will still be in the DB, but doesn't hurt there
+        # either. It makes for better diagnosis of failing tests.
+        if self.redirect_ubuntu_filebug:
+            pass
         LaunchpadFormView.initialize(self)
         if (not self.redirect_ubuntu_filebug and
             self.extra_data_token is not None and
@@ -473,7 +484,7 @@
                     distribution = self.context.distribution
 
                 try:
-                    distribution.guessPackageNames(packagename)
+                    distribution.guessPublishedSourcePackageName(packagename)
                 except NotFoundError:
                     if distribution.series:
                         # If a distribution doesn't have any series,
@@ -590,13 +601,9 @@
             # package name, so let the Soyuz API figure it out for us.
             packagename = str(packagename.name)
             try:
-                sourcepackagename, binarypackagename = (
-                    context.guessPackageNames(packagename))
+                sourcepackagename = context.guessPublishedSourcePackageName(
+                    packagename)
             except NotFoundError:
-                # guessPackageNames may raise NotFoundError. It would be
-                # nicer to allow people to indicate a package even if
-                # never published, but the quick fix for now is to note
-                # the issue and move on.
                 notifications.append(
                     "The package %s is not published in %s; the "
                     "bug was targeted only to the distribution."
@@ -608,7 +615,6 @@
                         packagename, context.displayname))
             else:
                 context = context.getSourcePackage(sourcepackagename.name)
-                params.binarypackagename = binarypackagename
 
         extra_data = self.extra_data
         if extra_data.extra_description:

=== modified file 'lib/lp/bugs/browser/tests/bug-views.txt'
--- lib/lp/bugs/browser/tests/bug-views.txt	2011-06-16 13:50:58 +0000
+++ lib/lp/bugs/browser/tests/bug-views.txt	2011-06-17 19:56:04 +0000
@@ -25,8 +25,7 @@
 if the user provides a valid package name when reporting the bug.
 
 If the package name entered by the user happens to be a binary package
-name, that information is recorded in the description, and the first
-comment, of the bug report.
+name, the bug is filed against the source package used to build that binary.
 
     >>> from zope.component import getMultiAdapter, getUtility
     >>> from canonical.launchpad.webapp.interfaces import (
@@ -70,16 +69,7 @@
 
     >>> latest_ubuntu_bugtask = ubuntu.searchTasks(search_params)[0]
 
-The user specified a binary package name, so that's been added to the
-bug description and the first comment:
-
-    >>> print latest_ubuntu_bugtask.bug.description
-    Binary package hint: linux-2.6.12
-    <BLANKLINE>
-    <BLANKLINE>
-    a bug in a bin pkg
-
-the source package from which the binary was built has been set on
+The source package from which the binary was built has been set on
 the bugtask.
 
     >>> print latest_ubuntu_bugtask.sourcepackagename.name
@@ -166,7 +156,11 @@
     3
     >>> [ (c.index, c.owner.name, c.text_contents)
     ...  for c in ubuntu_bugview.comments ]
+<<<<<<< TREE
     [(0, u'name16', u'Binary package hint: linux-2.6.12...'),
+=======
+    [(0, u'name16', u'a bug in a bin pkg'),
+>>>>>>> MERGE-SOURCE
      (1, u'name16', u'I can reproduce this bug.'),
      (2, u'name16', u'I can reproduce this bug.')]
 
@@ -178,17 +172,11 @@
 identical. Take as an example the first bug posted above:
 
     >>> print latest_ubuntu_bugtask.bug.description
-    Binary package hint: linux-2.6.12
-    <BLANKLINE>
-    <BLANKLINE>
     a bug in a bin pkg
 
 Its description has the same contents as the bug's first comment:
 
     >>> print latest_ubuntu_bugtask.bug.messages[0].text_contents
-    Binary package hint: linux-2.6.12
-    <BLANKLINE>
-    <BLANKLINE>
     a bug in a bin pkg
 
 The view class offers a method to check exactly that:

=== modified file 'lib/lp/bugs/browser/tests/test_bugtarget_filebug.py'
--- lib/lp/bugs/browser/tests/test_bugtarget_filebug.py	2011-04-29 01:55:28 +0000
+++ lib/lp/bugs/browser/tests/test_bugtarget_filebug.py	2011-06-17 19:56:04 +0000
@@ -8,6 +8,7 @@
     TooLong,
     TooShort,
     )
+from zope.security.proxy import removeSecurityProxy
 
 from canonical.launchpad.ftests import login
 from canonical.launchpad.testing.pages import (
@@ -21,6 +22,7 @@
     FileBugViewBase,
     )
 from lp.bugs.interfaces.bug import IBugAddForm
+from lp.bugs.publisher import BugsLayer
 from lp.testing import (
     login_person,
     TestCaseWithFactory,
@@ -394,3 +396,33 @@
             "source": product.displayname, "content": u"Include bug details",
             }]
         self.assertEqual(expected_guidelines, view.bug_reporting_guidelines)
+
+
+class TestFileBugSourcePackage(TestCaseWithFactory):
+
+    layer = DatabaseFunctionalLayer
+
+    def test_filebug_works_on_official_package_branch(self):
+        # It should be possible to file a bug against a source package
+        # when there is an official package branch.
+        user = self.factory.makePerson()
+        sourcepackage = self.factory.makeSourcePackage('my-package')
+        self.factory.makeRelatedBranchesForSourcePackage(
+            sourcepackage=sourcepackage)
+        removeSecurityProxy(sourcepackage.distribution).official_malone = True
+        login_person(user)
+
+        view = create_initialized_view(
+            context=sourcepackage.distribution, name='+filebug',
+            form={
+                'field.title': 'A bug',
+                'field.comment': 'A comment',
+                'field.bugtarget.distribution': (
+                    sourcepackage.distribution.name),
+                'field.packagename': 'my-package',
+                'field.actions.submit_bug': 'Submit Bug Request',
+            }, layer=BugsLayer, principal=user)
+        msg = "\n".join([
+            notification.message
+            for notification in view.request.response.notifications])
+        self.assertIn("Thank you for your bug report.", msg)

=== modified file 'lib/lp/bugs/browser/widgets/bugtask.py'
--- lib/lp/bugs/browser/widgets/bugtask.py	2011-06-02 21:31:51 +0000
+++ lib/lp/bugs/browser/widgets/bugtask.py	2011-06-17 19:56:04 +0000
@@ -496,7 +496,7 @@
         distribution = self.getDistribution()
 
         try:
-            source, binary = distribution.guessPackageNames(input)
+            source = distribution.guessPublishedSourcePackageName(input)
         except NotFoundError:
             try:
                 return self.convertTokensToValues([input])[0]

=== modified file 'lib/lp/bugs/doc/bugtask-package-widget.txt'
--- lib/lp/bugs/doc/bugtask-package-widget.txt	2011-01-11 11:51:27 +0000
+++ lib/lp/bugs/doc/bugtask-package-widget.txt	2011-06-17 19:56:04 +0000
@@ -49,13 +49,13 @@
     u'linux-source-2.6.15'
 
 For some distribution we don't know exactly which source packages it
-contains, so IDistribution.guessPackageNames will return a
+contains, so IDistribution.guessPublishedSourcePackageName will raise a
 NotFoundError.
 
     >>> debian_task = bug_one.bugtasks[-1]
     >>> debian_task.distribution.name
     u'debian'
-    >>> debian_task.distribution.guessPackageNames('evolution')
+    >>> debian_task.distribution.guessPublishedSourcePackageName('evolution')
     Traceback (most recent call last):
     ...
     NotFoundError...

=== modified file 'lib/lp/bugs/doc/bugzilla-import.txt'
--- lib/lp/bugs/doc/bugzilla-import.txt	2011-05-27 19:53:20 +0000
+++ lib/lp/bugs/doc/bugzilla-import.txt	2011-06-17 19:56:04 +0000
@@ -8,220 +8,222 @@
 We will start by defining a fake backend and some fake information for
 it to return:
 
-  >>> from datetime import datetime
-  >>> import pytz
-  >>> UTC = pytz.timezone('UTC')
-
-  >>> users = [
-  ...     ('test@xxxxxxxxxxxxx', 'Sample User'),
-  ...     ('foo.bar@xxxxxxxxxxxxx', 'Foo Bar'),
-  ...     ('new.user@xxxxxxxxxxxxx', 'New User')  # <- not in Launchpad
-  ...     ]
-
-  >>> buginfo = [
-  ...         (1,                 # bug_id
-  ...          1,                 # assigned_to
-  ...          '',                # bug_file_loc
-  ...          'normal',          # bug_severity
-  ...          'NEW',             # status
-  ...          datetime(2005, 4, 1, tzinfo=UTC), # creation
-  ...          'Test bug 1',      # short_desc,
-  ...          'Linux',           # op_sys
-  ...          'P2',              # priority
-  ...          'Ubuntu',          # product
-  ...          'AMD64',           # rep_platform
-  ...          1,                 # reporter
-  ...          '---',             # version
-  ...          'mozilla-firefox', # component
-  ...          '',                # resolution
-  ...          'Ubuntu 5.10',     # milestone
-  ...          0,                 # qa_contact
-  ...          'status',          # status_whiteboard
-  ...          '',                # keywords
-  ...          ''),               # alias
-  ...         # A WONTFIX bug on a non-existant distro package
-  ...         (2, 1, 'http://www.ubuntu.com', 'enhancement', 'RESOLVED',
-  ...          datetime(2005, 4, 2, tzinfo=UTC), 'Test bug 2',
-  ...          'Linux', 'P1', 'Ubuntu', 'i386', 2, '---', 'unknown',
-  ...          'WONTFIX', '---', 0, '', '', ''),
-  ...         # An accepted bug:
-  ...         (3, 2, 'http://www.ubuntu.com', 'blocker', 'ASSIGNED',
-  ...          datetime(2005, 4, 3, tzinfo=UTC), 'Test bug 3',
-  ...          'Linux', 'P1', 'Ubuntu', 'i386', 1, '---', 'netapplet',
-  ...          '', '---', 0, '', '', 'xyz'),
-  ...         # A fixed bug
-  ...         (4, 1, 'http://www.ubuntu.com', 'blocker', 'CLOSED',
-  ...          datetime(2005, 4, 4, tzinfo=UTC), 'Test bug 4',
-  ...          'Linux', 'P1', 'Ubuntu', 'i386', 1, '---', 'mozilla-firefox',
-  ...          'FIXED', '---', 0, '', '', 'FooBar'),
-  ...         # An UPSTREAM bug
-  ...         (5, 1, 'http://bugzilla.gnome.org/bugs/show_bug.cgi?id=273041',
-  ...          'blocker', 'UPSTREAM',
-  ...          datetime(2005, 4, 4, tzinfo=UTC), 'Test bug 5',
-  ...          'Linux', 'P1', 'Ubuntu', 'i386', 1, '---', 'evolution',
-  ...          '', '---', 0, '', '', 'deb1234'),
-  ...     ]
-
-  >>> ccs = [[], [3], [], [], []]
-
-  >>> comments = [
-  ...     [(1, datetime(2005, 4, 1, tzinfo=UTC), 'First comment'),
-  ...      (2, datetime(2005, 4, 1, 1, tzinfo=UTC), 'Second comment')],
-  ...     [(1, datetime(2005, 4, 2, tzinfo=UTC), 'First comment'),
-  ...      (2, datetime(2005, 4, 2, 1, tzinfo=UTC), 'Second comment')],
-  ...     [(2, datetime(2005, 4, 3, tzinfo=UTC), 'First comment'),
-  ...      (1, datetime(2005, 4, 3, 1, tzinfo=UTC),
-  ...       'This is related to CVE-2005-1234'),
-  ...      (2, datetime(2005, 4, 3, 2, tzinfo=UTC),
-  ...       'Created an attachment (id=1)')],
-  ...     [(1, datetime(2005, 4, 4, tzinfo=UTC), 'First comment')],
-  ...     [(1, datetime(2005, 4, 5, tzinfo=UTC), 'First comment')],
-  ...     ]
-
-  >>> attachments = [
-  ...     [], [],
-  ...     [(1, datetime(2005, 4, 3, 2, tzinfo=UTC), 'An attachment',
-  ...       'text/x-patch', True, 'foo.patch', 'the data', 2)],
-  ...     [], []
-  ...     ]
-
-  >>> duplicates = [
-  ...     (1, 2),
-  ...     (3, 4),
-  ...     ]
-
-  >>> class FakeBackend:
-  ...     def lookupUser(self, user_id):
-  ...         return users[user_id - 1]
-  ...     def getBugInfo(self, bug_id):
-  ...         return buginfo[bug_id - 1]
-  ...     def getBugCcs(self, bug_id):
-  ...         return ccs[bug_id - 1]
-  ...     def getBugComments(self, bug_id):
-  ...         return comments[bug_id - 1]
-  ...     def getBugAttachments(self, bug_id):
-  ...         return attachments[bug_id - 1]
-  ...     def getDuplicates(self):
-  ...         return duplicates
-
-  >>> from itertools import chain
-  >>> from zope.component import getUtility
-  >>> from canonical.launchpad.ftests import login
-  >>> from lp.app.interfaces.launchpad import ILaunchpadCelebrities
-  >>> from lp.bugs.interfaces.bug import IBugSet
-  >>> from lp.bugs.scripts import bugzilla
-  >>> from lp.registry.interfaces.person import IPersonSet
+    >>> from datetime import datetime
+    >>> import pytz
+    >>> UTC = pytz.timezone('UTC')
+
+    >>> users = [
+    ...     ('test@xxxxxxxxxxxxx', 'Sample User'),
+    ...     ('foo.bar@xxxxxxxxxxxxx', 'Foo Bar'),
+    ...     ('new.user@xxxxxxxxxxxxx', 'New User')  # <- not in Launchpad
+    ...     ]
+
+    >>> buginfo = [
+    ...         (1,                 # bug_id
+    ...          1,                 # assigned_to
+    ...          '',                # bug_file_loc
+    ...          'normal',          # bug_severity
+    ...          'NEW',             # status
+    ...          datetime(2005, 4, 1, tzinfo=UTC), # creation
+    ...          'Test bug 1',      # short_desc,
+    ...          'Linux',           # op_sys
+    ...          'P2',              # priority
+    ...          'Ubuntu',          # product
+    ...          'AMD64',           # rep_platform
+    ...          1,                 # reporter
+    ...          '---',             # version
+    ...          'mozilla-firefox', # component
+    ...          '',                # resolution
+    ...          'Ubuntu 5.10',     # milestone
+    ...          0,                 # qa_contact
+    ...          'status',          # status_whiteboard
+    ...          '',                # keywords
+    ...          ''),               # alias
+    ...         # A WONTFIX bug on a non-existant distro package
+    ...         (2, 1, 'http://www.ubuntu.com', 'enhancement', 'RESOLVED',
+    ...          datetime(2005, 4, 2, tzinfo=UTC), 'Test bug 2',
+    ...          'Linux', 'P1', 'Ubuntu', 'i386', 2, '---', 'unknown',
+    ...          'WONTFIX', '---', 0, '', '', ''),
+    ...         # An accepted bug:
+    ...         (3, 2, 'http://www.ubuntu.com', 'blocker', 'ASSIGNED',
+    ...          datetime(2005, 4, 3, tzinfo=UTC), 'Test bug 3',
+    ...          'Linux', 'P1', 'Ubuntu', 'i386', 1, '---', 'netapplet',
+    ...          '', '---', 0, '', '', 'xyz'),
+    ...         # A fixed bug
+    ...         (4, 1, 'http://www.ubuntu.com', 'blocker', 'CLOSED',
+    ...          datetime(2005, 4, 4, tzinfo=UTC), 'Test bug 4',
+    ...          'Linux', 'P1', 'Ubuntu', 'i386', 1, '---', 'mozilla-firefox',
+    ...          'FIXED', '---', 0, '', '', 'FooBar'),
+    ...         # An UPSTREAM bug
+    ...         (5, 1,
+    ...          'http://bugzilla.gnome.org/bugs/show_bug.cgi?id=273041',
+    ...          'blocker', 'UPSTREAM',
+    ...          datetime(2005, 4, 4, tzinfo=UTC), 'Test bug 5',
+    ...          'Linux', 'P1', 'Ubuntu', 'i386', 1, '---', 'evolution',
+    ...          '', '---', 0, '', '', 'deb1234'),
+    ...     ]
+
+    >>> ccs = [[], [3], [], [], []]
+
+    >>> comments = [
+    ...     [(1, datetime(2005, 4, 1, tzinfo=UTC), 'First comment'),
+    ...      (2, datetime(2005, 4, 1, 1, tzinfo=UTC), 'Second comment')],
+    ...     [(1, datetime(2005, 4, 2, tzinfo=UTC), 'First comment'),
+    ...      (2, datetime(2005, 4, 2, 1, tzinfo=UTC), 'Second comment')],
+    ...     [(2, datetime(2005, 4, 3, tzinfo=UTC), 'First comment'),
+    ...      (1, datetime(2005, 4, 3, 1, tzinfo=UTC),
+    ...       'This is related to CVE-2005-1234'),
+    ...      (2, datetime(2005, 4, 3, 2, tzinfo=UTC),
+    ...       'Created an attachment (id=1)')],
+    ...     [(1, datetime(2005, 4, 4, tzinfo=UTC), 'First comment')],
+    ...     [(1, datetime(2005, 4, 5, tzinfo=UTC), 'First comment')],
+    ...     ]
+
+    >>> attachments = [
+    ...     [], [],
+    ...     [(1, datetime(2005, 4, 3, 2, tzinfo=UTC), 'An attachment',
+    ...       'text/x-patch', True, 'foo.patch', 'the data', 2)],
+    ...     [], []
+    ...     ]
+
+    >>> duplicates = [
+    ...     (1, 2),
+    ...     (3, 4),
+    ...     ]
+
+    >>> class FakeBackend:
+    ...     def lookupUser(self, user_id):
+    ...         return users[user_id - 1]
+    ...     def getBugInfo(self, bug_id):
+    ...         return buginfo[bug_id - 1]
+    ...     def getBugCcs(self, bug_id):
+    ...         return ccs[bug_id - 1]
+    ...     def getBugComments(self, bug_id):
+    ...         return comments[bug_id - 1]
+    ...     def getBugAttachments(self, bug_id):
+    ...         return attachments[bug_id - 1]
+    ...     def getDuplicates(self):
+    ...         return duplicates
+
+    >>> from itertools import chain
+    >>> from zope.component import getUtility
+    >>> from canonical.launchpad.ftests import login
+    >>> from lp.app.interfaces.launchpad import ILaunchpadCelebrities
+    >>> from lp.bugs.interfaces.bug import IBugSet
+    >>> from lp.bugs.scripts import bugzilla
+    >>> from lp.registry.interfaces.person import IPersonSet
 
 Get a reference to the Ubuntu bug tracker, and log in:
 
-  >>> login('bug-importer@xxxxxxxxxxxxx')
-  >>> bugtracker = getUtility(ILaunchpadCelebrities).ubuntu_bugzilla
+    >>> login('bug-importer@xxxxxxxxxxxxx')
+    >>> bugtracker = getUtility(ILaunchpadCelebrities).ubuntu_bugzilla
 
 Now we create a bugzilla.Bugzilla instance to handle the import, using
 our fake backend data:
 
-  >>> bz = bugzilla.Bugzilla(None)
-  >>> bz.backend = FakeBackend()
+    >>> bz = bugzilla.Bugzilla(None)
+    >>> bz.backend = FakeBackend()
 
 In order to verify that things get imported correctly, the following
 function will be used:
 
-  >>> def bugInfo(bug):
-  ...     print 'Title:', bug.title
-  ...     print 'Reporter:', bug.owner.displayname
-  ...     print 'Created:', bug.datecreated
-  ...     if bug.name:
-  ...         print 'Nick: %s' % bug.name
-  ...     print 'Subscribers:'
-  ...     subscriber_names = sorted(
-  ...         p.displayname for p in chain(
-  ...             bug.getDirectSubscribers(),
-  ...             bug.getIndirectSubscribers()))
-  ...     for subscriber_name in subscriber_names:
-  ...         print '    %s' % subscriber_name
-  ...     for task in bug.bugtasks:
-  ...         print 'Task:', task.bugtargetdisplayname
-  ...         print '    Status:', task.status.name
-  ...         if task.product:
-  ...             print '    Product:', task.product.name
-  ...         if task.distribution:
-  ...             print '    Distro:', task.distribution.name
-  ...         if task.sourcepackagename:
-  ...             print '    Source package:', task.sourcepackagename.name
-  ...         if task.assignee:
-  ...             print '    Assignee:', task.assignee.displayname
-  ...         if task.importance:
-  ...             print '    Importance:', task.importance.name
-  ...         if task.statusexplanation:
-  ...             print '    Explanation:', task.statusexplanation
-  ...         if task.milestone:
-  ...             print '    Milestone:', task.milestone.name
-  ...         if task.bugwatch:
-  ...             print '    Watch:', task.bugwatch.url
-  ...     if bug.cves:
-  ...         print 'CVEs:'
-  ...     for cve in bug.cves:
-  ...         print '    %s' % cve.displayname
-  ...     print 'Messages:'
-  ...     for message in bug.messages:
-  ...         print '    Author:', message.owner.displayname
-  ...         print '    Date:', message.datecreated
-  ...         print '    Subject:', message.subject
-  ...         print '    %s' % message.text_contents
-  ...         print
-  ...     if bug.attachments.any():
-  ...         print 'Attachments:'
-  ...     for attachment in bug.attachments:
-  ...         print '    Title:', attachment.title
-  ...         print '    Type:', attachment.type.name
-  ...         print '    Name:', attachment.libraryfile.filename
-  ...         print '    Mime type:', attachment.libraryfile.mimetype
+    >>> def bugInfo(bug):
+    ...     print 'Title:', bug.title
+    ...     print 'Reporter:', bug.owner.displayname
+    ...     print 'Created:', bug.datecreated
+    ...     if bug.name:
+    ...         print 'Nick: %s' % bug.name
+    ...     print 'Subscribers:'
+    ...     subscriber_names = sorted(
+    ...         p.displayname for p in chain(
+    ...             bug.getDirectSubscribers(),
+    ...             bug.getIndirectSubscribers()))
+    ...     for subscriber_name in subscriber_names:
+    ...         print '    %s' % subscriber_name
+    ...     for task in bug.bugtasks:
+    ...         print 'Task:', task.bugtargetdisplayname
+    ...         print '    Status:', task.status.name
+    ...         if task.product:
+    ...             print '    Product:', task.product.name
+    ...         if task.distribution:
+    ...             print '    Distro:', task.distribution.name
+    ...         if task.sourcepackagename:
+    ...             print '    Source package:', task.sourcepackagename.name
+    ...         if task.assignee:
+    ...             print '    Assignee:', task.assignee.displayname
+    ...         if task.importance:
+    ...             print '    Importance:', task.importance.name
+    ...         if task.statusexplanation:
+    ...             print '    Explanation:', task.statusexplanation
+    ...         if task.milestone:
+    ...             print '    Milestone:', task.milestone.name
+    ...         if task.bugwatch:
+    ...             print '    Watch:', task.bugwatch.url
+    ...     if bug.cves:
+    ...         print 'CVEs:'
+    ...     for cve in bug.cves:
+    ...         print '    %s' % cve.displayname
+    ...     print 'Messages:'
+    ...     for message in bug.messages:
+    ...         print '    Author:', message.owner.displayname
+    ...         print '    Date:', message.datecreated
+    ...         print '    Subject:', message.subject
+    ...         print '    %s' % message.text_contents
+    ...         print
+    ...     if bug.attachments.any():
+    ...         print 'Attachments:'
+    ...     for attachment in bug.attachments:
+    ...         print '    Title:', attachment.title
+    ...         print '    Type:', attachment.type.name
+    ...         print '    Name:', attachment.libraryfile.filename
+    ...         print '    Mime type:', attachment.libraryfile.mimetype
 
 
 Now we import bug #1 and check the results:
 
-  >>> bug = bz.handleBug(1)
-  >>> bugInfo(bug)
-  Title: Test bug 1
-  Reporter: Sample Person
-  Created: 2005-04-01 00:00:00+00:00
-  Subscribers:
+    >>> bug = bz.handleBug(1)
+    >>> bugInfo(bug)
+    Title: Test bug 1
+    Reporter: Sample Person
+    Created: 2005-04-01 00:00:00+00:00
+    Subscribers:
       Foo Bar
       Sample Person
       Ubuntu Team
-  Task: mozilla-firefox (Ubuntu)
+    Task: mozilla-firefox (Ubuntu)
       Status: NEW
       Distro: ubuntu
       Source package: mozilla-firefox
       Assignee: Sample Person
       Importance: MEDIUM
-      Explanation: status (Bugzilla status=NEW, product=Ubuntu, component=mozilla-firefox)
+      Explanation: status (Bugzilla status=NEW, product=Ubuntu,
+        component=mozilla-firefox)
       Milestone: ubuntu-5.10
-  Messages:
+    Messages:
       Author: Sample Person
       Date: 2005-04-01 00:00:00+00:00
       Subject: Test bug 1
       First comment
-  <BLANKLINE>
+    <BLANKLINE>
       Author: Foo Bar
       Date: 2005-04-01 01:00:00+00:00
       Subject: Re: Test bug 1
       Second comment
-  <BLANKLINE>
+    <BLANKLINE>
 
 As well as importing the bug, a bug watch is created, linking the new
 Launchpad bug to the original Bugzilla bug:
 
-  >>> linked_bug = getUtility(IBugSet).queryByRemoteBug(bugtracker, 1)
-  >>> linked_bug == bug
-  True
+    >>> linked_bug = getUtility(IBugSet).queryByRemoteBug(bugtracker, 1)
+    >>> linked_bug == bug
+    True
 
 This bug watch link is used to prevent multiple imports of the same
 bug.
 
-  >>> second_import = bz.handleBug(1)
-  >>> bug == second_import
-  True
+    >>> second_import = bz.handleBug(1)
+    >>> bug == second_import
+    True
 
 
 Next we try bug #2, which is assigned to a non-existant source
@@ -232,45 +234,48 @@
    effect of the import, because they are subscribed to the bug.
  * The "RESOLVED WONTFIX" status is converted to a status of INVALID.
  * The fact that the "unknown" package does not exist in Ubuntu has
-   been logged, along with the exception raised by guessPackageNames().
-
-  >>> print getUtility(IPersonSet).getByEmail('new.user@xxxxxxxxxxxxx')
-  None
-  >>> bug = bz.handleBug(2)
-  WARNING:lp.bugs.scripts.bugzilla:could not find package name for "unknown": 'Unknown package: unknown'
-  >>> import transaction
-  >>> transaction.commit()
-
-  >>> bugInfo(bug)
-  Title: Test bug 2
-  Reporter: Foo Bar
-  Created: 2005-04-02 00:00:00+00:00
-  Subscribers:
+   been logged, along with the exception raised by
+   guessPublishedSourcePackageName().
+
+    >>> print getUtility(IPersonSet).getByEmail('new.user@xxxxxxxxxxxxx')
+    None
+    >>> bug = bz.handleBug(2)
+    WARNING:lp.bugs.scripts.bugzilla:could not find package name for
+    "unknown": 'Unknown package: unknown'
+    >>> import transaction
+    >>> transaction.commit()
+
+    >>> bugInfo(bug)
+    Title: Test bug 2
+    Reporter: Foo Bar
+    Created: 2005-04-02 00:00:00+00:00
+    Subscribers:
       Foo Bar
       New User
       Sample Person
       Ubuntu Team
-  Task: Ubuntu
+    Task: Ubuntu
       Status: INVALID
       Distro: ubuntu
       Assignee: Sample Person
       Importance: WISHLIST
-      Explanation: Bugzilla status=RESOLVED WONTFIX, product=Ubuntu, component=unknown
-  Messages:
+      Explanation: Bugzilla status=RESOLVED WONTFIX, product=Ubuntu,
+        component=unknown
+    Messages:
       Author: Sample Person
       Date: 2005-04-02 00:00:00+00:00
       Subject: Test bug 2
       First comment
-  <BLANKLINE>
+    <BLANKLINE>
       http://www.ubuntu.com
-  <BLANKLINE>
+    <BLANKLINE>
       Author: Foo Bar
       Date: 2005-04-02 01:00:00+00:00
       Subject: Re: Test bug 2
       Second comment
-  <BLANKLINE>
-  >>> getUtility(IPersonSet).getByEmail('new.user@xxxxxxxxxxxxx')
-  <Person at ...>
+    <BLANKLINE>
+    >>> getUtility(IPersonSet).getByEmail('new.user@xxxxxxxxxxxxx')
+    <Person at ...>
 
 
 Now import an ASSIGNED bug.  Things to note about this import:
@@ -279,44 +284,45 @@
    and CVE to be established.
  * The attachment on this bug is imported
 
-  >>> bug = bz.handleBug(3)
-  >>> bugInfo(bug)
-  Title: Test bug 3
-  Reporter: Sample Person
-  Created: 2005-04-03 00:00:00+00:00
-  Nick: xyz
-  Subscribers:
+    >>> bug = bz.handleBug(3)
+    >>> bugInfo(bug)
+    Title: Test bug 3
+    Reporter: Sample Person
+    Created: 2005-04-03 00:00:00+00:00
+    Nick: xyz
+    Subscribers:
       Foo Bar
       Sample Person
       Ubuntu Team
-  Task: netapplet (Ubuntu)
+    Task: netapplet (Ubuntu)
       Status: CONFIRMED
       Distro: ubuntu
       Source package: netapplet
       Assignee: Foo Bar
       Importance: CRITICAL
-      Explanation: Bugzilla status=ASSIGNED, product=Ubuntu, component=netapplet
-  CVEs:
+      Explanation: Bugzilla status=ASSIGNED, product=Ubuntu,
+        component=netapplet
+    CVEs:
       CVE-2005-1234
-  Messages:
+    Messages:
       Author: Foo Bar
       Date: 2005-04-03 00:00:00+00:00
       Subject: Test bug 3
       First comment
-  <BLANKLINE>
+    <BLANKLINE>
       http://www.ubuntu.com
-  <BLANKLINE>
+    <BLANKLINE>
       Author: Sample Person
       Date: 2005-04-03 01:00:00+00:00
       Subject: Re: Test bug 3
       This is related to CVE-2005-1234
-  <BLANKLINE>
+    <BLANKLINE>
       Author: Foo Bar
       Date: 2005-04-03 02:00:00+00:00
       Subject: Re: Test bug 3
       Created an attachment (id=1)
-  <BLANKLINE>
-  Attachments:
+    <BLANKLINE>
+    Attachments:
       Title: An attachment
       Type: PATCH
       Name: foo.patch
@@ -325,31 +331,32 @@
 
 Next we import a fixed bug:
 
-  >>> bug = bz.handleBug(4)
-  >>> bugInfo(bug)
-  Title: Test bug 4
-  Reporter: Sample Person
-  Created: 2005-04-04 00:00:00+00:00
-  Nick: foobar
-  Subscribers:
+    >>> bug = bz.handleBug(4)
+    >>> bugInfo(bug)
+    Title: Test bug 4
+    Reporter: Sample Person
+    Created: 2005-04-04 00:00:00+00:00
+    Nick: foobar
+    Subscribers:
       Foo Bar
       Sample Person
       Ubuntu Team
-  Task: mozilla-firefox (Ubuntu)
+    Task: mozilla-firefox (Ubuntu)
       Status: FIXRELEASED
       Distro: ubuntu
       Source package: mozilla-firefox
       Assignee: Sample Person
       Importance: CRITICAL
-      Explanation: Bugzilla status=CLOSED FIXED, product=Ubuntu, component=mozilla-firefox
-  Messages:
+      Explanation: Bugzilla status=CLOSED FIXED, product=Ubuntu,
+        component=mozilla-firefox
+    Messages:
       Author: Sample Person
       Date: 2005-04-04 00:00:00+00:00
       Subject: Test bug 4
       First comment
-  <BLANKLINE>
+    <BLANKLINE>
       http://www.ubuntu.com
-  <BLANKLINE>
+    <BLANKLINE>
 
 
 The Ubuntu bugzilla uses the UPSTREAM state to categorise bugs that
@@ -369,40 +376,41 @@
     ...     sourcepackagename=evolution_dsp.sourcepackagename)
     >>> transaction.commit()
 
-  >>> bug = bz.handleBug(5)
-  >>> bugInfo(bug)
-  Title: Test bug 5
-  Reporter: Sample Person
-  Created: 2005-04-04 00:00:00+00:00
-  Subscribers:
+    >>> bug = bz.handleBug(5)
+    >>> bugInfo(bug)
+    Title: Test bug 5
+    Reporter: Sample Person
+    Created: 2005-04-04 00:00:00+00:00
+    Subscribers:
       Sample Person
       Ubuntu Team
-  Task: Evolution
+    Task: Evolution
       Status: NEW
       Product: evolution
       Importance: UNDECIDED
       Watch: http://bugzilla.gnome.org/bugs/show_bug.cgi?id=273041
-  Task: evolution (Ubuntu)
+    Task: evolution (Ubuntu)
       Status: NEW
       Distro: ubuntu
       Source package: evolution
       Assignee: Sample Person
       Importance: CRITICAL
-      Explanation: Bugzilla status=UPSTREAM, product=Ubuntu, component=evolution
-  Task: evolution (Debian)
+      Explanation: Bugzilla status=UPSTREAM, product=Ubuntu,
+        component=evolution
+    Task: evolution (Debian)
       Status: NEW
       Distro: debian
       Source package: evolution
       Importance: UNDECIDED
       Watch: http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1234
-  Messages:
+    Messages:
       Author: Sample Person
       Date: 2005-04-05 00:00:00+00:00
       Subject: Test bug 5
       First comment
-  <BLANKLINE>
-  http://bugzilla.gnome.org/bugs/show_bug.cgi?id=273041
-  <BLANKLINE>
+    <BLANKLINE>
+    http://bugzilla.gnome.org/bugs/show_bug.cgi?id=273041
+    <BLANKLINE>
 
 XXX mpt 20060404: In sampledata Evolution uses Malone officially, so adding
 a watch to its external bug tracker is a bad example.
@@ -413,47 +421,47 @@
 
 Bugzilla severities are mapped to the equivalent Launchpad importance values:
 
-  >>> bug = bugzilla.Bug(bz.backend, 1)
-  >>> class FakeBugTask:
-  ...     def transitionToStatus(self, status, user):
-  ...         self.status = status
-  ...     def transitionToImportance(self, importance, user):
-  ...         self.importance = importance
-  >>> bugtask = FakeBugTask()
-  >>> for severity in ['blocker', 'critical', 'major', 'normal',
-  ...                  'minor', 'trivial', 'enhancement']:
-  ...     bug.bug_severity = severity
-  ...     bug.mapSeverity(bugtask)
-  ...     print '%-11s  %s' % (severity, bugtask.importance.name)
-  blocker      CRITICAL
-  critical     CRITICAL
-  major        HIGH
-  normal       MEDIUM
-  minor        LOW
-  trivial      LOW
-  enhancement  WISHLIST
+    >>> bug = bugzilla.Bug(bz.backend, 1)
+    >>> class FakeBugTask:
+    ...     def transitionToStatus(self, status, user):
+    ...         self.status = status
+    ...     def transitionToImportance(self, importance, user):
+    ...         self.importance = importance
+    >>> bugtask = FakeBugTask()
+    >>> for severity in ['blocker', 'critical', 'major', 'normal',
+    ...                  'minor', 'trivial', 'enhancement']:
+    ...     bug.bug_severity = severity
+    ...     bug.mapSeverity(bugtask)
+    ...     print '%-11s  %s' % (severity, bugtask.importance.name)
+    blocker      CRITICAL
+    critical     CRITICAL
+    major        HIGH
+    normal       MEDIUM
+    minor        LOW
+    trivial      LOW
+    enhancement  WISHLIST
 
 
 Status Mapping
 --------------
 
-  >>> for status in ['UNCONFIRMED', 'NEW', 'ASSIGNED', 'REOPENED',
-  ...                'NEEDINFO', 'UPSTREAM', 'PENDINGUPLOAD',
-  ...                'RESOLVED', 'VERIFIED', 'CLOSED']:
-  ...     bug.bug_status = status
-  ...     bugtask.statusexplanation = ''
-  ...     bug.mapStatus(bugtask)
-  ...     print '%-13s  %s' % (status, bugtask.status.name)
-  UNCONFIRMED    NEW
-  NEW            NEW
-  ASSIGNED       CONFIRMED
-  REOPENED       NEW
-  NEEDINFO       INCOMPLETE
-  UPSTREAM       NEW
-  PENDINGUPLOAD  FIXCOMMITTED
-  RESOLVED       INVALID
-  VERIFIED       INVALID
-  CLOSED         INVALID
+    >>> for status in ['UNCONFIRMED', 'NEW', 'ASSIGNED', 'REOPENED',
+    ...                'NEEDINFO', 'UPSTREAM', 'PENDINGUPLOAD',
+    ...                'RESOLVED', 'VERIFIED', 'CLOSED']:
+    ...     bug.bug_status = status
+    ...     bugtask.statusexplanation = ''
+    ...     bug.mapStatus(bugtask)
+    ...     print '%-13s  %s' % (status, bugtask.status.name)
+    UNCONFIRMED    NEW
+    NEW            NEW
+    ASSIGNED       CONFIRMED
+    REOPENED       NEW
+    NEEDINFO       INCOMPLETE
+    UPSTREAM       NEW
+    PENDINGUPLOAD  FIXCOMMITTED
+    RESOLVED       INVALID
+    VERIFIED       INVALID
+    CLOSED         INVALID
 
 (note that RESOLVED, VERIFIED and CLOSED have been mapped to INVALID
 here because the Bugzilla resolution is set to WONTFIX).
@@ -461,26 +469,26 @@
 
 If the bug has been resolved, the resolution will affect the status:
 
-  >>> bug.priority = 'P2'
-  >>> bug.bug_status = 'RESOLVED'
-  >>> for resolution in ['FIXED', 'INVALID', 'WONTFIX', 'NOTABUG',
-  ...                    'NOTWARTY', 'UNIVERSE', 'LATER', 'REMIND',
-  ...                    'DUPLICATE', 'WORKSFORME', 'MOVED']:
-  ...     bug.resolution = resolution
-  ...     bugtask.statusexplanation = ''
-  ...     bug.mapStatus(bugtask)
-  ...     print '%-10s  %s' % (resolution, bugtask.status.name)
-  FIXED       FIXRELEASED
-  INVALID     INVALID
-  WONTFIX     INVALID
-  NOTABUG     INVALID
-  NOTWARTY    INVALID
-  UNIVERSE    INVALID
-  LATER       INVALID
-  REMIND      INVALID
-  DUPLICATE   INVALID
-  WORKSFORME  INVALID
-  MOVED       INVALID
+    >>> bug.priority = 'P2'
+    >>> bug.bug_status = 'RESOLVED'
+    >>> for resolution in ['FIXED', 'INVALID', 'WONTFIX', 'NOTABUG',
+    ...                    'NOTWARTY', 'UNIVERSE', 'LATER', 'REMIND',
+    ...                    'DUPLICATE', 'WORKSFORME', 'MOVED']:
+    ...     bug.resolution = resolution
+    ...     bugtask.statusexplanation = ''
+    ...     bug.mapStatus(bugtask)
+    ...     print '%-10s  %s' % (resolution, bugtask.status.name)
+    FIXED       FIXRELEASED
+    INVALID     INVALID
+    WONTFIX     INVALID
+    NOTABUG     INVALID
+    NOTWARTY    INVALID
+    UNIVERSE    INVALID
+    LATER       INVALID
+    REMIND      INVALID
+    DUPLICATE   INVALID
+    WORKSFORME  INVALID
+    MOVED       INVALID
 
 
 Bug Target Mapping
@@ -495,36 +503,37 @@
 name, the bug is targeted at that package in ubuntu.  If it isn't,
 then the bug is filed directly against the distribution.
 
-  >>> def showMapping(product, component):
-  ...     bug.product = product
-  ...     bug.component = component
-  ...     target = bz.getLaunchpadBugTarget(bug)
-  ...     distribution = target.get('distribution')
-  ...     if distribution:
-  ...         print 'Distribution:', distribution.name
-  ...     spn = target.get('sourcepackagename')
-  ...     if spn:
-  ...         print 'Source package:', spn.name
-  ...     product = target.get('product')
-  ...     if product:
-  ...         print 'Product:', product.name
-
-  >>> showMapping('Ubuntu', 'mozilla-firefox')
-  Distribution: ubuntu
-  Source package: mozilla-firefox
-
-  >>> showMapping('Ubuntu', 'netapplet')
-  Distribution: ubuntu
-  Source package: netapplet
-
-  >>> showMapping('Ubuntu', 'unknown-package-name')
-  WARNING:lp.bugs.scripts.bugzilla:could not find package name for "unknown-package-name": 'Unknown package: unknown-package-name'
-  Distribution: ubuntu
-
-  >>> showMapping('not-Ubuntu', 'general')
-  Traceback (most recent call last):
+    >>> def showMapping(product, component):
+    ...     bug.product = product
+    ...     bug.component = component
+    ...     target = bz.getLaunchpadBugTarget(bug)
+    ...     distribution = target.get('distribution')
+    ...     if distribution:
+    ...         print 'Distribution:', distribution.name
+    ...     spn = target.get('sourcepackagename')
+    ...     if spn:
+    ...         print 'Source package:', spn.name
+    ...     product = target.get('product')
+    ...     if product:
+    ...         print 'Product:', product.name
+
+    >>> showMapping('Ubuntu', 'mozilla-firefox')
+    Distribution: ubuntu
+    Source package: mozilla-firefox
+
+    >>> showMapping('Ubuntu', 'netapplet')
+    Distribution: ubuntu
+    Source package: netapplet
+
+    >>> showMapping('Ubuntu', 'unknown-package-name')
+    WARNING:lp.bugs.scripts.bugzilla:could not find package name for
+    "unknown-package-name": 'Unknown package: unknown-package-name'
+    Distribution: ubuntu
+
+    >>> showMapping('not-Ubuntu', 'general')
+    Traceback (most recent call last):
     ...
-  AssertionError: product must be Ubuntu
+    AssertionError: product must be Ubuntu
 
 
 Duplicate Bug Handling
@@ -533,21 +542,21 @@
 The Bugzilla duplicate bugs table can be used to mark the
 corresponding Launchpad bugs as duplicates too:
 
-  >>> from lp.testing.faketransaction import FakeTransaction
-  >>> bz.processDuplicates(FakeTransaction())
+    >>> from lp.testing.faketransaction import FakeTransaction
+    >>> bz.processDuplicates(FakeTransaction())
 
 Now check that the bugs have been marked duplicate:
 
-  >>> bug1 = getUtility(IBugSet).queryByRemoteBug(bugtracker, 1)
-  >>> bug2 = getUtility(IBugSet).queryByRemoteBug(bugtracker, 2)
-  >>> bug3 = getUtility(IBugSet).queryByRemoteBug(bugtracker, 3)
-  >>> bug4 = getUtility(IBugSet).queryByRemoteBug(bugtracker, 4)
+    >>> bug1 = getUtility(IBugSet).queryByRemoteBug(bugtracker, 1)
+    >>> bug2 = getUtility(IBugSet).queryByRemoteBug(bugtracker, 2)
+    >>> bug3 = getUtility(IBugSet).queryByRemoteBug(bugtracker, 3)
+    >>> bug4 = getUtility(IBugSet).queryByRemoteBug(bugtracker, 4)
 
-  >>> print bug1.duplicateof
-  None
-  >>> bug2.duplicateof == bug1
-  True
-  >>> bug3.duplicateof == None
-  True
-  >>> bug4.duplicateof == bug3
-  True
+    >>> print bug1.duplicateof
+    None
+    >>> bug2.duplicateof == bug1
+    True
+    >>> bug3.duplicateof == None
+    True
+    >>> bug4.duplicateof == bug3
+    True

=== modified file 'lib/lp/bugs/interfaces/bug.py'
--- lib/lp/bugs/interfaces/bug.py	2011-06-16 13:50:58 +0000
+++ lib/lp/bugs/interfaces/bug.py	2011-06-17 19:56:04 +0000
@@ -98,7 +98,7 @@
 
     def __init__(self, owner, title, comment=None, description=None, msg=None,
                  status=None, datecreated=None, security_related=False,
-                 private=False, subscribers=(), binarypackagename=None,
+                 private=False, subscribers=(),
                  tags=None, subscribe_owner=True, filed_by=None,
                  importance=None, milestone=None, assignee=None):
         self.owner = owner
@@ -114,7 +114,6 @@
         self.product = None
         self.distribution = None
         self.sourcepackagename = None
-        self.binarypackagename = binarypackagename
         self.tags = tags
         self.subscribe_owner = subscribe_owner
         self.filed_by = filed_by
@@ -1138,9 +1137,6 @@
 
           * if either product or distribution is specified, an appropiate
             bug task will be created
-
-          * binarypackagename, if not None, will be added to the bug's
-            description
         """
 
     def createBugWithoutTarget(bug_params):

=== modified file 'lib/lp/bugs/model/bug.py'
--- lib/lp/bugs/model/bug.py	2011-06-16 13:50:58 +0000
+++ lib/lp/bugs/model/bug.py	2011-06-17 19:56:04 +0000
@@ -230,7 +230,7 @@
         bug_params, names=[
             "owner", "title", "comment", "description", "msg",
             "datecreated", "security_related", "private",
-            "distribution", "sourcepackagename", "binarypackagename",
+            "distribution", "sourcepackagename",
             "product", "status", "subscribers", "tags",
             "subscribe_owner", "filed_by", "importance",
             "milestone", "assignee"])
@@ -2537,13 +2537,6 @@
         assert params.comment is None or params.msg is None, (
             "Expected either a comment or a msg, but got both.")
 
-        # Store binary package name in the description, because
-        # storing it as a separate field was a maintenance burden to
-        # developers.
-        if params.binarypackagename:
-            params.comment = "Binary package hint: %s\n\n%s" % (
-                params.binarypackagename.name, params.comment)
-
         # Create the bug comment if one was given.
         if params.comment:
             rfc822msgid = make_msgid('malonedeb')

=== modified file 'lib/lp/bugs/scripts/bugzilla.py'
--- lib/lp/bugs/scripts/bugzilla.py	2011-05-27 21:12:25 +0000
+++ lib/lp/bugs/scripts/bugzilla.py	2011-06-17 19:56:04 +0000
@@ -59,6 +59,7 @@
 
 logger = logging.getLogger('lp.bugs.scripts.bugzilla')
 
+
 def _add_tz(dt):
     """Convert a naiive datetime value to a UTC datetime value."""
     assert dt.tzinfo is None, 'add_tz() only accepts naiive datetime values'
@@ -66,6 +67,7 @@
                              dt.hour, dt.minute, dt.second,
                              dt.microsecond, tzinfo=pytz.timezone('UTC'))
 
+
 class BugzillaBackend:
     """A wrapper for all the MySQL database access.
 
@@ -208,6 +210,7 @@
                             'ORDER BY dupe, dupe_of')
         return [(dupe_of, dupe) for (dupe_of, dupe) in self.cursor.fetchall()]
 
+
 class Bug:
     """Representation of a Bugzilla Bug"""
     def __init__(self, backend, bug_id):
@@ -369,8 +372,8 @@
 
         return person
 
-    def _getPackageNames(self, bug):
-        """Returns the source and binary package names for the given bug."""
+    def _getPackageName(self, bug):
+        """Returns the source package name for the given bug."""
         # we currently only support mapping Ubuntu bugs ...
         if bug.product != 'Ubuntu':
             raise AssertionError('product must be Ubuntu')
@@ -389,19 +392,17 @@
             pkgname = bug.component.encode('ASCII')
 
         try:
-            srcpkg, binpkg = self.ubuntu.guessPackageNames(pkgname)
+            return self.ubuntu.guessPublishedSourcePackageName(pkgname)
         except NotFoundError, e:
             logger.warning('could not find package name for "%s": %s',
                            pkgname, str(e))
-            srcpkg = binpkg = None
-
-        return srcpkg, binpkg
+            return None
 
     def getLaunchpadBugTarget(self, bug):
         """Returns a dictionary of arguments to createBug() that correspond
         to the given bugzilla bug.
         """
-        srcpkg, binpkg = self._getPackageNames(bug)
+        srcpkg = self._getPackageName(bug)
         return {
             'distribution': self.ubuntu,
             'sourcepackagename': srcpkg,
@@ -435,7 +436,7 @@
         This function relies on the package -> product linkage having been
         entered in advance.
         """
-        srcpkgname, binpkgname = self._getPackageNames(bug)
+        srcpkgname = self._getPackageName(bug)
         # find a product series
         series = None
         for series in self.ubuntu.series:
@@ -450,6 +451,7 @@
             return None
 
     _bug_re = re.compile('bug\s*#?\s*(?P<id>\d+)', re.IGNORECASE)
+
     def replaceBugRef(self, match):
         # XXX: jamesh 2005-10-24:
         # this is where bug number rewriting would be plugged in
@@ -457,7 +459,6 @@
         url = '%s/%d' % (canonical_url(self.bugtracker), bug_id)
         return '%s [%s]' % (match.group(0), url)
 
-
     def handleBug(self, bug_id):
         """Maybe import a single bug.
 
@@ -617,8 +618,10 @@
          * bug A' is a duplicate of bug B'
          * bug A is not currently a duplicate of any other bug.
         """
+
         logger.info('Processing duplicate bugs')
         bugmap = {}
+
         def getlpbug(bugid):
             """Get the Launchpad bug corresponding to the given remote ID
 

=== modified file 'lib/lp/bugs/stories/guided-filebug/xx-bug-reporting-tools.txt'
--- lib/lp/bugs/stories/guided-filebug/xx-bug-reporting-tools.txt	2011-04-20 12:59:55 +0000
+++ lib/lp/bugs/stories/guided-filebug/xx-bug-reporting-tools.txt	2011-06-17 19:56:04 +0000
@@ -1,4 +1,5 @@
-= Bug Reporting Tools =
+Bug Reporting Tools
+===================
 
 In order to produce better bug reports, a bug reporting tool on the
 user's computer can upload a message containing extra information about
@@ -54,7 +55,8 @@
     ...     job.job.complete()
     ...     logout()
 
-== Guided +filebug ==
+Guided +filebug
+===============
 
 The most common case will be that the user is sent to the guided
 +filebug page and the user goes through the workflow there.
@@ -105,7 +107,8 @@
     >>> user_browser.getControl('Further information').value
     ''
     >>> user_browser.getControl('Submit Bug Report').click()
-    >>> for error in find_tags_by_class(user_browser.contents, 'message error'):
+    >>> for error in find_tags_by_class(
+    ...     user_browser.contents, 'message error'):
     ...     print error.renderContents()
     There is 1 error.
 
@@ -121,12 +124,6 @@
     >>> user_browser.url
     'http://bugs.launchpad.dev/ubuntu/+source/mozilla-firefox/+bug/...'
 
-Some extra text was appended to the description.
-
-    >>> find_tag_by_id(
-    ...     user_browser.contents, 'edit-description').renderContents()
-    '...<p>Binary package hint: mozilla-firefox</p>\n<p>A bug description...'
-
 Two attachments were added.
 
     >>> attachment_portlet = find_portlet(
@@ -149,7 +146,8 @@
     ----------------------------------------
 
 
-=== Initial bug summary ===
+Initial bug summary
+-------------------
 
 If the uploaded message contains a Subject field in the initial headers,
 that will be used to automatically fill in a suggested title.
@@ -177,7 +175,8 @@
     >>> user_browser.getControl('Summary', index=0).value
     'Another summary'
 
-=== Tags ===
+Tags
+----
 
 If the uploaded message contains a Tags field, the tags widget will be
 initialized with that value.
@@ -234,7 +233,8 @@
     Tags: bar foo...
 
 
-=== References to HWDB submissions ===
+References to HWDB submissions
+------------------------------
 
 The uploaded message may contain a header "HWDB-Submission", its value
 should be a sequence of HWDB submission keys, separated by ', *'.

=== modified file 'lib/lp/bugs/xmlrpc/bug.py'
--- lib/lp/bugs/xmlrpc/bug.py	2010-08-20 20:31:18 +0000
+++ lib/lp/bugs/xmlrpc/bug.py	2011-06-17 19:56:04 +0000
@@ -70,7 +70,8 @@
 
             if package:
                 try:
-                    spname, bpname = distro_object.guessPackageNames(package)
+                    spname = distro_object.guessPublishedSourcePackageName(
+                        package)
                 except NotFoundError:
                     return faults.NoSuchPackage(package)
 

=== modified file 'lib/lp/registry/interfaces/distribution.py'
--- lib/lp/registry/interfaces/distribution.py	2011-06-03 09:31:08 +0000
+++ lib/lp/registry/interfaces/distribution.py	2011-06-17 19:56:04 +0000
@@ -567,14 +567,21 @@
         Raises NotFoundError if it fails to find the named file.
         """
 
-    def guessPackageNames(pkgname):
-        """Try and locate source and binary package name objects that
-        are related to the provided name --  which could be either a
-        source or a binary package name. Returns a tuple of
-        (sourcepackagename, binarypackagename) based on the current
-        publishing status of these binary / source packages. Raises
-        NotFoundError if it fails to find any package published with
-        that name in the distribution.
+    def guessPublishedSourcePackageName(pkgname):
+        """Return the "published" SourcePackageName related to pkgname.
+
+        If pkgname corresponds to a source package that was published in
+        any of the distribution series, that's the SourcePackageName that is
+        returned.
+
+        If there is any official source package branch linked, then that
+        source package name is returned.
+
+        Otherwise, try to find a published binary package name and then return
+        the source package name from which it comes from.
+
+        :raises NotFoundError: when pkgname doesn't correspond to either a
+            published source or binary package name in this distribution.
         """
 
     def getAllPPAs():

=== modified file 'lib/lp/registry/model/distribution.py'
--- lib/lp/registry/model/distribution.py	2011-06-08 23:31:41 +0000
+++ lib/lp/registry/model/distribution.py	2011-06-17 19:56:04 +0000
@@ -119,10 +119,12 @@
     HasBugHeatMixin,
     OfficialBugTagTargetMixin,
     )
-from lp.bugs.model.bugtask import BugTask
 from lp.bugs.model.structuralsubscription import (
     StructuralSubscriptionTargetMixin,
     )
+from lp.code.interfaces.seriessourcepackagebranch import (
+    IFindOfficialBranchLinks,
+    )
 from lp.registry.errors import NoSuchDistroSeries
 from lp.registry.interfaces.distribution import (
     IBaseDistribution,
@@ -649,7 +651,8 @@
         """See `IBugTarget`."""
         return get_bug_tags("BugTask.distribution = %s" % sqlvalues(self))
 
-    def getUsedBugTagsWithOpenCounts(self, user, tag_limit=0, include_tags=None):
+    def getUsedBugTagsWithOpenCounts(self, user, tag_limit=0,
+                                     include_tags=None):
         """See IBugTarget."""
         # Circular fail.
         from lp.bugs.model.bugsummary import BugSummary
@@ -1370,7 +1373,7 @@
         # results will only see DSPCs
         return DecoratedResultSet(results, result_to_dspc)
 
-    def guessPackageNames(self, pkgname):
+    def guessPublishedSourcePackageName(self, pkgname):
         """See `IDistribution`"""
         assert isinstance(pkgname, basestring), (
             "Expected string. Got: %r" % pkgname)
@@ -1386,78 +1389,34 @@
                                 'published in it'
                                 % (self.displayname, pkgname))
 
-        # The way this method works is that is tries to locate a pair
-        # of packages related to that name. If it locates a source
-        # package it then tries to see if it has been published at any
-        # point, and gets the binary package from the publishing
-        # record.
-        #
-        # If that fails (no source package by that name, or not
-        # published) then it'll search binary packages, then find the
-        # source package most recently associated with it, first in
-        # the current distroseries and then across the whole
-        # distribution.
-        #
-        # XXX kiko 2006-07-28:
-        # Note that the strategy of falling back to previous
-        # distribution series might be revisited in the future; for
-        # instance, when people file bugs, it might actually be bad for
-        # us to allow them to be associated with obsolete packages.
-
-        bpph_location_clauses = [
-            DistroSeries.distribution == self,
-            DistroArchSeries.distroseriesID == DistroSeries.id,
-            BinaryPackagePublishingHistory.distroarchseriesID ==
-                DistroArchSeries.id,
-            BinaryPackagePublishingHistory.archiveID.is_in(
-                self.all_distro_archive_ids),
-            BinaryPackagePublishingHistory.dateremoved == None,
-            BinaryPackageRelease.id ==
-                BinaryPackagePublishingHistory.binarypackagereleaseID,
-            ]
-
         sourcepackagename = SourcePackageName.selectOneBy(name=pkgname)
         if sourcepackagename:
             # Note that in the source package case, we don't restrict
             # the search to the distribution release, making a best
             # effort to find a package.
-            publishing = SourcePackagePublishingHistory.selectFirst('''
-                SourcePackagePublishingHistory.distroseries =
-                    DistroSeries.id AND
-                DistroSeries.distribution = %s AND
-                SourcePackagePublishingHistory.archive IN %s AND
-                SourcePackagePublishingHistory.sourcepackagerelease =
-                    SourcePackageRelease.id AND
-                SourcePackageRelease.sourcepackagename = %s AND
-                SourcePackagePublishingHistory.status IN %s
-                ''' % sqlvalues(self,
-                                self.all_distro_archive_ids,
-                                sourcepackagename,
-                                (PackagePublishingStatus.PUBLISHED,
-                                 PackagePublishingStatus.PENDING)),
-                clauseTables=['SourcePackageRelease', 'DistroSeries'],
-                distinct=True,
-                orderBy="id")
+            publishing = IStore(SourcePackagePublishingHistory).find(
+                SourcePackagePublishingHistory,
+                SourcePackagePublishingHistory.archiveID == Archive.id,
+                Archive.distribution == self,
+                Archive.purpose.is_in(MAIN_ARCHIVE_PURPOSES),
+                SourcePackagePublishingHistory.sourcepackagereleaseID ==
+                    SourcePackageRelease.id,
+                SourcePackageRelease.sourcepackagename == sourcepackagename,
+                SourcePackagePublishingHistory.status.is_in(
+                    (PackagePublishingStatus.PUBLISHED,
+                     PackagePublishingStatus.PENDING)
+                    )).order_by(
+                        Desc(SourcePackagePublishingHistory.id)).first()
             if publishing is not None:
-                # Attempt to find a published binary package of the
-                # same name.
-                bpph = IStore(BinaryPackagePublishingHistory).find(
-                    BinaryPackagePublishingHistory,
-                    BinaryPackageRelease.binarypackagename ==
-                        BinaryPackageName.id,
-                    BinaryPackageName.name == sourcepackagename.name,
-                    BinaryPackageBuild.id == BinaryPackageRelease.buildID,
-                    SourcePackageRelease.id ==
-                        BinaryPackageBuild.source_package_release_id,
-                    SourcePackageRelease.sourcepackagename ==
-                        sourcepackagename,
-                    *bpph_location_clauses).any()
-                if bpph is not None:
-                    bpr = bpph.binarypackagerelease
-                    return (sourcepackagename, bpr.binarypackagename)
-                # No binary with a similar name, so just return None
-                # rather than returning some arbitrary binary package.
-                return (sourcepackagename, None)
+                return sourcepackagename
+
+            # Look to see if there is an official source package branch.
+            # That's considered "published" enough.
+            branch_links = getUtility(IFindOfficialBranchLinks)
+            results = branch_links.findForDistributionSourcePackage(
+                self.getSourcePackage(sourcepackagename))
+            if results.any() is not None:
+                return sourcepackagename
 
         # At this point we don't have a published source package by
         # that name, so let's try to find a binary package and work
@@ -1470,12 +1429,18 @@
             # the sourcepackagename from that.
             bpph = IStore(BinaryPackagePublishingHistory).find(
                 BinaryPackagePublishingHistory,
+                BinaryPackagePublishingHistory.archiveID == Archive.id,
+                Archive.distribution == self,
+                Archive.purpose.is_in(MAIN_ARCHIVE_PURPOSES),
+                BinaryPackagePublishingHistory.binarypackagereleaseID ==
+                    BinaryPackageRelease.id,
                 BinaryPackageRelease.binarypackagename == binarypackagename,
-                *bpph_location_clauses).order_by(
+                BinaryPackagePublishingHistory.dateremoved == None,
+                ).order_by(
                     Desc(BinaryPackagePublishingHistory.id)).first()
             if bpph is not None:
                 spr = bpph.binarypackagerelease.build.source_package_release
-                return (spr.sourcepackagename, binarypackagename)
+                return spr.sourcepackagename
 
         # We got nothing so signal an error.
         if sourcepackagename is None:

=== modified file 'lib/lp/registry/tests/test_distribution.py'
--- lib/lp/registry/tests/test_distribution.py	2011-05-14 15:02:13 +0000
+++ lib/lp/registry/tests/test_distribution.py	2011-06-17 19:56:04 +0000
@@ -7,6 +7,7 @@
 
 from lazr.lifecycle.snapshot import Snapshot
 import soupmatchers
+from testtools import ExpectedException
 from testtools.matchers import (
     MatchesAny,
     Not,
@@ -14,11 +15,13 @@
 from zope.component import getUtility
 from zope.security.proxy import removeSecurityProxy
 
+from canonical.database.constants import UTC_NOW
 from canonical.launchpad.webapp import canonical_url
 from canonical.testing.layers import (
     DatabaseFunctionalLayer,
     LaunchpadFunctionalLayer,
     )
+from lp.app.errors import NotFoundError
 from lp.registry.errors import NoSuchDistroSeries
 from lp.registry.interfaces.distribution import IDistribution
 from lp.registry.interfaces.person import IPersonSet
@@ -41,9 +44,6 @@
 
     layer = DatabaseFunctionalLayer
 
-    def setUp(self):
-        super(TestDistribution, self).setUp('foo.bar@xxxxxxxxxxxxx')
-
     def test_distribution_repr_ansii(self):
         # Verify that ANSI displayname is ascii safe.
         distro = self.factory.makeDistribution(
@@ -59,6 +59,127 @@
         ignore, displayname, name = repr(distro).rsplit(' ', 2)
         self.assertEqual("'\\u0170-distro'", displayname)
 
+    def test_guessPublishedSourcePackageName_no_distro_series(self):
+        # Distribution without a series raises NotFoundError
+        distro = self.factory.makeDistribution()
+        with ExpectedException(NotFoundError, '.*has no series.*'):
+            distro.guessPublishedSourcePackageName('package')
+
+    def test_guessPublishedSourcePackageName_invalid_name(self):
+        # Invalid name raises a NotFoundError
+        distro = self.factory.makeDistribution()
+        with ExpectedException(NotFoundError, "'Invalid package name.*"):
+            distro.guessPublishedSourcePackageName('a*package')
+
+    def test_guessPublishedSourcePackageName_nothing_published(self):
+        distroseries = self.factory.makeDistroSeries()
+        with ExpectedException(NotFoundError, "'Unknown package:.*"):
+            distroseries.distribution.guessPublishedSourcePackageName(
+                'a-package')
+
+    def test_guessPublishedSourcePackageName_ignored_removed(self):
+        # Removed binary package are ignored.
+        distroseries = self.factory.makeDistroSeries()
+        self.factory.makeBinaryPackagePublishingHistory(
+            archive=distroseries.main_archive,
+            binarypackagename='binary-package', dateremoved=UTC_NOW)
+        with ExpectedException(NotFoundError, ".*Binary package.*"):
+            distroseries.distribution.guessPublishedSourcePackageName(
+                'binary-package')
+
+    def test_guessPublishedSourcePackageName_sourcepackage_name(self):
+        distroseries = self.factory.makeDistroSeries()
+        spph = self.factory.makeSourcePackagePublishingHistory(
+            distroseries=distroseries, sourcepackagename='my-package')
+        self.assertEquals(
+            spph.sourcepackagerelease.sourcepackagename,
+            distroseries.distribution.guessPublishedSourcePackageName(
+                'my-package'))
+
+    def test_guessPublishedSourcePackageName_binarypackage_name(self):
+        distroseries = self.factory.makeDistroSeries()
+        spph = self.factory.makeSourcePackagePublishingHistory(
+            distroseries=distroseries, sourcepackagename='my-package')
+        self.factory.makeBinaryPackagePublishingHistory(
+            archive=distroseries.main_archive,
+            binarypackagename='binary-package',
+            source_package_release=spph.sourcepackagerelease)
+        self.assertEquals(
+            spph.sourcepackagerelease.sourcepackagename,
+            distroseries.distribution.guessPublishedSourcePackageName(
+                'binary-package'))
+
+    def test_guessPublishedSourcePackageName_exlude_ppa(self):
+        # Package published in PPAs are not considered to be part of the
+        # distribution.
+        distroseries = self.factory.makeUbuntuDistroSeries()
+        ppa_archive = self.factory.makeArchive()
+        self.factory.makeSourcePackagePublishingHistory(
+            distroseries=distroseries, sourcepackagename='my-package',
+            archive=ppa_archive)
+        with ExpectedException(NotFoundError, ".*not published in.*"):
+            distroseries.distribution.guessPublishedSourcePackageName(
+                'my-package')
+
+    def test_guessPublishedSourcePackageName_exlude_other_distro(self):
+        # Published source package are only found in the distro
+        # in which they were published.
+        distroseries1 = self.factory.makeDistroSeries()
+        distroseries2 = self.factory.makeDistroSeries()
+        spph = self.factory.makeSourcePackagePublishingHistory(
+            distroseries=distroseries1, sourcepackagename='my-package')
+        self.assertEquals(
+            spph.sourcepackagerelease.sourcepackagename,
+            distroseries1.distribution.guessPublishedSourcePackageName(
+                'my-package'))
+        with ExpectedException(NotFoundError, ".*not published in.*"):
+            distroseries2.distribution.guessPublishedSourcePackageName(
+                'my-package')
+
+    def test_guessPublishedSourcePackageName_looks_for_source_first(self):
+        # If both a binary and source package name shares the same name,
+        # the source package will be returned (and the one from the unrelated
+        # binary).
+        distroseries = self.factory.makeDistroSeries()
+        my_spph = self.factory.makeSourcePackagePublishingHistory(
+            distroseries=distroseries, sourcepackagename='my-package')
+        self.factory.makeBinaryPackagePublishingHistory(
+            archive=distroseries.main_archive,
+            binarypackagename='my-package', sourcepackagename='other-package')
+        self.assertEquals(
+            my_spph.sourcepackagerelease.sourcepackagename,
+            distroseries.distribution.guessPublishedSourcePackageName(
+                'my-package'))
+
+    def test_guessPublishedSourcePackageName_uses_latest(self):
+        # If multiple binaries match, it will return the source of the latest
+        # one published.
+        distroseries = self.factory.makeDistroSeries()
+        self.factory.makeBinaryPackagePublishingHistory(
+            archive=distroseries.main_archive,
+            sourcepackagename='old-source-name',
+            binarypackagename='my-package')
+        self.factory.makeBinaryPackagePublishingHistory(
+            archive=distroseries.main_archive,
+            sourcepackagename='new-source-name',
+            binarypackagename='my-package')
+        self.assertEquals(
+            'new-source-name',
+            distroseries.distribution.guessPublishedSourcePackageName(
+                'my-package').name)
+
+    def test_guessPublishedSourcePackageName_official_package_branch(self):
+        # It consider that a sourcepackage that has an official package
+        # branch is published.
+        sourcepackage = self.factory.makeSourcePackage(
+            sourcepackagename='my-package')
+        self.factory.makeRelatedBranchesForSourcePackage(
+            sourcepackage=sourcepackage)
+        self.assertEquals(
+            'my-package',
+            sourcepackage.distribution.guessPublishedSourcePackageName(
+                'my-package').name)
+
 
 class TestDistributionCurrentSourceReleases(
     TestDistroSeriesCurrentSourceReleases):

=== modified file 'lib/lp/soyuz/doc/distribution.txt'
--- lib/lp/soyuz/doc/distribution.txt	2011-05-27 19:53:20 +0000
+++ lib/lp/soyuz/doc/distribution.txt	2011-06-17 19:56:04 +0000
@@ -5,62 +5,17 @@
 objects for the distribution.
 
 
-Guessing package names
-----------------------
-
-IDistribution allows us to retrieve packages by name, returning a tuple
-of Source/BinaryPackageName instances published within this
-distribution:
-
     >>> from lp.registry.interfaces.distribution import IDistributionSet
     >>> from lp.registry.interfaces.pocket import PackagePublishingPocket
-    >>> from lp.registry.interfaces.sourcepackagename import ISourcePackageName
     >>> from lp.soyuz.enums import PackagePublishingStatus
-    >>> from lp.soyuz.interfaces.binarypackagename import IBinaryPackageName
-
-    >>> distroset = getUtility(IDistributionSet)
-    >>> gentoo = distroset.getByName("gentoo")
-    >>> ubuntu = distroset.get(1)
-
-    >>> source_name, bin_name = ubuntu.guessPackageNames('pmount')
-    >>> ISourcePackageName.providedBy(source_name)
-    True
-
-    >>> IBinaryPackageName.providedBy(bin_name)
-    True
-
-    >>> source_name.name, bin_name.name
-    (u'pmount', u'pmount')
-
-Prevents wrong usage by and assertion error:
-
-    >>> name_tuple = ubuntu.guessPackageNames(ubuntu)
-    Traceback (most recent call last):
-    ...
-    AssertionError: Expected string. Got: <Distribution ...>
-
-Raises NotFoundError for following conditions:
-
-    >>> name_tuple = ubuntu.guessPackageNames('@#$')
-    Traceback (most recent call last):
-    ...
-    NotFoundError: 'Invalid package name: @#$'
-
-    >>> name_tuple = ubuntu.guessPackageNames('zeca')
-    Traceback (most recent call last):
-    ...
-    NotFoundError: 'Unknown package: zeca'
-
-    >>> name_tuple = ubuntu.guessPackageNames('1234')
-    Traceback (most recent call last):
-    ...
-    NotFoundError: 'Unknown package: 1234'
-
-Packages only published in PPAs will not be found in the Ubuntu archive.
-Here, 'at' is published in mark's PPA only:
+
+    >>> ubuntu = getUtility(IDistributionSet).getByName('ubuntu')
+    >>> debian = getUtility(IDistributionSet).getByName('debian')
+
+(Create some data that is depended upon by later tests. It was part of a
+test "narrative" that was converted to unit tests.... for obvious reasons.)
 
     >>> from lp.soyuz.tests.ppa import publishToPPA
-    >>> ubuntutest = distroset.getByName("ubuntutest")
     >>> publishToPPA(
     ...     person_name='cprov',
     ...     sourcepackage_name='at', sourcepackage_version='0.00',
@@ -68,131 +23,6 @@
     ...     distribution_name='ubuntutest',
     ...     distroseries_name='hoary-test',
     ...     publishing_status=PackagePublishingStatus.PUBLISHED)
-    >>> name_tuple = ubuntutest.guessPackageNames('at')
-    Traceback (most recent call last):
-    ...
-    NotFoundError: u'Package at not published in ubuntutest'
-
-It also raises NotFoundError on distributions with no series:
-
-    >>> source_name, bin_name = gentoo.guessPackageNames('pmount')
-    Traceback (most recent call last):
-    ...
-    NotFoundError: u"Gentoo has no series; 'pmount' was never published in it"
-
-A distroseries can only be created by the distro owner or the admin
-team.
-
-    >>> gentoo.newSeries('gentoo-two', 'Gentoo Two',
-    ...                  'Gentoo Two Dot Oh', 'Gentoo 2', 'G2',
-    ...                  '2.0', None, gentoo.owner)
-    Traceback (most recent call last):
-    ...
-    Unauthorized: (<Distribution...>, 'newSeries', 'launchpad.Moderate')
-
-    >>> login('mark@xxxxxxxxxxx')
-    >>> from lp.registry.interfaces.distroseries import IDistroSeriesSet
-    >>> distroseriesset = getUtility(IDistroSeriesSet)
-    >>> gentoo_two = gentoo.newSeries('gentoo-two', 'Gentoo Two',
-    ...                               'Gentoo Two Dot Oh', 'Gentoo 2', 'G2',
-    ...                               '2.0', None, gentoo.owner)
-
-    # Reverting the logged in user.
-
-    >>> login(ANONYMOUS)
-
-Even if we add a series to Gentoo, no packages have ever been published
-in it, and therefore guessPackageNames will still fail:
-
-    >>> source_name, bin_name = gentoo.guessPackageNames('pmount')
-    Traceback (most recent call last):
-    ...
-    NotFoundError: u'Package pmount not published in Gentoo'
-
-It will find packages that are at the PENDING publishing state in
-addition to PUBLISHED ones:
-
-    >>> login("admin@xxxxxxxxxxxxx")
-    >>> from lp.soyuz.tests.test_publishing import (
-    ...     SoyuzTestPublisher)
-    >>> test_publisher = SoyuzTestPublisher()
-    >>> ignore = test_publisher.setUpDefaultDistroSeries(
-    ...     ubuntu['breezy-autotest'])
-    >>> ignore = test_publisher.getPubSource(
-    ...     sourcename="pendingpackage",
-    ...     status=PackagePublishingStatus.PENDING)
-    >>> login(ANONYMOUS)
-    >>> (source, binary) = ubuntu.guessPackageNames("pendingpackage")
-    >>> print source.name
-    pendingpackage
-
-It also works if we look for a package name which is the name of both
-binary and source packages but for which only the source is published:
-
-    >>> from lp.app.interfaces.launchpad import ILaunchpadCelebrities
-    >>> debian = getUtility(ILaunchpadCelebrities).debian
-    >>> source_name, bin_name = debian.guessPackageNames('alsa-utils')
-    >>> print bin_name
-    None
-
-    >>> source_name.name
-    u'alsa-utils'
-
-It's possible for a binary package to have the same name as a source
-package, yet not be derived from that source package. In this case, we
-want to prefer the source package with that name.
-
-First, we need a function to help testing:
-
-    >>> def print_guessed_names(package_name):
-    ...     source, binary = ubuntu.guessPackageNames(package_name)
-    ...     print "source: %r" % source.name
-    ...     print "binary: %r" % getattr(binary, 'name', None)
-
-Note that source packages can produces lots of differently named binary
-packages so only return a match if it's got the same name as the source
-package rather than returning an arbitrary binary package:
-
-Both iceweasel and mozilla-firefox source packages produce mozilla-
-firefox binary packages.
-
-    >>> print_guessed_names('mozilla-firefox')
-    source: u'mozilla-firefox'
-    binary: u'mozilla-firefox'
-
-    >>> print_guessed_names('iceweasel')
-    source: u'iceweasel'
-    binary: None
-
-If we don't get a hit on the source package we search binary packages.
-Because there is a many to one relationship from binary packages to
-source packages we can always return a source package name even if it
-differs:
-
-    >>> print_guessed_names('linux-2.6.12')
-    source: u'linux-source-2.6.15'
-    binary: u'linux-2.6.12'
-
-If there are multiple matching binary packages, the source of the latest
-publication is used. If we create a new 'linux' source with a 'linux-2.6.12'
-binary, 'linux' will be returned instead of 'linux-source-2.6.15'.
-
-    >>> from lp.soyuz.tests.test_publishing import (
-    ...     SoyuzTestPublisher)
-    >>> test_publisher = SoyuzTestPublisher()
-    >>> hoary = test_publisher.setUpDefaultDistroSeries(
-    ...     ubuntu.getSeries('hoary'))
-    >>> fake_chroot = test_publisher.addMockFile('fake_chroot.tar.gz')
-    >>> unused = hoary['i386'].addOrUpdateChroot(fake_chroot)
-    >>> login('admin@xxxxxxxxxxxxx')
-    >>> test_publisher.getPubBinaries(
-    ...     'linux-2.6.12', architecturespecific=True)
-    [<BinaryPackagePublishingHistory ...>]
-    >>> login(ANONYMOUS)
-
-    >>> print_guessed_names('linux-2.6.12')
-    source: u'linux'
-    binary: u'linux-2.6.12'
 
 
 Handling Personal Package Archives

=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py	2011-06-14 08:25:41 +0000
+++ lib/lp/testing/factory.py	2011-06-17 19:56:04 +0000
@@ -3486,8 +3486,10 @@
                 distribution=distroseries.distribution,
                 purpose=ArchivePurpose.PRIMARY)
 
-        if sourcepackagename is None:
-            sourcepackagename = self.makeSourcePackageName()
+        if (sourcepackagename is None or
+            isinstance(sourcepackagename, basestring)):
+            sourcepackagename = self.getOrMakeSourcePackageName(
+                sourcepackagename)
 
         if component is None:
             component = self.makeComponent()
@@ -3554,13 +3556,16 @@
 
     def makeBinaryPackageBuild(self, source_package_release=None,
             distroarchseries=None, archive=None, builder=None,
-            status=None, pocket=None, date_created=None, processor=None):
+            status=None, pocket=None, date_created=None, processor=None,
+            sourcepackagename=None):
         """Create a BinaryPackageBuild.
 
         If archive is not supplied, the source_package_release is used
         to determine archive.
         :param source_package_release: The SourcePackageRelease this binary
             build uses as its source.
+        :param sourcepackagename: when source_package_release is None, the
+            sourcepackagename from which the build will come.
         :param distroarchseries: The DistroArchSeries to use.
         :param archive: The Archive to use.
         :param builder: An optional builder to assign.
@@ -3587,7 +3592,8 @@
             multiverse = self.makeComponent(name='multiverse')
             source_package_release = self.makeSourcePackageRelease(
                 archive, component=multiverse,
-                distroseries=distroarchseries.distroseries)
+                distroseries=distroarchseries.distroseries,
+                sourcepackagename=sourcepackagename)
             self.makeSourcePackagePublishingHistory(
                 distroseries=source_package_release.upload_distroseries,
                 archive=archive, sourcepackagerelease=source_package_release,
@@ -3685,12 +3691,15 @@
         return spph
 
     def makeBinaryPackagePublishingHistory(self, binarypackagerelease=None,
+                                           binarypackagename=None,
                                            distroarchseries=None,
                                            component=None, section_name=None,
                                            priority=None, status=None,
                                            scheduleddeletiondate=None,
                                            dateremoved=None,
-                                           pocket=None, archive=None):
+                                           pocket=None, archive=None,
+                                           source_package_release=None,
+                                           sourcepackagename=None):
         """Make a `BinaryPackagePublishingHistory`."""
         if distroarchseries is None:
             if archive is None:
@@ -3720,8 +3729,10 @@
             # in the same archive and suite.
             binarypackagebuild = self.makeBinaryPackageBuild(
                 archive=archive, distroarchseries=distroarchseries,
-                pocket=pocket)
+                pocket=pocket, source_package_release=source_package_release,
+                sourcepackagename=sourcepackagename)
             binarypackagerelease = self.makeBinaryPackageRelease(
+                binarypackagename=binarypackagename,
                 build=binarypackagebuild,
                 component=component,
                 section_name=section_name,
@@ -3785,8 +3796,10 @@
         """Make a `BinaryPackageRelease`."""
         if build is None:
             build = self.makeBinaryPackageBuild()
-        if binarypackagename is None:
-            binarypackagename = self.makeBinaryPackageName()
+        if (binarypackagename is None or
+            isinstance(binarypackagename, basestring)):
+            binarypackagename = self.getOrMakeBinaryPackageName(
+                binarypackagename)
         if version is None:
             version = build.source_package_release.version
         if binpackageformat is None: