← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~cjwatson/launchpad/codeimport-git-worker into lp:launchpad

 

Colin Watson has proposed merging lp:~cjwatson/launchpad/codeimport-git-worker into lp:launchpad with lp:~cjwatson/launchpad/codeimport-create-hosting as a prerequisite.

Commit message:
Add a Git-to-Git import worker.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #1469459 in Launchpad itself: "import external code into a LP git repo (natively)"
  https://bugs.launchpad.net/launchpad/+bug/1469459

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/codeimport-git-worker/+merge/308142

This takes the easy approach for now: clone existing imported repository, fetch-mirror remote repository into it, and push it back.  Once turnip has more advanced plumbing then we can always switch over to use that (although it would involve some work setting up a turnip fixture).
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/codeimport-git-worker into lp:launchpad.
=== modified file 'configs/development/launchpad-lazr.conf'
--- configs/development/launchpad-lazr.conf	2016-06-30 16:05:11 +0000
+++ configs/development/launchpad-lazr.conf	2016-10-11 15:36:32 +0000
@@ -55,6 +55,7 @@
 
 [codeimport]
 bazaar_branch_store: file:///tmp/bazaar-branches
+git_repository_store: https://git.launchpad.dev/
 foreign_tree_store: file:///tmp/foreign-branches
 
 [codeimportdispatcher]

=== modified file 'configs/testrunner-appserver/launchpad-lazr.conf'
--- configs/testrunner-appserver/launchpad-lazr.conf	2014-02-27 08:39:44 +0000
+++ configs/testrunner-appserver/launchpad-lazr.conf	2016-10-11 15:36:32 +0000
@@ -8,6 +8,9 @@
 [codehosting]
 launch: False
 
+[codeimport]
+macaroon_secret_key: dev-macaroon-secret
+
 [google_test_service]
 launch: False
 

=== modified file 'lib/lp/codehosting/codeimport/tests/servers.py'
--- lib/lp/codehosting/codeimport/tests/servers.py	2016-02-05 16:51:12 +0000
+++ lib/lp/codehosting/codeimport/tests/servers.py	2016-10-11 15:36:32 +0000
@@ -253,12 +253,15 @@
         else:
             return local_path_to_url(self.repository_path)
 
-    def createRepository(self, path):
-        GitRepo.init(path)
+    def createRepository(self, path, bare=False):
+        if bare:
+            GitRepo.init_bare(path)
+        else:
+            GitRepo.init(path)
 
-    def start_server(self):
+    def start_server(self, bare=False):
         super(GitServer, self).start_server()
-        self.createRepository(self.repository_path)
+        self.createRepository(self.repository_path, bare=bare)
         if self._use_server:
             repo = GitRepo(self.repository_path)
             self._server = TCPGitServerThread(

=== modified file 'lib/lp/codehosting/codeimport/tests/test_worker.py'
--- lib/lp/codehosting/codeimport/tests/test_worker.py	2016-10-11 15:36:32 +0000
+++ lib/lp/codehosting/codeimport/tests/test_worker.py	2016-10-11 15:36:32 +0000
@@ -45,16 +45,26 @@
     )
 from dulwich.repo import Repo as GitRepo
 from fixtures import FakeLogger
+from pymacaroons import Macaroon
 import subvertpy
 import subvertpy.client
 import subvertpy.ra
-from testtools.matchers import Equals
+from testtools.matchers import (
+    Equals,
+    Matcher,
+    MatchesListwise,
+    Mismatch,
+    )
+from zope.component import getUtility
 
 from lp.app.enums import InformationType
+from lp.code.enums import TargetRevisionControlSystems
 from lp.code.interfaces.codehosting import (
     branch_id_alias,
     compose_public_url,
     )
