← Back to team overview

launchpad-reviewers team mailing list archive

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

 

Colin Watson has proposed merging lp:~cjwatson/launchpad/codeimport-git-model into lp:launchpad with lp:~cjwatson/launchpad/git-repository-type as a prerequisite.

Commit message:
Add basic model for code imports that target Git.

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-model/+merge/307466

Add basic model for code imports that target Git.

This is overlong, but since I had to introduce the concept of a different target revision control system type here, it was tough to make it any shorter.  ICodeImport.git_repository is exported read-only on the webservice, but that should be pretty harmless; nothing else here should be user-visible yet.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/codeimport-git-model into lp:launchpad.
=== modified file 'lib/lp/code/configure.zcml'
--- lib/lp/code/configure.zcml	2016-06-20 20:32:36 +0000
+++ lib/lp/code/configure.zcml	2016-10-03 17:03:17 +0000
@@ -633,6 +633,8 @@
     <allow attributes="id
                        date_created
                        branch
+                       git_repository
+                       target
                        registrant
                        owner
                        assignee

=== modified file 'lib/lp/code/doc/codeimport.txt'
--- lib/lp/code/doc/codeimport.txt	2016-10-03 17:03:12 +0000
+++ lib/lp/code/doc/codeimport.txt	2016-10-03 17:03:17 +0000
@@ -52,8 +52,12 @@
 
 The rcs_type field, which indicates whether the import is from CVS or
 Subversion, takes values from the 'RevisionControlSystems' vocabulary.
+Similarly, target_rcs_type takes values from 'TargetRevisionControlSystems'.
 
-    >>> from lp.code.enums import RevisionControlSystems
+    >>> from lp.code.enums import (
+    ...     RevisionControlSystems,
+    ...     TargetRevisionControlSystems,
+    ...     )
     >>> for item in RevisionControlSystems:
     ...     print item.title
     Concurrent Versions System
@@ -62,6 +66,10 @@
     Git
     Mercurial
     Bazaar
+    >>> for item in TargetRevisionControlSystems:
+    ...     print item.title
+    Bazaar
+    Git
 
 
 Import from CVS
@@ -71,12 +79,14 @@
 in the repository, known as the "module".
 
     >>> cvs = RevisionControlSystems.CVS
+    >>> target_bzr = TargetRevisionControlSystems.BZR
     >>> cvs_root = ':pserver:anonymous@xxxxxxxxxxxxxxx:/cvsroot'
     >>> cvs_module = 'hello'
     >>> context = factory.makeProduct(name='widget')
     >>> cvs_import = code_import_set.new(
     ...     registrant=nopriv, context=context, branch_name='trunk-cvs',
-    ...     rcs_type=cvs, cvs_root=cvs_root, cvs_module=cvs_module)
+    ...     rcs_type=cvs, cvs_root=cvs_root, cvs_module=cvs_module,
+    ...     target_rcs_type=target_bzr)
     >>> verifyObject(ICodeImport, removeSecurityProxy(cvs_import))
     True
 
@@ -136,7 +146,7 @@
     >>> svn_url = 'svn://svn.example.com/trunk'
     >>> svn_import = code_import_set.new(
     ...     registrant=nopriv, context=context, branch_name='trunk-svn',
-    ...     rcs_type=svn, url=svn_url)
+    ...     rcs_type=svn, url=svn_url, target_rcs_type=target_bzr)
     >>> verifyObject(ICodeImport, removeSecurityProxy(svn_import))
     True
 
@@ -164,7 +174,7 @@
     >>> git_url = 'git://git.example.com/hello.git'
     >>> git_import = code_import_set.new(
     ...     registrant=nopriv, context=context, branch_name='trunk-git',
-    ...     rcs_type=git, url=git_url)
+    ...     rcs_type=git, url=git_url, target_rcs_type=target_bzr)
     >>> verifyObject(ICodeImport, removeSecurityProxy(git_import))
     True
 

=== modified file 'lib/lp/code/emailtemplates/code-import-status-updated.txt'
--- lib/lp/code/emailtemplates/code-import-status-updated.txt	2011-12-18 22:31:46 +0000
+++ lib/lp/code/emailtemplates/code-import-status-updated.txt	2016-10-03 17:03:17 +0000
@@ -3,5 +3,5 @@
 %(body)s
 
 -- 
-%(branch)s
+%(target)s
 %(rationale)s%(unsubscribe)s

=== modified file 'lib/lp/code/emailtemplates/new-code-import.txt'
--- lib/lp/code/emailtemplates/new-code-import.txt	2011-12-18 22:31:46 +0000
+++ lib/lp/code/emailtemplates/new-code-import.txt	2016-10-03 17:03:17 +0000
@@ -1,5 +1,5 @@
 A new %(rcs_type)s code import has been requested by %(person)s:
-    %(branch)s
+    %(target)s
 from
     %(location)s
 

=== modified file 'lib/lp/code/enums.py'
--- lib/lp/code/enums.py	2016-10-03 17:03:12 +0000
+++ lib/lp/code/enums.py	2016-10-03 17:03:17 +0000
@@ -1,4 +1,4 @@
-# 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).
 
 """Enumerations used in the lp/code modules."""
@@ -24,6 +24,7 @@
     'GitRepositoryType',
     'NON_CVS_RCS_TYPES',
     'RevisionControlSystems',
+    'TargetRevisionControlSystems',
     ]
 
 from lazr.enum import (
@@ -400,6 +401,25 @@
         """)
 
 
+class TargetRevisionControlSystems(EnumeratedType):
+    """Target Revision Control Systems
+
+    Revision control systems that can be the target of a code import.
+    """
+
+    BZR = Item("""
+        Bazaar
+
+        Import to Bazaar.
+        """)
+
+    GIT = Item("""
+        Git
+
+        Import to Git.
+        """)
+
+
 class CodeImportReviewStatus(DBEnumeratedType):
     """CodeImport review status.
 

=== modified file 'lib/lp/code/interfaces/codeimport.py'
--- lib/lp/code/interfaces/codeimport.py	2016-09-30 13:27:27 +0000
+++ lib/lp/code/interfaces/codeimport.py	2016-10-03 17:03:17 +0000
@@ -43,6 +43,7 @@
     RevisionControlSystems,
     )
 from lp.code.interfaces.branch import IBranch
