← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~jelmer/launchpad/stacked-code-imports into lp:launchpad

 

Jelmer Vernooij has proposed merging lp:~jelmer/launchpad/stacked-code-imports into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #485932 in Launchpad itself: "should stack foreign branch imports"
  https://bugs.launchpad.net/launchpad/+bug/485932

For more details, see:
https://code.launchpad.net/~jelmer/launchpad/stacked-code-imports/+merge/76998

Stack code imports on the default development focus branch.

This should make imports of branches that are related to the development focus a lot faster, and use less disk space.

This will help significantly with e.g. imports of new gcc branches.

For now, this only stacks onto public branches.
-- 
https://code.launchpad.net/~jelmer/launchpad/stacked-code-imports/+merge/76998
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~jelmer/launchpad/stacked-code-imports into lp:launchpad.
=== modified file 'lib/lp/codehosting/codeimport/tests/test_worker.py'
--- lib/lp/codehosting/codeimport/tests/test_worker.py	2011-09-19 06:57:55 +0000
+++ lib/lp/codehosting/codeimport/tests/test_worker.py	2011-09-26 15:09:29 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
+
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Tests for the code import worker."""
@@ -49,7 +49,14 @@
 import subvertpy.ra
 
 from canonical.config import config
-from canonical.testing.layers import BaseLayer
+from canonical.testing.layers import (
+    BaseLayer,
+    DatabaseFunctionalLayer,
+    )
+from lp.code.interfaces.codehosting import (
+    branch_id_alias,
+    compose_public_url,
+    )
 from lp.codehosting import load_optional_plugin
 from lp.codehosting.codeimport.tarball import (
     create_tarball,
@@ -67,6 +74,7 @@
     BzrImportWorker,
     BzrSvnImportWorker,
     CodeImportBranchOpenPolicy,
+    CodeImportSourceDetails,
     CodeImportWorkerExitCode,
     CSCVSImportWorker,
     ForeignTreeStore,
@@ -85,7 +93,11 @@
     )
 from lp.codehosting.tests.helpers import create_branch_with_one_revision
 from lp.services.log.logger import BufferLogger
-from lp.testing import TestCase
+from lp.testing import (
+    TestCase,
+    TestCaseWithFactory,
+    )
+from zope.security.proxy import removeSecurityProxy
 
 
 class ForeignBranchPluginLayer(BaseLayer):
@@ -807,7 +819,7 @@
         raise NotImplementedError(
             "Override this with a VCS-specific implementation.")
 
-    def makeSourceDetails(self, module_name, files):
+    def makeSourceDetails(self, module_name, files, stacked_on_url=None):
         """Make a `CodeImportSourceDetails` that points to a real repository.
 
         This should set `self.foreign_commit_count` to an appropriate value.
@@ -954,7 +966,7 @@
         self.foreign_commit_count += 1
         shutil.rmtree('working_dir')
 
-    def makeSourceDetails(self, module_name, files):
+    def makeSourceDetails(self, module_name, files, stacked_on_url=None):
         """Make a CVS `CodeImportSourceDetails` pointing at a real CVS repo.
         """
         cvs_server = CVSServer(self.makeTemporaryDirectory())
@@ -966,7 +978,8 @@
         self.foreign_commit_count = 2
 
         return self.factory.makeCodeImportSourceDetails(
-            rcstype='cvs', cvs_root=cvs_server.getRoot(), cvs_module='trunk')
+            rcstype='cvs', cvs_root=cvs_server.getRoot(), cvs_module='trunk',
+            stacked_on_url=stacked_on_url)
 
 
 class SubversionImportHelpers:
@@ -991,7 +1004,7 @@
         self.foreign_commit_count += 1
         shutil.rmtree('working_tree')
 
-    def makeSourceDetails(self, branch_name, files):
+    def makeSourceDetails(self, branch_name, files, stacked_on_url=None):
         """Make a SVN `CodeImportSourceDetails` pointing at a real SVN repo.
         """
         svn_server = SubversionServer(self.makeTemporaryDirectory())