+from lp.code.interfaces.codeimportjob import ICodeImportJobWorkflow
+from lp.code.tests.helpers import GitHostingFixture
 import lp.codehosting
 from lp.codehosting.codeimport.tarball import (
     create_tarball,
@@ -90,7 +100,9 @@
 from lp.codehosting.tests.helpers import create_branch_with_one_revision
 from lp.services.config import config
 from lp.services.log.logger import BufferLogger
+from lp.services.macaroons.interfaces import IMacaroonIssuer
 from lp.testing import (
+    celebrity_logged_in,
     TestCase,
     TestCaseWithFactory,
     )
@@ -1049,7 +1061,7 @@
         worker = self.makeImportWorker(
             self.factory.makeCodeImportSourceDetails(
                 rcstype=self.rcstype, url="file:///local/path"),
-            opener_policy=CodeImportBranchOpenPolicy("bzr"))
+            opener_policy=CodeImportBranchOpenPolicy("bzr", "bzr"))
         self.assertEqual(
             CodeImportWorkerExitCode.FAILURE_FORBIDDEN, worker.run())
 
@@ -1285,7 +1297,7 @@
 
     def setUp(self):
         super(CodeImportBranchOpenPolicyTests, self).setUp()
-        self.policy = CodeImportBranchOpenPolicy("bzr")
+        self.policy = CodeImportBranchOpenPolicy("bzr", "bzr")
 
     def test_follows_references(self):
         self.assertEquals(True, self.policy.shouldFollowReferences())
@@ -1309,14 +1321,22 @@
         self.assertGoodUrl("bzr://bzr.example.com/somebzrurl/")
         self.assertBadUrl("bzr://bazaar.launchpad.dev/example")
 
-    def test_checkOneURL_git(self):
-        self.policy = CodeImportBranchOpenPolicy("git")
+    def test_checkOneURL_git_to_bzr(self):
+        self.policy = CodeImportBranchOpenPolicy("git", "bzr")
         self.assertBadUrl("/etc/passwd")
         self.assertBadUrl("file:///etc/passwd")
         self.assertBadUrl("unknown-scheme://devpad/")
         self.assertGoodUrl("git://git.example.com/repo")
         self.assertGoodUrl("git://git.launchpad.dev/example")
 
+    def test_checkOneURL_git_to_git(self):
+        self.policy = CodeImportBranchOpenPolicy("git", "git")
+        self.assertBadUrl("/etc/passwd")
+        self.assertBadUrl("file:///etc/passwd")
+        self.assertBadUrl("unknown-scheme://devpad/")
+        self.assertGoodUrl("git://git.example.com/repo")
+        self.assertBadUrl("git://git.launchpad.dev/example")
+
 
 class RedirectTests(http_utils.TestCaseWithRedirectedWebserver, TestCase):
 
@@ -1387,6 +1407,19 @@
             CodeImportWorkerExitCode.FAILURE_INVALID, worker.run())
 
 
+class CodeImportJobMacaroonVerifies(Matcher):
+    """Matches if a code-import-job macaroon can be verified."""
+
+    def __init__(self, context):
+        self.context = context
+
+    def match(self, macaroon_raw):
+        issuer = getUtility(IMacaroonIssuer, 'code-import-job')
+        macaroon = Macaroon.deserialize(macaroon_raw)
+        if not issuer.verifyMacaroon(macaroon, self.context):
+            return Mismatch("Macaroon '%s' does not verify" % macaroon_raw)
+
+
 class CodeImportSourceDetailsTests(TestCaseWithFactory):
 
     layer = DatabaseFunctionalLayer
@@ -1395,8 +1428,12 @@
         # Use an admin user as we aren't checking edit permissions here.
         TestCaseWithFactory.setUp(self, 'admin@xxxxxxxxxxxxx')
 
-    def assertArgumentsMatch(self, code_import, matcher):
+    def assertArgumentsMatch(self, code_import, matcher, start_job=False):
         job = self.factory.makeCodeImportJob(code_import=code_import)
+        if start_job:
+            machine = self.factory.makeCodeImportMachine(set_online=True)
+            with celebrity_logged_in("vcs_imports"):
+                getUtility(ICodeImportJobWorkflow).startJob(job, machine)
         details = CodeImportSourceDetails.fromCodeImportJob(job)
         self.assertThat(details.asArguments(), matcher)
 
@@ -1416,6 +1453,19 @@
                 str(code_import.branch.id), 'git',
                 'git://git.example.com/project.git']))
 
