← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~benji/launchpad/fix-help-link-2 into lp:launchpad/db-devel

 

Benji York has proposed merging lp:~benji/launchpad/fix-help-link-2 into lp:launchpad/db-devel.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~benji/launchpad/fix-help-link-2/+merge/58521

This branch fixes a small bug: the help link next to the "Stop my emails
from this subscription" link wasn't appearing on Chrome. I'm still not
sure why not, but the work-around was simple enough.
-- 
https://code.launchpad.net/~benji/launchpad/fix-help-link-2/+merge/58521
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~benji/launchpad/fix-help-link-2 into lp:launchpad/db-devel.
=== modified file 'lib/canonical/configure.zcml'
--- lib/canonical/configure.zcml	2011-04-19 14:13:21 +0000
+++ lib/canonical/configure.zcml	2011-04-20 15:32:20 +0000
@@ -134,4 +134,7 @@
 
     <include package="canonical.launchpad" />
     <include package="canonical.lazr" />
+    <include zcml:condition="installed canonical.shipit"
+             package="canonical.shipit" />
+
 </configure>

=== modified file 'lib/canonical/launchpad/webapp/error.py'
--- lib/canonical/launchpad/webapp/error.py	2011-04-19 04:17:54 +0000
+++ lib/canonical/launchpad/webapp/error.py	2011-04-20 15:32:20 +0000
@@ -241,6 +241,10 @@
         # is really just a guess and I don't think any clients actually
         # pay attention to it - it is just a hint.
         request.response.setHeader('Retry-After', 900)
+        # Reset the timeout timer, so that we can issue db queries when
+        # rendering the page.
+        clear_request_started()
+        set_request_started()
 
 
 class InvalidBatchSizeView(SystemErrorView):

=== added file 'lib/canonical/launchpad/webapp/tests/test_request_expire_render.py'
--- lib/canonical/launchpad/webapp/tests/test_request_expire_render.py	1970-01-01 00:00:00 +0000
+++ lib/canonical/launchpad/webapp/tests/test_request_expire_render.py	2011-04-20 15:32:20 +0000
@@ -0,0 +1,27 @@
+# Copyright 2009 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Test for rendering the time out OOPS page."""
+
+__metaclass__ = type
+
+__all__ = [
+    'test_suite',
+    ]
+
+
+from canonical.launchpad.testing.systemdocs import (
+    LayeredDocFileSuite,
+    setUp,
+    tearDown,
+    )
+from canonical.testing.layers import LaunchpadFunctionalLayer
+
+
+def test_suite():
+    suite = LayeredDocFileSuite(
+            'test_request_expire_render.txt',
+            layer=LaunchpadFunctionalLayer, setUp=setUp, tearDown=tearDown,
+            )
+    return suite
+

=== added file 'lib/canonical/launchpad/webapp/tests/test_request_expire_render.txt'
--- lib/canonical/launchpad/webapp/tests/test_request_expire_render.txt	1970-01-01 00:00:00 +0000
+++ lib/canonical/launchpad/webapp/tests/test_request_expire_render.txt	2011-04-20 15:32:20 +0000
@@ -0,0 +1,55 @@
+= Rendering timeout exceptions =
+
+When a page takes too long to render, a timeout exception is raised. A
+timeout exception should provide IRequestExpired.
+
+    >>> from zope.interface import implements
+    >>> from canonical.database.interfaces import IRequestExpired
+    >>> class TimeoutException:
+    ...     implements(IRequestExpired)
+
+After a timeout has happened, we can no longer do any more db queries,
+since get_request_remaining_seconds will raise. However, if the user is logged
+in, a query will be issued to render the "Logged in as No Privileges Person".
+
+    >>> from canonical.config import config
+    >>> from textwrap import dedent
+    >>> test_data = dedent("""
+    ...     [database]
+    ...     db_statement_timeout: 10000
+    ...     """)
+    >>> config.push('test_data', test_data)
+
+    >>> from time import time
+    >>> from canonical.launchpad.webapp.adapter import (
+    ...     get_request_remaining_seconds, set_request_started)
+    >>> login('no-priv@xxxxxxxxxxxxx')
+    >>> too_early = time() - (config.database.db_statement_timeout / 1000 + 1)
+    >>> set_request_started(too_early)
+    >>> get_request_remaining_seconds()
+    Traceback (most recent call last):
+    ...
+    RequestExpired: request expired.
+
+Before the OOPS page is rendered, the timeout is reset, so that we can
+issue the DB query and render the page, showing the OOPS id to the user.
+
+    >>> from canonical.launchpad.webapp.interaction import (
+    ...     get_current_principal)
+    >>> from canonical.launchpad.webapp.servers import LaunchpadTestRequest
+    >>> request = LaunchpadTestRequest(environ={'PATH_INFO': '/'})
+    >>> request.oopsid = 'OOPS_ID_MARKER'
+    >>> request.setPrincipal(get_current_principal())
+
+    >>> from zope.component import getMultiAdapter
+    >>> timeout_exception = TimeoutException()
+    >>> exception_view = getMultiAdapter(
+    ...     (timeout_exception, request), name="index.html")
+    >>> get_request_remaining_seconds() > 0
+    True
+    >>> print exception_view()
+    <...OOPS_ID_MARKER...
+
+    >>> test_config_data = config.pop('test_data')
+    >>> from canonical.launchpad.webapp.adapter import clear_request_started
+    >>> clear_request_started()

