← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~cjwatson/launchpad:oci-git-owner-default into launchpad:master

 

Colin Watson has proposed merging ~cjwatson/launchpad:oci-git-owner-default into launchpad:master with ~cjwatson/launchpad:oci-git-more-tests as a prerequisite.

Commit message:
Add owner-default repositories for OCI projects

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #1847444 in Launchpad itself: "Support OCI image building"
  https://bugs.launchpad.net/launchpad/+bug/1847444

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/376142
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:oci-git-owner-default into launchpad:master.
diff --git a/lib/lp/code/adapters/gitcollection.py b/lib/lp/code/adapters/gitcollection.py
index 1b0f162..41282b2 100644
--- a/lib/lp/code/adapters/gitcollection.py
+++ b/lib/lp/code/adapters/gitcollection.py
@@ -7,8 +7,10 @@ __metaclass__ = type
 __all__ = [
     'git_collection_for_distribution',
     'git_collection_for_distro_source_package',
+    'git_collection_for_oci_project',
     'git_collection_for_person',
     'git_collection_for_person_distro_source_package',
+    'git_collection_for_person_oci_project',
     'git_collection_for_person_product',
     'git_collection_for_project',
     'git_collection_for_project_group',
@@ -41,6 +43,11 @@ def git_collection_for_distro_source_package(distro_source_package):
         distro_source_package)
 
 
+def git_collection_for_oci_project(oci_project):
+    """Adapt an OCI project to a Git repository collection."""
+    return getUtility(IAllGitRepositories).inOCIProject(oci_project)
+
+
 def git_collection_for_person(person):
     """Adapt a person to a Git repository collection."""
     return getUtility(IAllGitRepositories).ownedBy(person)
@@ -60,3 +67,11 @@ def git_collection_for_person_distro_source_package(person_dsp):
     collection = collection.inDistributionSourcePackage(
         person_dsp.distro_source_package)
     return collection
+
+
+def git_collection_for_person_oci_project(person_oci_project):
+    """Adapt a PersonOCIProject to a Git repository collection."""
+    collection = getUtility(IAllGitRepositories).ownedBy(
+        person_oci_project.person)
+    collection = collection.inOCIProject(person_oci_project.oci_project)
+    return collection
diff --git a/lib/lp/code/configure.zcml b/lib/lp/code/configure.zcml
index 8f50dce..d8ab7ee 100644
--- a/lib/lp/code/configure.zcml
+++ b/lib/lp/code/configure.zcml
@@ -1028,6 +1028,10 @@
       provides="lp.code.interfaces.gitcollection.IGitCollection"
       factory="lp.code.adapters.gitcollection.git_collection_for_distro_source_package"/>
   <adapter
+      for="lp.registry.interfaces.ociproject.IOCIProject"
+      provides="lp.code.interfaces.gitcollection.IGitCollection"
+      factory="lp.code.adapters.gitcollection.git_collection_for_oci_project"/>
+  <adapter
       for="lp.registry.interfaces.person.IPerson"
       provides="lp.code.interfaces.gitcollection.IGitCollection"
       factory="lp.code.adapters.gitcollection.git_collection_for_person"/>
@@ -1039,6 +1043,10 @@
       for="lp.registry.interfaces.persondistributionsourcepackage.IPersonDistributionSourcePackage"
       provides="lp.code.interfaces.gitcollection.IGitCollection"
       factory="lp.code.adapters.gitcollection.git_collection_for_person_distro_source_package"/>
+  <adapter
+      for="lp.registry.interfaces.personociproject.IPersonOCIProject"
+      provides="lp.code.interfaces.gitcollection.IGitCollection"
+      factory="lp.code.adapters.gitcollection.git_collection_for_person_oci_project"/>
   <securedutility
       class="lp.code.model.gitcollection.GenericGitCollection"
       provides="lp.code.interfaces.gitcollection.IAllGitRepositories">
@@ -1052,6 +1060,7 @@
   <adapter factory="lp.code.model.defaultgit.OCIProjectDefaultGitRepository" />
   <adapter factory="lp.code.model.defaultgit.OwnerProjectDefaultGitRepository" />
   <adapter factory="lp.code.model.defaultgit.OwnerPackageDefaultGitRepository" />
+  <adapter factory="lp.code.model.defaultgit.OwnerOCIProjectDefaultGitRepository" />
 
   <class class="lp.code.model.gitlookup.GitLookup">
     <allow interface="lp.code.interfaces.gitlookup.IGitLookup" />