+    def test_git_to_git_arguments(self):
+        self.pushConfig('codeimport', macaroon_secret_key='some-secret')
+        self.useFixture(GitHostingFixture())
+        code_import = self.factory.makeCodeImport(
+            git_repo_url="git://git.example.com/project.git",
+            target_rcs_type=TargetRevisionControlSystems.GIT)
+        self.assertArgumentsMatch(
+            code_import, MatchesListwise([
+                Equals(code_import.git_repository.unique_name),
+                Equals('git:git'), Equals('git://git.example.com/project.git'),
+                CodeImportJobMacaroonVerifies(code_import.git_repository)]),
+            start_job=True)
+
     def test_cvs_arguments(self):
         code_import = self.factory.makeCodeImport(
             cvs_root=':pserver:foo@xxxxxxxxxxx/bar', cvs_module='bar')

=== modified file 'lib/lp/codehosting/codeimport/tests/test_workermonitor.py'
--- lib/lp/codehosting/codeimport/tests/test_workermonitor.py	2016-10-11 15:36:32 +0000
+++ lib/lp/codehosting/codeimport/tests/test_workermonitor.py	2016-10-11 15:36:32 +0000
@@ -11,8 +11,10 @@
 import os
 import shutil
 import StringIO
+import subprocess
 import tempfile
 import urllib
+from urlparse import urlsplit
 
 from bzrlib.branch import Branch
 from bzrlib.tests import (
@@ -40,11 +42,14 @@
     CodeImportResultStatus,
     CodeImportReviewStatus,
     RevisionControlSystems,
+    TargetRevisionControlSystems,
     )
+from lp.code.interfaces.branch import IBranch
 from lp.code.interfaces.codeimport import ICodeImportSet
 from lp.code.interfaces.codeimportjob import ICodeImportJobSet
 from lp.code.model.codeimport import CodeImport
 from lp.code.model.codeimportjob import CodeImportJob
+from lp.code.tests.helpers import GitHostingFixture
 from lp.codehosting.codeimport.tests.servers import (
     BzrServer,
     CVSServer,
@@ -65,6 +70,10 @@
     ExitQuietly,
     )
 from lp.services.config import config
+from lp.services.config.fixture import (
+    ConfigFixture,
+    ConfigUseFixture,
+    )
 from lp.services.log.logger import BufferLogger
 from lp.services.twistedsupport import suppress_stderr
 from lp.services.twistedsupport.tests.test_processmonitor import (
@@ -669,7 +678,7 @@
         return protocol
 
 
-class TestWorkerMonitorIntegration(TestCaseInTempDir):
+class TestWorkerMonitorIntegration(TestCaseInTempDir, TestCase):
 
     layer = ZopelessAppServerLayer
     run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=60)
@@ -724,7 +733,7 @@
         return self.factory.makeCodeImport(
             svn_branch_url=url, rcs_type=RevisionControlSystems.BZR_SVN)
 
-    def makeGitCodeImport(self):
+    def makeGitCodeImport(self, target_rcs_type=None):
         """Make a `CodeImport` that points to a real Git repository."""
         self.git_server = GitServer(self.repo_path, use_server=False)
         self.git_server.start_server()
@@ -734,7 +743,8 @@
         self.foreign_commit_count = 1
 
         return self.factory.makeCodeImport(
-            git_repo_url=self.git_server.get_url())
+            git_repo_url=self.git_server.get_url(),
+            target_rcs_type=target_rcs_type)
 
     def makeBzrCodeImport(self):
         """Make a `CodeImport` that points to a real Bazaar branch."""
@@ -761,8 +771,9 @@
         job = getUtility(ICodeImportJobSet).getJobForMachine('machine', 10)
         self.assertEqual(code_import, job.code_import)
         source_details = CodeImportSourceDetails.fromCodeImportJob(job)
-        clean_up_default_stores_for_import(source_details)
-        self.addCleanup(clean_up_default_stores_for_import, source_details)
+        if IBranch.providedBy(code_import.target):
+            clean_up_default_stores_for_import(source_details)
+            self.addCleanup(clean_up_default_stores_for_import, source_details)
         return job
 
     def assertCodeImportResultCreated(self, code_import):