+from lp.code.interfaces.gitrepository import IGitRepository
 from lp.services.fields import (
     PublicPersonChoice,
     URIField,
@@ -86,10 +87,18 @@
 
     branch = exported(
         ReferenceChoice(
-            title=_('Branch'), required=True, readonly=True,
+            title=_('Branch'), required=False, readonly=True,
             vocabulary='Branch', schema=IBranch,
             description=_("The Bazaar branch produced by the "
                 "import system.")))
+    git_repository = exported(
+        ReferenceChoice(
+            title=_('Git repository'), required=False, readonly=True,
+            vocabulary='GitRepository', schema=IGitRepository,
+            description=_(
+                "The Git repository produced by the import system.")))
+    target = Attribute(
+        "The branch/repository produced by the import system (VCS-agnostic).")
 
     registrant = PublicPersonChoice(
         title=_('Registrant'), required=True, readonly=True,
@@ -220,8 +229,8 @@
 class ICodeImportSet(Interface):
     """Interface representing the set of code imports."""
 
-    def new(registrant, context, branch_name, rcs_type, url=None,
-            cvs_root=None, cvs_module=None, review_status=None,
+    def new(registrant, context, branch_name, rcs_type, target_rcs_type,
+            url=None, cvs_root=None, cvs_module=None, review_status=None,
             owner=None):
         """Create a new CodeImport.
 
@@ -238,7 +247,10 @@
         """
 
     def getByBranch(branch):
-        """Get the CodeImport, if any, associated to a Branch."""
+        """Get the CodeImport, if any, associated with a Branch."""
+
+    def getByGitRepository(repository):
+        """Get the CodeImport, if any, associated with a GitRepository."""
 
     def getByCVSDetails(cvs_root, cvs_module):
         """Get the CodeImport with the specified CVS details."""

=== modified file 'lib/lp/code/interfaces/gitnamespace.py'
--- lib/lp/code/interfaces/gitnamespace.py	2016-10-03 17:03:12 +0000
+++ lib/lp/code/interfaces/gitnamespace.py	2016-10-03 17:03:17 +0000
@@ -1,4 +1,4 @@
-# Copyright 2015 Canonical Ltd.  This software is licensed under the
+# Copyright 2015-2016 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Interface for a Git repository namespace."""
@@ -22,6 +22,7 @@
 from lp.registry.interfaces.distributionsourcepackage import (
     IDistributionSourcePackage,
     )
+from lp.registry.interfaces.person import IPerson
 from lp.registry.interfaces.product import IProduct
 
 
@@ -96,6 +97,9 @@
     supports_merge_proposals = Attribute(
         "Does this namespace support merge proposals at all?")
 
+    supports_code_imports = Attribute(
+        "Does this namespace support code imports at all?")
+
     allow_recipe_name_from_target = Attribute(
         "Can recipe names reasonably be generated from the target name "
         "rather than the branch name?")
@@ -188,8 +192,10 @@
         return getUtility(IGitNamespaceSet).get(
             owner, distribution=target.distribution,
             sourcepackagename=target.sourcepackagename)
+    elif IPerson.providedBy(target):
+        return getUtility(IGitNamespaceSet).get(owner)
     else:
-        return getUtility(IGitNamespaceSet).get(owner)
+        raise AssertionError("No Git namespace defined for %s" % target)
 
 
 # Marker for references to Git URL layouts: ##GITNAMESPACE##

=== modified file 'lib/lp/code/mail/codeimport.py'
--- lib/lp/code/mail/codeimport.py	2016-10-03 17:03:12 +0000
+++ lib/lp/code/mail/codeimport.py	2016-10-03 17:03:17 +0000
@@ -39,7 +39,7 @@
         # test.
         return
     user = IPerson(event.user)
-    subject = 'New code import: %s' % code_import.branch.unique_name
+    subject = 'New code import: %s' % code_import.target.unique_name
     if code_import.rcs_type == RevisionControlSystems.CVS:
         location = '%s, %s' % (code_import.cvs_root, code_import.cvs_module)
     else:
@@ -52,7 +52,7 @@
         }
     body = get_email_template('new-code-import.txt', app='code') % {
         'person': code_import.registrant.displayname,
-        'branch': canonical_url(code_import.branch),
+        'target': canonical_url(code_import.target),
         'rcs_type': rcs_type_map[code_import.rcs_type],
         'location': location,
         }
@@ -61,7 +61,7 @@
         user.displayname, user.preferredemail.email)
 
     vcs_imports = getUtility(ILaunchpadCelebrities).vcs_imports
-    headers = {'X-Launchpad-Branch': code_import.branch.unique_name,
+    headers = {'X-Launchpad-Branch': code_import.target.unique_name,
                'X-Launchpad-Message-Rationale':
                    'Operator @%s' % vcs_imports.name,
                'X-Launchpad-Message-For': vcs_imports.name,
@@ -103,7 +103,7 @@
             raise AssertionError('Unexpected review status for code import.')
 
     details_change_prefix = '\n'.join(textwrap.wrap(
-        "%s is now being imported from:" % code_import.branch.unique_name))
+        "%s is now being imported from:" % code_import.target.unique_name))
     if code_import.rcs_type == RevisionControlSystems.CVS:
         if (CodeImportEventDataType.OLD_CVS_ROOT in event_data or
             CodeImportEventDataType.OLD_CVS_MODULE in event_data):
@@ -142,26 +142,26 @@
 
 
 def code_import_updated(code_import, event, new_whiteboard, person):
-    """Email the branch subscribers, and the vcs-imports team with new status.
+    """Email the target subscribers, and the vcs-imports team with new status.
     """
-    branch = code_import.branch
-    recipients = branch.getNotificationRecipients()
+    target = code_import.target
+    recipients = target.getNotificationRecipients()
     # Add in the vcs-imports user.
     vcs_imports = getUtility(ILaunchpadCelebrities).vcs_imports
     herder_rationale = 'Operator @%s' % vcs_imports.name
     recipients.add(vcs_imports, None, herder_rationale)
 
-    headers = {'X-Launchpad-Branch': branch.unique_name}
+    headers = {'X-Launchpad-Branch': target.unique_name}
 
     subject = 'Code import %s status: %s' % (
-        code_import.branch.unique_name, code_import.review_status.title)
+        code_import.target.unique_name, code_import.review_status.title)
 
     email_template = get_email_template(
         'code-import-status-updated.txt', app='code')
     template_params = {
         'body': make_email_body_for_code_import_update(
             code_import, event, new_whiteboard),
-        'branch': canonical_url(code_import.branch)}
+        'target': canonical_url(code_import.target)}
 
     if person:
         from_address = format_address(
@@ -194,7 +194,7 @@
                     # Give the users a link to unsubscribe.
                     template_params['unsubscribe'] = (
                         "\nTo unsubscribe from this branch go to "
-                        "%s/+edit-subscription." % canonical_url(branch))
+                        "%s/+edit-subscription." % canonical_url(target))
                 else:
                     template_params['unsubscribe'] = ''
                 for_person = subscription.person

=== modified file 'lib/lp/code/mail/tests/test_codeimport.py'
--- lib/lp/code/mail/tests/test_codeimport.py	2014-06-10 16:13:03 +0000
+++ lib/lp/code/mail/tests/test_codeimport.py	2016-10-03 17:03:17 +0000
@@ -1,4 +1,4 @@
-# Copyright 2010-2014 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).
 
 """Tests for code import related mailings"""
@@ -7,7 +7,10 @@
 
 import transaction
 
-from lp.code.enums import RevisionControlSystems
+from lp.code.enums import (
+    RevisionControlSystems,
+    TargetRevisionControlSystems,
+    )
 from lp.services.mail import stub
 from lp.testing import (
     login_person,
@@ -21,8 +24,8 @@
 
     layer = DatabaseFunctionalLayer
 
-    def test_cvs_import(self):
-        # Test the email for a new CVS import.
+    def test_cvs_to_bzr_import(self):
+        # Test the email for a new CVS-to-Bazaar import.
         eric = self.factory.makePerson(name='eric')
         fooix = self.factory.makeProduct(name='fooix')
         # Eric needs to be logged in for the mail to be sent.
@@ -44,8 +47,8 @@
             '-- \nYou are getting this email because you are a member of the '
             'vcs-imports team.\n', msg.get_payload(decode=True))
 
-    def test_svn_import(self):
-        # Test the email for a new subversion import.
+    def test_svn_to_bzr_import(self):
+        # Test the email for a new Subversion-to-Bazaar import.
         eric = self.factory.makePerson(name='eric')
         fooix = self.factory.makeProduct(name='fooix')
         # Eric needs to be logged in for the mail to be sent.
@@ -67,8 +70,8 @@
             '-- \nYou are getting this email because you are a member of the '
             'vcs-imports team.\n', msg.get_payload(decode=True))
 
-    def test_git_import(self):
-        # Test the email for a new git import.
+    def test_git_to_bzr_import(self):
+        # Test the email for a new git-to-Bazaar import.
         eric = self.factory.makePerson(name='eric')
         fooix = self.factory.makeProduct(name='fooix')
         # Eric needs to be logged in for the mail to be sent.
@@ -90,6 +93,30 @@
             '-- \nYou are getting this email because you are a member of the '
             'vcs-imports team.\n', msg.get_payload(decode=True))
 
+    def test_git_to_git_import(self):
+        # Test the email for a new git-to-git import.
+        eric = self.factory.makePerson(name='eric')
+        fooix = self.factory.makeProduct(name='fooix')
+        # Eric needs to be logged in for the mail to be sent.
+        login_person(eric)
+        self.factory.makeProductCodeImport(
+            git_repo_url='git://git.example.com/fooix.git',
+            branch_name=u'master', product=fooix, registrant=eric,
+            target_rcs_type=TargetRevisionControlSystems.GIT)
+        transaction.commit()
+        msg = message_from_string(stub.test_emails[0][2])
+        self.assertEqual('code-import', msg['X-Launchpad-Notification-Type'])
+        self.assertEqual('~eric/fooix/+git/master', msg['X-Launchpad-Branch'])
+        self.assertEqual(
+            'A new git code import has been requested '
+            'by Eric:\n'
+            '    http://code.launchpad.dev/~eric/fooix/+git/master\n'
+            'from\n'
+            '    git://git.example.com/fooix.git\n'
+            '\n'
+            '-- \nYou are getting this email because you are a member of the '
+            'vcs-imports team.\n', msg.get_payload(decode=True))
+
     def test_new_source_package_import(self):
         # Test the email for a new sourcepackage import.
         eric = self.factory.makePerson(name='eric')

=== modified file 'lib/lp/code/model/codeimport.py'
--- lib/lp/code/model/codeimport.py	2016-09-30 13:27:27 +0000
+++ lib/lp/code/model/codeimport.py	2016-10-03 17:03:17 +0000
@@ -26,7 +26,10 @@
     Func,
     Select,
     )
-from storm.locals import Store
+from storm.locals import (
+    Int,
+    Store,
+    )
 from storm.references import Reference
 from zope.component import getUtility
 from zope.event import notify
@@ -38,14 +41,17 @@
     CodeImportJobState,
     CodeImportResultStatus,
     CodeImportReviewStatus,
+    GitRepositoryType,
     NON_CVS_RCS_TYPES,
     RevisionControlSystems,
+    TargetRevisionControlSystems,
     )
 from lp.code.errors import (
     CodeImportAlreadyRequested,
     CodeImportAlreadyRunning,
     CodeImportNotInReviewedState,
     )
+from lp.code.interfaces.branch import IBranch
 from lp.code.interfaces.branchtarget import IBranchTarget
 from lp.code.interfaces.codeimport import (
     ICodeImport,
@@ -53,6 +59,8 @@
     )
 from lp.code.interfaces.codeimportevent import ICodeImportEventSet
 from lp.code.interfaces.codeimportjob import ICodeImportJobWorkflow
+from lp.code.interfaces.gitnamespace import get_git_namespace
+from lp.code.interfaces.gitrepository import IGitRepository
 from lp.code.mail.codeimport import code_import_updated
 from lp.code.model.codeimportjob import CodeImportJobWorkflow
 from lp.code.model.codeimportresult import CodeImportResult
@@ -71,8 +79,31 @@
     _table = 'CodeImport'
     _defaultOrder = ['id']
 
+    def __init__(self, target=None, *args, **kwargs):
+        if target is not None:
+            assert 'branch' not in kwargs
+            assert 'repository' not in kwargs
+            if IBranch.providedBy(target):
+                kwargs['branch'] = target
+            elif IGitRepository.providedBy(target):
+                kwargs['git_repository'] = target
+            else:
+                raise AssertionError("Unknown code import target %s" % target)
+        super(CodeImport, self).__init__(*args, **kwargs)
+
     date_created = UtcDateTimeCol(notNull=True, default=DEFAULT)
-    branch = ForeignKey(dbName='branch', foreignKey='Branch', notNull=True)
+    branch = ForeignKey(dbName='branch', foreignKey='Branch', notNull=False)
+    git_repositoryID = Int(name='git_repository', allow_none=True)
+    git_repository = Reference(git_repositoryID, 'GitRepository.id')
+
+    @property
+    def target(self):
+        if self.branch is not None:
+            return self.branch
+        else:
+            assert self.git_repository is not None
+            return self.git_repository
+
     registrant = ForeignKey(
         dbName='registrant', foreignKey='Person',
         storm_validator=validate_public_person, notNull=True)
@@ -175,12 +206,14 @@
         new_whiteboard = None
         if 'whiteboard' in data:
             whiteboard = data.pop('whiteboard')
-            if whiteboard != self.branch.whiteboard:
-                if whiteboard is None:
-                    new_whiteboard = ''
-                else:
-                    new_whiteboard = whiteboard
-                self.branch.whiteboard = whiteboard
+            # XXX cjwatson 2016-10-03: Do we need something similar for Git?
+            if self.branch is not None:
+                if whiteboard != self.branch.whiteboard:
+                    if whiteboard is None:
+                        new_whiteboard = ''
+                    else:
+                        new_whiteboard = whiteboard
+                    self.branch.whiteboard = whiteboard
         token = event_set.beginModify(self)
         for name, value in data.items():
             setattr(self, name, value)
@@ -196,7 +229,7 @@
         return event
 
     def __repr__(self):
-        return "<CodeImport for %s>" % self.branch.unique_name
+        return "<CodeImport for %s>" % self.target.unique_name
 
     def tryFailingImportAgain(self, user):
         """See `ICodeImport`."""
@@ -233,7 +266,7 @@
 class CodeImportSet:
     """See `ICodeImportSet`."""
 
-    def new(self, registrant, context, branch_name, rcs_type,
+    def new(self, registrant, context, branch_name, rcs_type, target_rcs_type,
             url=None, cvs_root=None, cvs_module=None, review_status=None,
             owner=None):
         """See `ICodeImportSet`."""
@@ -247,22 +280,37 @@
             raise AssertionError(
                 "Don't know how to sanity check source details for unknown "
                 "rcs_type %s" % rcs_type)
-        target = IBranchTarget(context)
+        if owner is None:
+            owner = registrant
+        if target_rcs_type == TargetRevisionControlSystems.BZR:
+            target = IBranchTarget(context)
+            namespace = target.getNamespace(owner)
+        elif target_rcs_type == TargetRevisionControlSystems.GIT:
+            if rcs_type != RevisionControlSystems.GIT:
+                raise AssertionError(
+                    "Can't import rcs_type %s into a Git repository" %
+                    rcs_type)
+            target = namespace = get_git_namespace(context, owner)
+        else:
+            raise AssertionError(
+                "Can't import to target_rcs_type %s" % target_rcs_type)
         if review_status is None:
             # Auto approve imports.
             review_status = CodeImportReviewStatus.REVIEWED
         if not target.supports_code_imports:
             raise AssertionError("%r doesn't support code imports" % target)
-        if owner is None:
-            owner = registrant
         # Create the branch for the CodeImport.
-        namespace = target.getNamespace(owner)
-        import_branch = namespace.createBranch(
-            branch_type=BranchType.IMPORTED, name=branch_name,
-            registrant=registrant)
+        if target_rcs_type == TargetRevisionControlSystems.BZR:
+            import_target = namespace.createBranch(
+                branch_type=BranchType.IMPORTED, name=branch_name,
+                registrant=registrant)
+        else:
+            import_target = namespace.createRepository(
+                repository_type=GitRepositoryType.IMPORTED, name=branch_name,
+                registrant=registrant)
 
         code_import = CodeImport(
-            registrant=registrant, owner=owner, branch=import_branch,
+            registrant=registrant, owner=owner, target=import_target,
             rcs_type=rcs_type, url=url,
             cvs_root=cvs_root, cvs_module=cvs_module,
             review_status=review_status)
@@ -303,6 +351,9 @@
         """See `ICodeImportSet`."""
         return CodeImport.selectOneBy(branch=branch)
 
+    def getByGitRepository(self, repository):
+        return CodeImport.selectOneBy(git_repository=repository)
+
     def search(self, review_status=None, rcs_type=None):
         """See `ICodeImportSet`."""
         clauses = []

=== modified file 'lib/lp/code/model/codeimportjob.py'
--- lib/lp/code/model/codeimportjob.py	2015-07-08 16:05:11 +0000
+++ lib/lp/code/model/codeimportjob.py	2016-10-03 17:03:17 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009 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).
 
 """Database classes for the CodeImportJob table."""
@@ -154,10 +154,10 @@
         """See `ICodeImportJobWorkflow`."""
         assert code_import.review_status == CodeImportReviewStatus.REVIEWED, (
             "Review status of %s is not REVIEWED: %s" % (
-            code_import.branch.unique_name, code_import.review_status.name))
+            code_import.target.unique_name, code_import.review_status.name))
         assert code_import.import_job is None, (
             "Already associated to a CodeImportJob: %s" % (
-            code_import.branch.unique_name))
+            code_import.target.unique_name))
 
         if interval is None:
             interval = code_import.effective_update_interval
@@ -182,13 +182,13 @@
         """See `ICodeImportJobWorkflow`."""
         assert code_import.review_status != CodeImportReviewStatus.REVIEWED, (
             "The review status of %s is %s." % (
-            code_import.branch.unique_name, code_import.review_status.name))
+            code_import.target.unique_name, code_import.review_status.name))
         assert code_import.import_job is not None, (
             "Not associated to a CodeImportJob: %s" % (
-            code_import.branch.unique_name,))
+            code_import.target.unique_name,))
         assert code_import.import_job.state == CodeImportJobState.PENDING, (
             "The CodeImportJob associated to %s is %s." % (
-            code_import.branch.unique_name,
+            code_import.target.unique_name,
             code_import.import_job.state.name))
         # CodeImportJobWorkflow is the only class that is allowed to delete
         # CodeImportJob rows, so destroySelf is not exposed in ICodeImportJob.
@@ -198,12 +198,12 @@
         """See `ICodeImportJobWorkflow`."""
         assert import_job.state == CodeImportJobState.PENDING, (
             "The CodeImportJob associated with %s is %s."
-            % (import_job.code_import.branch.unique_name,
+            % (import_job.code_import.target.unique_name,
                import_job.state.name))
         assert import_job.requesting_user is None, (
             "The CodeImportJob associated with %s "
             "was already requested by %s."
-            % (import_job.code_import.branch.unique_name,
+            % (import_job.code_import.target.unique_name,
                import_job.requesting_user.name))
         # CodeImportJobWorkflow is the only class that is allowed to set the
         # date_due and requesting_user attributes of CodeImportJob, they are
@@ -219,7 +219,7 @@
         """See `ICodeImportJobWorkflow`."""
         assert import_job.state == CodeImportJobState.PENDING, (
             "The CodeImportJob associated with %s is %s."
-            % (import_job.code_import.branch.unique_name,
+            % (import_job.code_import.target.unique_name,
                import_job.state.name))
         assert machine.state == CodeImportMachineState.ONLINE, (
             "The machine %s is %s."
@@ -241,7 +241,7 @@
         """See `ICodeImportJobWorkflow`."""
         assert import_job.state == CodeImportJobState.RUNNING, (
             "The CodeImportJob associated with %s is %s."
-            % (import_job.code_import.branch.unique_name,
+            % (import_job.code_import.target.unique_name,
                import_job.state.name))
         # CodeImportJobWorkflow is the only class that is allowed to
         # set the heartbeat and logtail attributes of CodeImportJob,
@@ -281,7 +281,7 @@
         """See `ICodeImportJobWorkflow`."""
         assert import_job.state == CodeImportJobState.RUNNING, (
             "The CodeImportJob associated with %s is %s."
-            % (import_job.code_import.branch.unique_name,
+            % (import_job.code_import.target.unique_name,
                import_job.state.name))
         code_import = import_job.code_import
         machine = import_job.machine
@@ -311,7 +311,8 @@
             naked_import.date_last_successful = result.date_created
         # If the status was successful and revisions were imported, arrange
         # for the branch to be mirrored.
-        if status == CodeImportResultStatus.SUCCESS:
+        if (status == CodeImportResultStatus.SUCCESS and
+                code_import.branch is not None):
             code_import.branch.requestMirror()
         getUtility(ICodeImportEventSet).newFinish(
             code_import, machine)
@@ -320,7 +321,7 @@
         """See `ICodeImportJobWorkflow`."""
         assert import_job.state == CodeImportJobState.RUNNING, (
             "The CodeImportJob associated with %s is %s."
-            % (import_job.code_import.branch.unique_name,
+            % (import_job.code_import.target.unique_name,
                import_job.state.name))
         # Cribbing from codeimport-job.txt, this method does four things:
         # 1) deletes the passed in job,

=== modified file 'lib/lp/code/model/gitnamespace.py'
--- lib/lp/code/model/gitnamespace.py	2016-10-03 17:03:12 +0000
+++ lib/lp/code/model/gitnamespace.py	2016-10-03 17:03:17 +0000
@@ -1,4 +1,4 @@
-# Copyright 2015 Canonical Ltd.  This software is licensed under the
+# Copyright 2015-2016 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Implementations of `IGitNamespace`."""
@@ -238,6 +238,7 @@
     has_defaults = False
     allow_push_to_set_default = False
     supports_merge_proposals = False
+    supports_code_imports = False
     allow_recipe_name_from_target = False
 
     def __init__(self, person):
@@ -316,6 +317,7 @@
     has_defaults = True
     allow_push_to_set_default = True
     supports_merge_proposals = True
+    supports_code_imports = True
     allow_recipe_name_from_target = True
 
     def __init__(self, person, project):
@@ -401,6 +403,7 @@
     has_defaults = True
     allow_push_to_set_default = False
     supports_merge_proposals = True
+    supports_code_imports = True
     allow_recipe_name_from_target = True
 
     def __init__(self, person, distro_source_package):

=== modified file 'lib/lp/code/model/hasbranches.py'
--- lib/lp/code/model/hasbranches.py	2016-10-03 10:54:48 +0000
+++ lib/lp/code/model/hasbranches.py	2016-10-03 17:03:17 +0000
@@ -16,7 +16,10 @@
 from zope.component import getUtility
 from zope.security.proxy import removeSecurityProxy
 
-from lp.code.enums import BranchMergeProposalStatus
+from lp.code.enums import (
+    BranchMergeProposalStatus,
+    TargetRevisionControlSystems,
+    )
 from lp.code.interfaces.branch import DEFAULT_BRANCH_STATUS_IN_LISTING
 from lp.code.interfaces.branchcollection import (
     IAllBranches,
@@ -130,5 +133,6 @@
             owner=None):
         """See `IHasCodeImports`."""
         return getUtility(ICodeImportSet).new(
-            registrant, self, branch_name, rcs_type, url=url,
+            registrant, self, branch_name, rcs_type,
+            TargetRevisionControlSystems.BZR, url=url,
             cvs_root=cvs_root, cvs_module=cvs_module, owner=owner)

=== modified file 'lib/lp/code/model/tests/test_codeimport.py'
--- lib/lp/code/model/tests/test_codeimport.py	2016-09-30 13:27:27 +0000
+++ lib/lp/code/model/tests/test_codeimport.py	2016-10-03 17:03:17 +0000
@@ -7,10 +7,15 @@
     datetime,
     timedelta,
     )
