launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #24993
[Merge] ~pappacena/launchpad:git-fork-backend into launchpad:master
Thiago F. Pappacena has proposed merging ~pappacena/launchpad:git-fork-backend into launchpad:master.
Commit message:
Fork method on GitRepository to allow users to asynchronously create a copy of a repository
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~pappacena/launchpad/+git/launchpad/+merge/387146
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~pappacena/launchpad:git-fork-backend into launchpad:master.
diff --git a/database/schema/security.cfg b/database/schema/security.cfg
index 93dc9f8..6ee31c0 100644
--- a/database/schema/security.cfg
+++ b/database/schema/security.cfg
@@ -739,6 +739,29 @@ public.webhookjob = SELECT, INSERT
public.xref = SELECT, INSERT, DELETE
type=user
+[gitrepo-creator]
+groups=script
+public.branchmergeproposal = SELECT, DELETE
+public.codeimport = SELECT, DELETE
+public.gitactivity = SELECT, DELETE
+public.gitjob = SELECT, INSERT, DELETE
+public.gitref = SELECT, DELETE
+public.gitrepository = SELECT, UPDATE, DELETE
+public.gitrule = SELECT, DELETE
+public.gitrulegrant = SELECT, DELETE
+public.gitsubscription = SELECT, DELETE
+public.job = SELECT, UPDATE, INSERT, DELETE
+public.ocirecipe = SELECT, DELETE
+public.person = SELECT
+public.product = SELECT
+public.snap = SELECT, DELETE
+public.sourcepackagerecipe = SELECT, DELETE
+public.sourcepackagerecipedata = SELECT, DELETE
+public.sourcepackagerecipedatainstruction = SELECT, DELETE
+public.webhook = SELECT, DELETE
+public.webhookjob = SELECT, DELETE
+type=user
+
[targetnamecacheupdater]
groups=script
public.binarypackagename = SELECT
diff --git a/lib/lp/code/configure.zcml b/lib/lp/code/configure.zcml
index 898e645..d660366 100644
--- a/lib/lp/code/configure.zcml
+++ b/lib/lp/code/configure.zcml
@@ -1111,6 +1111,11 @@
provides="lp.code.interfaces.gitjob.IGitRepositoryModifiedMailJobSource">
<allow interface="lp.code.interfaces.gitjob.IGitRepositoryModifiedMailJobSource" />
</securedutility>
+ <securedutility
+ component="lp.code.model.gitjob.GitRepositoryConfirmCreationJob"
+ provides="lp.code.interfaces.gitjob.IGitRepositoryConfirmCreationJobSource">
+ <allow interface="lp.code.interfaces.gitjob.IGitRepositoryConfirmCreationJobSource" />
+ </securedutility>
<class class="lp.code.model.gitjob.GitRefScanJob">
<allow interface="lp.code.interfaces.gitjob.IGitJob" />
<allow interface="lp.code.interfaces.gitjob.IGitRefScanJob" />
@@ -1123,6 +1128,10 @@
<allow interface="lp.code.interfaces.gitjob.IGitJob" />
<allow interface="lp.code.interfaces.gitjob.IGitRepositoryModifiedMailJob" />
</class>
+ <class class="lp.code.model.gitjob.GitRepositoryConfirmCreationJob">
+ <allow interface="lp.code.interfaces.gitjob.IGitJob" />
+ <allow interface="lp.code.interfaces.gitjob.IGitRepositoryConfirmCreationJob" />
+ </class>
<lp:help-folder folder="help" name="+help-code" />
diff --git a/lib/lp/code/interfaces/githosting.py b/lib/lp/code/interfaces/githosting.py
index 378930a..d854174 100644
--- a/lib/lp/code/interfaces/githosting.py
+++ b/lib/lp/code/interfaces/githosting.py
@@ -1,4 +1,4 @@
-# Copyright 2015-2018 Canonical Ltd. This software is licensed under the
+# Copyright 2015-2020 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Interface for communication with the Git hosting service."""
@@ -14,13 +14,15 @@ from zope.interface import Interface
class IGitHostingClient(Interface):
"""Interface for the internal API provided by the Git hosting service."""
- def create(path, clone_from=None):
+ def create(path, clone_from=None, async_create=False):
"""Create a Git repository.
:param path: Physical path of the new repository on the hosting
service.
:param clone_from: If not None, clone the new repository from this
other physical path.
+ :param async_create: Do not block the call until the repository is
+ actually created.
"""
def getProperties(path):
diff --git a/lib/lp/code/interfaces/gitjob.py b/lib/lp/code/interfaces/gitjob.py
index 4f31b19..e143515 100644
--- a/lib/lp/code/interfaces/gitjob.py
+++ b/lib/lp/code/interfaces/gitjob.py
@@ -1,4 +1,4 @@
-# Copyright 2015 Canonical Ltd. This software is licensed under the
+# Copyright 2015-2020 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""GitJob interfaces."""
@@ -9,6 +9,8 @@ __all__ = [
'IGitJob',
'IGitRefScanJob',
'IGitRefScanJobSource',
+ 'IGitRepositoryConfirmCreationJob',
+ 'IGitRepositoryConfirmCreationJobSource',
'IGitRepositoryModifiedMailJob',
'IGitRepositoryModifiedMailJobSource',
'IReclaimGitRepositorySpaceJob',
@@ -93,3 +95,18 @@ class IGitRepositoryModifiedMailJobSource(IJobSource):
:param repository_delta: An `IGitRepositoryDelta` describing the
changes.
"""
+
+
+class IGitRepositoryConfirmCreationJob(IRunnableJob):
+ """"A Job to confirm the async creation of a GitRepository on code
+ hosting service."""
+
+
+class IGitRepositoryConfirmCreationJobSource(IJobSource):
+
+ def create(repository):
+ """Confirms or abort a repository creation by checking it on the
+ code hosting service.
+
+ :param repository: The `IGitRepository` in process of creation.
+ """
diff --git a/lib/lp/code/interfaces/gitnamespace.py b/lib/lp/code/interfaces/gitnamespace.py
index fd7e3e7..c933b67 100644
--- a/lib/lp/code/interfaces/gitnamespace.py
+++ b/lib/lp/code/interfaces/gitnamespace.py
@@ -40,8 +40,16 @@ class IGitNamespace(Interface):
def createRepository(repository_type, registrant, name,
information_type=None, date_created=None,
target_default=False, owner_default=False,
- with_hosting=False, status=None):
- """Create and return an `IGitRepository` in this namespace."""
+ with_hosting=False, async_hosting=False, status=None):
+ """Create and return an `IGitRepository` in this namespace.
+
+ :param with_hosting: If True, also creates the repository on git
+ hosting service.
+ :param async_hosting: If with_hosting is True, this controls if the
+ call to create repository on hosting service will be done
+ asynchronously, or it will block until the service creates the
+ repository.
+ """
def isNameUsed(name):
"""Is 'name' already used in this namespace?"""
diff --git a/lib/lp/code/interfaces/gitrepository.py b/lib/lp/code/interfaces/gitrepository.py
index 189b426..d0d1dfe 100644
--- a/lib/lp/code/interfaces/gitrepository.py
+++ b/lib/lp/code/interfaces/gitrepository.py
@@ -967,6 +967,13 @@ class IGitRepositorySet(Interface):
:param with_hosting: Create the repository on the hosting service.
"""
+ def fork(origin, user):
+ """Fork a repository to the given user's account.
+
+ :param origin: The original GitRepository.
+ :param user: The user forking the repository.
+ :return: The newly created GitRepository."""
+
# Marker for references to Git URL layouts: ##GITNAMESPACE##
@call_with(user=REQUEST_USER)
@operation_parameters(
diff --git a/lib/lp/code/model/githosting.py b/lib/lp/code/model/githosting.py
index 94d6538..e2a4462 100644
--- a/lib/lp/code/model/githosting.py
+++ b/lib/lp/code/model/githosting.py
@@ -90,13 +90,20 @@ class GitHostingClient:
def _delete(self, path, **kwargs):
return self._request("delete", path, **kwargs)
- def create(self, path, clone_from=None):
+ def create(self, path, clone_from=None, async_create=False):
"""See `IGitHostingClient`."""
try:
if clone_from:
request = {"repo_path": path, "clone_from": clone_from}
else:
request = {"repo_path": path}
+ if async_create:
+ # XXX pappacena 2020-07-02: async forces to clone_refs
+ # because it's only used in situations where this is
+ # desirable for now. We might need to add "clone_refs" as
+ # parameter in the future.
+ request['async'] = True
+ request['clone_refs'] = clone_from is not None
self._post("/repo", json=request)
except requests.RequestException as e:
raise GitRepositoryCreationFault(
diff --git a/lib/lp/code/model/gitjob.py b/lib/lp/code/model/gitjob.py
index 3c041da..4fde969 100644
--- a/lib/lp/code/model/gitjob.py
+++ b/lib/lp/code/model/gitjob.py
@@ -1,4 +1,4 @@
-# Copyright 2015-2018 Canonical Ltd. This software is licensed under the
+# Copyright 2015-2020 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
__metaclass__ = type
@@ -12,6 +12,8 @@ __all__ = [
'ReclaimGitRepositorySpaceJob',
]
+from datetime import timedelta
+
from lazr.delegates import delegate_to
from lazr.enum import (
DBEnumeratedType,
@@ -37,12 +39,15 @@ from lp.app.errors import NotFoundError
from lp.code.enums import (
GitActivityType,
GitPermissionType,
+ GitRepositoryStatus,
)
from lp.code.interfaces.githosting import IGitHostingClient
from lp.code.interfaces.gitjob import (
IGitJob,
IGitRefScanJob,
IGitRefScanJobSource,
+ IGitRepositoryConfirmCreationJob,
+ IGitRepositoryConfirmCreationJobSource,
IGitRepositoryModifiedMailJob,
IGitRepositoryModifiedMailJobSource,
IReclaimGitRepositorySpaceJob,
@@ -100,6 +105,13 @@ class GitJobType(DBEnumeratedType):
modifications.
""")
+ REPOSITORY_CONFIRM_ASYNC_CREATION = DBItem(3, """
+ Repository confirm async creation
+
+ This job runs to pool code hosting to confirm or abort an async
+ repository creation request.
+ """)
+
@implementer(IGitJob)
class GitJob(StormBase):
@@ -404,3 +416,65 @@ class GitRepositoryModifiedMailJob(GitJobDerived):
def run(self):
"""See `IGitRepositoryModifiedMailJob`."""
self.getMailer().sendAll()
+
+
+@implementer(IGitRepositoryConfirmCreationJob)
+@provider(IGitRepositoryConfirmCreationJobSource)
+class GitRepositoryConfirmCreationJob(GitJobDerived):
+ """A job that checks if an async repository creation has finished
+ already."""
+
+ class RepositoryNotReady(Exception):
+ pass
+
+ class_job_type = GitJobType.REPOSITORY_CONFIRM_ASYNC_CREATION
+
+ config = config.IGitRepositoryModifiedMailJobSource
+
+ retry_error_types = (RepositoryNotReady, )
+ retry_delay = timedelta(seconds=5)
+ max_retries = 600 # 600 retires * 5 seconds = 50 minutes
+
+ @classmethod
+ def create(cls, repository):
+ """See ``"""
+ metadata = {}
+ git_job = GitJob(repository, cls.class_job_type, metadata)
+
+ job = cls(git_job)
+ job.celeryRunOnCommit()
+ return job
+
+ def run(self):
+ """See `GitRepositoryConfirmCreationJob`."""
+ log.debug(
+ "Trying to confirm availability of git repository %s",
+ self.repository)
+ hosting_path = self.repository.getInternalPath()
+ props = getUtility(IGitHostingClient).getProperties(
+ hosting_path)
+ log.debug(
+ "Git repository %s properties on code hosting: %s",
+ self.repository, props)
+ if props.get("is_available"):
+ naked_repo = removeSecurityProxy(self.repository)
+ naked_repo.status = GitRepositoryStatus.AVAILABLE
+ IStore(naked_repo).flush()
+ log.info(
+ "Git repository %s availability confirmed.", self.repository)
+ return
+
+ # If we didn't reach the max retries, raise something to retry in
+ # some seconds.
+ if self.attempt_count < self.max_retries:
+ log.info(
+ "Git repository %s is not available on code hosting yet. "
+ "Retrying later.",
+ self.repository)
+ raise GitRepositoryConfirmCreationJob.RepositoryNotReady()
+
+ # We have tried enough. We should abort this repository creation.
+ log.error(
+ "Git repository %s availability could not be confirmed. Removing.",
+ self.repository)
+ self.repository.destroySelf(break_references=True)
diff --git a/lib/lp/code/model/gitnamespace.py b/lib/lp/code/model/gitnamespace.py
index 2a358f6..099b595 100644
--- a/lib/lp/code/model/gitnamespace.py
+++ b/lib/lp/code/model/gitnamespace.py
@@ -33,6 +33,7 @@ from lp.code.enums import (
BranchSubscriptionDiffSize,
BranchSubscriptionNotificationLevel,
CodeReviewNotificationLevel,
+ GitRepositoryStatus,
)
from lp.code.errors import (
GitDefaultConflict,
@@ -75,7 +76,8 @@ class _BaseGitNamespace:
reviewer=None, information_type=None,
date_created=DEFAULT, description=None,
target_default=False, owner_default=False,
- with_hosting=False, status=None):
+ with_hosting=False, async_hosting=False,
+ status=GitRepositoryStatus.AVAILABLE):
"""See `IGitNamespace`."""
repository_set = getUtility(IGitRepositorySet)
@@ -123,7 +125,8 @@ class _BaseGitNamespace:
clone_from_repository = repository.getClonedFrom()
repository._createOnHostingService(
- clone_from_repository=clone_from_repository)
+ clone_from_repository=clone_from_repository,
+ async_create=async_hosting)
return repository
diff --git a/lib/lp/code/model/gitrepository.py b/lib/lp/code/model/gitrepository.py
index 77a4bbd..6a3fdab 100644
--- a/lib/lp/code/model/gitrepository.py
+++ b/lib/lp/code/model/gitrepository.py
@@ -116,7 +116,10 @@ from lp.code.interfaces.gitcollection import (
IGitCollection,
)
from lp.code.interfaces.githosting import IGitHostingClient
-from lp.code.interfaces.gitjob import IGitRefScanJobSource
+from lp.code.interfaces.gitjob import (
+ IGitRefScanJobSource,
+ IGitRepositoryConfirmCreationJobSource,
+ )
from lp.code.interfaces.gitlookup import IGitLookup
from lp.code.interfaces.gitnamespace import (
get_git_namespace,
@@ -359,7 +362,8 @@ class GitRepository(StormBase, WebhookTargetMixin, GitIdentityMixin):
self.owner_default = False
self.target_default = False
- def _createOnHostingService(self, clone_from_repository=None):
+ def _createOnHostingService(
+ self, clone_from_repository=None, async_create=False):
"""Create this repository on the hosting service."""
hosting_path = self.getInternalPath()
if clone_from_repository is not None:
@@ -367,7 +371,8 @@ class GitRepository(StormBase, WebhookTargetMixin, GitIdentityMixin):
else:
clone_from_path = None
getUtility(IGitHostingClient).create(
- hosting_path, clone_from=clone_from_path)
+ hosting_path, clone_from=clone_from_path,
+ async_create=async_create)
def getClonedFrom(self):
"""See `IGitRepository`"""
@@ -1728,13 +1733,28 @@ class GitRepositorySet:
def new(self, repository_type, registrant, owner, target, name,
information_type=None, date_created=DEFAULT, description=None,
- with_hosting=False):
+ with_hosting=False, async_hosting=False,
+ status=GitRepositoryStatus.AVAILABLE):
"""See `IGitRepositorySet`."""
namespace = get_git_namespace(target, owner)
return namespace.createRepository(
repository_type, registrant, name,
information_type=information_type, date_created=date_created,
- description=description, with_hosting=with_hosting)
+ description=description, with_hosting=with_hosting,
+ async_hosting=async_hosting, status=status)
+
+ def fork(self, origin, user):
+ repository = self.new(
+ repository_type=GitRepositoryType.HOSTED,
+ registrant=user, owner=user, target=origin.target,
+ name=origin.name,
+ information_type=origin.information_type,
+ date_created=UTC_NOW, description=origin.description,
+ with_hosting=True, async_hosting=True,
+ status=GitRepositoryStatus.CREATING)
+ # Start pooling job to check when the repository will be ready.
+ getUtility(IGitRepositoryConfirmCreationJobSource).create(repository)
+ return repository
def getByPath(self, user, path):
"""See `IGitRepositorySet`."""
diff --git a/lib/lp/code/model/tests/test_githosting.py b/lib/lp/code/model/tests/test_githosting.py
index d966dbe..16717c4 100644
--- a/lib/lp/code/model/tests/test_githosting.py
+++ b/lib/lp/code/model/tests/test_githosting.py
@@ -2,7 +2,7 @@
# NOTE: The first line above must stay first; do not move the copyright
# notice to the top. See http://www.python.org/dev/peps/pep-0263/.
#
-# Copyright 2016-2019 Canonical Ltd. This software is licensed under the
+# Copyright 2016-2020 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Unit tests for `GitHostingClient`.
@@ -23,7 +23,6 @@ from lazr.restful.utils import get_current_browser_request
import responses
from six.moves.urllib.parse import (
parse_qsl,
- urljoin,
urlsplit,
)
from testtools.matchers import (
@@ -130,6 +129,13 @@ class TestGitHostingClient(TestCase):
"repo", method="POST",
json_data={"repo_path": "123", "clone_from": "122"})
+ def test_create_async(self):
+ with self.mockRequests("POST"):
+ self.client.create("123", clone_from="122", async_create=True)
+ self.assertRequest(
+ "repo", method="POST",
+ json_data={"repo_path": "123", "clone_from": "122", "async": True})
+
def test_create_failure(self):
with self.mockRequests("POST", status=400):
self.assertRaisesWithContent(
diff --git a/lib/lp/code/model/tests/test_gitjob.py b/lib/lp/code/model/tests/test_gitjob.py
index 5964944..617eb44 100644
--- a/lib/lp/code/model/tests/test_gitjob.py
+++ b/lib/lp/code/model/tests/test_gitjob.py
@@ -1,4 +1,4 @@
-# Copyright 2015-2019 Canonical Ltd. This software is licensed under the
+# Copyright 2015-2020 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Tests for `GitJob`s."""
@@ -24,6 +24,7 @@ from testtools.matchers import (
MatchesStructure,
)
import transaction
+from zope.component import getUtility
from zope.interface import providedBy
from zope.security.proxy import removeSecurityProxy
@@ -31,6 +32,7 @@ from lp.code.adapters.gitrepository import GitRepositoryDelta
from lp.code.enums import (
GitGranteeType,
GitObjectType,
+ GitRepositoryStatus,
)
from lp.code.interfaces.branchmergeproposal import (
BRANCH_MERGE_PROPOSAL_WEBHOOKS_FEATURE_FLAG,
@@ -40,17 +42,21 @@ from lp.code.interfaces.gitjob import (
IGitRefScanJob,
IReclaimGitRepositorySpaceJob,
)
+from lp.code.interfaces.gitrepository import IGitRepositorySet
from lp.code.model.gitjob import (
describe_repository_delta,
GitJob,
GitJobDerived,
GitJobType,
GitRefScanJob,
+ GitRepositoryConfirmCreationJob,
ReclaimGitRepositorySpaceJob,
)
+from lp.code.model.gitrepository import GitRepository
from lp.code.tests.helpers import GitHostingFixture
from lp.services.config import config
from lp.services.database.constants import UTC_NOW
+from lp.services.database.interfaces import IStore
from lp.services.features.testing import FeatureFixture
from lp.services.job.runner import JobRunner
from lp.services.utils import seconds_since_epoch
@@ -484,5 +490,70 @@ class TestDescribeRepositoryDelta(TestCaseWithFactory):
snapshot, repository)
+class TestGitRepositoryConfirmCreationJob(TestCaseWithFactory):
+ """Tests for `GitRepositoryConfirmCreationJob`."""
+
+ layer = ZopelessDatabaseLayer
+
+ def test_confirms_repository_creation(self):
+ hosting_fixture = self.useFixture(GitHostingFixture())
+ project = self.factory.makeProduct()
+ repo = self.factory.makeGitRepository(target=project)
+
+ another_user = self.factory.makePerson()
+ forked = getUtility(IGitRepositorySet).fork(repo, another_user)
+ self.assertEqual(GitRepositoryStatus.CREATING, forked.status)
+
+ # Run the job that checks if repository was confirmed
+ [job] = GitRepositoryConfirmCreationJob.iterReady()
+ self.assertEqual(forked, job.repository)
+ with dbuser("gitrepo-creator"):
+ JobRunner([job]).runAll()
+
+ self.assertEqual(
+ [((forked.getInternalPath(), ), {})],
+ hosting_fixture.getProperties.calls)
+ self.assertEqual(GitRepositoryStatus.AVAILABLE, forked.status)
+
+ def test_aborts_repository_creation(self):
+ # Makes sure that after max_retries, the job gives up and deletes
+ # the repository being created.
+ hosting_fixture = self.useFixture(GitHostingFixture())
+ hosting_fixture.getProperties.result["is_available"] = False
+ project = self.factory.makeProduct()
+ repo = self.factory.makeGitRepository(target=project)
+
+ another_user = self.factory.makePerson()
+ forked = getUtility(IGitRepositorySet).fork(repo, another_user)
+ self.assertEqual(GitRepositoryStatus.CREATING, forked.status)
+
+ # Run the job that checks if repository was confirmed
+ [job] = GitRepositoryConfirmCreationJob.iterReady()
+
+ # Asserts that the first run raises an error that should be retried.
+ self.assertRaises(
+ GitRepositoryConfirmCreationJob.RepositoryNotReady, job.run)
+ self.assertIn(
+ GitRepositoryConfirmCreationJob.RepositoryNotReady,
+ GitRepositoryConfirmCreationJob.retry_error_types)
+
+ # Pretends that this is the last retry.
+ job.max_retries = 2
+ job.attempt_count = 2
+ self.assertEqual(forked, job.repository)
+ with dbuser("gitrepo-creator"):
+ JobRunner([job]).runAll()
+
+ self.assertEqual(
+ 0, len(list(GitRepositoryConfirmCreationJob.iterReady())))
+ # Asserts it called twice the git hosting service to get properties.
+ self.assertEqual(
+ [((forked.getInternalPath(), ), {})] * 2,
+ hosting_fixture.getProperties.calls)
+ store = IStore(forked)
+ resultset = store.find(GitRepository, GitRepository.id == forked.id)
+ self.assertEqual(0, resultset.count())
+
+
# XXX cjwatson 2015-03-12: We should test that the jobs work via Celery too,
# but that isn't feasible until we have a proper turnip fixture.
diff --git a/lib/lp/code/model/tests/test_gitrepository.py b/lib/lp/code/model/tests/test_gitrepository.py
index a8b9c35..7780f22 100644
--- a/lib/lp/code/model/tests/test_gitrepository.py
+++ b/lib/lp/code/model/tests/test_gitrepository.py
@@ -117,6 +117,7 @@ from lp.code.model.gitjob import (
GitJob,
GitJobType,
GitRefScanJob,
+ GitRepositoryConfirmCreationJob,
ReclaimGitRepositorySpaceJob,
)
from lp.code.model.gitrepository import (
@@ -3271,7 +3272,8 @@ class TestGitRepositorySet(TestCaseWithFactory):
self.assertThat(repository, MatchesStructure.byEquality(
registrant=owner, owner=owner, target=target, name=name))
self.assertEqual(
- [((repository.getInternalPath(),), {"clone_from": None})],
+ [((repository.getInternalPath(),),
+ {'async_create': False, "clone_from": None})],
hosting_fixture.create.calls)
def test_provides_IGitRepositorySet(self):
@@ -3559,6 +3561,26 @@ class TestGitRepositorySet(TestCaseWithFactory):
self.assertFalse(repo1.target_default)
self.assertTrue(repo2.target_default)
+ def test_fork_git_repository_creates_status_check_job(self):
+ self.useFixture(GitHostingFixture())
+ project = self.factory.makeProduct()
+ repo = self.factory.makeGitRepository(target=project)
+
+ another_user = self.factory.makePerson()
+ forked = getUtility(IGitRepositorySet).fork(repo, another_user)
+
+ self.assertThat(forked, MatchesStructure(
+ status=Equals(GitRepositoryStatus.CREATING),
+ repository_type=Equals(GitRepositoryType.HOSTED),
+ target=Equals(project),
+ information_type=Equals(repo.information_type),
+ description=Equals(repo.description),
+ registrant=Equals(another_user),
+ owner=Equals(another_user)))
+
+ [job] = GitRepositoryConfirmCreationJob.iterReady()
+ self.assertEqual(forked, job.repository)
+
class TestGitRepositorySetDefaultsMixin:
diff --git a/lib/lp/code/tests/helpers.py b/lib/lp/code/tests/helpers.py
index 46f12ed..a141d26 100644
--- a/lib/lp/code/tests/helpers.py
+++ b/lib/lp/code/tests/helpers.py
@@ -1,4 +1,4 @@
-# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2020 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Helper functions for code testing live here."""
@@ -49,8 +49,8 @@ from lp.code.model.seriessourcepackagebranch import (
from lp.registry.interfaces.pocket import PackagePublishingPocket
from lp.registry.interfaces.series import SeriesStatus
from lp.services.database.sqlbase import cursor
-from lp.services.propertycache import get_property_cache
from lp.services.memcache.testing import MemcacheFixture
+from lp.services.propertycache import get_property_cache
from lp.testing import (
run_with_login,
time_counter,
@@ -345,7 +345,7 @@ class GitHostingFixture(fixtures.Fixture):
merges=None, blob=None, disable_memcache=True):
self.create = FakeMethod()
self.getProperties = FakeMethod(
- result={"default_branch": default_branch})
+ result={"default_branch": default_branch, "is_available": True})
self.setProperties = FakeMethod()
self.getRefs = FakeMethod(result=({} if refs is None else refs))
self.getCommits = FakeMethod(
diff --git a/lib/lp/services/config/schema-lazr.conf b/lib/lp/services/config/schema-lazr.conf
index ffd76d7..d2933b3 100644
--- a/lib/lp/services/config/schema-lazr.conf
+++ b/lib/lp/services/config/schema-lazr.conf
@@ -1872,6 +1872,10 @@ module: lp.registry.interfaces.persontransferjob
dbuser: person-transfer-job
crontab_group: MAIN
+[IGitRepositoryConfirmCreationJobSource]
+module: lp.code.interfaces.gitjob
+dbuser: gitrepo-creator
+
[IGitRefScanJobSource]
module: lp.code.interfaces.gitjob
dbuser: branchscanner
References