diff --git a/lib/lp/code/interfaces/gitrepository.py b/lib/lp/code/interfaces/gitrepository.py
index 7a6d9d9..1b35e21 100644
--- a/lib/lp/code/interfaces/gitrepository.py
+++ b/lib/lp/code/interfaces/gitrepository.py
@@ -73,11 +73,12 @@ from lp.code.interfaces.hasrecipes import IHasRecipes
 from lp.registry.interfaces.distributionsourcepackage import (
     IDistributionSourcePackage,
     )
-from lp.registry.interfaces.ociprojectname import IOCIProjectName
+from lp.registry.interfaces.ociproject import IOCIProject
 from lp.registry.interfaces.person import IPerson
 from lp.registry.interfaces.persondistributionsourcepackage import (
     IPersonDistributionSourcePackageFactory,
     )
+from lp.registry.interfaces.personociproject import IPersonOCIProjectFactory
 from lp.registry.interfaces.personproduct import IPersonProductFactory
 from lp.registry.interfaces.product import IProduct
 from lp.registry.interfaces.role import IPersonRoles
@@ -947,8 +948,9 @@ class IGitRepositorySet(Interface):
             repository.
         :param registrant: The `IPerson` who registered the new repository.
         :param owner: The `IPerson` who owns the new repository.
-        :param target: The `IProduct`, `IDistributionSourcePackage`, or
-            `IPerson` that the new repository is associated with.
+        :param target: The `IProduct`, `IDistributionSourcePackage`,
+            `IOCIProjectName`, or `IPerson` that the new repository is
+            associated with.
         :param name: The repository name.
         :param information_type: Set the repository's information type to
             one different from the target's default.  The type must conform
@@ -1163,11 +1165,14 @@ class GitIdentityMixin:
             elif IDistributionSourcePackage.providedBy(self.target):
                 factory = getUtility(IPersonDistributionSourcePackageFactory)
                 default = factory.create(self.owner, self.target)
+            elif IOCIProject.providedBy(self.target):
+                factory = getUtility(IPersonOCIProjectFactory)
+                default = factory.create(self.owner, self.target)
             else:
                 # Also enforced by database constraint.
                 raise AssertionError(
-                    "Only projects or packages can have owner-target default "
-                    "repositories.")
+                    "Only projects, packages, or OCI projects can have "
+                    "owner-target default repositories.")
             defaults.append(ICanHasDefaultGitRepository(default))
         return sorted(defaults)
 
diff --git a/lib/lp/code/model/defaultgit.py b/lib/lp/code/model/defaultgit.py
index 0a87759..5862719 100644
--- a/lib/lp/code/model/defaultgit.py
+++ b/lib/lp/code/model/defaultgit.py
@@ -19,6 +19,7 @@ from lp.registry.interfaces.ociproject import IOCIProject
 from lp.registry.interfaces.persondistributionsourcepackage import (
     IPersonDistributionSourcePackage,
     )
+from lp.registry.interfaces.personociproject import IPersonOCIProject
 from lp.registry.interfaces.personproduct import IPersonProduct
 from lp.registry.interfaces.product import IProduct
 
@@ -124,3 +125,22 @@ class OwnerPackageDefaultGitRepository(BaseDefaultGitRepository):
         return "~%s/%s/+source/%s" % (
             self.context.person.name, dsp.distribution.name,
             dsp.sourcepackagename.name)
+
+
+@adapter(IPersonOCIProject)
+@implementer(ICanHasDefaultGitRepository)
+class OwnerOCIProjectDefaultGitRepository(BaseDefaultGitRepository):
+    """Implement an owner's default Git repository for an OCI project."""
+
+    sort_order = 1
+
+    def __init__(self, person_oci_project):
+        self.context = person_oci_project
+
+    @property
+    def path(self):
+        """See `ICanHasDefaultGitRepository`."""
+        oci_project = self.context.oci_project
+        return "~%s/%s/+oci/%s" % (
+            self.context.person.name, oci_project.pillar.name,
+            oci_project.name)
diff --git a/lib/lp/code/model/gitrepository.py b/lib/lp/code/model/gitrepository.py
index 966db3a..2975d1d 100644
--- a/lib/lp/code/model/gitrepository.py
+++ b/lib/lp/code/model/gitrepository.py
@@ -1757,6 +1757,10 @@ class GitRepositorySet:
             clauses.append(GitRepository.distribution == target.distribution)
             clauses.append(
                 GitRepository.sourcepackagename == target.sourcepackagename)
+        elif IOCIProject.providedBy(target):
+            clauses.append(GitRepository.distribution == target.distribution)
+            clauses.append(
+                GitRepository.ociprojectname == target.ociprojectname)
         else:
             raise GitTargetError(
                 "Personal repositories cannot be defaults for any target.")