+from functools import partial
 
 import pytz
 from sqlobject import SQLObjectNotFound
 from storm.store import Store
+from testscenarios import (
+    load_tests_apply_scenarios,
+    WithScenarios,
+    )
 from zope.component import getUtility
 from zope.security.proxy import removeSecurityProxy
 
@@ -19,12 +24,14 @@
     CodeImportResultStatus,
     CodeImportReviewStatus,
     RevisionControlSystems,
+    TargetRevisionControlSystems,
     )
 from lp.code.errors import (
     BranchCreatorNotMemberOfOwnerTeam,
     CodeImportAlreadyRequested,
     CodeImportAlreadyRunning,
     CodeImportNotInReviewedState,
+    GitRepositoryCreatorNotMemberOfOwnerTeam,
     )
 from lp.code.interfaces.branchtarget import IBranchTarget
 from lp.code.interfaces.codeimportjob import ICodeImportJobWorkflow
@@ -51,63 +58,100 @@
     )
 
 
-class TestCodeImportCreation(TestCaseWithFactory):
+class TestCodeImportBase(WithScenarios, TestCaseWithFactory):
+
+    scenarios = [
+        ("Branch", {
+            "target_rcs_type": TargetRevisionControlSystems.BZR,
+            "supports_source_cvs": True,
+            "supports_source_svn": True,
+            "supports_source_bzr": True,
+            }),
+        ("GitRepository", {
+            "target_rcs_type": TargetRevisionControlSystems.GIT,
+            "supports_source_cvs": False,
+            "supports_source_svn": False,
+            "supports_source_bzr": False,
+            }),
+        ]
+
+
+class TestCodeImportCreation(TestCodeImportBase):
     """Test the creation of CodeImports."""
 
     layer = DatabaseFunctionalLayer
 
     def test_new_svn_import_svn_scheme(self):
         """A subversion import can use the svn:// scheme."""
