← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~ruinedyourlife/launchpad:expose-git-path-and-git-repository-url into launchpad:master

 

Quentin Debhi has proposed merging ~ruinedyourlife/launchpad:expose-git-path-and-git-repository-url into launchpad:master with ~ruinedyourlife/launchpad:fetch-service-configuration-for-craft-builds as a prerequisite.

Commit message:
Expose git_path & git_repository_url

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~ruinedyourlife/launchpad/+git/launchpad/+merge/474651
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~ruinedyourlife/launchpad:expose-git-path-and-git-repository-url into launchpad:master.
diff --git a/lib/lp/crafts/interfaces/craftrecipe.py b/lib/lp/crafts/interfaces/craftrecipe.py
index ef8e752..401ecfb 100644
--- a/lib/lp/crafts/interfaces/craftrecipe.py
+++ b/lib/lp/crafts/interfaces/craftrecipe.py
@@ -74,7 +74,7 @@ from lp.code.interfaces.gitref import IGitRef
 from lp.code.interfaces.gitrepository import IGitRepository
 from lp.registry.interfaces.person import IPerson
 from lp.registry.interfaces.product import IProduct
-from lp.services.fields import PersonChoice, PublicPersonChoice
+from lp.services.fields import PersonChoice, PublicPersonChoice, URIField
 from lp.snappy.validators.channels import channels_validator
 
 CRAFT_RECIPE_ALLOW_CREATE = "craft.recipe.create.enabled"
@@ -559,24 +559,48 @@ class ICraftRecipeEditableAttributes(Interface):
         )
     )
 