@@ -773,10 +784,19 @@
 
     def assertBranchImportedOKForCodeImport(self, code_import):
         """Assert that a branch was pushed into the default branch store."""
-        url = get_default_bazaar_branch_store()._getMirrorURL(
-            code_import.branch.id)
-        branch = Branch.open(url)
-        self.assertEqual(self.foreign_commit_count, branch.revno())
+        if IBranch.providedBy(code_import.target):
+            url = get_default_bazaar_branch_store()._getMirrorURL(
+                code_import.branch.id)
+            branch = Branch.open(url)
+            commit_count = branch.revno()
+        else:
+            repo_path = os.path.join(
+                urlsplit(config.codeimport.git_repository_store).path,
+                code_import.target.unique_name)
+            commit_count = int(subprocess.check_output(
+                ["git", "rev-list", "--count", "HEAD"],
+                cwd=repo_path, universal_newlines=True))
+        self.assertEqual(self.foreign_commit_count, commit_count)
 
     def assertImported(self, ignored, code_import_id):
         """Assert that the `CodeImport` of the given id was imported."""
@@ -841,6 +861,35 @@
         result = self.performImport(job_id)
         return result.addCallback(self.assertImported, code_import_id)
 
+    def test_import_git_to_git(self):
+        # Create a Git-to-Git CodeImport and import it.
+        target_store = tempfile.mkdtemp()
+        self.addCleanup(shutil.rmtree, target_store)
+        config_name = self.getUniqueString()
+        config_fixture = self.useFixture(ConfigFixture(
+            config_name, self.layer.config_fixture.instance_name))
+        setting_lines = [
+            "[codeimport]",
+            "git_repository_store: file://%s" % target_store,
+            "macaroon_secret_key: some-secret",
+            ]
+        config_fixture.add_section("\n" + "\n".join(setting_lines))
+        self.useFixture(ConfigUseFixture(config_name))
+        self.useFixture(GitHostingFixture())
+        job = self.getStartedJobForImport(self.makeGitCodeImport(
+            target_rcs_type=TargetRevisionControlSystems.GIT))
+        code_import_id = job.code_import.id
+        job_id = job.id
+        self.layer.txn.commit()
+        target_repo_path = os.path.join(
+            target_store, job.code_import.target.unique_name)
+        os.makedirs(target_repo_path)
+        target_git_server = GitServer(target_repo_path, use_server=False)
+        target_git_server.start_server(bare=True)
+        self.addCleanup(target_git_server.stop_server)
+        result = self.performImport(job_id)
+        return result.addCallback(self.assertImported, code_import_id)
+
     def test_import_bzr(self):
         # Create a Bazaar CodeImport and import it.
         job = self.getStartedJobForImport(self.makeBzrCodeImport())

=== modified file 'lib/lp/codehosting/codeimport/worker.py'
--- lib/lp/codehosting/codeimport/worker.py	2016-10-11 15:36:32 +0000
+++ lib/lp/codehosting/codeimport/worker.py	2016-10-11 15:36:32 +0000
@@ -21,7 +21,13 @@
 
 
 import os
+import re
 import shutil
+import subprocess
+from urlparse import (
+    urlsplit,
+    urlunsplit,
+    )
 
 # FIRST Ensure correct plugins are loaded. Do not delete this comment or the
 # line below this comment.
@@ -60,10 +66,16 @@
     InvalidURIError,
     URI,
     )
+from pymacaroons import Macaroon
 import SCM
+from zope.component import getUtility
+from zope.security.proxy import removeSecurityProxy
 
 from lp.code.enums import RevisionControlSystems
-from lp.code.interfaces.branch import get_blacklisted_hostnames
+from lp.code.interfaces.branch import (
+    get_blacklisted_hostnames,
+    IBranch,
+    )
 from lp.code.interfaces.codehosting import (
     branch_id_alias,
     compose_public_url,
@@ -80,6 +92,7 @@
     SafeBranchOpener,
     )
 from lp.services.config import config
+from lp.services.macaroons.interfaces import IMacaroonIssuer
 from lp.services.propertycache import cachedproperty
 
 
