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):
_______________________________________________ Mailing list: https://launchpad.net/~launchpad-reviewers Post to : launchpad-reviewers@lists.launchpad.net Unsubscribe : https://launchpad.net/~launchpad-reviewers More help : https://help.launchpad.net/ListHelp