=== added symlink 'lib/canonical/shipit'
=== target is u'../../sourcecode/shipit'
=== modified file 'lib/lp/app/javascript/picker.js'
--- lib/lp/app/javascript/picker.js	2011-04-20 06:29:11 +0000
+++ lib/lp/app/javascript/picker.js	2011-04-20 15:32:20 +0000
@@ -243,8 +243,8 @@
           '<div class="transparent important-notice-popup">',
             '<div class="validation-content-placeholder"></div>',
             '<div class="extra-form-buttons">',
-              '<button class="yes_button" type="button"></button>',
-              '<button class="no_button" type="button"></button>',
+              '<button class="yes_button" type="button"/>',
+              '<button class="no_button" type="button"/>',
             '</div>',
           '</div>',
         '</div>'].join(''));
@@ -358,13 +358,9 @@
         });
     var picker = new Y.lazr.Picker(new_config);
 
-    // We don't want the Y.lazr.Picker default save to fire since this hides
-    // the form. We want to do this ourselves after any validation has had a
-    // chance to be performed.
-    picker.publish('save', { defaultFn: function(){} } );
-    
     picker.subscribe('save', function (e) {
         Y.log('Got save event.');
+        e.preventDefault();
         var picker_result = e.details[Y.lazr.Picker.SAVE_RESULT];
         var do_save = function() {
             if (Y.Lang.isFunction(config.save)) {

=== renamed file 'lib/lp/app/javascript/tests/test_picker.html' => 'lib/lp/app/javascript/tests/picker.html'
--- lib/lp/app/javascript/tests/test_picker.html	2011-04-19 11:27:26 +0000
+++ lib/lp/app/javascript/tests/picker.html	2011-04-20 15:32:20 +0000
@@ -24,7 +24,7 @@
   <script type="text/javascript" src="../picker.js"></script>
 
   <!-- The test suite -->
-  <script type="text/javascript" src="test_picker.js"></script>
+  <script type="text/javascript" src="picker.js"></script>
 </head>
 <body class="yui3-skin-sam">
   <div class="yui3-widget yui3-activator yui3-activator-focused">

=== renamed file 'lib/lp/app/javascript/tests/test_picker.js' => 'lib/lp/app/javascript/tests/picker.js'
--- lib/lp/app/javascript/tests/test_picker.js	2011-04-15 08:16:32 +0000
+++ lib/lp/app/javascript/tests/picker.js	2011-04-20 15:32:20 +0000
@@ -118,29 +118,6 @@
         Assert.isTrue(save_flag.event_has_fired, "save event wasn't fired.");
     },
 
-    test_TextFieldPickerPlugin_selected_item_is_saved: function () {
-        // Test that the picker saves the selected value to its associated
-        // textfield if one is defined.
-        var search_input = Y.Node.create(
-                '<input id="field.initval" value="foo" />');
-        node = Y.one(document.body).appendChild(search_input);
-        this.create_picker();
-        this.picker.plug(Y.lazr.TextFieldPickerPlugin,
-                         {input_element: '[id="field.initval"]'});
-        this.picker.set('results', this.vocabulary);
-        this.picker.render();
-        var got_focus = false;
-        search_input.on('focus', function(e) {
-            got_focus = true;
-        });
-        simulate(
-            this.picker.get('boundingBox').one('.yui3-picker-results'),
-                'li:nth-child(1)', 'click');
-        Assert.areEqual(
-            'fred', Y.one('[id="field.initval"]').get("value"));
-        Assert.isTrue(got_focus, "focus didn't go to the search input.");
-    },
-
     test_confirmation_yes: function() {
         // Test that the picker saves the selected value if the user answers
         // "Yes" to a confirmation request.

=== modified file 'lib/lp/code/interfaces/codehosting.py'
--- lib/lp/code/interfaces/codehosting.py	2011-04-18 01:37:03 +0000
+++ lib/lp/code/interfaces/codehosting.py	2011-04-20 15:32:20 +0000
@@ -8,7 +8,6 @@
 __metaclass__ = type
 __all__ = [
     'BRANCH_ALIAS_PREFIX',
-    'branch_id_alias',
     'BRANCH_ID_ALIAS_PREFIX',
     'BRANCH_TRANSPORT',
     'compose_public_url',
@@ -61,11 +60,6 @@
 BRANCH_ID_ALIAS_PREFIX = '+branch-id'
 
 
-def branch_id_alias(branch):
-    """Return the path using the branch id alias."""
-    return '/%s/%s' % (BRANCH_ID_ALIAS_PREFIX, branch.id)
-
-
 # The scheme types that are supported for codehosting.
 SUPPORTED_SCHEMES = 'bzr+ssh', 'http'
 

=== modified file 'lib/lp/code/model/tests/test_branch.py'
--- lib/lp/code/model/tests/test_branch.py	2011-04-18 01:37:03 +0000
+++ lib/lp/code/model/tests/test_branch.py	2011-04-20 15:32:20 +0000
@@ -79,7 +79,7 @@
     )
 from lp.code.interfaces.branchnamespace import IBranchNamespaceSet
 from lp.code.interfaces.branchrevision import IBranchRevision
-from lp.code.interfaces.codehosting import branch_id_alias
+from lp.code.interfaces.codehosting import BRANCH_ID_ALIAS_PREFIX
 from lp.code.interfaces.linkedbranch import ICanHasLinkedBranch
 from lp.code.interfaces.seriessourcepackagebranch import (
     IFindOfficialBranchLinks,
@@ -187,7 +187,8 @@
         branch = self.factory.makeAnyBranch()
         stacked_on = self.factory.makeAnyBranch()
         login_person(branch.owner)
-        stacked_on_location = branch_id_alias(stacked_on)
+        stacked_on_location = '/%s/%s' % (
+            BRANCH_ID_ALIAS_PREFIX, stacked_on.id)
         branch.branchChanged(stacked_on_location, '', *self.arbitrary_formats)
         self.assertEqual(stacked_on, branch.stacked_on)
 

=== modified file 'lib/lp/code/model/tests/test_branchlookup.py'
--- lib/lp/code/model/tests/test_branchlookup.py	2011-04-18 01:37:03 +0000
+++ lib/lp/code/model/tests/test_branchlookup.py	2011-04-20 15:32:20 +0000
@@ -25,10 +25,7 @@
     ILinkedBranchTraverser,
     )
 from lp.code.interfaces.branchnamespace import get_branch_namespace
-from lp.code.interfaces.codehosting import (
-    branch_id_alias,
-    BRANCH_ID_ALIAS_PREFIX,
-    )
+from lp.code.interfaces.codehosting import BRANCH_ID_ALIAS_PREFIX
 from lp.code.interfaces.linkedbranch import ICanHasLinkedBranch
 from lp.registry.errors import (
     NoSuchDistroSeries,
@@ -140,28 +137,28 @@
         owner = self.factory.makePerson()
         branch = self.factory.makeAnyBranch(owner=owner, private=True)
         with person_logged_in(owner):
-            path = branch_id_alias(branch)
+            path = '/%s/%s' % (BRANCH_ID_ALIAS_PREFIX, branch.id)
         result = self.branch_set.getIdAndTrailingPath(path)
         self.assertEqual((None, None), result)
 
     def test_branch_id_alias_public(self):
         # Public branches can be accessed.
         branch = self.factory.makeAnyBranch()
-        path = branch_id_alias(branch)
+        path = '/%s/%s' % (BRANCH_ID_ALIAS_PREFIX, branch.id)
         result = self.branch_set.getIdAndTrailingPath(path)
         self.assertEqual((branch.id, ''), result)
 
     def test_branch_id_alias_public_slash(self):
         # A trailing slash is returned as the extra path.
         branch = self.factory.makeAnyBranch()
-        path = '%s/' % branch_id_alias(branch)
+        path = '/%s/%s/' % (BRANCH_ID_ALIAS_PREFIX, branch.id)
         result = self.branch_set.getIdAndTrailingPath(path)
         self.assertEqual((branch.id, '/'), result)
 
     def test_branch_id_alias_public_with_path(self):
         # All the path after the number is returned as the trailing path.
         branch = self.factory.makeAnyBranch()
-        path = '%s/foo' % branch_id_alias(branch)
+        path = '/%s/%s/foo' % (BRANCH_ID_ALIAS_PREFIX, branch.id)
         result = self.branch_set.getIdAndTrailingPath(path)
         self.assertEqual((branch.id, '/foo'), result)
 

=== modified file 'lib/lp/code/xmlrpc/codehosting.py'
--- lib/lp/code/xmlrpc/codehosting.py	2011-04-18 01:37:03 +0000
+++ lib/lp/code/xmlrpc/codehosting.py	2011-04-20 15:32:20 +0000
@@ -59,7 +59,6 @@
 from lp.code.interfaces.branchtarget import IBranchTarget
 from lp.code.interfaces.codehosting import (
     BRANCH_ALIAS_PREFIX,
-    branch_id_alias,
     BRANCH_ID_ALIAS_PREFIX,
     BRANCH_TRANSPORT,
     CONTROL_TRANSPORT,
@@ -321,12 +320,12 @@
         if default_branch is None:
             return
         try:
-            path = branch_id_alias(default_branch)
+            unique_name = default_branch.unique_name
         except Unauthorized:
             return
         return (
             CONTROL_TRANSPORT,
-            {'default_stack_on': escape(path)},
+            {'default_stack_on': escape('/' + unique_name)},
             trailing_path)
 
     def _translateBranchIdAlias(self, requester, path):

=== modified file 'lib/lp/code/xmlrpc/tests/test_codehosting.py'
--- lib/lp/code/xmlrpc/tests/test_codehosting.py	2011-04-18 01:37:03 +0000
+++ lib/lp/code/xmlrpc/tests/test_codehosting.py	2011-04-20 15:32:20 +0000
@@ -41,7 +41,6 @@
 from lp.code.interfaces.branchtarget import IBranchTarget
 from lp.code.interfaces.codehosting import (
     BRANCH_ALIAS_PREFIX,
-    branch_id_alias,
     BRANCH_ID_ALIAS_PREFIX,
     BRANCH_TRANSPORT,
     CONTROL_TRANSPORT,
@@ -994,8 +993,8 @@
         self.assertNotFound(requester, '/%s/.bzr' % BRANCH_ALIAS_PREFIX)
 
     def test_translatePath_branch_id_alias_bzrdir_content(self):
-        # translatePath('/+branch-id/.bzr/.*') *must* return not found,
-        # otherwise bzr will look for it and we don't have a global bzr dir.
+        # translatePath('/+branch-id/.bzr/.*') *must* return not found, otherwise
+        # bzr will look for it and we don't have a global bzr dir.
         requester = self.factory.makePerson()
         self.assertNotFound(
             requester, '/%s/.bzr/branch-format' % BRANCH_ID_ALIAS_PREFIX)
@@ -1010,7 +1009,7 @@
         # Make sure the trailing path is returned.
         requester = self.factory.makePerson()
         branch = removeSecurityProxy(self.factory.makeAnyBranch())
-        path = escape(u'%s/foo/bar' % branch_id_alias(branch))
+        path = escape(u'/%s/%s/foo/bar' % (BRANCH_ID_ALIAS_PREFIX, branch.id))
         translation = self.codehosting_api.translatePath(requester.id, path)
         self.assertEqual(
             (BRANCH_TRANSPORT, {'id': branch.id, 'writable': False}, 'foo/bar'),
@@ -1022,7 +1021,7 @@
         branch = removeSecurityProxy(
             self.factory.makeAnyBranch(
                 branch_type=BranchType.HOSTED, owner=requester))
-        path = escape(branch_id_alias(branch))
+        path = escape(u'/%s/%s' % (BRANCH_ID_ALIAS_PREFIX, branch.id))
         translation = self.codehosting_api.translatePath(requester.id, path)
         self.assertEqual(
             (BRANCH_TRANSPORT, {'id': branch.id, 'writable': False}, ''),
@@ -1035,7 +1034,7 @@
         branch = removeSecurityProxy(
             self.factory.makeAnyBranch(
                 branch_type=BranchType.HOSTED, private=True, owner=requester))
-        path = escape(branch_id_alias(branch))
+        path = escape(u'/%s/%s' % (BRANCH_ID_ALIAS_PREFIX, branch.id))
         translation = self.codehosting_api.translatePath(requester.id, path)
         self.assertEqual(
             (BRANCH_TRANSPORT, {'id': branch.id, 'writable': False}, ''),
@@ -1047,16 +1046,17 @@
         branch = removeSecurityProxy(
             self.factory.makeAnyBranch(
                 branch_type=BranchType.HOSTED, private=True))
-        path = escape(branch_id_alias(branch))
+        path = escape(u'/%s/%s' % (BRANCH_ID_ALIAS_PREFIX, branch.id))
         self.assertPermissionDenied(requester, path)
 
     def assertTranslationIsControlDirectory(self, translation,
                                             default_stacked_on,
                                             trailing_path):
         """Assert that 'translation' points to the right control transport."""
+        unique_name = escape(u'/' + default_stacked_on)
         expected_translation = (
             CONTROL_TRANSPORT,
-            {'default_stack_on': escape(default_stacked_on)}, trailing_path)
+            {'default_stack_on': unique_name}, trailing_path)
         self.assertEqual(expected_translation, translation)
 
     def test_translatePath_control_directory(self):
@@ -1067,7 +1067,7 @@
         login(ANONYMOUS)
         self.assertTranslationIsControlDirectory(
             translation,
-            default_stacked_on=branch_id_alias(branch),
+            default_stacked_on=branch.unique_name,
             trailing_path='.bzr')
 
     def test_translatePath_control_directory_no_stacked_set(self):
@@ -1093,7 +1093,7 @@
         login(ANONYMOUS)
         self.assertTranslationIsControlDirectory(
             translation,
-            default_stacked_on=branch_id_alias(branch),
+            default_stacked_on=branch.unique_name,
             trailing_path='.bzr')
 
     def test_translatePath_control_directory_other_owner(self):
@@ -1105,7 +1105,7 @@
         login(ANONYMOUS)
         self.assertTranslationIsControlDirectory(
             translation,
-            default_stacked_on=branch_id_alias(branch),
+            default_stacked_on=branch.unique_name,
             trailing_path='.bzr')
 
     def test_translatePath_control_directory_package_no_focus(self):
@@ -1131,7 +1131,7 @@
         login(ANONYMOUS)
         self.assertTranslationIsControlDirectory(
             translation,
-            default_stacked_on=branch_id_alias(branch),
+            default_stacked_on=branch.unique_name,
             trailing_path='.bzr')
 
 

=== modified file 'lib/lp/codehosting/inmemory.py'
--- lib/lp/codehosting/inmemory.py	2011-04-18 01:37:03 +0000
+++ lib/lp/codehosting/inmemory.py	2011-04-20 15:32:20 +0000
@@ -37,7 +37,6 @@
 from lp.code.interfaces.branchtarget import IBranchTarget
 from lp.code.interfaces.codehosting import (
     BRANCH_ALIAS_PREFIX,
-    branch_id_alias,
     BRANCH_ID_ALIAS_PREFIX,
     BRANCH_TRANSPORT,
     CONTROL_TRANSPORT,
@@ -803,10 +802,9 @@
             return
         if not self._canRead(requester, default_branch):
             return
-        path = branch_id_alias(default_branch)
         return (
             CONTROL_TRANSPORT,
-            {'default_stack_on': escape(path)},
+            {'default_stack_on': escape('/' + default_branch.unique_name)},
             trailing_path)
 
     def _serializeBranch(self, requester_id, branch, trailing_path,

=== modified file 'lib/lp/codehosting/tests/test_rewrite.py'
--- lib/lp/codehosting/tests/test_rewrite.py	2011-04-18 01:37:03 +0000
+++ lib/lp/codehosting/tests/test_rewrite.py	2011-04-20 15:32:20 +0000
@@ -16,7 +16,7 @@
 
 from canonical.config import config
 from canonical.testing.layers import DatabaseFunctionalLayer
-from lp.code.interfaces.codehosting import branch_id_alias
+from lp.code.interfaces.codehosting import BRANCH_ID_ALIAS_PREFIX
 from lp.codehosting.rewrite import BranchRewriter
 from lp.codehosting.vfs import branch_id_to_path
 from lp.services.log.logger import BufferLogger
@@ -102,8 +102,8 @@
             self.factory.makePackageBranch(private=False)]
         transaction.commit()
         output = [
-            rewriter.rewriteLine(
-                "%s/.bzr/README" % branch_id_alias(branch))
+            rewriter.rewriteLine("/%s/%s/.bzr/README" % (
+                    BRANCH_ID_ALIAS_PREFIX, branch.id))
             for branch in branches]
         expected = [
             'file:///var/tmp/bazaar.launchpad.dev/mirrors/%s/.bzr/README'
@@ -116,7 +116,8 @@
         # 'NULL'.  This is translated by apache to a 404.
         rewriter = self.makeRewriter()
         branch = self.factory.makeAnyBranch(private=True)
-        path = branch_id_alias(removeSecurityProxy(branch))
+        path = '/%s/%s' % (
+            BRANCH_ID_ALIAS_PREFIX, removeSecurityProxy(branch).id)
         transaction.commit()
         output = [
             rewriter.rewriteLine("%s/changes" % path),
@@ -129,7 +130,7 @@
         rewriter = self.makeRewriter()
         branch = self.factory.makeAnyBranch()
         transaction.commit()
-        path = "%s/.bzr/README" % branch_id_alias(branch)
+        path = "/%s/%s/.bzr/README" % (BRANCH_ID_ALIAS_PREFIX, branch.id)
         rewriter.rewriteLine(path)
         rewriter.rewriteLine(path)
         logging_output_lines = self.getLoggerOutput(

=== modified file 'lib/lp/codehosting/vfs/tests/test_branchfs.py'
--- lib/lp/codehosting/vfs/tests/test_branchfs.py	2011-04-18 23:05:14 +0000
+++ lib/lp/codehosting/vfs/tests/test_branchfs.py	2011-04-20 15:32:20 +0000
@@ -55,7 +55,6 @@
     )
 from lp.code.enums import BranchType
 from lp.code.interfaces.codehosting import (
-    branch_id_alias,
     BRANCH_TRANSPORT,
     CONTROL_TRANSPORT,
     )
@@ -267,7 +266,7 @@
 
         def check_control_file((transport, path)):
             self.assertEqual(
-                'default_stack_on = %s\n' % branch_id_alias(branch),
+                'default_stack_on = /%s\n' % branch.unique_name,
                 transport.get_bytes(path))
         return deferred.addCallback(check_control_file)
 

=== modified file 'lib/lp/codehosting/vfs/tests/test_filesystem.py'
--- lib/lp/codehosting/vfs/tests/test_filesystem.py	2011-04-18 23:05:14 +0000
+++ lib/lp/codehosting/vfs/tests/test_filesystem.py	2011-04-20 15:32:20 +0000
@@ -16,7 +16,6 @@
 from bzrlib.urlutils import escape
 
 from lp.code.interfaces.branchtarget import IBranchTarget
-from lp.code.interfaces.codehosting import branch_id_alias
 from lp.codehosting.inmemory import (
     InMemoryFrontend,
     XMLRPCWrapper,
@@ -140,9 +139,9 @@
         control_file = transport.get_bytes(
             '~%s/%s/.bzr/control.conf'
             % (self.requester.name, product.name))
-        stacked_on = IBranchTarget(product).default_stacked_on_branch
         self.assertEqual(
-            'default_stack_on = %s' % branch_id_alias(stacked_on),
+            'default_stack_on = /%s'
+            % IBranchTarget(product).default_stacked_on_branch.unique_name,
             control_file.strip())
 
     def test_can_open_product_control_dir(self):

=== modified file 'lib/lp/registry/javascript/structural-subscription.js'
--- lib/lp/registry/javascript/structural-subscription.js	2011-04-19 07:18:42 +0000
+++ lib/lp/registry/javascript/structural-subscription.js	2011-04-20 15:32:20 +0000
@@ -1204,8 +1204,9 @@
         var mute_link = node.one('a.mute-subscription');
         mute_link.on('click', make_mute_handler(filter_info, node));
     }
-    if (!filter_info.subscriber_is_team ||
-        filter_info.user_is_team_admin) {
+    var can_edit = (!filter_info.subscriber_is_team ||
+                    filter_info.user_is_team_admin);
+    if (can_edit) {
         var edit_link = node.one('a.edit-subscription');
         var edit_handler = make_edit_handler(
             subscription, filter_info, filter_id, config);
@@ -1276,9 +1277,6 @@
                 'do not receive emails from this subscription.</em>'));
     }
 
-    var can_edit = (!filter_info.subscriber_is_team ||
-                    filter_info.user_is_team_admin);
-
     var control = filter_node.appendChild(
         Y.Node.create('<span style="float: right"></span>'));
 
@@ -1291,6 +1289,9 @@
             '    href="/+help/structural-subscription-mute.html">'+
             '  <span class="invisible-link">Delivery help</span>'+
             '</a>'));
+        // For some reason the help link will not appear in Chrome unless
+        // there is a non-empty element immediately after the help node.
+        control.append(Y.Node.create('<span>&nbsp;</span>'));
         // We store a reference to the timeout that will hide the help link so
         // we can cancel it if needed.
         var hide_help_timeout;
@@ -1309,6 +1310,9 @@
         link.on('hover', show_help, hide_help);
         help.on('hover', show_help, hide_help);
     }
+
+    var can_edit = (!filter_info.subscriber_is_team ||
+                    filter_info.user_is_team_admin);
     if (can_edit) {
         // User can edit the subscription.
         control.append(Y.Node.create(

=== modified file 'lib/lp/registry/tests/test_distroseriesdifference.py'
--- lib/lp/registry/tests/test_distroseriesdifference.py	2011-04-19 12:00:38 +0000
+++ lib/lp/registry/tests/test_distroseriesdifference.py	2011-04-20 15:32:20 +0000
@@ -163,52 +163,6 @@
             DistroSeriesDifferenceStatus.RESOLVED,
             ds_diff.status)
 
-    def test_update_nulls_diffs_for_resolved(self):
-        # Resolved differences should null out the package_diff and
-        # parent_package_diff fields so the libraryfilealias gets
-        # considered for GC later.
-        derived_changelog = self.factory.makeChangelog(
-            versions=['1.0', '1.2'])
-        parent_changelog = self.factory.makeChangelog(
-            versions=['1.0', '1.3'])
-        transaction.commit() # Yay, librarian.
-        ds_diff = self.factory.makeDistroSeriesDifference(versions={
-            'derived': '1.2',
-            'parent': '1.3',
-            'base': '1.0',
-            },
-            changelogs={
-                'derived': derived_changelog,
-                'parent': parent_changelog,
-            })
-        person = self.factory.makePerson()
-        with person_logged_in(person):
-            ds_diff.requestPackageDiffs(person)
-        # The pre-test state is that there are diffs present:
-        self.assertIsNot(None, ds_diff.package_diff)
-        self.assertIsNot(None, ds_diff.parent_package_diff)
-
-        # Resolve the DSD by making the same package version published
-        # in parent and derived.
-        new_derived_pub = self.factory.makeSourcePackagePublishingHistory(
-            sourcepackagename=ds_diff.source_package_name,
-            distroseries=ds_diff.derived_series,
-            status=PackagePublishingStatus.PENDING,
-            version='1.4')
-        new_parent_pub = self.factory.makeSourcePackagePublishingHistory(
-            sourcepackagename=ds_diff.source_package_name,
-            distroseries=ds_diff.derived_series.parent_series,
-            status=PackagePublishingStatus.PENDING,
-            version='1.4')
-
-        # Packagediffs should be gone now.
-        was_updated = ds_diff.update()
-        self.assertTrue(was_updated)
-        self.assertEqual(
-            ds_diff.status, DistroSeriesDifferenceStatus.RESOLVED)
-        self.assertIs(None, ds_diff.package_diff)
-        self.assertIs(None, ds_diff.parent_package_diff)
-
     def test_update_re_opens_difference(self):
         # The status of a resolved difference will updated with new
         # uploads.

=== modified file 'lib/lp/registry/tests/test_person.py'
--- lib/lp/registry/tests/test_person.py	2011-04-20 01:57:24 +0000
+++ lib/lp/registry/tests/test_person.py	2011-04-20 15:32:20 +0000
@@ -67,7 +67,6 @@
     Person,
     )
 from lp.services.openid.model.openididentifier import OpenIdIdentifier
-from lp.services.propertycache import clear_property_cache
 from lp.soyuz.enums import (
     ArchivePurpose,
     ArchiveStatus,
@@ -1303,7 +1302,7 @@
 class TestGetRecipients(TestCaseWithFactory):
     """Tests for get_recipients"""
 
-    layer = DatabaseFunctionalLayer
+    layer = LaunchpadFunctionalLayer
 
     def setUp(self):
         super(TestGetRecipients, self).setUp()
@@ -1327,19 +1326,6 @@
         recipients = get_recipients(super_team)
         self.assertEqual(set([team]), set(recipients))
 
-    def test_get_recipients_team_with_unvalidated_address(self):
-        """Ensure get_recipients handles teams with non-preferred addresses.
-
-        If there is no preferred address but one or more non-preferred ones,
-        email should still be sent to the members.
-        """
-        owner = self.factory.makePerson(email='foo@xxxxxxx')
-        team = self.factory.makeTeam(owner, email='team@xxxxxxx')
-        self.assertContentEqual([team], get_recipients(team))
-        team.preferredemail.status = EmailAddressStatus.NEW
-        clear_property_cache(team)
-        self.assertContentEqual([owner], get_recipients(team))
-
     def makePersonWithNoPreferredEmail(self, **kwargs):
         kwargs['email_address_status'] = EmailAddressStatus.NEW
         return self.factory.makePerson(**kwargs)

=== removed file 'lib/lp/services/timeline/nestingtimedaction.py'
--- lib/lp/services/timeline/nestingtimedaction.py	2011-04-20 05:05:06 +0000
+++ lib/lp/services/timeline/nestingtimedaction.py	1970-01-01 00:00:00 +0000
@@ -1,35 +0,0 @@
-# Copyright 2010 Canonical Ltd.  This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""Time an action which calls other timed actions."""
-
-
-__all__ = ['NestingTimedAction']
-
-__metaclass__ = type
-
-
-import datetime
-
-from timedaction import TimedAction
-
-
-class NestingTimedAction(TimedAction):
-    """A variation of TimedAction which creates a nested environment.
-    
-    This is done by recording two 0 length timed actions in the timeline:
-    one at the start of the action and one at the end, with -start and
-    -stop appended to their categories.
-
-    See `TimedAction` for more information.
-    """
-
-    def _init(self):
-        self.duration = datetime.timedelta()
-        self._category = self.category
-        self.category = self._category + '-start'
-
-    def finish(self):
-        """Mark the TimedAction as finished."""
-        end = self.timeline.start(self._category + '-stop', self.detail)
-        end.duration = datetime.timedelta()

=== removed file 'lib/lp/services/timeline/tests/test_nestingtimedaction.py'
--- lib/lp/services/timeline/tests/test_nestingtimedaction.py	2011-04-20 05:05:06 +0000
+++ lib/lp/services/timeline/tests/test_nestingtimedaction.py	1970-01-01 00:00:00 +0000
@@ -1,27 +0,0 @@
-# Copyright 2010 Canonical Ltd.  This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""Tests of the TimedAction class."""
-
-__metaclass__ = type
-
-import datetime
-
-import testtools
-
-from lp.services.timeline.nestingtimedaction import NestingTimedAction
-from lp.services.timeline.timeline import Timeline
-
-
-class TestNestingTimedAction(testtools.TestCase):
-
-    def test_finish_adds_action(self):
-        timeline = Timeline()
-        action = NestingTimedAction("Sending mail", None, timeline)
-        action.finish()
-        self.assertEqual(1, len(timeline.actions))
-        self.assertEqual(datetime.timedelta(), timeline.actions[-1].duration)
-
-    def test__init__sets_duration(self):
-        action = NestingTimedAction("Sending mail", None)
-        self.assertEqual(datetime.timedelta(), action.duration)

=== modified file 'lib/lp/services/timeline/tests/test_timeline.py'
--- lib/lp/services/timeline/tests/test_timeline.py	2011-04-20 04:06:53 +0000
+++ lib/lp/services/timeline/tests/test_timeline.py	2011-04-20 15:32:20 +0000
@@ -37,69 +37,6 @@
         self.assertRaises(OverlappingActionError, timeline.start,
             "Sending mail", "Noone")
 
-    def test_nested_start_permitted(self):
-        # When explicitly requested a nested start can be done
-        timeline = Timeline()
-        action = timeline.start("Calling openid", "hostname", allow_nested=True)
-        child = timeline.start("SQL Callback", "SELECT...")
-
-    def test_nested_start_is_not_transitive(self):
-        # nesting is explicit at each level - not inherited.
-        timeline = Timeline()
-        action = timeline.start("Calling openid", "hostname", allow_nested=True)
-        child = timeline.start("SQL Callback", "SELECT...")
-        self.assertRaises(OverlappingActionError, timeline.start,
-            "Sending mail", "Noone")
-
-    def test_multiple_nested_children_permitted(self):
-        # nesting is not reset by each action that is added.
-        timeline = Timeline()
-        action = timeline.start("Calling openid", "hostname", allow_nested=True)
-        child = timeline.start("SQL Callback", "SELECT...")
-        child.finish()
-        child = timeline.start("SQL Callback", "SELECT...")
-
-    def test_multiple_starts_after_nested_group_prevented(self):
-        # nesting stops being permitted when the nesting action is finished.
-        timeline = Timeline()
-        action = timeline.start("Calling openid", "hostname", allow_nested=True)
-        action.finish()
-        child = timeline.start("SQL Callback", "SELECT...")
-        self.assertRaises(OverlappingActionError, timeline.start,
-            "Sending mail", "Noone")
-
-    def test_nesting_within_nesting_permitted(self):
-        timeline = Timeline()
-        action = timeline.start("Calling openid", "hostname", allow_nested=True)
-        middle = timeline.start("Calling otherlibrary", "", allow_nested=True)
-        child = timeline.start("SQL Callback", "SELECT...")
-
-    def test_finishing_nested_within_nested_leaves_outer_nested_nesting(self):
-        timeline = Timeline()
-        action = timeline.start("Calling openid", "hostname", allow_nested=True)
-        middle = timeline.start("Calling otherlibrary", "", allow_nested=True)
-        middle.finish()
-        child = timeline.start("SQL Callback", "SELECT...")
-
-    def test_nested_actions_recorded_as_two_zero_length_actions(self):
-        timeline = Timeline()
-        action = timeline.start("Calling openid", "hostname", allow_nested=True)
-        child = timeline.start("SQL Callback", "SELECT...")
-        child.finish()
-        action.finish()
-        self.assertEqual(3, len(timeline.actions))
-        self.assertEqual(datetime.timedelta(), timeline.actions[0].duration)
-        self.assertEqual(datetime.timedelta(), timeline.actions[2].duration)
-
-    def test_nested_category_labels(self):
-        # To identify start/stop pairs '-start' and '-stop' are put onto the
-        # category of nested actions:
-        timeline = Timeline()
-        action = timeline.start("Calling openid", "hostname", allow_nested=True)
-        action.finish()
-        self.assertEqual('Calling openid-start', timeline.actions[0].category)
-        self.assertEqual('Calling openid-stop', timeline.actions[1].category)
-
     def test_start_after_finish_works(self):
         timeline = Timeline()
         action = timeline.start("Sending mail", "Noone")

=== modified file 'lib/lp/services/timeline/timedaction.py'
--- lib/lp/services/timeline/timedaction.py	2011-04-20 04:06:53 +0000
+++ lib/lp/services/timeline/timedaction.py	2011-04-20 15:32:20 +0000
@@ -42,15 +42,9 @@
         self.category = category
         self.detail = detail
         self.timeline = timeline
-        self._init()
-
-    def _init(self):
-        # hook for child classes.
-        pass
 
     def __repr__(self):
-        return "<%s %s[%s]>" % (self.__class__, self.category,
-            self.detail[:20])
+        return "<TimedAction %s[%s]>" % (self.category, self.detail[:20])
 
     def logTuple(self):
         """Return a 4-tuple suitable for errorlog's use."""

=== modified file 'lib/lp/services/timeline/timeline.py'
--- lib/lp/services/timeline/timeline.py	2011-04-20 04:06:53 +0000
+++ lib/lp/services/timeline/timeline.py	2011-04-20 15:32:20 +0000
@@ -12,25 +12,19 @@
 from pytz import utc as UTC
 
 from timedaction import TimedAction
-from nestingtimedaction import NestingTimedAction
 
 
 class OverlappingActionError(Exception):
     """A new action was attempted without finishing the prior one."""
-    # To make analysis easy we do not permit overlapping actions by default:
-    # each action that is being timed and accrued must complete before the next
+    # To make analysis easy we do not permit overlapping actions: each
+    # action that is being timed and accrued must complete before the next
     # is started. This means, for instance, that sending mail cannot do SQL
     # queries, as both are timed and accrued. OTOH it makes analysis and
     # serialisation of timelines simpler, and for the current use cases in 
     # Launchpad this is sufficient. This constraint should not be considered
     # sacrosanct - if, in future, we desire timelines with overlapping actions,
     # as long as the OOPS analysis code is extended to generate sensible
-    # reports in those situations, this can be changed. In the interim, actions
-    # can be explicitly setup to permit nesting by passing allow_nested=True
-    # which will cause the action to be recorded with 0 duration and a -start
-    # and -stop suffix added to its category. This is potentially lossy but
-    # good enough to get nested metrics and can be iterated on in the future to
-    # do an actual stacked/tree model of actions - if needed.
+    # reports in those situations, this can be changed.
 
 
 class Timeline:
@@ -54,20 +48,14 @@
         self.actions = actions
         self.baseline = datetime.datetime.now(UTC)
 
-    def start(self, category, detail, allow_nested=False):
+    def start(self, category, detail):
         """Create a new TimedAction at the end of the timeline.
 
         :param category: the category for the action.
         :param detail: The detail for the action.
-        :param allow_nested: If True treat this action as a nested action -
-            record it twice with 0 duration, once at the start and once at the
-            finish.
         :return: A TimedAction for that category and detail.
         """
-        if allow_nested:
-            result = NestingTimedAction(category, detail, self)
-        else:
-            result = TimedAction(category, detail, self)
+        result = TimedAction(category, detail, self)
         if self.actions and self.actions[-1].duration is None:
             raise OverlappingActionError(self.actions[-1], result)
         self.actions.append(result)

=== modified file 'lib/lp/translations/windmill/tests/test_sourcepackage_sharing_details.py'
--- lib/lp/translations/windmill/tests/test_sourcepackage_sharing_details.py	2011-04-20 06:12:26 +0000
+++ lib/lp/translations/windmill/tests/test_sourcepackage_sharing_details.py	2011-04-20 15:32:20 +0000
@@ -66,7 +66,7 @@
         client.click(xpath='//*[@id="translation-incomplete-picker"]/a')
         client.click(id='field.translations_usage.1')
         client.click(
-            xpath=overlay.visible_xpath + '//button[@type="submit"]')
+            xpath=overlay.visible_xpath + '//input[@value="Submit"]')
         client.waits.forElementProperty(
             id='translation-incomplete', option='className|sprite no unseen',
             timeout=FOR_ELEMENT)
@@ -74,7 +74,7 @@
             xpath='//*[@id="upstream-sync-incomplete-picker"]/a')
         client.click(id='field.translations_autoimport_mode.2')
         client.click(
-            xpath=overlay.visible_xpath + '//button[@type="submit"]')
+            xpath=overlay.visible_xpath + '//input[@value="Submit"]')
         client.waits.forElementProperty(
             id='upstream-sync-incomplete',
             option='className|sprite no unseen', timeout=FOR_ELEMENT)

=== modified file 'utilities/sourcedeps.conf'
--- utilities/sourcedeps.conf	2011-04-19 14:13:21 +0000
+++ utilities/sourcedeps.conf	2011-04-20 15:32:20 +0000
@@ -15,3 +15,4 @@
 subvertpy lp:~launchpad-pqm/subvertpy/trunk;revno=2042
 python-debian lp:~launchpad-pqm/python-debian/devel;revno=186
 testresources lp:~launchpad-pqm/testresources/dev;revno=16
+shipit lp:~launchpad-pqm/shipit/trunk;revno=8923 optional

=== modified file 'versions.cfg'
--- versions.cfg	2011-04-20 02:37:00 +0000
+++ versions.cfg	2011-04-20 15:32:20 +0000
@@ -32,7 +32,7 @@
 lazr.batchnavigator = 1.2.2
 lazr.config = 1.1.3
 lazr.delegates = 1.2.0
-lazr.enum = 1.1.3
+lazr.enum = 1.1.2
 lazr.lifecycle = 1.1
 lazr.restful = 0.18.1
 lazr.restfulclient = 0.11.2