-    git_repository = ReferenceChoice(
-        title=_("Git repository"),
-        schema=IGitRepository,
-        vocabulary="GitRepository",
-        required=False,
-        readonly=True,
-        description=_(
-            "A Git repository with a branch containing a craft.yaml recipe."
-        ),
+    git_repository = exported(
+        ReferenceChoice(
+            title=_("Git repository"),
+            schema=IGitRepository,
+            vocabulary="GitRepository",
+            required=False,
+            readonly=True,
+            description=_(
+                "A Git repository with a branch containing a sourcecraft.yaml "
+                "recipe."
+            ),
+        )
     )
 
-    git_path = TextLine(
-        title=_("Git branch path"),
-        required=False,
-        readonly=True,
-        description=_(
-            "The path of the Git branch containing a craft.yaml recipe."
-        ),
+    git_path = exported(
+        TextLine(
+            title=_("Git branch path"),
+            required=False,
+            readonly=True,
+            description=_(
+                "The path of the Git branch containing a sourcecraft.yaml "
+                "recipe."
+            ),
+        )
+    )
+
+    git_repository_url = exported(
+        URIField(
+            title=_("Git repository URL"),
+            required=False,
+            readonly=True,
+            description=_(
+                "The URL of a Git repository with a branch containing a "
+                "sourcecraft.yaml at the top level."
+            ),
+            allowed_schemes=["git", "http", "https"],
+            allow_userinfo=True,
+            allow_port=True,
+            allow_query=False,
+            allow_fragment=False,
+            trailing_slash=False,
+        )
     )
 
     git_ref = exported(
@@ -776,6 +800,9 @@ class ICraftRecipeSet(Interface):
         project,
         name,
         description=None,
+        git_repository=None,
+        git_repository_url=None,
+        git_path=None,
         git_ref=None,
         build_path=None,
         require_virtualized=True,
diff --git a/lib/lp/crafts/model/craftrecipe.py b/lib/lp/crafts/model/craftrecipe.py
index aea0593..1693b16 100644
--- a/lib/lp/crafts/model/craftrecipe.py
+++ b/lib/lp/crafts/model/craftrecipe.py
@@ -38,6 +38,7 @@ from lp.app.enums import (
     PUBLIC_INFORMATION_TYPES,
     InformationType,
 )
+from lp.app.errors import IncompatibleArguments
 from lp.buildmaster.enums import BuildStatus
 from lp.buildmaster.interfaces.buildqueue import IBuildQueueSet
 from lp.buildmaster.model.builder import Builder
@@ -48,7 +49,7 @@ from lp.code.interfaces.gitcollection import (
     IAllGitRepositories,
     IGitCollection,
 )
-from lp.code.interfaces.gitref import IGitRef
+from lp.code.interfaces.gitref import IGitRef, IGitRefRemoteSet
 from lp.code.interfaces.gitrepository import IGitRepository
 from lp.code.model.gitcollection import GenericGitCollection
 from lp.code.model.gitref import GitRef
@@ -172,6 +173,8 @@ class CraftRecipe(StormBase):
 
     git_path = Unicode(name="git_path", allow_none=True)
 
+    git_repository_url = Unicode(name="git_repository_url", allow_none=True)
+
     build_path = Unicode(name="build_path", allow_none=True)
 
     require_virtualized = Bool(name="require_virtualized")
@@ -269,6 +272,10 @@ class CraftRecipe(StormBase):
     def _git_ref(self):
         if self.git_repository is not None:
             return self.git_repository.getRefByPath(self.git_path)
+        elif self.git_repository_url is not None:
+            return getUtility(IGitRefRemoteSet).new(
+                self.git_repository_url, self.git_path
+            )
         else:
             return None
 
@@ -282,9 +289,11 @@ class CraftRecipe(StormBase):
         """See `ICraftRecipe`."""
         if value is not None:
             self.git_repository = value.repository
+            self.git_repository_url = value.repository_url
             self.git_path = value.path
         else:
             self.git_repository = None
+            self.git_repository_url = None
             self.git_path = None
         get_property_cache(self)._git_ref = value
 
@@ -663,6 +672,9 @@ class CraftRecipeSet:
         project,
         name,
         description=None,
+        git_repository=None,
+        git_repository_url=None,
+        git_path=None,
         git_ref=None,
         build_path=None,
         require_virtualized=True,
@@ -689,6 +701,34 @@ class CraftRecipeSet:
                     % (registrant.displayname, owner.displayname)
                 )
 
+        if (
+            sum(
+                [
+                    git_repository is not None,
+                    git_repository_url is not None,
+                    git_ref is not None,
+                ]
+            )
+            > 1
+        ):
+            raise IncompatibleArguments(
+                "You cannot specify more than one of 'git_repository', "
+                "'git_repository_url', and 'git_ref'."
+            )
+        if (git_repository is None and git_repository_url is None) != (
+            git_path is None
+        ):
+            raise IncompatibleArguments(
+                "You must specify both or neither of "
+                "'git_repository'/'git_repository_url' and 'git_path'."
+            )
+        if git_repository is not None:
+            git_ref = git_repository.getRefByPath(git_path)
+        elif git_repository_url is not None:
+            git_ref = getUtility(IGitRefRemoteSet).new(
+                git_repository_url, git_path
+            )
+
         if git_ref is None:
             raise NoSourceForCraftRecipe
         if self.exists(owner, project, name):
@@ -788,7 +828,12 @@ class CraftRecipeSet:
 
         git_collection = removeSecurityProxy(getUtility(IAllGitRepositories))
         git_recipes = _getRecipes(git_collection)
-        return git_recipes
+        git_url_recipes = IStore(CraftRecipe).find(
+            CraftRecipe,
+            CraftRecipe.owner == person,
+            CraftRecipe.git_repository_url != None,
+        )
+        return git_recipes.union(git_url_recipes)
 
     def findByProject(self, project, visible_by_user=None):
         """See `ICraftRecipeSet`."""
diff --git a/lib/lp/crafts/model/craftrecipebuildbehaviour.py b/lib/lp/crafts/model/craftrecipebuildbehaviour.py
index 5a328ad..bfcd9ff 100644
--- a/lib/lp/crafts/model/craftrecipebuildbehaviour.py
+++ b/lib/lp/crafts/model/craftrecipebuildbehaviour.py
@@ -96,7 +96,12 @@ class CraftRecipeBuildBehaviour(BuilderProxyMixin, BuildFarmJobBehaviourBase):
         if build.recipe.build_path is not None:
             args["build_path"] = build.recipe.build_path
         if build.recipe.git_ref is not None:
-            args["git_repository"] = build.recipe.git_repository.git_https_url
+            if build.recipe.git_ref.repository_url is not None:
+                args["git_repository"] = build.recipe.git_ref.repository_url
+            else:
+                args["git_repository"] = (
+                    build.recipe.git_repository.git_https_url
+                )
             # "git clone -b" doesn't accept full ref names.  If this becomes
             # a problem then we could change launchpad-buildd to do "git
             # clone" followed by "git checkout" instead.
diff --git a/lib/lp/crafts/tests/test_craftrecipe.py b/lib/lp/crafts/tests/test_craftrecipe.py
index b5c2903..7425438 100644
--- a/lib/lp/crafts/tests/test_craftrecipe.py
+++ b/lib/lp/crafts/tests/test_craftrecipe.py
@@ -626,6 +626,17 @@ class TestCraftRecipeSet(TestCaseWithFactory):
         self.assertEqual([], recipe.store_channels)
         self.assertFalse(recipe.use_fetch_service)
 
+    def test_creation_git_url(self):
+        # A craft recipe can be backed directly by a URL for an external Git
+        # repository, rather than a Git repository hosted in Launchpad.
+        ref = self.factory.makeGitRefRemote()
+        components = self.makeCraftRecipeComponents(git_ref=ref)
+        craft_recipe = getUtility(ICraftRecipeSet).new(**components)
+        self.assertIsNone(craft_recipe.git_repository)
+        self.assertEqual(ref.repository_url, craft_recipe.git_repository_url)
+        self.assertEqual(ref.path, craft_recipe.git_path)
+        self.assertEqual(ref, craft_recipe.git_ref)
+
     def test_creation_no_source(self):
         # Attempting to create a craft recipe without a Git repository
         # fails.
diff --git a/lib/lp/crafts/tests/test_craftrecipebuildbehaviour.py b/lib/lp/crafts/tests/test_craftrecipebuildbehaviour.py
index 62bdbfd..83ebd8f 100644
--- a/lib/lp/crafts/tests/test_craftrecipebuildbehaviour.py
+++ b/lib/lp/crafts/tests/test_craftrecipebuildbehaviour.py
@@ -459,6 +459,51 @@ class TestAsyncCraftRecipeBuildBehaviour(
         self.assertTrue(args["private"])
 
     @defer.inlineCallbacks
+    def test_extraBuildArgs_git_url(self):
+        # extraBuildArgs returns appropriate arguments if asked to build a
+        # job for a Git branch backed by a URL for an external repository.
+        url = "https://git.example.org/foo";
+        ref = self.factory.makeGitRefRemote(
+            repository_url=url, path="refs/heads/master"
+        )
+        job = self.makeJob(git_ref=ref)
+        (
+            expected_archives,
+            expected_trusted_keys,
+        ) = yield get_sources_list_for_building(
+            job, job.build.distro_arch_series, None
+        )
+        for archive_line in expected_archives:
+            self.assertIn("universe", archive_line)
+        with dbuser(config.builddmaster.dbuser):
+            args = yield job.extraBuildArgs()
+        self.assertThat(
+            args,
+            MatchesDict(
+                {
+                    "archive_private": Is(False),
+                    "archives": Equals(expected_archives),
+                    "arch_tag": Equals("i386"),
+                    "build_url": Equals(canonical_url(job.build)),
+                    "builder_constraints": Equals([]),
+                    "channels": Equals({}),
+                    "fast_cleanup": Is(True),
+                    "git_repository": Equals(url),
+                    "git_path": Equals("master"),
+                    "name": Equals("test-craft"),
+                    "private": Is(False),
+                    "proxy_url": ProxyURLMatcher(job, self.now),
+                    "revocation_endpoint": RevocationEndpointMatcher(
+                        job, self.now
+                    ),
+                    "series": Equals("unstable"),
+                    "trusted_keys": Equals(expected_trusted_keys),
+                    "use_fetch_service": Is(False),
+                }
+            ),
+        )
+
+    @defer.inlineCallbacks
     def test_composeBuildRequest_proxy_url_set(self):
         job = self.makeJob()
         build_request = yield job.composeBuildRequest(None)