-        code_import = CodeImportSet().new(
+        create_func = partial(
+            CodeImportSet().new,
             registrant=self.factory.makePerson(),
             context=self.factory.makeProduct(),
-            branch_name='imported',
+            branch_name=u'imported',
             rcs_type=RevisionControlSystems.BZR_SVN,
+            target_rcs_type=self.target_rcs_type,
             url=self.factory.getUniqueURL(scheme="svn"))
-        self.assertEqual(
-            CodeImportReviewStatus.REVIEWED,
-            code_import.review_status)
-        # No job is created for the import.
-        self.assertIsNot(None, code_import.import_job)
+        if self.supports_source_svn:
+            code_import = create_func()
+            self.assertEqual(
+                CodeImportReviewStatus.REVIEWED,
+                code_import.review_status)
+            # No job is created for the import.
+            self.assertIsNot(None, code_import.import_job)
+        else:
+            self.assertRaises(AssertionError, create_func)
 
     def test_reviewed_svn_import(self):
         """A specific review status can be set for a new import."""
-        code_import = CodeImportSet().new(
+        create_func = partial(
+            CodeImportSet().new,
             registrant=self.factory.makePerson(),
             context=self.factory.makeProduct(),
-            branch_name='imported',
+            branch_name=u'imported',
             rcs_type=RevisionControlSystems.BZR_SVN,
+            target_rcs_type=self.target_rcs_type,
             url=self.factory.getUniqueURL(),
             review_status=None)
-        self.assertEqual(
-            CodeImportReviewStatus.REVIEWED,
-            code_import.review_status)
-        # A job is created for the import.
-        self.assertIsNot(None, code_import.import_job)
+        if self.supports_source_svn:
+            code_import = create_func()
+            self.assertEqual(
+                CodeImportReviewStatus.REVIEWED,
+                code_import.review_status)
+            # A job is created for the import.
+            self.assertIsNot(None, code_import.import_job)
+        else:
+            self.assertRaises(AssertionError, create_func)
 
     def test_cvs_import_reviewed(self):
         """A new CVS code import should have REVIEWED status."""
-        code_import = CodeImportSet().new(
+        create_func = partial(
+            CodeImportSet().new,
             registrant=self.factory.makePerson(),
             context=self.factory.makeProduct(),
-            branch_name='imported',
+            branch_name=u'imported',
             rcs_type=RevisionControlSystems.CVS,
+            target_rcs_type=self.target_rcs_type,
             cvs_root=self.factory.getUniqueURL(),
-            cvs_module='module',
+            cvs_module=u'module',
             review_status=None)
-        self.assertEqual(
-            CodeImportReviewStatus.REVIEWED,
-            code_import.review_status)
-        # A job is created for the import.
-        self.assertIsNot(None, code_import.import_job)
+        if self.supports_source_cvs:
+            code_import = create_func()
+            self.assertEqual(
+                CodeImportReviewStatus.REVIEWED,
+                code_import.review_status)
+            # A job is created for the import.
+            self.assertIsNot(None, code_import.import_job)
+        else:
+            self.assertRaises(AssertionError, create_func)
 
     def test_git_import_git_scheme(self):
         """A git import can have a git:// style URL."""
         code_import = CodeImportSet().new(
             registrant=self.factory.makePerson(),
             context=self.factory.makeProduct(),
-            branch_name='imported',
+            branch_name=u'imported',
             rcs_type=RevisionControlSystems.GIT,
+            target_rcs_type=self.target_rcs_type,
             url=self.factory.getUniqueURL(scheme="git"),
             review_status=None)
         self.assertEqual(
@@ -121,8 +165,9 @@
         code_import = CodeImportSet().new(
             registrant=self.factory.makePerson(),
             context=self.factory.makeProduct(),
-            branch_name='imported',
+            branch_name=u'imported',
             rcs_type=RevisionControlSystems.GIT,
+            target_rcs_type=self.target_rcs_type,
             url=self.factory.getUniqueURL(),
             review_status=None)
         self.assertEqual(
@@ -133,18 +178,24 @@
 
     def test_bzr_import_reviewed(self):
         """A new bzr import is always reviewed by default."""
-        code_import = CodeImportSet().new(
+        create_func = partial(
+            CodeImportSet().new,
             registrant=self.factory.makePerson(),
             context=self.factory.makeProduct(),
-            branch_name='mirrored',
+            branch_name=u'mirrored',
             rcs_type=RevisionControlSystems.BZR,
+            target_rcs_type=self.target_rcs_type,
             url=self.factory.getUniqueURL(),
             review_status=None)
-        self.assertEqual(
-            CodeImportReviewStatus.REVIEWED,
-            code_import.review_status)
-        # A job is created for the import.
-        self.assertIsNot(None, code_import.import_job)
+        if self.supports_source_bzr:
+            code_import = create_func()
+            self.assertEqual(
+                CodeImportReviewStatus.REVIEWED,
+                code_import.review_status)
+            # A job is created for the import.
+            self.assertIsNot(None, code_import.import_job)
+        else:
+            self.assertRaises(AssertionError, create_func)
 
     def test_junk_code_import_rejected(self):
         """You are not allowed to create code imports targetting +junk."""
@@ -152,8 +203,9 @@
         self.assertRaises(AssertionError, CodeImportSet().new,
             registrant=registrant,
             context=registrant,
-            branch_name='imported',
+            branch_name=u'imported',
             rcs_type=RevisionControlSystems.GIT,
+            target_rcs_type=self.target_rcs_type,
             url=self.factory.getUniqueURL(),
             review_status=None)
 
@@ -161,19 +213,27 @@
         """Test that we can create an import targetting a source package."""
         registrant = self.factory.makePerson()
         source_package = self.factory.makeSourcePackage()
+        if self.target_rcs_type == TargetRevisionControlSystems.BZR:
+            context = source_package
+        else:
+            context = source_package.distribution_sourcepackage
         code_import = CodeImportSet().new(
             registrant=registrant,
-            context=source_package,
-            branch_name='imported',
+            context=context,
+            branch_name=u'imported',
             rcs_type=RevisionControlSystems.GIT,
+            target_rcs_type=self.target_rcs_type,
             url=self.factory.getUniqueURL(),
             review_status=None)
         code_import = removeSecurityProxy(code_import)
         self.assertEqual(registrant, code_import.registrant)
-        self.assertEqual(registrant, code_import.branch.owner)
-        self.assertEqual(
-            IBranchTarget(source_package), code_import.branch.target)
-        self.assertEqual(source_package, code_import.branch.sourcepackage)
+        if self.target_rcs_type == TargetRevisionControlSystems.BZR:
+            self.assertEqual(registrant, code_import.branch.owner)
+            self.assertEqual(IBranchTarget(context), code_import.branch.target)
+            self.assertEqual(source_package, code_import.branch.sourcepackage)
+        else:
+            self.assertEqual(registrant, code_import.git_repository.owner)
+            self.assertEqual(context, code_import.git_repository.target)
         # And a job is still created
         self.assertIsNot(None, code_import.import_job)
 
@@ -183,20 +243,29 @@
         owner = self.factory.makeTeam()
         removeSecurityProxy(registrant).join(owner)
         source_package = self.factory.makeSourcePackage()
+        if self.target_rcs_type == TargetRevisionControlSystems.BZR:
+            context = source_package
+        else:
+            context = source_package.distribution_sourcepackage
         code_import = CodeImportSet().new(
             registrant=registrant,
-            context=source_package,
-            branch_name='imported',
+            context=context,
+            branch_name=u'imported',
             rcs_type=RevisionControlSystems.GIT,
+            target_rcs_type=self.target_rcs_type,
             url=self.factory.getUniqueURL(),
             review_status=None, owner=owner)
         code_import = removeSecurityProxy(code_import)
         self.assertEqual(registrant, code_import.registrant)
-        self.assertEqual(owner, code_import.branch.owner)
-        self.assertEqual(registrant, code_import.branch.registrant)
-        self.assertEqual(
-            IBranchTarget(source_package), code_import.branch.target)
-        self.assertEqual(source_package, code_import.branch.sourcepackage)
+        if self.target_rcs_type == TargetRevisionControlSystems.BZR:
+            self.assertEqual(owner, code_import.branch.owner)
+            self.assertEqual(registrant, code_import.branch.registrant)
+            self.assertEqual(IBranchTarget(context), code_import.branch.target)
+            self.assertEqual(source_package, code_import.branch.sourcepackage)
+        else:
+            self.assertEqual(owner, code_import.git_repository.owner)
+            self.assertEqual(registrant, code_import.git_repository.registrant)
+            self.assertEqual(context, code_import.git_repository.target)
         # And a job is still created
         self.assertIsNot(None, code_import.import_job)
 
@@ -205,30 +274,39 @@
         registrant = self.factory.makePerson()
         owner = self.factory.makeTeam()
         source_package = self.factory.makeSourcePackage()
+        if self.target_rcs_type == TargetRevisionControlSystems.BZR:
+            context = source_package
+            expected_exception = BranchCreatorNotMemberOfOwnerTeam
+        else:
+            context = source_package.distribution_sourcepackage
+            expected_exception = GitRepositoryCreatorNotMemberOfOwnerTeam
         self.assertRaises(
-            BranchCreatorNotMemberOfOwnerTeam,
+            expected_exception,
             CodeImportSet().new,
             registrant=registrant,
-            context=source_package,
-            branch_name='imported',
+            context=context,
+            branch_name=u'imported',
             rcs_type=RevisionControlSystems.GIT,
+            target_rcs_type=self.target_rcs_type,
             url=self.factory.getUniqueURL(),
             review_status=None, owner=owner)
 
 
-class TestCodeImportDeletion(TestCaseWithFactory):
+class TestCodeImportDeletion(TestCodeImportBase):
     """Test the deletion of CodeImports."""
 
     layer = LaunchpadFunctionalLayer
 
     def test_delete(self):
         """Ensure CodeImport objects can be deleted via CodeImportSet."""
-        code_import = self.factory.makeCodeImport()
+        code_import = self.factory.makeCodeImport(
+            target_rcs_type=self.target_rcs_type)
         CodeImportSet().delete(code_import)
 
     def test_deleteIncludesJob(self):
         """Ensure deleting CodeImport objects deletes associated jobs."""
-        code_import = self.factory.makeCodeImport()
+        code_import = self.factory.makeCodeImport(
+            target_rcs_type=self.target_rcs_type)
         login_person(getUtility(ILaunchpadCelebrities).vcs_imports.teamowner)
         job_id = code_import.import_job.id
         CodeImportJobSet().getById(job_id)
@@ -240,7 +318,10 @@
 
     def test_deleteIncludesEvent(self):
         """Ensure deleting CodeImport objects deletes associated events."""
-        code_import_event = self.factory.makeCodeImportEvent()
+        code_import = self.factory.makeCodeImport(
+            target_rcs_type=self.target_rcs_type)
+        code_import_event = self.factory.makeCodeImportEvent(
+            code_import=code_import)
         code_import_event_id = code_import_event.id
         CodeImportSet().delete(code_import_event.code_import)
         # CodeImportEvent.get should not raise anything.
@@ -251,7 +332,10 @@
 
     def test_deleteIncludesResult(self):
         """Ensure deleting CodeImport objects deletes associated results."""
-        code_import_result = self.factory.makeCodeImportResult()
+        code_import = self.factory.makeCodeImport(
+            target_rcs_type=self.target_rcs_type)
+        code_import_result = self.factory.makeCodeImportResult(
+            code_import=code_import)
         code_import_result_id = code_import_result.id
         CodeImportSet().delete(code_import_result.code_import)
         # CodeImportResult.get should not raise anything.
@@ -261,7 +345,7 @@
             SQLObjectNotFound, CodeImportResult.get, code_import_result_id)
 
 
-class TestCodeImportStatusUpdate(TestCaseWithFactory):
+class TestCodeImportStatusUpdate(TestCodeImportBase):
     """Test the status updates of CodeImports."""
 
     layer = DatabaseFunctionalLayer
@@ -276,7 +360,8 @@
             job.destroySelf()
 
     def makeApprovedImportWithPendingJob(self):
-        code_import = self.factory.makeCodeImport()
+        code_import = self.factory.makeCodeImport(
+            target_rcs_type=self.target_rcs_type)
         code_import.updateFromData(
             {'review_status': CodeImportReviewStatus.REVIEWED},
             self.import_operator)
@@ -290,7 +375,8 @@
 
     def test_approve(self):
         # Approving a code import will create a job for it.
-        code_import = self.factory.makeCodeImport()
+        code_import = self.factory.makeCodeImport(
+            target_rcs_type=self.target_rcs_type)
         code_import.updateFromData(
             {'review_status': CodeImportReviewStatus.REVIEWED},
             self.import_operator)
@@ -300,7 +386,8 @@
 
     def test_suspend_no_job(self):
         # Suspending a new import has no impact on jobs.
-        code_import = self.factory.makeCodeImport()
+        code_import = self.factory.makeCodeImport(
+            target_rcs_type=self.target_rcs_type)
         code_import.updateFromData(
             {'review_status': CodeImportReviewStatus.SUSPENDED},
             self.import_operator)
@@ -330,7 +417,8 @@
 
     def test_invalidate_no_job(self):
         # Invalidating a new import has no impact on jobs.
-        code_import = self.factory.makeCodeImport()
+        code_import = self.factory.makeCodeImport(
+            target_rcs_type=self.target_rcs_type)
         code_import.updateFromData(
             {'review_status': CodeImportReviewStatus.INVALID},
             self.import_operator)
@@ -360,7 +448,8 @@
 
     def test_markFailing_no_job(self):
         # Marking a new import as failing has no impact on jobs.
-        code_import = self.factory.makeCodeImport()
+        code_import = self.factory.makeCodeImport(
+            target_rcs_type=self.target_rcs_type)
         code_import.updateFromData(
             {'review_status': CodeImportReviewStatus.FAILING},
             self.import_operator)
@@ -389,14 +478,15 @@
             CodeImportReviewStatus.FAILING, code_import.review_status)
 
 
-class TestCodeImportResultsAttribute(TestCaseWithFactory):
+class TestCodeImportResultsAttribute(TestCodeImportBase):
     """Test the results attribute of a CodeImport."""
 
     layer = LaunchpadFunctionalLayer
 
     def setUp(self):
         TestCaseWithFactory.setUp(self)
-        self.code_import = self.factory.makeCodeImport()
+        self.code_import = self.factory.makeCodeImport(
+            target_rcs_type=self.target_rcs_type)
 
     def tearDown(self):
         super(TestCodeImportResultsAttribute, self).tearDown()
@@ -454,7 +544,7 @@
         self.assertEqual(third, results[2])
 
 
-class TestConsecutiveFailureCount(TestCaseWithFactory):
+class TestConsecutiveFailureCount(TestCodeImportBase):
     """Tests for `ICodeImport.consecutive_failure_count`."""
 
     layer = LaunchpadZopelessLayer
@@ -495,27 +585,31 @@
 
     def test_consecutive_failure_count_zero_initially(self):
         # A new code import has a consecutive_failure_count of 0.
-        code_import = self.factory.makeCodeImport()
+        code_import = self.factory.makeCodeImport(
+            target_rcs_type=self.target_rcs_type)
         self.assertEqual(0, code_import.consecutive_failure_count)
 
     def test_consecutive_failure_count_succeed(self):
         # A code import that has succeeded once has a
         # consecutive_failure_count of 0.
-        code_import = self.factory.makeCodeImport()
+        code_import = self.factory.makeCodeImport(
+            target_rcs_type=self.target_rcs_type)
         self.succeedImport(code_import)
         self.assertEqual(0, code_import.consecutive_failure_count)
 
     def test_consecutive_failure_count_fail(self):
         # A code import that has failed once has a consecutive_failure_count
         # of 1.
-        code_import = self.factory.makeCodeImport()
+        code_import = self.factory.makeCodeImport(
+            target_rcs_type=self.target_rcs_type)
         self.failImport(code_import)
         self.assertEqual(1, code_import.consecutive_failure_count)
 
     def test_consecutive_failure_count_succeed_succeed_no_changes(self):
         # A code import that has succeeded then succeeded with no changes has
         # a consecutive_failure_count of 0.
-        code_import = self.factory.makeCodeImport()
+        code_import = self.factory.makeCodeImport(
+            target_rcs_type=self.target_rcs_type)
         self.succeedImport(code_import)
         self.succeedImport(
             code_import, CodeImportResultStatus.SUCCESS_NOCHANGE)
@@ -524,7 +618,8 @@
     def test_consecutive_failure_count_succeed_succeed_partial(self):
         # A code import that has succeeded then succeeded with no changes has
         # a consecutive_failure_count of 0.
-        code_import = self.factory.makeCodeImport()
+        code_import = self.factory.makeCodeImport(
+            target_rcs_type=self.target_rcs_type)
         self.succeedImport(code_import)
         self.succeedImport(
             code_import, CodeImportResultStatus.SUCCESS_NOCHANGE)
@@ -533,7 +628,8 @@
     def test_consecutive_failure_count_fail_fail(self):
         # A code import that has failed twice has a consecutive_failure_count
         # of 2.
-        code_import = self.factory.makeCodeImport()
+        code_import = self.factory.makeCodeImport(
+            target_rcs_type=self.target_rcs_type)
         self.failImport(code_import)
         self.failImport(code_import)
         self.assertEqual(2, code_import.consecutive_failure_count)
@@ -541,7 +637,8 @@
     def test_consecutive_failure_count_fail_fail_succeed(self):
         # A code import that has failed twice then succeeded has a
         # consecutive_failure_count of 0.
-        code_import = self.factory.makeCodeImport()
+        code_import = self.factory.makeCodeImport(
+            target_rcs_type=self.target_rcs_type)
         self.failImport(code_import)
         self.failImport(code_import)
         self.succeedImport(code_import)
@@ -550,7 +647,8 @@
     def test_consecutive_failure_count_fail_succeed_fail(self):
         # A code import that has failed then succeeded then failed again has a
         # consecutive_failure_count of 1.
-        code_import = self.factory.makeCodeImport()
+        code_import = self.factory.makeCodeImport(
+            target_rcs_type=self.target_rcs_type)
         self.failImport(code_import)
         self.succeedImport(code_import)
         self.failImport(code_import)
@@ -559,7 +657,8 @@
     def test_consecutive_failure_count_succeed_fail_succeed(self):
         # A code import that has succeeded then failed then succeeded again
         # has a consecutive_failure_count of 0.
-        code_import = self.factory.makeCodeImport()
+        code_import = self.factory.makeCodeImport(
+            target_rcs_type=self.target_rcs_type)
         self.succeedImport(code_import)
         self.failImport(code_import)
         self.succeedImport(code_import)
@@ -568,8 +667,10 @@
     def test_consecutive_failure_count_other_import_non_interference(self):
         # The failure or success of other code imports does not affect
         # consecutive_failure_count.
-        code_import = self.factory.makeCodeImport()
-        other_import = self.factory.makeCodeImport()
+        code_import = self.factory.makeCodeImport(
+            target_rcs_type=self.target_rcs_type)
+        other_import = self.factory.makeCodeImport(
+            target_rcs_type=self.target_rcs_type)
         self.failImport(code_import)
         self.assertEqual(1, code_import.consecutive_failure_count)
         self.failImport(other_import)
@@ -584,7 +685,7 @@
         self.assertEqual(1, code_import.consecutive_failure_count)
 
 
-class TestTryFailingImportAgain(TestCaseWithFactory):
+class TestTryFailingImportAgain(TestCodeImportBase):
     """Tests for `ICodeImport.tryFailingImportAgain`."""
 
     layer = DatabaseFunctionalLayer
@@ -599,6 +700,7 @@
         outcomes = {}
         for status in CodeImportReviewStatus.items:
             code_import = self.factory.makeCodeImport(
+                target_rcs_type=self.target_rcs_type,
                 review_status=CodeImportReviewStatus.NEW)
             code_import.updateFromData(
                 {'review_status': status}, self.factory.makePerson())
@@ -619,7 +721,8 @@
     def test_resetsStatus(self):
         # tryFailingImportAgain sets the review_status of the import back to
         # REVIEWED.
-        code_import = self.factory.makeCodeImport()
+        code_import = self.factory.makeCodeImport(
+            target_rcs_type=self.target_rcs_type)
         code_import.updateFromData(
             {'review_status': CodeImportReviewStatus.FAILING},
             self.factory.makePerson())
@@ -630,7 +733,8 @@
 
     def test_requestsImport(self):
         # tryFailingImportAgain requests an import.
-        code_import = self.factory.makeCodeImport()
+        code_import = self.factory.makeCodeImport(
+            target_rcs_type=self.target_rcs_type)
         code_import.updateFromData(
             {'review_status': CodeImportReviewStatus.FAILING},
             self.factory.makePerson())
@@ -640,7 +744,7 @@
             requester, code_import.import_job.requesting_user)
 
 
-class TestRequestImport(TestCaseWithFactory):
+class TestRequestImport(TestCodeImportBase):
     """Tests for `ICodeImport.requestImport`."""
 
     layer = DatabaseFunctionalLayer
@@ -651,7 +755,8 @@
 
     def test_requestsJob(self):
         code_import = self.factory.makeCodeImport(
-            git_repo_url=self.factory.getUniqueURL())
+            git_repo_url=self.factory.getUniqueURL(),
+            target_rcs_type=self.target_rcs_type)
         requester = self.factory.makePerson()
         old_date = code_import.import_job.date_due
         code_import.requestImport(requester)
@@ -660,7 +765,8 @@
 
     def test_noop_if_already_requested(self):
         code_import = self.factory.makeCodeImport(
-            git_repo_url=self.factory.getUniqueURL())
+            git_repo_url=self.factory.getUniqueURL(),
+            target_rcs_type=self.target_rcs_type)
         requester = self.factory.makePerson()
         code_import.requestImport(requester)
         old_date = code_import.import_job.date_due
@@ -672,7 +778,8 @@
 
     def test_optional_error_if_already_requested(self):
         code_import = self.factory.makeCodeImport(
-            git_repo_url=self.factory.getUniqueURL())
+            git_repo_url=self.factory.getUniqueURL(),
+            target_rcs_type=self.target_rcs_type)
         requester = self.factory.makePerson()
         code_import.requestImport(requester)
         e = self.assertRaises(
@@ -681,10 +788,14 @@
         self.assertEqual(requester, e.requesting_user)
 
     def test_exception_on_disabled(self):
-        # get an SVN request which is suspended
+        # get an SVN/Git (as appropriate) request which is suspended
+        if self.supports_source_svn:
+            kwargs = {"svn_branch_url": self.factory.getUniqueURL()}
+        else:
+            kwargs = {"git_repo_url": self.factory.getUniqueURL()}
         code_import = self.factory.makeCodeImport(
-            svn_branch_url=self.factory.getUniqueURL(),
-            review_status=CodeImportReviewStatus.SUSPENDED)
+            target_rcs_type=self.target_rcs_type,
+            review_status=CodeImportReviewStatus.SUSPENDED, **kwargs)
         requester = self.factory.makePerson()
         # which leads to an exception if we try and ask for an import
         self.assertRaises(
@@ -693,10 +804,14 @@
 
     def test_exception_if_already_running(self):
         code_import = self.factory.makeCodeImport(
-            git_repo_url=self.factory.getUniqueURL())
+            git_repo_url=self.factory.getUniqueURL(),
+            target_rcs_type=self.target_rcs_type)
         code_import = make_running_import(factory=self.factory,
             code_import=code_import)
         requester = self.factory.makePerson()
         self.assertRaises(
             CodeImportAlreadyRunning, code_import.requestImport,
             requester)