@@ -1002,7 +1015,8 @@
         svn_branch_url = svn_branch_url.replace('://localhost/', ':///')
         self.foreign_commit_count = 2
         return self.factory.makeCodeImportSourceDetails(
-            rcstype=self.rcstype, url=svn_branch_url)
+            rcstype=self.rcstype, url=svn_branch_url,
+            stacked_on_url=stacked_on_url)
 
 
 class TestSubversionImport(WorkerTest, SubversionImportHelpers,
@@ -1092,6 +1106,26 @@
         self.assertEqual(
             CodeImportWorkerExitCode.SUCCESS, worker.run())
 
+    def test_stacked(self):
+        stacked_on = self.make_branch('stacked-on')
+        source_details = self.makeSourceDetails(
+            'trunk', [('README', 'Original contents')],
+            stacked_on_url=stacked_on.base)
+        stacked_on.fetch(Branch.open(source_details.url))
+        self.assertTrue(len(stacked_on.repository.all_revision_ids()) > 0)
+        worker = self.makeImportWorker(
+            source_details,
+            opener_policy=AcceptAnythingPolicy())
+        self.makeForeignCommit(source_details)
+        self.assertEqual(
+            CodeImportWorkerExitCode.SUCCESS, worker.run())
+        branch = self.getStoredBazaarBranch(worker)
+        # There should only be one revision there, the other
+        # one is in the stacked-on repository.
+        self.addCleanup(stacked_on.lock_read().unlock)
+        self.assertEquals(1, len(stacked_on.repository.revisions.keys()))
+        self.assertEquals(stacked_on.base, branch.get_stacked_on_url())
+
 
 class TestGitImport(WorkerTest, TestActualImportMixin,
                     PullingImportWorkerTests):
@@ -1131,7 +1165,7 @@
             committer="Joe Random Hacker <joe@xxxxxxxxxxx>", ref=ref)
         self.foreign_commit_count += 1
 
-    def makeSourceDetails(self, branch_name, files):
+    def makeSourceDetails(self, branch_name, files, stacked_on_url=None):
         """Make a Git `CodeImportSourceDetails` pointing at a real Git repo.
         """
         repository_path = self.makeTemporaryDirectory()
@@ -1143,7 +1177,8 @@
         self.foreign_commit_count = 1
 
         return self.factory.makeCodeImportSourceDetails(
-            rcstype='git', url=git_server.get_url())
+            rcstype='git', url=git_server.get_url(),
+            stacked_on_url=stacked_on_url)
 
     def test_non_master(self):
         # non-master branches can be specified in the import URL.
@@ -1212,7 +1247,7 @@
             text=message, user="Jane Random Hacker", force=1, extra=extra)
         self.foreign_commit_count += 1
 
-    def makeSourceDetails(self, branch_name, files):
+    def makeSourceDetails(self, branch_name, files, stacked_on_url=None):
         """Make a Mercurial `CodeImportSourceDetails` pointing at a real repo.
         """
         repository_path = self.makeTemporaryDirectory()
@@ -1224,7 +1259,8 @@
         self.foreign_commit_count = 1
 
         return self.factory.makeCodeImportSourceDetails(
-            rcstype='hg', url=hg_server.get_url())
+            rcstype='hg', url=hg_server.get_url(),
+            stacked_on_url=stacked_on_url)
 
     def test_non_default(self):
         # non-default branches can be specified in the import URL.
@@ -1291,7 +1327,7 @@
             committer="Joe Random Hacker <joe@xxxxxxxxxxx>")
         self.foreign_commit_count += 1
 
-    def makeSourceDetails(self, branch_name, files):
+    def makeSourceDetails(self, branch_name, files, stacked_on_url=None):
         """Make Bzr `CodeImportSourceDetails` pointing at a real Bzr repo.
         """
         repository_path = self.makeTemporaryDirectory()