diff --git a/lib/lp/code/model/tests/test_gitcollection.py b/lib/lp/code/model/tests/test_gitcollection.py
index 91f6e4b..f96e8e8 100644
--- a/lib/lp/code/model/tests/test_gitcollection.py
+++ b/lib/lp/code/model/tests/test_gitcollection.py
@@ -44,6 +44,7 @@ from lp.registry.interfaces.person import TeamMembershipPolicy
 from lp.registry.model.persondistributionsourcepackage import (
     PersonDistributionSourcePackage,
     )
+from lp.registry.model.personociproject import PersonOCIProject
 from lp.registry.model.personproduct import PersonProduct
 from lp.services.database.interfaces import IStore
 from lp.services.webapp.publisher import canonical_url
@@ -85,6 +86,10 @@ class TestGitCollectionAdaptation(TestCaseWithFactory):
         # collection.
         self.assertCollection(self.factory.makeDistributionSourcePackage())
 
+    def test_oci_project(self):
+        # An OCI project can be adapted to a Git repository collection.
+        self.assertCollection(self.factory.makeOCIProject())
+
     def test_person(self):
         # A person can be adapted to a Git repository collection.
         self.assertCollection(self.factory.makePerson())
@@ -101,6 +106,12 @@ class TestGitCollectionAdaptation(TestCaseWithFactory):
         self.assertCollection(
             PersonDistributionSourcePackage(dsp.distribution.owner, dsp))
 
+    def test_person_oci_project(self):
+        # A PersonOCIProject can be adapted to a Git repository collection.
+        oci_project = self.factory.makeOCIProject()
+        self.assertCollection(
+            PersonOCIProject(oci_project.pillar.owner, oci_project))
+
 
 class TestGenericGitCollection(TestCaseWithFactory):
 
diff --git a/lib/lp/code/model/tests/test_gitrepository.py b/lib/lp/code/model/tests/test_gitrepository.py
index 991ba7b..acb5fff 100644
--- a/lib/lp/code/model/tests/test_gitrepository.py
+++ b/lib/lp/code/model/tests/test_gitrepository.py
@@ -136,6 +136,7 @@ from lp.registry.interfaces.person import IPerson
 from lp.registry.interfaces.persondistributionsourcepackage import (
     IPersonDistributionSourcePackageFactory,
     )
+from lp.registry.interfaces.personociproject import IPersonOCIProjectFactory
 from lp.registry.interfaces.personproduct import IPersonProductFactory
 from lp.registry.tests.test_accesspolicy import get_policies_for_artifact
 from lp.services.authserver.xmlrpc import AuthServerAPIView
@@ -604,6 +605,21 @@ class TestGitIdentityMixin(TestCaseWithFactory):
                 repository.owner.name, dsp.distribution.name,
                 dsp.sourcepackagename.name))
 
+    def test_git_identity_owner_default_for_oci_project(self):
+        # If a repository is a person's default for an OCI project, then its
+        # Git identity is a combination of the person name and the OCI
+        # project path.
+        oci_project = self.factory.makeOCIProject()
+        repository = self.factory.makeGitRepository(target=oci_project)
+        with person_logged_in(repository.owner) as user:
+            self.repository_set.setDefaultRepositoryForOwner(
+                repository.owner, oci_project, repository, user)
+        self.assertGitIdentity(
+            repository,
+            "~%s/%s/+oci/%s" % (
+                repository.owner.name, oci_project.pillar.name,
+                oci_project.name))
+
     def test_identities_no_defaults(self):
         # If there are no defaults, the only repository identity is the
         # unique name.
@@ -663,7 +679,8 @@ class TestGitIdentityMixin(TestCaseWithFactory):
 
     def test_default_for_oci_project(self):
         # If a repository is the default for an OCI project, then that is
-        # the preferred identity.
+        # the preferred identity.  Target defaults are preferred over
+        # owner-target defaults.
         mint = self.factory.makeDistribution(name="mint")
         eric = self.factory.makePerson(name="eric")
         mint_choc = self.factory.makeOCIProject(
@@ -672,13 +689,19 @@ class TestGitIdentityMixin(TestCaseWithFactory):
             owner=eric, target=mint_choc, name="choc-repo")
         oci_project = repository.target
         with admin_logged_in():
+            self.repository_set.setDefaultRepositoryForOwner(
+                repository.owner, oci_project, repository, repository.owner)
             self.repository_set.setDefaultRepository(
                 oci_project, repository, force_oci=True)