+
+
+load_tests = load_tests_apply_scenarios

=== modified file 'lib/lp/registry/browser/product.py'
--- lib/lp/registry/browser/product.py	2016-09-30 13:27:27 +0000
+++ lib/lp/registry/browser/product.py	2016-10-03 17:03:17 +0000
@@ -145,7 +145,10 @@
 from lp.code.browser.codeimport import validate_import_url
 from lp.code.browser.sourcepackagerecipelisting import HasRecipesMenuMixin
 from lp.code.browser.vcslisting import TargetDefaultVCSNavigationMixin
-from lp.code.enums import RevisionControlSystems
+from lp.code.enums import (
+    RevisionControlSystems,
+    TargetRevisionControlSystems,
+    )
 from lp.code.errors import BranchExists
 from lp.code.interfaces.branch import IBranch
 from lp.code.interfaces.branchjob import IRosettaUploadJobSource
@@ -1988,6 +1991,7 @@
                         context=self.context,
                         branch_name=branch_name,
                         rcs_type=rcs_item,
+                        target_rcs_type=TargetRevisionControlSystems.BZR,
                         url=url,
                         cvs_root=cvs_root,
                         cvs_module=cvs_module)

=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py	2016-10-03 17:03:12 +0000
+++ lib/lp/testing/factory.py	2016-10-03 17:03:17 +0000
@@ -114,6 +114,7 @@
     GitObjectType,
     GitRepositoryType,
     RevisionControlSystems,