@@ -1303,7 +1339,8 @@
         self.foreign_commit_count = 1
 
         return self.factory.makeCodeImportSourceDetails(
-            rcstype='bzr', url=bzr_server.get_url())
+            rcstype='bzr', url=bzr_server.get_url(),
+            stacked_on_url=stacked_on_url)
 
     def test_partial(self):
         self.skip(
@@ -1441,3 +1478,88 @@
             self.old_server.stop_server()
         self.assertEqual(
             CodeImportWorkerExitCode.FAILURE_INVALID, worker.run())
+
+
+class CodeImportSourceDetailsTests(TestCaseWithFactory):
+
+    layer = DatabaseFunctionalLayer
+
+    def setUp(self):
+        # Use an admin user as we aren't checking edit permissions here.
+        TestCaseWithFactory.setUp(self, 'admin@xxxxxxxxxxxxx')
+
+    def test_bzr_arguments(self):
+        code_import = self.factory.makeCodeImport(
+            bzr_branch_url="http://example.com/foo";)
+        arguments = CodeImportSourceDetails.fromCodeImport(
+            code_import).asArguments()
+        self.assertEquals([
+            str(code_import.branch.id), 'bzr', 'http://example.com/foo'],
+            arguments)
+
+    def test_hg_arguments(self):
+        code_import = self.factory.makeCodeImport(
+            hg_repo_url="http://example.com/foo";)
+        arguments = CodeImportSourceDetails.fromCodeImport(
+            code_import).asArguments()
+        self.assertEquals([
+            str(code_import.branch.id), 'hg', 'http://example.com/foo'],
+            arguments)
+
+    def test_git_arguments(self):
+        code_import = self.factory.makeCodeImport(
+                git_repo_url="git://git.example.com/project.git")
+        arguments = CodeImportSourceDetails.fromCodeImport(
+            code_import).asArguments()
+        self.assertEquals([
+            str(code_import.branch.id), 'git',
+            'git://git.example.com/project.git'],
+            arguments)
+
+    def test_cvs_arguments(self):
+        code_import = self.factory.makeCodeImport(
+            cvs_root=':pserver:foo@xxxxxxxxxxx/bar', cvs_module='bar')
+        arguments = CodeImportSourceDetails.fromCodeImport(
+            code_import).asArguments()
+        self.assertEquals([
+            str(code_import.branch.id), 'cvs',
+            ':pserver:foo@xxxxxxxxxxx/bar', 'bar'],
+            arguments)
+
+    def test_svn_arguments(self):
+        code_import = self.factory.makeCodeImport(
+                svn_branch_url='svn://svn.example.com/trunk')
+        arguments = CodeImportSourceDetails.fromCodeImport(
+            code_import).asArguments()
+        self.assertEquals([
+            str(code_import.branch.id), 'svn',
+            'svn://svn.example.com/trunk'],
+            arguments)
+
+    def test_bzr_stacked(self):
+        devfocus = self.factory.makeAnyBranch(private=False)
+        devfocus.default_stacked_on_branch = devfocus
+        code_import = self.factory.makeCodeImport(
+                bzr_branch_url='bzr://bzr.example.com/foo',
+                target=devfocus.target)
+        details = CodeImportSourceDetails.fromCodeImport(
+            code_import)
+        self.assertEquals([
+            str(code_import.branch.id), 'bzr',
+            'bzr://bzr.example.com/foo',
+            compose_public_url('http', branch_id_alias(devfocus))],
+            details.asArguments())
+
+    def test_bzr_stacked_private(self):
+        # Code imports can't be stacked on private branches.
+        devfocus = self.factory.makeAnyBranch(private=True)
+        devfocus.default_stacked_on_branch = devfocus
+        code_import = self.factory.makeCodeImport(
+                target=devfocus.target,
+                bzr_branch_url='bzr://bzr.example.com/foo')
+        details = CodeImportSourceDetails.fromCodeImport(
+            code_import)
+        self.assertEquals([
+            str(code_import.branch.id), 'bzr',
+            'bzr://bzr.example.com/foo'],
+            details.asArguments())

=== modified file 'lib/lp/codehosting/codeimport/worker.py'
--- lib/lp/codehosting/codeimport/worker.py	2011-09-19 06:57:55 +0000
+++ lib/lp/codehosting/codeimport/worker.py	2011-09-26 15:09:29 +0000
@@ -41,7 +41,8 @@
     )
 from bzrlib.transport import (
     do_catching_redirections,
-    get_transport,
+    get_transport_from_path,
+    get_transport_from_url,
     )
 import bzrlib.ui
 from bzrlib.upgrade import upgrade
@@ -61,6 +62,10 @@
 from canonical.config import config
 from lp.code.enums import RevisionControlSystems
 from lp.code.interfaces.branch import get_blacklisted_hostnames
