← Back to team overview

launchpad-reviewers team mailing list archive

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

 

Colin Watson has proposed merging lp:~cjwatson/launchpad/codeimport-git-webservice into lp:launchpad with lp:~cjwatson/launchpad/codeimport-git-edit-views as a prerequisite.

Commit message:
Allow creating Git-targeted code imports on the webservice.

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-webservice/+merge/308399

The branch_name parameter is slightly unfortunate now, but it's probably better than introducing a new endpoint or having a mostly-duplicate repository_name parameter.

I need to do penance for not unittestifying the doctests, but I ran out of time.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/codeimport-git-webservice into lp:launchpad.
=== modified file 'lib/lp/code/errors.py'
--- lib/lp/code/errors.py	2016-10-05 09:29:41 +0000
+++ lib/lp/code/errors.py	2016-10-13 15:58:50 +0000
@@ -27,7 +27,9 @@
     'CannotUpgradeNonHosted',
     'CodeImportAlreadyRequested',
     'CodeImportAlreadyRunning',
+    'CodeImportInvalidTargetType',
     'CodeImportNotInReviewedState',
+    'CodeImportGitTargetFeatureDisabled',
     'ClaimReviewFailed',
     'DiffNotFound',
     'GitDefaultConflict',
@@ -488,6 +490,25 @@
 
 
 @error_status(httplib.BAD_REQUEST)
+class CodeImportInvalidTargetType(Exception):
+    """Raised for code imports with an invalid target for their type."""
+
+    def __init__(self, target, target_rcs_type):
+        super(CodeImportInvalidTargetType, self).__init__(
+            "Objects of type %s do not support code imports targeting %s." %
+            (target.__class__.__name__, target_rcs_type))
+
+
+@error_status(httplib.UNAUTHORIZED)
+class CodeImportGitTargetFeatureDisabled(Exception):
+    """Only certain users can create new Git-targeted code imports."""
+
+    def __init__(self):
+        super(CodeImportGitTargetFeatureDisabled, self).__init__(
+            "You do not have permission to create Git-targeted code imports.")
+
+
+@error_status(httplib.BAD_REQUEST)
 class TooNewRecipeFormat(Exception):
     """The format of the recipe supplied was too new."""
 

=== modified file 'lib/lp/code/interfaces/codeimport.py'
--- lib/lp/code/interfaces/codeimport.py	2016-10-13 15:58:50 +0000
+++ lib/lp/code/interfaces/codeimport.py	2016-10-13 15:58:50 +0000
@@ -6,6 +6,7 @@
 __metaclass__ = type
 
 __all__ = [
+    'CODE_IMPORT_GIT_TARGET_FEATURE_FLAG',
     'ICodeImport',
     'ICodeImportSet',
     ]
@@ -51,6 +52,9 @@
     )
 
 
+CODE_IMPORT_GIT_TARGET_FEATURE_FLAG = u'code.import.git_target'
+
+
 def validate_cvs_root(cvsroot):
     try:
         root = CVSRoot(cvsroot)

=== modified file 'lib/lp/code/interfaces/hasbranches.py'
--- lib/lp/code/interfaces/hasbranches.py	2016-09-07 02:46:20 +0000
+++ lib/lp/code/interfaces/hasbranches.py	2016-10-13 15:58:50 +0000
@@ -7,6 +7,8 @@
 __all__ = [
     'IHasBranches',
     'IHasCodeImports',
+    'IHasCodeImportsToBazaar',
+    'IHasCodeImportsToGit',
     'IHasMergeProposals',
     'IHasRequestedReviews',
     ]
@@ -35,6 +37,7 @@
     BranchLifecycleStatus,
     BranchMergeProposalStatus,
     RevisionControlSystems,
+    TargetRevisionControlSystems,
     )
 
 
@@ -154,13 +157,14 @@
     This interface defines the common methods that for working with them.
     """
 
-    # In order to minimise dependancies the returns_collection is defined as
+    # In order to minimise dependencies the returns_collection is defined as
     # Interface here and defined fully in the circular imports file.
 
     @operation_parameters(
         branch_name=TextLine(
             title=_('Name of branch to create'), required=True),
         rcs_type=Choice(vocabulary=RevisionControlSystems, required=True),
+        target_rcs_type=Choice(vocabulary=TargetRevisionControlSystems),
         url=TextLine(title=_('Foreign VCS URL')),
         cvs_root=TextLine(title=_('CVS root URL')),
         cvs_module=TextLine(title=_('CVS module to import')),
@@ -170,19 +174,31 @@
     @call_with(registrant=REQUEST_USER)
     @export_factory_operation(Interface, [])  # Really ICodeImport.
     @operation_for_version('beta')
-    def newCodeImport(registrant=None, branch_name=None, rcs_type=None,
-                      url=None, cvs_root=None, cvs_module=None, owner=None):
+    def newCodeImport(registrant=None, branch_name=None,
+                      rcs_type=None, target_rcs_type=None, url=None,
+                      cvs_root=None, cvs_module=None, owner=None):
         """Create a new code import.
 
         :param registrant: The IPerson to record as the registrant of the
