← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~jugmac00/launchpad:expose-git_path-and-git_repository_url-via-api into launchpad:master

 

Jürgen Gmach has proposed merging ~jugmac00/launchpad:expose-git_path-and-git_repository_url-via-api into launchpad:master.

Commit message:
[WIP] Expose git_path and git_repository_url for rock recipes

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~jugmac00/launchpad/+git/launchpad/+merge/474463
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~jugmac00/launchpad:expose-git_path-and-git_repository_url-via-api into launchpad:master.
diff --git a/lib/lp/rocks/interfaces/rockrecipe.py b/lib/lp/rocks/interfaces/rockrecipe.py
index ca87032..152f965 100644
--- a/lib/lp/rocks/interfaces/rockrecipe.py
+++ b/lib/lp/rocks/interfaces/rockrecipe.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
 
 ROCK_RECIPE_ALLOW_CREATE = "rock.recipe.create.enabled"
@@ -558,25 +558,48 @@ class IRockRecipeEditableAttributes(Interface):
         )
     )
 
-    git_repository = ReferenceChoice(
-        title=_("Git repository"),
-        schema=IGitRepository,
-        vocabulary="GitRepository",
-        required=False,
-        readonly=True,
-        description=_(
-            "A Git repository with a branch containing a rockcraft.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 rockcraft.yaml "
+                "recipe."
+            ),
+        )
     )
 
-    git_path = TextLine(
-        title=_("Git branch path"),
-        required=False,
-        readonly=True,
-        description=_(
-            "The path of the Git branch containing a rockcraft.yaml recipe."
-        ),
+    git_path = exported(
+        TextLine(
+            title=_("Git branch path"),
+            required=False,
+            readonly=True,
+            description=_(
+                "The path of the Git branch containing a rockcraft.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 "
+                "rockcraft.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(
@@ -758,6 +781,9 @@ class IRockRecipeSet(Interface):
             "project",
             "name",
             "description",
+            "git_repository",
+            "git_repository_url",
+            "git_path",
             "git_ref",
             "build_path",
             "auto_build",
@@ -774,6 +800,9 @@ class IRockRecipeSet(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/rocks/model/rockrecipe.py b/lib/lp/rocks/model/rockrecipe.py
index 48a45e6..e0dd4c2 100644
--- a/lib/lp/rocks/model/rockrecipe.py
+++ b/lib/lp/rocks/model/rockrecipe.py
@@ -48,7 +48,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
@@ -105,6 +105,7 @@ from lp.services.job.interfaces.job import JobStatus
 from lp.services.job.model.job import Job
 from lp.services.librarian.model import LibraryFileAlias
 from lp.services.propertycache import cachedproperty, get_property_cache
+from lp.soyuz.interfaces.buildrecords import IncompatibleArguments
 from lp.soyuz.model.distroarchseries import DistroArchSeries, PocketChroot
 
 
@@ -242,6 +243,8 @@ class RockRecipe(StormBase):
     )
     git_repository = Reference(git_repository_id, "GitRepository.id")
 
+    git_repository_url = Unicode(name="git_repository_url", allow_none=True)
+
     git_path = Unicode(name="git_path", allow_none=True)
 
     build_path = Unicode(name="build_path", allow_none=True)
@@ -341,6 +344,10 @@ class RockRecipe(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
 
@@ -354,9 +361,11 @@ class RockRecipe(StormBase):
         """See `IRockRecipe`."""
         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
 
@@ -767,6 +776,9 @@ class RockRecipeSet:
         project,
         name,
         description=None,
+        git_repository=None,
+        git_repository_url=None,
+        git_path=None,
         git_ref=None,
         build_path=None,
         require_virtualized=True,
@@ -793,6 +805,34 @@ class RockRecipeSet:
                     % (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 NoSourceForRockRecipe
         if self.exists(owner, project, name):
@@ -873,7 +913,12 @@ class RockRecipeSet:
 
         git_collection = removeSecurityProxy(getUtility(IAllGitRepositories))
         git_recipes = _getRecipes(git_collection)
-        return git_recipes
+        git_url_recipes = IStore(RockRecipe).find(
+            RockRecipe,
+            RockRecipe.owner == person,
+            RockRecipe.git_repository_url != None,
+        )
+        return git_recipes.union(git_url_recipes)
 
     def findByProject(self, project, visible_by_user=None):
         """See `IRockRecipeSet`."""
diff --git a/lib/lp/rocks/model/rockrecipebuildbehaviour.py b/lib/lp/rocks/model/rockrecipebuildbehaviour.py
index a3bf315..fa5858e 100644
--- a/lib/lp/rocks/model/rockrecipebuildbehaviour.py
+++ b/lib/lp/rocks/model/rockrecipebuildbehaviour.py
@@ -96,7 +96,12 @@ class RockRecipeBuildBehaviour(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.snap.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/rocks/tests/test_rockrecipe.py b/lib/lp/rocks/tests/test_rockrecipe.py
index 1b4dc47..019d704 100644
--- a/lib/lp/rocks/tests/test_rockrecipe.py
+++ b/lib/lp/rocks/tests/test_rockrecipe.py
@@ -1148,6 +1148,18 @@ class TestRockRecipeSet(TestCaseWithFactory):
         self.assertEqual([], recipe.store_channels)
         self.assertFalse(recipe.use_fetch_service)
 
+    def test_creation_git_url(self):
+        # A rock 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.makeRockRecipeComponents(git_ref=ref)
+        rock_recipe = getUtility(IRockRecipeSet).new(**components)
+        self.assertIsNone(rock_recipe.branch)
+        self.assertIsNone(rock_recipe.git_repository)
+        self.assertEqual(ref.repository_url, rock_recipe.git_repository_url)
+        self.assertEqual(ref.path, rock_recipe.git_path)
+        self.assertEqual(ref, rock_recipe.git_ref)
+
     def test_creation_no_source(self):
         # Attempting to create a rock recipe without a Git repository
         # fails.
diff --git a/lib/lp/rocks/tests/test_rockrecipebuildbehaviour.py b/lib/lp/rocks/tests/test_rockrecipebuildbehaviour.py
index f2b8bac..45fcd2a 100644
--- a/lib/lp/rocks/tests/test_rockrecipebuildbehaviour.py
+++ b/lib/lp/rocks/tests/test_rockrecipebuildbehaviour.py
@@ -460,6 +460,52 @@ class TestAsyncRockRecipeBuildBehaviour(
         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_source_tarball": Is(False),
+                    "build_url": Equals(canonical_url(job.build)),
+                    "builder_constraints": Equals([]),
+                    "fast_cleanup": Is(True),
+                    "git_repository": Equals(url),
+                    "git_path": Equals("master"),
+                    "name": Equals("test-snap"),
+                    "private": Is(False),
+                    "proxy_url": ProxyURLMatcher(job, self.now),
+                    "revocation_endpoint": RevocationEndpointMatcher(
+                        job, self.now
+                    ),
+                    "series": Equals("unstable"),
+                    "trusted_keys": Equals(expected_trusted_keys),
+                    "target_architectures": Equals(["i386"]),
+                    "use_fetch_service": Is(None),
+                }
+            ),
+        )
+
+    @defer.inlineCallbacks
     def test_composeBuildRequest_proxy_url_set(self):
         job = self.makeJob()
         build_request = yield job.composeBuildRequest(None)

Follow ups