+from lp.code.interfaces.codehosting import (
+    branch_id_alias,
+    compose_public_url,
+    )
 from lp.codehosting.codeimport.foreigntree import (
     CVSWorkingTree,
     SubversionWorkingTree,
@@ -150,7 +155,7 @@
         return urljoin(self.transport.base, '%08x' % db_branch_id)
 
     def pull(self, db_branch_id, target_path, required_format,
-             needs_tree=False):
+             needs_tree=False, stacked_on_url=None):
         """Pull down the Bazaar branch of an import to `target_path`.
 
         :return: A Bazaar branch for the code import corresponding to the
@@ -164,6 +169,8 @@
                 target_path, format=required_format)
             if needs_tree:
                 local_branch.bzrdir.create_workingtree()
+            if stacked_on_url:
+                local_branch.set_stacked_on_url(stacked_on_url)
             return local_branch
         # The proper thing to do here would be to call
         # "remote_bzr_dir.sprout()".  But 2a fetch slowly checks which
@@ -172,7 +179,7 @@
         # at the vfs level.
         control_dir = remote_bzr_dir.root_transport.relpath(
             remote_bzr_dir.transport.abspath('.'))
-        target = get_transport(target_path)
+        target = get_transport_from_path(target_path)
         target_control = target.clone(control_dir)
         target_control.create_prefix()
         remote_bzr_dir.transport.copy_tree_to_transport(target_control)
@@ -187,7 +194,8 @@
             local_bzr_dir.create_workingtree()
         return local_bzr_dir.open_branch()
 
-    def push(self, db_branch_id, bzr_branch, required_format):
+    def push(self, db_branch_id, bzr_branch, required_format,
+             stacked_on_url=None):
         """Push up `bzr_branch` as the Bazaar branch for `code_import`.
 
         :return: A boolean that is true if the push was non-trivial
@@ -219,6 +227,10 @@
                     upgrade_url, format=required_format)
             else:
                 old_branch = None
+        # This can be done safely, since only modern formats are used to import
+        # to.
+        if stacked_on_url is not None:
+            remote_branch.set_stacked_on_url(stacked_on_url)
         pull_result = remote_branch.pull(bzr_branch, overwrite=True)
         # Because of the way we do incremental imports, there may be revisions
         # in the branch's repo that are not in the ancestry of the branch tip.
@@ -237,7 +249,7 @@
 def get_default_bazaar_branch_store():
     """Return the default `BazaarBranchStore`."""
     return BazaarBranchStore(
-        get_transport(config.codeimport.bazaar_branch_store))
+        get_transport_from_url(config.codeimport.bazaar_branch_store))
 
 
 class CodeImportSourceDetails:
@@ -260,12 +272,13 @@
     """
 
     def __init__(self, branch_id, rcstype, url=None, cvs_root=None,
-                 cvs_module=None):
+                 cvs_module=None, stacked_on_url=None):
         self.branch_id = branch_id
         self.rcstype = rcstype
         self.url = url
         self.cvs_root = cvs_root
         self.cvs_module = cvs_module
+        self.stacked_on_url = stacked_on_url
 
     @classmethod
     def fromArguments(cls, arguments):
@@ -273,34 +286,55 @@
         branch_id = int(arguments.pop(0))
         rcstype = arguments.pop(0)
         if rcstype in ['svn', 'bzr-svn', 'git', 'hg', 'bzr']:
-            [url] = arguments
+            url = arguments.pop(0)
+            try:
+                stacked_on_url = arguments.pop(0)
+            except IndexError:
+                stacked_on_url = None
             cvs_root = cvs_module = None
         elif rcstype == 'cvs':
             url = None
+            stacked_on_url = None
             [cvs_root, cvs_module] = arguments
         else:
             raise AssertionError("Unknown rcstype %r." % rcstype)
-        return cls(branch_id, rcstype, url, cvs_root, cvs_module)
+        return cls(
+            branch_id, rcstype, url, cvs_root, cvs_module, stacked_on_url)
 
     @classmethod
     def fromCodeImport(cls, code_import):
         """Convert a `CodeImport` to an instance."""
-        branch_id = code_import.branch.id
+        branch = code_import.branch
+        if branch.stacked_on is not None and not branch.stacked_on.private:
+            stacked_path = branch_id_alias(branch.stacked_on)
+            stacked_on_url = compose_public_url('http', stacked_path)
+        else:
+            stacked_on_url = None
         if code_import.rcs_type == RevisionControlSystems.SVN:
-            return cls(branch_id, 'svn', str(code_import.url))
+            return cls(
+                branch.id, 'svn', str(code_import.url),
+                stacked_on_url=stacked_on_url)
         elif code_import.rcs_type == RevisionControlSystems.BZR_SVN:
-            return cls(branch_id, 'bzr-svn', str(code_import.url))
+            return cls(
+                branch.id, 'bzr-svn', str(code_import.url),
+                stacked_on_url=stacked_on_url)
         elif code_import.rcs_type == RevisionControlSystems.CVS:
             return cls(
-                branch_id, 'cvs',
+                branch.id, 'cvs',
                 cvs_root=str(code_import.cvs_root),
                 cvs_module=str(code_import.cvs_module))
         elif code_import.rcs_type == RevisionControlSystems.GIT:
-            return cls(branch_id, 'git', str(code_import.url))
+            return cls(
+                branch.id, 'git', str(code_import.url),
+                stacked_on_url=stacked_on_url)
         elif code_import.rcs_type == RevisionControlSystems.HG:
-            return cls(branch_id, 'hg', str(code_import.url))
+            return cls(
+                branch.id, 'hg', str(code_import.url),
+                stacked_on_url=stacked_on_url)
         elif code_import.rcs_type == RevisionControlSystems.BZR:
-            return cls(branch_id, 'bzr', str(code_import.url))
+            return cls(
+                branch.id, 'bzr', str(code_import.url),
+                stacked_on_url=stacked_on_url)
         else:
             raise AssertionError("Unknown rcstype %r." % code_import.rcs_type)
 
@@ -310,6 +344,8 @@
         result = [str(self.branch_id), self.rcstype]
         if self.rcstype in ['svn', 'bzr-svn', 'git', 'hg', 'bzr']:
             result.append(self.url)
+            if self.stacked_on_url is not None:
+                result.append(self.stacked_on_url)
         elif self.rcstype == 'cvs':
             result.append(self.cvs_root)
             result.append(self.cvs_module)
@@ -367,12 +403,12 @@
         :param filename: The name of the file to retrieve (must be a filename,
             not a path).
         :param dest_transport: The transport to retrieve the file to,
-            defaulting to ``get_transport('.')``.
+            defaulting to ``get_transport_from_path('.')``.
         :return: A boolean, true if the file was found and retrieved, false
             otherwise.
         """
         if dest_transport is None:
-            dest_transport = get_transport('.')
+            dest_transport = get_transport_from_path('.')
         remote_name = self._getRemoteName(filename)
         if self._transport.has(remote_name):
             dest_transport.put_file(
@@ -390,7 +426,7 @@
             defaulting to ``get_transport('.')``.
         """
         if source_transport is None:
-            source_transport = get_transport('.')
+            source_transport = get_transport_from_path('.')
         remote_name = self._getRemoteName(filename)
         local_file = source_transport.get(filename)
         self._transport.create_prefix()
@@ -506,7 +542,8 @@
             shutil.rmtree(self.BZR_BRANCH_PATH)
         return self.bazaar_branch_store.pull(
             self.source_details.branch_id, self.BZR_BRANCH_PATH,
-            self.required_format, self.needs_bzr_tree)
+            self.required_format, self.needs_bzr_tree,
+            stacked_on_url=self.source_details.stacked_on_url)
 
     def pushBazaarBranch(self, bazaar_branch):
         """Push the updated Bazaar branch to the server.
@@ -515,7 +552,8 @@
         """
         return self.bazaar_branch_store.push(
             self.source_details.branch_id, bazaar_branch,
-            self.required_format)
+            self.required_format,
+            stacked_on_url=self.source_details.stacked_on_url)
 
     def getWorkingDirectory(self):
         """The directory we should change to and store all scratch files in.
@@ -699,7 +737,7 @@
                     last_error = e
             else:
                 raise last_error
-        transport = get_transport(url)
+        transport = get_transport_from_url(url)
         transport, format = do_catching_redirections(find_format, transport,
             redirected)
         return format.open(transport)

=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py	2011-09-23 13:14:36 +0000
+++ lib/lp/testing/factory.py	2011-09-26 15:09:29 +0000
@@ -472,7 +472,8 @@
         return epoch + timedelta(minutes=self.getUniqueInteger())
 
     def makeCodeImportSourceDetails(self, branch_id=None, rcstype=None,
-                                    url=None, cvs_root=None, cvs_module=None):
+                                    url=None, cvs_root=None, cvs_module=None,
+                                    stacked_on_url=None):
         if branch_id is None:
             branch_id = self.getUniqueInteger()
         if rcstype is None:
@@ -494,7 +495,8 @@
         else:
             raise AssertionError("Unknown rcstype %r." % rcstype)
         return CodeImportSourceDetails(
-            branch_id, rcstype, url, cvs_root, cvs_module)
+            branch_id, rcstype, url, cvs_root, cvs_module,
+            stacked_on_url=stacked_on_url)
 
 
 class BareLaunchpadObjectFactory(ObjectFactory):