-            import
-        :param branch_name: The name of the branch to create.
+            import.
+        :param branch_name: The name of the branch or repository to create.
         :param rcs_type: The type of the foreign VCS.
+        :param target_rcs_type: The type of the branch or repository to
+            create (Bazaar or Git).
         :param url: The URL to import from if the VCS type uses a single URL
             (i.e. isn't CVS).
         :param cvs_root: The CVSROOT for a CVS import.
         :param cvs_module: The module to import for a CVS import.
-        :param owner: Who should own the created branch, or None for it to
-            be the same as the registrant, or the caller over the API.
+        :param owner: Who should own the created branch or repository, or
+            None for it to be the same as the registrant, or the caller over
+            the API.
         :returns: An instance of `ICodeImport`.
         """
+
+
+class IHasCodeImportsToBazaar(IHasCodeImports):
+    """Marker interface for targets that support code imports to Bazaar."""
+
+
+class IHasCodeImportsToGit(IHasCodeImports):
+    """Marker interface for targets that support code imports to Git."""

=== modified file 'lib/lp/code/model/codeimport.py'
--- lib/lp/code/model/codeimport.py	2016-10-13 15:58:50 +0000
+++ lib/lp/code/model/codeimport.py	2016-10-13 15:58:50 +0000
@@ -49,6 +49,7 @@
 from lp.code.errors import (
     CodeImportAlreadyRequested,
     CodeImportAlreadyRunning,
+    CodeImportInvalidTargetType,
     CodeImportNotInReviewedState,
     )
 from lp.code.interfaces.branch import IBranch
@@ -62,6 +63,10 @@
 from lp.code.interfaces.githosting import IGitHostingClient
 from lp.code.interfaces.gitnamespace import get_git_namespace
 from lp.code.interfaces.gitrepository import IGitRepository
+from lp.code.interfaces.hasbranches import (
+    IHasCodeImportsToBazaar,
+    IHasCodeImportsToGit,
+    )
 from lp.code.mail.codeimport import code_import_updated
 from lp.code.model.codeimportjob import CodeImportJobWorkflow
 from lp.code.model.codeimportresult import CodeImportResult
@@ -291,9 +296,13 @@
         if owner is None:
             owner = registrant
         if target_rcs_type == TargetRevisionControlSystems.BZR:
+            if not IHasCodeImportsToBazaar.providedBy(context):
+                raise CodeImportInvalidTargetType(context, target_rcs_type)
             target = IBranchTarget(context)
             namespace = target.getNamespace(owner)
         elif target_rcs_type == TargetRevisionControlSystems.GIT:
+            if not IHasCodeImportsToGit.providedBy(context):
+                raise CodeImportInvalidTargetType(context, target_rcs_type)
             if rcs_type != RevisionControlSystems.GIT:
                 raise AssertionError(
                     "Can't import rcs_type %s into a Git repository" %

=== modified file 'lib/lp/code/model/hasbranches.py'
--- lib/lp/code/model/hasbranches.py	2016-10-03 17:00:56 +0000
+++ lib/lp/code/model/hasbranches.py	2016-10-13 15:58:50 +0000
@@ -20,18 +20,23 @@
     BranchMergeProposalStatus,
     TargetRevisionControlSystems,
     )
+from lp.code.errors import CodeImportGitTargetFeatureDisabled
 from lp.code.interfaces.branch import DEFAULT_BRANCH_STATUS_IN_LISTING
 from lp.code.interfaces.branchcollection import (
     IAllBranches,
     IBranchCollection,
     )
-from lp.code.interfaces.codeimport import ICodeImportSet
+from lp.code.interfaces.codeimport import (
+    CODE_IMPORT_GIT_TARGET_FEATURE_FLAG,
+    ICodeImportSet,
+    )
 from lp.code.interfaces.gitcollection import (
     IAllGitRepositories,
     IGitCollection,
     )
 from lp.registry.interfaces.sourcepackage import ISourcePackage
 from lp.services.database.decoratedresultset import DecoratedResultSet
+from lp.services.features import getFeatureFlag
 
 
 class HasBranchesMixin:
@@ -129,10 +134,14 @@
 class HasCodeImportsMixin:
 
     def newCodeImport(self, registrant=None, branch_name=None,
-            rcs_type=None, url=None, cvs_root=None, cvs_module=None,
-            owner=None):
+            rcs_type=None, target_rcs_type=None, url=None,
+            cvs_root=None, cvs_module=None, owner=None):
         """See `IHasCodeImports`."""
+        if target_rcs_type is None:
+            target_rcs_type = TargetRevisionControlSystems.BZR
+        if (target_rcs_type == TargetRevisionControlSystems.GIT and
+                not getFeatureFlag(CODE_IMPORT_GIT_TARGET_FEATURE_FLAG)):
+            raise CodeImportGitTargetFeatureDisabled
         return getUtility(ICodeImportSet).new(
-            registrant, self, branch_name, rcs_type,
-            TargetRevisionControlSystems.BZR, url=url,
-            cvs_root=cvs_root, cvs_module=cvs_module, owner=owner)
+            registrant, self, branch_name, rcs_type, target_rcs_type,
+            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-10-12 12:51:05 +0000
+++ lib/lp/code/model/tests/test_codeimport.py	2016-10-13 15:58:50 +0000
@@ -30,6 +30,7 @@
     BranchCreatorNotMemberOfOwnerTeam,
     CodeImportAlreadyRequested,
     CodeImportAlreadyRunning,
+    CodeImportInvalidTargetType,
     CodeImportNotInReviewedState,
     GitRepositoryCreatorNotMemberOfOwnerTeam,
     )
@@ -218,7 +219,7 @@
     def test_junk_code_import_rejected(self):
         """You are not allowed to create code imports targetting +junk."""
         registrant = self.factory.makePerson()
-        self.assertRaises(AssertionError, CodeImportSet().new,
+        self.assertRaises(CodeImportInvalidTargetType, CodeImportSet().new,
             registrant=registrant,
             context=registrant,
             branch_name=u'imported',

=== modified file 'lib/lp/code/stories/webservice/xx-code-import.txt'
--- lib/lp/code/stories/webservice/xx-code-import.txt	2014-02-25 01:48:48 +0000
+++ lib/lp/code/stories/webservice/xx-code-import.txt	2016-10-13 15:58:50 +0000
@@ -5,8 +5,9 @@
 if it is an import branch.
 
     >>> from zope.security.proxy import removeSecurityProxy
+    >>> from lp.code.tests.helpers import GitHostingFixture
+    >>> from lp.services.webapp.interfaces import OAuthPermission
     >>> from lp.testing.pages import webservice_for_person
-    >>> from lp.services.webapp.interfaces import OAuthPermission
 
 First we create some objects for use in the tests.
 
@@ -59,6 +60,8 @@
     Reviewed
     >>> print representation['rcs_type']
     Subversion via bzr-svn
+    >>> print representation['target_rcs_type']
+    Bazaar
     >>> print representation['url']
     http://svn.domain.com/source
     >>> print representation['cvs_root']
@@ -109,6 +112,8 @@
     Reviewed
     >>> print representation['rcs_type']
     Subversion via bzr-svn
+    >>> print representation['target_rcs_type']
+    Bazaar
     >>> print representation['url']
     http://svn.domain.com/package_source
     >>> print representation['cvs_root']
@@ -119,7 +124,8 @@
     None
 
 
-== Creating Imports ==
+Creating Imports
+----------------
 
 We can create an import using the API by calling a method on the project.
 
@@ -137,8 +143,12 @@
     http://.../~import-owner/scruff/new-import/+code-import
     >>> print representation['branch_link']
     http://.../~import-owner/scruff/new-import
+    >>> print representation['git_repository_link']
+    None
     >>> print representation['rcs_type']
     Git
+    >>> print representation['target_rcs_type']
+    Bazaar
     >>> print representation['url'] == new_remote_url
     True
     >>> print representation['cvs_root']
@@ -164,8 +174,12 @@
     http://.../~import-owner/scruff/cvs-import/+code-import
     >>> print representation['branch_link']
     http://.../~import-owner/scruff/cvs-import
+    >>> print representation['git_repository_link']
+    None
     >>> print representation['rcs_type']
     Concurrent Versions System
+    >>> print representation['target_rcs_type']
+    Bazaar
     >>> print representation['url']
     None
     >>> print representation['cvs_root'] == new_remote_url
@@ -175,6 +189,51 @@
     >>> print representation['date_last_successful']
     None
 
+We can create a Git-to-Git import, provided that we have the feature flag.
+
+    >>> from lp.code.interfaces.codeimport import (
+    ...     CODE_IMPORT_GIT_TARGET_FEATURE_FLAG,
+    ...     )
+    >>> from lp.services.features.testing import FeatureFixture
+
+    >>> product_url = '/' + product_name
+    >>> new_remote_url = factory.getUniqueURL()
+    >>> response = import_webservice.named_post(
+    ...     product_url, 'newCodeImport', branch_name='new-import',
+    ...     rcs_type='Git', target_rcs_type='Git', url=new_remote_url)
+    >>> print response.status
+    401
+    >>> print response.body
+    You do not have permission to create Git-targeted code imports.
+    >>> with FeatureFixture({CODE_IMPORT_GIT_TARGET_FEATURE_FLAG: u'on'}):
+    ...     with GitHostingFixture():
+    ...         response = import_webservice.named_post(
+    ...             product_url, 'newCodeImport', branch_name='new-import',
+    ...             rcs_type='Git', target_rcs_type='Git', url=new_remote_url)
+    >>> print response.status
+    201
+    >>> location = response.getHeader('Location')
+    >>> response = import_webservice.get(location)
+    >>> representation = response.jsonBody()
+    >>> print representation['self_link']
+    http://.../~import-owner/scruff/+git/new-import/+code-import
+    >>> print representation['branch_link']
+    None
+    >>> print representation['git_repository_link']
+    http://.../~import-owner/scruff/+git/new-import
+    >>> print representation['rcs_type']
+    Git
+    >>> print representation['target_rcs_type']
+    Git
+    >>> print representation['url'] == new_remote_url
+    True
+    >>> print representation['cvs_root']
+    None
+    >>> print representation['cvs_module']
+    None
+    >>> print representation['date_last_successful']
+    None
+
 We can also create an import targetting a source package.
 
     >>> source_package_url = (
@@ -193,7 +252,46 @@
     http://.../~import-owner/scruffbuntu/manic/scruff/new-import/+code-import
     >>> print representation['branch_link']
     http://.../~import-owner/scruffbuntu/manic/scruff/new-import
-    >>> print representation['rcs_type']
+    >>> print representation['git_repository_link']
+    None
+    >>> print representation['rcs_type']
+    Git
+    >>> print representation['target_rcs_type']
+    Bazaar
+    >>> print representation['url'] == new_remote_url
+    True
+    >>> print representation['cvs_root']
+    None
+    >>> print representation['cvs_module']
+    None
+    >>> print representation['date_last_successful']
+    None
+
+We can create a Git-to-Git import targetting a distribution source package.
+
+    >>> distro_source_package_url = (
+    ...     '/' + distribution.name + '/+source/' + source_package.name)
+    >>> new_remote_url = factory.getUniqueURL()
+    >>> with FeatureFixture({CODE_IMPORT_GIT_TARGET_FEATURE_FLAG: u'on'}):
+    ...     with GitHostingFixture():
+    ...         response = import_webservice.named_post(
+    ...             distro_source_package_url, 'newCodeImport',
+    ...             branch_name='new-import', rcs_type='Git',
+    ...             target_rcs_type='Git', url=new_remote_url)
+    >>> print response.status
+    201
+    >>> location = response.getHeader('Location')
+    >>> response = import_webservice.get(location)
+    >>> representation = response.jsonBody()
+    >>> print representation['self_link']
+    http://.../~import-owner/scruffbuntu/+source/scruff/+git/new-import/+code-import
+    >>> print representation['branch_link']
+    None
+    >>> print representation['git_repository_link']
+    http://.../~import-owner/scruffbuntu/+source/scruff/+git/new-import
+    >>> print representation['rcs_type']
+    Git
+    >>> print representation['target_rcs_type']
     Git
     >>> print representation['url'] == new_remote_url
     True
@@ -220,8 +318,12 @@
     http://.../~import-owner-team/scruff/team-import/+code-import
     >>> print representation['branch_link']
     http://.../~import-owner-team/scruff/team-import
+    >>> print representation['git_repository_link']
+    None
     >>> print representation['rcs_type']
     Git
+    >>> print representation['target_rcs_type']
+    Bazaar
     >>> print representation['url'] == new_remote_url
     True
     >>> print representation['cvs_root']
@@ -232,7 +334,8 @@
     None
 
 
-== Requesting an Import ==
+Requesting an Import
+--------------------
 
 You can request that an approved, working import happen soon over the
 API using the requestImport() method.

=== modified file 'lib/lp/registry/configure.zcml'
--- lib/lp/registry/configure.zcml	2016-04-13 13:28:10 +0000
+++ lib/lp/registry/configure.zcml	2016-10-13 15:58:50 +0000
@@ -607,6 +607,11 @@
 
         <allow
             interface="lp.code.interfaces.hasgitrepositories.IHasGitRepositories" />
+
+        <!-- IHasCodeImports -->
+
+        <allow
+            interface="lp.code.interfaces.hasbranches.IHasCodeImports" />
     </class>
     <adapter
         provides="lp.registry.interfaces.distribution.IDistribution"

=== modified file 'lib/lp/registry/interfaces/distributionsourcepackage.py'
--- lib/lp/registry/interfaces/distributionsourcepackage.py	2015-10-12 16:16:28 +0000
+++ lib/lp/registry/interfaces/distributionsourcepackage.py	2016-10-13 15:58:50 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2012 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).
 
 """Source package in Distribution interfaces."""
@@ -32,6 +32,7 @@
     )
 from lp.code.interfaces.hasbranches import (
     IHasBranches,
+    IHasCodeImportsToGit,
     IHasMergeProposals,
     )
 from lp.code.interfaces.hasgitrepositories import IHasGitRepositories
@@ -44,7 +45,7 @@
                                  IHasMergeProposals, IHasOfficialBugTags,
                                  IStructuralSubscriptionTarget,
                                  IQuestionTarget, IHasDrivers,
-                                 IHasGitRepositories):
+                                 IHasGitRepositories, IHasCodeImportsToGit):
     """Represents a source package in a distribution.
 
     Create IDistributionSourcePackages by invoking

=== modified file 'lib/lp/registry/interfaces/product.py'
--- lib/lp/registry/interfaces/product.py	2015-10-01 17:32:41 +0000
+++ lib/lp/registry/interfaces/product.py	2016-10-13 15:58:50 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2013 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).
 
 """Interfaces including and related to IProduct."""