@@ -88,14 +101,16 @@
 
     In summary:
      - follow references,
-     - only open non-Launchpad URLs for imports from Bazaar
+     - only open non-Launchpad URLs for imports from Bazaar to Bazaar or
+       from Git to Git
      - only open the allowed schemes
     """
 
     allowed_schemes = ['http', 'https', 'svn', 'git', 'ftp', 'bzr']
 
-    def __init__(self, rcstype):
+    def __init__(self, rcstype, target_rcstype):
         self.rcstype = rcstype
+        self.target_rcstype = target_rcstype
 
     def shouldFollowReferences(self):
         """See `BranchOpenPolicy.shouldFollowReferences`.
@@ -124,9 +139,7 @@
             uri = URI(url)
         except InvalidURIError:
             raise BadUrl(url)
-        # XXX cjwatson 2015-06-12: Once we have imports into Git, this
-        # should be extended to prevent Git-to-Git self-imports as well.
-        if self.rcstype == "bzr":
+        if self.rcstype == self.target_rcstype:
             launchpad_domain = config.vhost.mainsite.hostname
             if uri.underDomain(launchpad_domain):
                 raise BadUrl(url)
@@ -269,36 +282,50 @@
     of the information suitable for passing around on executables' command
     lines.
 
-    :ivar target_id: The id of the Bazaar branch associated with this code
-        import, used for locating the existing import and the foreign tree.
+    :ivar target_id: The id of the Bazaar branch or the path of the Git
+        repository associated with this code import, used for locating the
+        existing import and the foreign tree.
     :ivar rcstype: 'cvs', 'git', 'bzr-svn', 'bzr' as appropriate.
+    :ivar target_rcstype: 'bzr' or 'git' as appropriate.
     :ivar url: The branch URL if rcstype in ['bzr-svn', 'git', 'bzr'], None
         otherwise.
     :ivar cvs_root: The $CVSROOT if rcstype == 'cvs', None otherwise.
     :ivar cvs_module: The CVS module if rcstype == 'cvs', None otherwise.
     :ivar stacked_on_url: The URL of the branch that the associated branch
         is stacked on, if any.