+    TargetRevisionControlSystems,
     )
 from lp.code.errors import UnknownBranchTypeError
 from lp.code.interfaces.branch import IBranch
@@ -2374,21 +2375,28 @@
                        cvs_module=None, context=None, branch_name=None,
                        git_repo_url=None,
                        bzr_branch_url=None, registrant=None,
-                       rcs_type=None, review_status=None):
+                       rcs_type=None, target_rcs_type=None,
+                       review_status=None):
         """Create and return a new, arbitrary code import.
 
         The type of code import will be inferred from the source details
-        passed in, but defaults to a Subversion import from an arbitrary
-        unique URL.
+        passed in, but defaults to a Subversion->Bazaar import from an
+        arbitrary unique URL.  (If the target type is specified as Git, then
+        the source type instead defaults to Git.)
         """
+        if target_rcs_type is None:
+            target_rcs_type = TargetRevisionControlSystems.BZR
         if (svn_branch_url is cvs_root is cvs_module is git_repo_url is
             bzr_branch_url is None):
-            svn_branch_url = self.getUniqueURL()
+            if target_rcs_type == TargetRevisionControlSystems.BZR:
+                svn_branch_url = self.getUniqueURL()
+            else:
+                git_repo_url = self.getUniqueURL()
 
         if context is None:
             context = self.makeProduct()
         if branch_name is None:
-            branch_name = self.getUniqueString('name')
+            branch_name = self.getUniqueUnicode('name')
         if registrant is None:
             registrant = self.makePerson()
 
@@ -2398,23 +2406,27 @@
             return code_import_set.new(
                 registrant, context, branch_name,
                 rcs_type=RevisionControlSystems.BZR_SVN,
+                target_rcs_type=target_rcs_type,
                 url=svn_branch_url, review_status=review_status)
         elif git_repo_url is not None:
             assert rcs_type in (None, RevisionControlSystems.GIT)
             return code_import_set.new(
                 registrant, context, branch_name,
                 rcs_type=RevisionControlSystems.GIT,
+                target_rcs_type=target_rcs_type,
                 url=git_repo_url, review_status=review_status)
         elif bzr_branch_url is not None:
             return code_import_set.new(
                 registrant, context, branch_name,
                 rcs_type=RevisionControlSystems.BZR,
+                target_rcs_type=target_rcs_type,
                 url=bzr_branch_url, review_status=review_status)
         else:
             assert rcs_type in (None, RevisionControlSystems.CVS)
             return code_import_set.new(
                 registrant, context, branch_name,
                 rcs_type=RevisionControlSystems.CVS,
+                target_rcs_type=target_rcs_type,
                 cvs_root=cvs_root, cvs_module=cvs_module,
                 review_status=review_status)
 
@@ -2440,9 +2452,10 @@
             changelog += entry
         return self.makeLibraryFileAlias(content=changelog.encode("utf-8"))
 
-    def makeCodeImportEvent(self):
+    def makeCodeImportEvent(self, code_import=None):
         """Create and return a CodeImportEvent."""
-        code_import = self.makeCodeImport()
+        if code_import is None:
+            code_import = self.makeCodeImport()
         person = self.makePerson()
         code_import_event_set = getUtility(ICodeImportEventSet)
         return code_import_event_set.newCreate(code_import, person)


Follow ups