@@ -100,7 +100,8 @@
     )
 from lp.code.interfaces.hasbranches import (
     IHasBranches,
-    IHasCodeImports,
+    IHasCodeImportsToBazaar,
+    IHasCodeImportsToGit,
     IHasMergeProposals,
     )
 from lp.code.interfaces.hasgitrepositories import IHasGitRepositories
@@ -484,7 +485,8 @@
     IHasMugshot, IHasSprints, IHasTranslationImports,
     ITranslationPolicy, IKarmaContext, IMakesAnnouncements,
     IOfficialBugTagTargetPublic, IHasOOPSReferences,
-    IHasRecipes, IHasCodeImports, IServiceUsage, IHasGitRepositories):
+    IHasRecipes, IHasCodeImportsToBazaar, IHasCodeImportsToGit,
+    IServiceUsage, IHasGitRepositories):
     """Public IProduct properties."""
 
     registrant = exported(

=== modified file 'lib/lp/registry/interfaces/sourcepackage.py'
--- lib/lp/registry/interfaces/sourcepackage.py	2016-06-01 01:59:32 +0000
+++ lib/lp/registry/interfaces/sourcepackage.py	2016-10-13 15:58:50 +0000
@@ -51,7 +51,7 @@
     )
 from lp.code.interfaces.hasbranches import (
     IHasBranches,
-    IHasCodeImports,
+    IHasCodeImportsToBazaar,
     IHasMergeProposals,
     )
 from lp.registry.interfaces.productseries import IProductSeries