+    :ivar macaroon: A macaroon granting authority to push to the target
+        repository if target_rcstype == 'git', None otherwise.
     """
 
-    def __init__(self, target_id, rcstype, url=None, cvs_root=None,
-                 cvs_module=None, stacked_on_url=None):
+    def __init__(self, target_id, rcstype, target_rcstype, url=None,
+                 cvs_root=None, cvs_module=None, stacked_on_url=None,
+                 macaroon=None):
         self.target_id = target_id
         self.rcstype = rcstype
+        self.target_rcstype = target_rcstype
         self.url = url
         self.cvs_root = cvs_root
         self.cvs_module = cvs_module
         self.stacked_on_url = stacked_on_url
+        self.macaroon = macaroon
 
     @classmethod
     def fromArguments(cls, arguments):
         """Convert command line-style arguments to an instance."""
-        target_id = int(arguments.pop(0))
+        target_id = arguments.pop(0)
         rcstype = arguments.pop(0)
+        if ':' in rcstype:
+            rcstype, target_rcstype = rcstype.split(':', 1)
+        else:
+            target_rcstype = 'bzr'
         if rcstype in ['bzr-svn', 'git', 'bzr']:
             url = arguments.pop(0)
-            try:
-                stacked_on_url = arguments.pop(0)
-            except IndexError:
+            if target_rcstype == 'bzr':
+                try:
+                    stacked_on_url = arguments.pop(0)
+                except IndexError:
+                    stacked_on_url = None
+            else:
                 stacked_on_url = None
             cvs_root = cvs_module = None
         elif rcstype == 'cvs':
@@ -307,35 +334,54 @@
             [cvs_root, cvs_module] = arguments
         else:
             raise AssertionError("Unknown rcstype %r." % rcstype)
+        if target_rcstype == 'bzr':
+            target_id = int(target_id)
+            macaroon = None
+        elif target_rcstype == 'git':
+            macaroon = Macaroon.deserialize(arguments.pop(0))
+        else:
+            raise AssertionError("Unknown target_rcstype %r." % target_rcstype)
         return cls(
-            target_id, rcstype, url, cvs_root, cvs_module, stacked_on_url)
+            target_id, rcstype, target_rcstype, url, cvs_root, cvs_module,
+            stacked_on_url, macaroon)
 
     @classmethod
     def fromCodeImportJob(cls, job):
         """Convert a `CodeImportJob` to an instance."""
         code_import = job.code_import
         target = code_import.target
-        if target.stacked_on is not None and not target.stacked_on.private:
-            stacked_path = branch_id_alias(target.stacked_on)
-            stacked_on_url = compose_public_url('http', stacked_path)
+        if IBranch.providedBy(target):
+            if target.stacked_on is not None and not target.stacked_on.private:
+                stacked_path = branch_id_alias(target.stacked_on)
+                stacked_on_url = compose_public_url('http', stacked_path)
+            else:
+                stacked_on_url = None
+            target_id = target.id
         else:
-            stacked_on_url = None
+            target_id = target.unique_name
         if code_import.rcs_type == RevisionControlSystems.BZR_SVN:
             return cls(
-                target.id, 'bzr-svn', str(code_import.url),
+                target_id, 'bzr-svn', 'bzr', str(code_import.url),
                 stacked_on_url=stacked_on_url)
         elif code_import.rcs_type == RevisionControlSystems.CVS:
             return cls(
-                target.id, 'cvs',
+                target_id, 'cvs', 'bzr',
                 cvs_root=str(code_import.cvs_root),
                 cvs_module=str(code_import.cvs_module))
         elif code_import.rcs_type == RevisionControlSystems.GIT:
-            return cls(
-                target.id, 'git', str(code_import.url),
-                stacked_on_url=stacked_on_url)
+            if IBranch.providedBy(target):
+                return cls(
+                    target_id, 'git', 'bzr', str(code_import.url),
+                    stacked_on_url=stacked_on_url)
+            else:
+                issuer = getUtility(IMacaroonIssuer, 'code-import-job')
+                macaroon = removeSecurityProxy(issuer).issueMacaroon(job)
+                return cls(
+                    target_id, 'git', 'git', str(code_import.url),
+                    macaroon=macaroon)
         elif code_import.rcs_type == RevisionControlSystems.BZR:
             return cls(
-                target.id, 'bzr', str(code_import.url),
+                target_id, 'bzr', 'bzr', str(code_import.url),
                 stacked_on_url=stacked_on_url)
         else:
             raise AssertionError("Unknown rcstype %r." % code_import.rcs_type)
@@ -343,7 +389,14 @@
     def asArguments(self):
         """Return a list of arguments suitable for passing to a child process.
         """
-        result = [str(self.target_id), self.rcstype]
+        result = [str(self.target_id)]
+        if self.target_rcstype == 'bzr':
+            result.append(self.rcstype)
+        elif self.target_rcstype == 'git':
+            result.append('%s:%s' % (self.rcstype, self.target_rcstype))
+        else:
+            raise AssertionError(
+                "Unknown target_rcstype %r." % self.target_rcstype)
         if self.rcstype in ['bzr-svn', 'git', 'bzr']:
             result.append(self.url)
             if self.stacked_on_url is not None:
@@ -353,6 +406,8 @@
             result.append(self.cvs_module)
         else:
             raise AssertionError("Unknown rcstype %r." % self.rcstype)
+        if self.target_rcstype == 'git':
+            result.append(self.macaroon.serialize())
         return result
 
 
@@ -918,3 +973,67 @@
         """See `PullingImportWorker.probers`."""
         from bzrlib.bzrdir import BzrProber, RemoteBzrProber
         return [BzrProber, RemoteBzrProber]
+
+
+class GitToGitImportWorker(ImportWorker):
+    """An import worker for imports from Git to Git."""
+
+    def _runGit(self, *args, **kwargs):
+        """Run git with arguments, sending output to the logger."""
+        cmd = ["git"] + list(args)
+        git_process = subprocess.Popen(
+            cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, **kwargs)
+        for line in git_process.stdout:
+            line = line.decode("UTF-8", "replace").rstrip("\n")
+            # Remove any user/password data from URLs.
+            line = re.sub(r"://([^:]*:[^@]*@)(\S+)", r"://\2", line)
+            self._logger.info(line)
+        retcode = git_process.wait()
+        if retcode:
+            raise subprocess.CalledProcessError(retcode, cmd)
+
+    def _doImport(self):
+        self._logger.info("Starting job.")
+        self._logger.info(config.codeimport.git_repository_store)
+        try:
+            self._opener_policy.checkOneURL(self.source_details.url)
+        except BadUrl as e:
+            self._logger.info("Invalid URL: %s" % e)
+            return CodeImportWorkerExitCode.FAILURE_FORBIDDEN
+        unauth_target_url = urljoin(
+            config.codeimport.git_repository_store,
+            self.source_details.target_id)
+        split = urlsplit(unauth_target_url)
+        if split.hostname:
+            target_netloc = ":%s@%s" % (
+                self.source_details.macaroon.serialize(), split.hostname)
+        else:
+            target_netloc = ""
+        target_url = urlunsplit([
+            split.scheme, target_netloc, split.path, "", ""])
+        # XXX cjwatson 2016-10-11: Ideally we'd put credentials in a
+        # credentials store instead.  However, git only accepts credentials
+        # that have both a non-empty username and a non-empty password.
+        self._logger.info("Getting existing repository from hosting service.")
+        try:
+            self._runGit("clone", "--bare", target_url, "repository")
+        except subprocess.CalledProcessError as e:
+            self._logger.info(
+                "Unable to get existing repository from hosting service: "
+                "%s" % e)
+            return CodeImportWorkerExitCode.FAILURE
+        self._logger.info("Fetching remote repository.")
+        try:
+            self._runGit(
+                "remote", "add", "-f", "--mirror=fetch",
+                "mirror", self.source_details.url, cwd="repository")
+        except subprocess.CalledProcessError as e:
+            self._logger.info("Unable to fetch remote repository: %s" % e)
+            return CodeImportWorkerExitCode.FAILURE_INVALID
+        self._logger.info("Pushing repository to hosting service.")
+        try:
+            self._runGit("push", "--mirror", target_url, cwd="repository")
+        except subprocess.CalledProcessError as e:
+            self._logger.info("Unable to push to hosting service: %s" % e)
+            return CodeImportWorkerExitCode.FAILURE
+        return CodeImportWorkerExitCode.SUCCESS

=== modified file 'lib/lp/services/config/fixture.py'
--- lib/lp/services/config/fixture.py	2011-12-29 05:29:36 +0000
+++ lib/lp/services/config/fixture.py	2016-10-11 15:36:32 +0000
@@ -1,4 +1,4 @@
-# Copyright 2010 Canonical Ltd.  This software is licensed under the
+# Copyright 2010-2016 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Fixtures related to configs.
@@ -50,11 +50,11 @@
 
     def setUp(self):
         super(ConfigFixture, self).setUp()
-        root = 'configs/' + self.instance_name
+        root = os.path.join(config.root, 'configs', self.instance_name)
         os.mkdir(root)
         self.absroot = os.path.abspath(root)
         self.addCleanup(shutil.rmtree, self.absroot)
-        source = 'configs/' + self.copy_from_instance
+        source = os.path.join(config.root, 'configs', self.copy_from_instance)
         for basename in os.listdir(source):
             if basename == 'launchpad-lazr.conf':
                 self.add_section(self._extend_str % self.copy_from_instance)

=== modified file 'lib/lp/services/config/schema-lazr.conf'
--- lib/lp/services/config/schema-lazr.conf	2016-10-11 15:36:32 +0000
+++ lib/lp/services/config/schema-lazr.conf	2016-10-11 15:36:32 +0000
@@ -378,6 +378,10 @@
 # datatype: string
 bazaar_branch_store: sftp://hoover@escudero/srv/importd/www/
 
+# Where the Git imports are stored.
+# datatype: string
+git_repository_store: none
+
 # The default value of the update interval of a code import from
 # Subversion, in seconds.
 # datatype: integer
@@ -417,7 +421,7 @@
 svn_revisions_import_limit: 500
 
 # Secret key for macaroons used to grant git push permission to workers.
-macaroon_secret_key:
+macaroon_secret_key: none
 
 [codeimportdispatcher]
 # The directory where the code import worker should be directed to

=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py	2016-10-11 15:36:32 +0000
+++ lib/lp/testing/factory.py	2016-10-11 15:36:32 +0000
@@ -493,12 +493,15 @@
         return epoch + timedelta(minutes=self.getUniqueInteger())
 
     def makeCodeImportSourceDetails(self, target_id=None, rcstype=None,
-                                    url=None, cvs_root=None, cvs_module=None,
-                                    stacked_on_url=None):
+                                    target_rcstype=None, url=None,
+                                    cvs_root=None, cvs_module=None,
+                                    stacked_on_url=None, macaroon=None):
         if target_id is None:
             target_id = self.getUniqueInteger()
         if rcstype is None:
             rcstype = 'bzr-svn'
+        if target_rcstype is None:
+            target_rcstype = 'bzr'
         if rcstype in ['bzr-svn', 'bzr']:
             assert cvs_root is cvs_module is None
             if url is None:
@@ -516,8 +519,8 @@
         else:
             raise AssertionError("Unknown rcstype %r." % rcstype)
         return CodeImportSourceDetails(
-            target_id, rcstype, url, cvs_root, cvs_module,
-            stacked_on_url=stacked_on_url)
+            target_id, rcstype, target_rcstype, url, cvs_root, cvs_module,
+            stacked_on_url=stacked_on_url, macaroon=macaroon)
 
 
 class BareLaunchpadObjectFactory(ObjectFactory):

=== modified file 'scripts/code-import-worker.py'
--- scripts/code-import-worker.py	2015-06-12 14:20:12 +0000
+++ scripts/code-import-worker.py	2016-10-11 15:36:32 +0000
@@ -1,6 +1,6 @@
 #!/usr/bin/python -S
 #
-# Copyright 2009-2015 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2016 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Process a code import described by the command line arguments.
@@ -30,6 +30,7 @@
     CSCVSImportWorker,
     get_default_bazaar_branch_store,
     GitImportWorker,
+    GitToGitImportWorker,
     )
 from lp.codehosting.safe_open import AcceptAnythingPolicy
 from lp.services import scripts
@@ -37,7 +38,7 @@
 
 
 opener_policies = {
-    "anything": lambda rcstype: AcceptAnythingPolicy(),
+    "anything": lambda rcstype, target_rcstype: AcceptAnythingPolicy(),
     "default": CodeImportBranchOpenPolicy,
     }
 
@@ -72,7 +73,10 @@
         force_bzr_to_use_urllib()
         source_details = CodeImportSourceDetails.fromArguments(self.args)
         if source_details.rcstype == 'git':
-            import_worker_cls = GitImportWorker
+            if source_details.target_rcstype == 'bzr':
+                import_worker_cls = GitImportWorker
+            else:
+                import_worker_cls = GitToGitImportWorker
         elif source_details.rcstype == 'bzr-svn':
             import_worker_cls = BzrSvnImportWorker
         elif source_details.rcstype == 'bzr':
@@ -83,11 +87,15 @@
             raise AssertionError(
                 'unknown rcstype %r' % source_details.rcstype)
         opener_policy = opener_policies[self.options.access_policy](
-            source_details.rcstype)
-        import_worker = import_worker_cls(
-            source_details,
-            get_transport(config.codeimport.foreign_tree_store),
-            get_default_bazaar_branch_store(), self.logger, opener_policy)
+            source_details.rcstype, source_details.target_rcstype)
+        if source_details.target_rcstype == 'bzr':
+            import_worker = import_worker_cls(
+                source_details,
+                get_transport(config.codeimport.foreign_tree_store),
+                get_default_bazaar_branch_store(), self.logger, opener_policy)
+        else:
+            import_worker = import_worker_cls(
+                source_details, self.logger, opener_policy)
         return import_worker.run()
 
 


Follow ups