+        eric_oci_project = getUtility(IPersonOCIProjectFactory).create(
+            eric, oci_project)
         self.assertEqual(
-            [ICanHasDefaultGitRepository(oci_project)],
+            [ICanHasDefaultGitRepository(target)
+             for target in (oci_project, eric_oci_project)],
             repository.getRepositoryDefaults())
         self.assertEqual(
             [("mint/+oci/choc", oci_project),
+             ("~eric/mint/+oci/choc", eric_oci_project),
              ("~eric/mint/+oci/choc/+git/choc-repo", repository)],
             repository.getRepositoryIdentities())
 
@@ -3585,6 +3608,12 @@ class TestGitRepositorySetDefaultsOwnerPackage(
     pass
 
 
+class TestGitRepositorySetDefaultsOwnerOCIProject(
+    TestGitRepositorySetDefaultsOwnerMixin,
+    TestGitRepositorySetDefaultsOCIProject):
+    pass
+
+
 class TestGitRepositoryWebservice(TestCaseWithFactory):
     """Tests for the webservice."""
 
diff --git a/lib/lp/code/xmlrpc/tests/test_git.py b/lib/lp/code/xmlrpc/tests/test_git.py
index 106d1e9..8db278b 100644
--- a/lib/lp/code/xmlrpc/tests/test_git.py
+++ b/lib/lp/code/xmlrpc/tests/test_git.py
@@ -967,6 +967,44 @@ class TestGitAPI(TestGitAPIMixin, TestCaseWithFactory):
         self.assertTrue(repository.owner_default)
         self.assertEqual(team, repository.owner)
 
+    def test_translatePath_create_oci_project_owner_default(self):
+        # A repository can be created and immediately set as its owner's
+        # default for an OCI project.
+        requester = self.factory.makePerson()
+        oci_project = self.factory.makeOCIProject()
+        path = u"/~%s/%s/+oci/%s" % (
+            requester.name, oci_project.pillar.name, oci_project.name)
+        repository = self.assertCreates(requester, path)
+        self.assertFalse(repository.target_default)
+        self.assertTrue(repository.owner_default)
+        self.assertEqual(requester, repository.owner)
+
+    def test_translatePath_create_oci_project_team_owner_default(self):
+        # The owner of a team can create a team-owned repository and
+        # immediately set it as that team's default for an OCI project.
+        requester = self.factory.makePerson()
+        team = self.factory.makeTeam(owner=requester)
+        oci_project = self.factory.makeOCIProject()
+        path = u"/~%s/%s/+oci/%s" % (
+            team.name, oci_project.pillar.name, oci_project.name)
+        repository = self.assertCreates(requester, path)
+        self.assertFalse(repository.target_default)
+        self.assertTrue(repository.owner_default)
+        self.assertEqual(team, repository.owner)
+
+    def test_translatePath_create_oci_project_team_member_default(self):
+        # A non-owner member of a team can create a team-owned repository
+        # and immediately set it as that team's default for an OCI project.
+        requester = self.factory.makePerson()
+        team = self.factory.makeTeam(members=[requester])
+        oci_project = self.factory.makeOCIProject()
+        path = u"/~%s/%s/+oci/%s" % (
+            team.name, oci_project.pillar.name, oci_project.name)
+        repository = self.assertCreates(requester, path)
+        self.assertFalse(repository.target_default)
+        self.assertTrue(repository.owner_default)
+        self.assertEqual(team, repository.owner)
+
     def test_translatePath_create_broken_hosting_service(self):
         # If the hosting service is down, trying to create a repository
         # fails and doesn't leave junk around in the Launchpad database.
diff --git a/lib/lp/registry/browser/person.py b/lib/lp/registry/browser/person.py
index 1c86ace..e11c156 100644
--- a/lib/lp/registry/browser/person.py
+++ b/lib/lp/registry/browser/person.py
@@ -161,6 +161,7 @@ from lp.registry.interfaces.mailinglist import (
 from lp.registry.interfaces.mailinglistsubscription import (
     MailingListAutoSubscribePolicy,
     )
+from lp.registry.interfaces.ociproject import IOCIProject
 from lp.registry.interfaces.person import (
     IPerson,
     IPersonClaim,
@@ -169,6 +170,7 @@ from lp.registry.interfaces.person import (
 from lp.registry.interfaces.persondistributionsourcepackage import (
     IPersonDistributionSourcePackageFactory,
     )
+from lp.registry.interfaces.personociproject import IPersonOCIProjectFactory
 from lp.registry.interfaces.personproduct import IPersonProductFactory
 from lp.registry.interfaces.persontransferjob import (
     IPersonDeactivateJobSource,
@@ -392,19 +394,21 @@ class BranchTraversalMixin:
             for _ in range(num_traversed):
                 self.request.stepstogo.consume()
 
+            using_pillar_alias = False
             if IProduct.providedBy(target):
                 if target.name != pillar_name:
-                    # This repository was accessed through one of its
-                    # project's aliases, so we must redirect to its
-                    # canonical URL.
-                    return self.redirectSubTree(canonical_url(repository))
-
-            if IDistributionSourcePackage.providedBy(target):
+                    using_pillar_alias = True
+            elif IDistributionSourcePackage.providedBy(target):
                 if target.distribution.name != pillar_name:
-                    # This branch or repository was accessed through one of its
-                    # distribution's aliases, so we must redirect to its
-                    # canonical URL.
-                    return self.redirectSubTree(canonical_url(repository))
+                    using_pillar_alias = True
+            elif IOCIProject.providedBy(target):
+                if target.pillar.name != pillar_name:
+                    using_pillar_alias = True
+
+            if using_pillar_alias:
+                # This repository was accessed through one of its project's
+                # aliases, so we must redirect to its canonical URL.
+                return self.redirectSubTree(canonical_url(repository))
 
             return repository
         except (NotFoundError, InvalidNamespace, InvalidProductName):
@@ -412,7 +416,9 @@ class BranchTraversalMixin:
 
         # If the pillar is a product, then return the PersonProduct; if it
         # is a distribution and further segments provide a source package,
-        # then return the PersonDistributionSourcePackage.
+        # then return the PersonDistributionSourcePackage; if it is a
+        # distribution and further segments provide an OCI project, then
+        # return the PersonOCIProject.
         pillar = getUtility(IPillarNameSet).getByName(pillar_name)
         if IProduct.providedBy(pillar):
             person_product = getUtility(IPersonProductFactory).create(
@@ -424,24 +430,30 @@ class BranchTraversalMixin:
                     status=301)
             getUtility(IOpenLaunchBag).add(pillar)
             return person_product
-        elif IDistribution.providedBy(pillar):
-            if (len(self.request.stepstogo) >= 2 and
-                self.request.stepstogo.peek() == "+source"):
+        elif (IDistribution.providedBy(pillar) and
+                len(self.request.stepstogo) >= 2):
+            if self.request.stepstogo.peek() == "+source":
+                get_target = IDistribution(pillar).getSourcePackage
+                factory = getUtility(IPersonDistributionSourcePackageFactory)
+            elif self.request.stepstogo.peek() == "+oci":
+                get_target = IDistribution(pillar).getOCIProject
+                factory = getUtility(IPersonOCIProjectFactory)
+            else:
+                get_target, factory = None, None
+            if get_target is not None:
                 self.request.stepstogo.consume()
                 spn_name = self.request.stepstogo.consume()
-                dsp = IDistribution(pillar).getSourcePackage(spn_name)
-                if dsp is not None:
-                    factory = getUtility(
-                        IPersonDistributionSourcePackageFactory)
-                    person_dsp = factory.create(self.context, dsp)
+                target = get_target(spn_name)
+                if target is not None:
+                    person_target = factory.create(self.context, target)
                     # If accessed through an alias, redirect to the proper
                     # name.
                     if pillar.name != pillar_name:
                         return self.redirectSubTree(
-                            canonical_url(person_dsp, request=self.request),
+                            canonical_url(person_target, request=self.request),
                             status=301)
                     getUtility(IOpenLaunchBag).add(pillar)
-                    return person_dsp
+                    return person_target
 
         # Otherwise look for a branch.
         try:
diff --git a/lib/lp/registry/browser/tests/test_person.py b/lib/lp/registry/browser/tests/test_person.py
index cb6f95a..626cab4 100644
--- a/lib/lp/registry/browser/tests/test_person.py
+++ b/lib/lp/registry/browser/tests/test_person.py
@@ -163,6 +163,14 @@ class TestPersonNavigation(TestCaseWithFactory):
             dsp.sourcepackagename.name, repository.name)
         self.assertEqual(repository, test_traverse(url)[0])
 
+    def test_traverse_git_repository_oci_project(self):
+        oci_project = self.factory.makeOCIProject()
+        repository = self.factory.makeGitRepository(target=oci_project)
+        url = "/~%s/%s/+oci/%s/+git/%s" % (
+            repository.owner.name, oci_project.pillar.name, oci_project.name,
+            repository.name)
+        self.assertEqual(repository, test_traverse(url)[0])
+
     def test_traverse_git_repository_personal(self):
         person = self.factory.makePerson()
         repository = self.factory.makeGitRepository(