@@ -69,7 +69,7 @@
 
 
 class ISourcePackagePublic(IBugTarget, IHasBranches, IHasMergeProposals,
-                           IHasOfficialBugTags, IHasCodeImports,
+                           IHasOfficialBugTags, IHasCodeImportsToBazaar,
                            IHasTranslationImports, IHasTranslationTemplates,
                            IHasDrivers, IHasOwner):
     """Public attributes for SourcePackage."""

=== modified file 'lib/lp/registry/model/distributionsourcepackage.py'
--- lib/lp/registry/model/distributionsourcepackage.py	2015-10-12 16:16:28 +0000
+++ lib/lp/registry/model/distributionsourcepackage.py	2016-10-13 15:58:50 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2012 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).
 
 """Classes to represent source packages in a distribution."""
@@ -41,6 +41,7 @@
     )
 from lp.code.model.hasbranches import (
     HasBranchesMixin,
+    HasCodeImportsMixin,
     HasMergeProposalsMixin,
     )
 from lp.registry.interfaces.distributionsourcepackage import (
@@ -119,6 +120,7 @@
                                 SourcePackageQuestionTargetMixin,
                                 StructuralSubscriptionTargetMixin,
                                 HasBranchesMixin,
+                                HasCodeImportsMixin,
                                 HasCustomLanguageCodesMixin,
                                 HasMergeProposalsMixin,
                                 HasDriversMixin):


Follow ups