Merge ~pappacena/launchpad:git-fork-backend into launchpad:master

Proposed by Thiago F. Pappacena
Status: Merged
Approved by: Thiago F. Pappacena
Approved revision: 9867039b20fc26412e770d5474d53ad45edefac6
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~pappacena/launchpad:git-fork-backend
Merge into: launchpad:master
Diff against target: 412 lines (+138/-24)
13 files modified
lib/lp/code/interfaces/githosting.py (+4/-2)
lib/lp/code/interfaces/gitnamespace.py (+10/-2)
lib/lp/code/interfaces/gitrepository.py (+8/-0)
lib/lp/code/model/githosting.py (+9/-2)
lib/lp/code/model/gitjob.py (+2/-1)
lib/lp/code/model/gitnamespace.py (+14/-2)
lib/lp/code/model/gitrepository.py (+20/-4)
lib/lp/code/model/tests/test_githosting.py (+10/-2)
lib/lp/code/model/tests/test_gitjob.py (+1/-1)
lib/lp/code/model/tests/test_gitrepository.py (+34/-1)
lib/lp/code/tests/helpers.py (+1/-1)
lib/lp/code/xmlrpc/git.py (+9/-3)
lib/lp/code/xmlrpc/tests/test_git.py (+16/-3)
Reviewer Review Type Date Requested Status
Colin Watson (community) Approve
Review via email: mp+387146@code.launchpad.net

Commit message

Fork method on GitRepository to allow users to asynchronously create a copy of a repository

To post a comment you must log in.
Revision history for this message
Thiago F. Pappacena (pappacena) wrote :

This MP depends on https://code.launchpad.net/~pappacena/turnip/+git/turnip/+merge/386461 being approved and landed in production.

Revision history for this message
Colin Watson (cjwatson) wrote :

I haven't completely gone through this yet, but here are a few high-level/structural comments.

review: Needs Information
4774c21... by Thiago F. Pappacena

Merge branch 'master' into git-fork-backend

249af52... by Thiago F. Pappacena

Running confirmation job with existing db user

c313a77... by Thiago F. Pappacena

separating user that requested a fork and the new repository owner

Revision history for this message
Thiago F. Pappacena (pappacena) wrote :

I'm pushing some of the requested changes, and I could use your opinion on one of the comments, cjwatson.

Revision history for this message
Colin Watson (cjwatson) :
Revision history for this message
Thiago F. Pappacena (pappacena) :
f56198c... by Thiago F. Pappacena

Remove polling job to confirm repo creation

2eff6c9... by Thiago F. Pappacena

Loose permission check for confirm/abort repo creation

730a9dc... by Thiago F. Pappacena

Avoiding race condition on create/confirm API calls

6fdbd8c... by Thiago F. Pappacena

Fixing test

Revision history for this message
Colin Watson (cjwatson) :
review: Approve
ded71d0... by Thiago F. Pappacena

Merge branch 'master' into git-fork-backend

9867039... by Thiago F. Pappacena

Fixing repository name clash when forking

Revision history for this message
Thiago F. Pappacena (pappacena) wrote :

Pushed the requested changes. This should be good to go now.

Revision history for this message
Colin Watson (cjwatson) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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 @@
1# Copyright 2015-2018 Canonical Ltd. This software is licensed under the1# Copyright 2015-2020 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Interface for communication with the Git hosting service."""4"""Interface for communication with the Git hosting service."""
@@ -14,13 +14,15 @@ from zope.interface import Interface
14class IGitHostingClient(Interface):14class IGitHostingClient(Interface):
15 """Interface for the internal API provided by the Git hosting service."""15 """Interface for the internal API provided by the Git hosting service."""
1616
17 def create(path, clone_from=None):17 def create(path, clone_from=None, async_create=False):
18 """Create a Git repository.18 """Create a Git repository.
1919
20 :param path: Physical path of the new repository on the hosting20 :param path: Physical path of the new repository on the hosting
21 service.21 service.
22 :param clone_from: If not None, clone the new repository from this22 :param clone_from: If not None, clone the new repository from this
23 other physical path.23 other physical path.
24 :param async_create: Do not block the call until the repository is
25 actually created.
24 """26 """
2527
26 def getProperties(path):28 def getProperties(path):
diff --git a/lib/lp/code/interfaces/gitnamespace.py b/lib/lp/code/interfaces/gitnamespace.py
index 9c6886b..3a6e600 100644
--- a/lib/lp/code/interfaces/gitnamespace.py
+++ b/lib/lp/code/interfaces/gitnamespace.py
@@ -40,8 +40,16 @@ class IGitNamespace(Interface):
40 def createRepository(repository_type, registrant, name,40 def createRepository(repository_type, registrant, name,
41 information_type=None, date_created=None,41 information_type=None, date_created=None,
42 target_default=False, owner_default=False,42 target_default=False, owner_default=False,
43 with_hosting=False, status=None):43 with_hosting=False, async_hosting=False, status=None):
44 """Create and return an `IGitRepository` in this namespace."""44 """Create and return an `IGitRepository` in this namespace.
45
46 :param with_hosting: If True, also creates the repository on git
47 hosting service.
48 :param async_hosting: If with_hosting is True, this controls if the
49 call to create repository on hosting service will be done
50 asynchronously, or it will block until the service creates the
51 repository.
52 """
4553
46 def isNameUsed(name):54 def isNameUsed(name):
47 """Is 'name' already used in this namespace?"""55 """Is 'name' already used in this namespace?"""
diff --git a/lib/lp/code/interfaces/gitrepository.py b/lib/lp/code/interfaces/gitrepository.py
index 344bc43..201c9a4 100644
--- a/lib/lp/code/interfaces/gitrepository.py
+++ b/lib/lp/code/interfaces/gitrepository.py
@@ -968,6 +968,14 @@ class IGitRepositorySet(Interface):
968 :param with_hosting: Create the repository on the hosting service.968 :param with_hosting: Create the repository on the hosting service.
969 """969 """
970970
971 def fork(origin, requester, new_owner):
972 """Fork a repository to the given user's account.
973
974 :param origin: The original GitRepository.
975 :param requester: The IPerson performing this fork.
976 :param new_owner: The IPerson that will own the forked repository.
977 :return: The newly created GitRepository."""
978
971 # Marker for references to Git URL layouts: ##GITNAMESPACE##979 # Marker for references to Git URL layouts: ##GITNAMESPACE##
972 @call_with(user=REQUEST_USER)980 @call_with(user=REQUEST_USER)
973 @operation_parameters(981 @operation_parameters(
diff --git a/lib/lp/code/model/githosting.py b/lib/lp/code/model/githosting.py
index 94d6538..60dff43 100644
--- a/lib/lp/code/model/githosting.py
+++ b/lib/lp/code/model/githosting.py
@@ -1,4 +1,4 @@
1# Copyright 2015-2019 Canonical Ltd. This software is licensed under the1# Copyright 2015-2020 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Communication with the Git hosting service."""4"""Communication with the Git hosting service."""
@@ -90,13 +90,20 @@ class GitHostingClient:
90 def _delete(self, path, **kwargs):90 def _delete(self, path, **kwargs):
91 return self._request("delete", path, **kwargs)91 return self._request("delete", path, **kwargs)
9292
93 def create(self, path, clone_from=None):93 def create(self, path, clone_from=None, async_create=False):
94 """See `IGitHostingClient`."""94 """See `IGitHostingClient`."""
95 try:95 try:
96 if clone_from:96 if clone_from:
97 request = {"repo_path": path, "clone_from": clone_from}97 request = {"repo_path": path, "clone_from": clone_from}
98 else:98 else:
99 request = {"repo_path": path}99 request = {"repo_path": path}
100 if async_create:
101 # XXX pappacena 2020-07-02: async forces to clone_refs
102 # because it's only used in situations where this is
103 # desirable for now. We might need to add "clone_refs" as
104 # parameter in the future.
105 request['async'] = True
106 request['clone_refs'] = clone_from is not None
100 self._post("/repo", json=request)107 self._post("/repo", json=request)
101 except requests.RequestException as e:108 except requests.RequestException as e:
102 raise GitRepositoryCreationFault(109 raise GitRepositoryCreationFault(
diff --git a/lib/lp/code/model/gitjob.py b/lib/lp/code/model/gitjob.py
index 3c041da..ab37d2b 100644
--- a/lib/lp/code/model/gitjob.py
+++ b/lib/lp/code/model/gitjob.py
@@ -1,4 +1,4 @@
1# Copyright 2015-2018 Canonical Ltd. This software is licensed under the1# Copyright 2015-2020 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4__metaclass__ = type4__metaclass__ = type
@@ -12,6 +12,7 @@ __all__ = [
12 'ReclaimGitRepositorySpaceJob',12 'ReclaimGitRepositorySpaceJob',
13 ]13 ]
1414
15
15from lazr.delegates import delegate_to16from lazr.delegates import delegate_to
16from lazr.enum import (17from lazr.enum import (
17 DBEnumeratedType,18 DBEnumeratedType,
diff --git a/lib/lp/code/model/gitnamespace.py b/lib/lp/code/model/gitnamespace.py
index f9ac2f5..22e9d8f 100644
--- a/lib/lp/code/model/gitnamespace.py
+++ b/lib/lp/code/model/gitnamespace.py
@@ -14,6 +14,7 @@ __all__ = [
1414
15from lazr.lifecycle.event import ObjectCreatedEvent15from lazr.lifecycle.event import ObjectCreatedEvent
16from storm.locals import And16from storm.locals import And
17import transaction
17from zope.component import getUtility18from zope.component import getUtility
18from zope.event import notify19from zope.event import notify
19from zope.interface import implementer20from zope.interface import implementer
@@ -33,6 +34,7 @@ from lp.code.enums import (
33 BranchSubscriptionDiffSize,34 BranchSubscriptionDiffSize,
34 BranchSubscriptionNotificationLevel,35 BranchSubscriptionNotificationLevel,
35 CodeReviewNotificationLevel,36 CodeReviewNotificationLevel,
37 GitRepositoryStatus,
36 )38 )
37from lp.code.errors import (39from lp.code.errors import (
38 GitDefaultConflict,40 GitDefaultConflict,
@@ -75,7 +77,8 @@ class _BaseGitNamespace:
75 reviewer=None, information_type=None,77 reviewer=None, information_type=None,
76 date_created=DEFAULT, description=None,78 date_created=DEFAULT, description=None,
77 target_default=False, owner_default=False,79 target_default=False, owner_default=False,
78 with_hosting=False, status=None):80 with_hosting=False, async_hosting=False,
81 status=GitRepositoryStatus.AVAILABLE):
79 """See `IGitNamespace`."""82 """See `IGitNamespace`."""
80 repository_set = getUtility(IGitRepositorySet)83 repository_set = getUtility(IGitRepositorySet)
8184
@@ -119,11 +122,20 @@ class _BaseGitNamespace:
119122
120 # Flush to make sure that repository.id is populated.123 # Flush to make sure that repository.id is populated.
121 IStore(repository).flush()124 IStore(repository).flush()
125 if async_hosting:
126 # If we are going to run async creation, we need to be sure
127 # the transaction is committed.
128 # Async creation will run a callback on Launchpad, and if
129 # the creation is quick enough, it might try to confirm on
130 # Launchpad (in another transaction) the creation of this
131 # repo before this transaction is actually committed.
132 transaction.commit()
122 assert repository.id is not None133 assert repository.id is not None
123134
124 clone_from_repository = repository.getClonedFrom()135 clone_from_repository = repository.getClonedFrom()
125 repository._createOnHostingService(136 repository._createOnHostingService(
126 clone_from_repository=clone_from_repository)137 clone_from_repository=clone_from_repository,
138 async_create=async_hosting)
127139
128 return repository140 return repository
129141
diff --git a/lib/lp/code/model/gitrepository.py b/lib/lp/code/model/gitrepository.py
index 3d2408e..726e9f1 100644
--- a/lib/lp/code/model/gitrepository.py
+++ b/lib/lp/code/model/gitrepository.py
@@ -359,7 +359,8 @@ class GitRepository(StormBase, WebhookTargetMixin, GitIdentityMixin):
359 self.owner_default = False359 self.owner_default = False
360 self.target_default = False360 self.target_default = False
361361
362 def _createOnHostingService(self, clone_from_repository=None):362 def _createOnHostingService(
363 self, clone_from_repository=None, async_create=False):
363 """Create this repository on the hosting service."""364 """Create this repository on the hosting service."""
364 hosting_path = self.getInternalPath()365 hosting_path = self.getInternalPath()
365 if clone_from_repository is not None:366 if clone_from_repository is not None:
@@ -367,7 +368,8 @@ class GitRepository(StormBase, WebhookTargetMixin, GitIdentityMixin):
367 else:368 else:
368 clone_from_path = None369 clone_from_path = None
369 getUtility(IGitHostingClient).create(370 getUtility(IGitHostingClient).create(
370 hosting_path, clone_from=clone_from_path)371 hosting_path, clone_from=clone_from_path,
372 async_create=async_create)
371373
372 def getClonedFrom(self):374 def getClonedFrom(self):
373 """See `IGitRepository`"""375 """See `IGitRepository`"""
@@ -1729,13 +1731,27 @@ class GitRepositorySet:
17291731
1730 def new(self, repository_type, registrant, owner, target, name,1732 def new(self, repository_type, registrant, owner, target, name,
1731 information_type=None, date_created=DEFAULT, description=None,1733 information_type=None, date_created=DEFAULT, description=None,
1732 with_hosting=False):1734 with_hosting=False, async_hosting=False,
1735 status=GitRepositoryStatus.AVAILABLE):
1733 """See `IGitRepositorySet`."""1736 """See `IGitRepositorySet`."""
1734 namespace = get_git_namespace(target, owner)1737 namespace = get_git_namespace(target, owner)
1735 return namespace.createRepository(1738 return namespace.createRepository(
1736 repository_type, registrant, name,1739 repository_type, registrant, name,
1737 information_type=information_type, date_created=date_created,1740 information_type=information_type, date_created=date_created,
1738 description=description, with_hosting=with_hosting)1741 description=description, with_hosting=with_hosting,
1742 async_hosting=async_hosting, status=status)
1743
1744 def fork(self, origin, requester, new_owner):
1745 namespace = get_git_namespace(origin.target, new_owner)
1746 name = namespace.findUnusedName(origin.name)
1747 repository = self.new(
1748 repository_type=GitRepositoryType.HOSTED,
1749 registrant=requester, owner=new_owner, target=origin.target,
1750 name=name, information_type=origin.information_type,
1751 date_created=UTC_NOW, description=origin.description,
1752 with_hosting=True, async_hosting=True,
1753 status=GitRepositoryStatus.CREATING)
1754 return repository
17391755
1740 def getByPath(self, user, path):1756 def getByPath(self, user, path):
1741 """See `IGitRepositorySet`."""1757 """See `IGitRepositorySet`."""
diff --git a/lib/lp/code/model/tests/test_githosting.py b/lib/lp/code/model/tests/test_githosting.py
index d966dbe..b35fdd7 100644
--- a/lib/lp/code/model/tests/test_githosting.py
+++ b/lib/lp/code/model/tests/test_githosting.py
@@ -2,7 +2,7 @@
2# NOTE: The first line above must stay first; do not move the copyright2# NOTE: The first line above must stay first; do not move the copyright
3# notice to the top. See http://www.python.org/dev/peps/pep-0263/.3# notice to the top. See http://www.python.org/dev/peps/pep-0263/.
4#4#
5# Copyright 2016-2019 Canonical Ltd. This software is licensed under the5# Copyright 2016-2020 Canonical Ltd. This software is licensed under the
6# GNU Affero General Public License version 3 (see the file LICENSE).6# GNU Affero General Public License version 3 (see the file LICENSE).
77
8"""Unit tests for `GitHostingClient`.8"""Unit tests for `GitHostingClient`.
@@ -23,7 +23,6 @@ from lazr.restful.utils import get_current_browser_request
23import responses23import responses
24from six.moves.urllib.parse import (24from six.moves.urllib.parse import (
25 parse_qsl,25 parse_qsl,
26 urljoin,
27 urlsplit,26 urlsplit,
28 )27 )
29from testtools.matchers import (28from testtools.matchers import (
@@ -130,6 +129,15 @@ class TestGitHostingClient(TestCase):
130 "repo", method="POST",129 "repo", method="POST",
131 json_data={"repo_path": "123", "clone_from": "122"})130 json_data={"repo_path": "123", "clone_from": "122"})
132131
132 def test_create_async(self):
133 with self.mockRequests("POST"):
134 self.client.create("123", clone_from="122", async_create=True)
135 self.assertRequest(
136 "repo", method="POST",
137 json_data={
138 "repo_path": "123", "clone_from": "122", "async": True,
139 "clone_refs": True})
140
133 def test_create_failure(self):141 def test_create_failure(self):
134 with self.mockRequests("POST", status=400):142 with self.mockRequests("POST", status=400):
135 self.assertRaisesWithContent(143 self.assertRaisesWithContent(
diff --git a/lib/lp/code/model/tests/test_gitjob.py b/lib/lp/code/model/tests/test_gitjob.py
index 5964944..d4bfe20 100644
--- a/lib/lp/code/model/tests/test_gitjob.py
+++ b/lib/lp/code/model/tests/test_gitjob.py
@@ -1,4 +1,4 @@
1# Copyright 2015-2019 Canonical Ltd. This software is licensed under the1# Copyright 2015-2020 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Tests for `GitJob`s."""4"""Tests for `GitJob`s."""
diff --git a/lib/lp/code/model/tests/test_gitrepository.py b/lib/lp/code/model/tests/test_gitrepository.py
index 62bf865..7888d2f 100644
--- a/lib/lp/code/model/tests/test_gitrepository.py
+++ b/lib/lp/code/model/tests/test_gitrepository.py
@@ -2729,6 +2729,38 @@ class TestGitRepositoryMarkSnapsStale(TestCaseWithFactory):
2729 self.assertFalse(snap.is_stale)2729 self.assertFalse(snap.is_stale)
27302730
27312731
2732class TestGitRepositoryFork(TestCaseWithFactory):
2733
2734 layer = DatabaseFunctionalLayer
2735
2736 def setUp(self):
2737 super(TestGitRepositoryFork, self).setUp()
2738 self.hosting_fixture = self.useFixture(GitHostingFixture())
2739
2740 def test_fork(self):
2741 repo = self.factory.makeGitRepository()
2742 another_person = self.factory.makePerson()
2743 another_team = self.factory.makeTeam(members=[another_person])
2744
2745 forked_repo = getUtility(IGitRepositorySet).fork(
2746 repo, another_person, another_team)
2747 self.assertEqual(another_team, forked_repo.owner)
2748 self.assertEqual(another_person, forked_repo.registrant)
2749 self.assertEqual(1, self.hosting_fixture.create.call_count)
2750
2751 def test_fork_same_name(self):
2752 repo = self.factory.makeGitRepository()
2753
2754 person = self.factory.makePerson()
2755 same_name_repo = self.factory.makeGitRepository(
2756 owner=person, registrant=person,
2757 name=repo.name, target=repo.target)
2758
2759 forked_repo = getUtility(IGitRepositorySet).fork(repo, person, person)
2760 self.assertEqual(forked_repo.target, repo.target)
2761 self.assertEqual(forked_repo.name, "%s-1" % same_name_repo.name)
2762
2763
2732class TestGitRepositoryDetectMerges(TestCaseWithFactory):2764class TestGitRepositoryDetectMerges(TestCaseWithFactory):
27332765
2734 layer = ZopelessDatabaseLayer2766 layer = ZopelessDatabaseLayer
@@ -3271,7 +3303,8 @@ class TestGitRepositorySet(TestCaseWithFactory):
3271 self.assertThat(repository, MatchesStructure.byEquality(3303 self.assertThat(repository, MatchesStructure.byEquality(
3272 registrant=owner, owner=owner, target=target, name=name))3304 registrant=owner, owner=owner, target=target, name=name))
3273 self.assertEqual(3305 self.assertEqual(
3274 [((repository.getInternalPath(),), {"clone_from": None})],3306 [((repository.getInternalPath(),),
3307 {'async_create': False, "clone_from": None})],
3275 hosting_fixture.create.calls)3308 hosting_fixture.create.calls)
32763309
3277 def test_provides_IGitRepositorySet(self):3310 def test_provides_IGitRepositorySet(self):
diff --git a/lib/lp/code/tests/helpers.py b/lib/lp/code/tests/helpers.py
index a4f8416..4ef843d 100644
--- a/lib/lp/code/tests/helpers.py
+++ b/lib/lp/code/tests/helpers.py
@@ -355,7 +355,7 @@ class GitHostingFixture(fixtures.Fixture):
355 merges=None, blob=None, disable_memcache=True):355 merges=None, blob=None, disable_memcache=True):
356 self.create = FakeMethod()356 self.create = FakeMethod()
357 self.getProperties = FakeMethod(357 self.getProperties = FakeMethod(
358 result={"default_branch": default_branch})358 result={"default_branch": default_branch, "is_available": True})
359 self.setProperties = FakeMethod()359 self.setProperties = FakeMethod()
360 self.getRefs = FakeMethod(result=({} if refs is None else refs))360 self.getRefs = FakeMethod(result=({} if refs is None else refs))
361 self.getCommits = FakeMethod(361 self.getCommits = FakeMethod(
diff --git a/lib/lp/code/xmlrpc/git.py b/lib/lp/code/xmlrpc/git.py
index 0c3a2f1..c148acc 100644
--- a/lib/lp/code/xmlrpc/git.py
+++ b/lib/lp/code/xmlrpc/git.py
@@ -627,6 +627,15 @@ class GitAPI(LaunchpadXMLRPCView):
627 if requester == LAUNCHPAD_ANONYMOUS:627 if requester == LAUNCHPAD_ANONYMOUS:
628 requester = None628 requester = None
629629
630 if naked_repo.status != GitRepositoryStatus.CREATING:
631 raise faults.Unauthorized()
632
633 if requester == LAUNCHPAD_SERVICES and "macaroon" not in auth_params:
634 # For repo creation management operations, we trust
635 # LAUNCHPAD_SERVICES, since it should be just an internal call
636 # to confirm/abort repository creation.
637 return
638
630 verified = self._verifyAuthParams(requester, repository, auth_params)639 verified = self._verifyAuthParams(requester, repository, auth_params)
631 if verified is not None and verified.user is NO_USER:640 if verified is not None and verified.user is NO_USER:
632 # For internal-services authentication, we check if its using a641 # For internal-services authentication, we check if its using a
@@ -642,9 +651,6 @@ class GitAPI(LaunchpadXMLRPCView):
642 if requester != naked_repo.registrant:651 if requester != naked_repo.registrant:
643 raise faults.Unauthorized()652 raise faults.Unauthorized()
644653
645 if naked_repo.status != GitRepositoryStatus.CREATING:
646 raise faults.Unauthorized()
647
648 def _confirmRepoCreation(self, requester, translated_path, auth_params):654 def _confirmRepoCreation(self, requester, translated_path, auth_params):
649 naked_repo = removeSecurityProxy(655 naked_repo = removeSecurityProxy(
650 getUtility(IGitLookup).getByHostingPath(translated_path))656 getUtility(IGitLookup).getByHostingPath(translated_path))
diff --git a/lib/lp/code/xmlrpc/tests/test_git.py b/lib/lp/code/xmlrpc/tests/test_git.py
index c3b8126..9fb9b2e 100644
--- a/lib/lp/code/xmlrpc/tests/test_git.py
+++ b/lib/lp/code/xmlrpc/tests/test_git.py
@@ -401,9 +401,10 @@ class TestGitAPIMixin:
401 expected_status = GitRepositoryStatus.AVAILABLE401 expected_status = GitRepositoryStatus.AVAILABLE
402 expected_hosting_calls = 1402 expected_hosting_calls = 1
403 expected_hosting_call_args = [(repository.getInternalPath(),)]403 expected_hosting_call_args = [(repository.getInternalPath(),)]
404 expected_hosting_call_kwargs = [404 expected_hosting_call_kwargs = [{
405 {"clone_from": (cloned_from.getInternalPath()405 "clone_from": (cloned_from.getInternalPath()
406 if cloned_from else None)}]406 if cloned_from else None),
407 "async_create": False}]
407408
408 self.assertEqual(GitRepositoryType.HOSTED, repository.repository_type)409 self.assertEqual(GitRepositoryType.HOSTED, repository.repository_type)
409 self.assertEqual(expected_translation, translation)410 self.assertEqual(expected_translation, translation)
@@ -780,6 +781,12 @@ class TestGitAPI(TestGitAPIMixin, TestCaseWithFactory):
780 repo.status = GitRepositoryStatus.CREATING781 repo.status = GitRepositoryStatus.CREATING
781 self.assertConfirmsRepoCreation(owner, repo)782 self.assertConfirmsRepoCreation(owner, repo)
782783
784 def test_launchpad_service_confirm_git_repository_creation(self):
785 owner = self.factory.makePerson()
786 repo = removeSecurityProxy(self.factory.makeGitRepository(owner=owner))
787 repo.status = GitRepositoryStatus.CREATING
788 self.assertConfirmsRepoCreation(LAUNCHPAD_SERVICES, repo)
789
783 def test_only_requester_can_confirm_git_repository_creation(self):790 def test_only_requester_can_confirm_git_repository_creation(self):
784 requester = self.factory.makePerson()791 requester = self.factory.makePerson()
785 repo = removeSecurityProxy(self.factory.makeGitRepository())792 repo = removeSecurityProxy(self.factory.makeGitRepository())
@@ -961,6 +968,12 @@ class TestGitAPI(TestGitAPIMixin, TestCaseWithFactory):
961 repo.status = GitRepositoryStatus.CREATING968 repo.status = GitRepositoryStatus.CREATING
962 self.assertAbortsRepoCreation(requester, repo)969 self.assertAbortsRepoCreation(requester, repo)
963970
971 def test_launchpad_service_abort_git_repository_creation(self):
972 owner = self.factory.makePerson()
973 repo = removeSecurityProxy(self.factory.makeGitRepository(owner=owner))
974 repo.status = GitRepositoryStatus.CREATING
975 self.assertAbortsRepoCreation(LAUNCHPAD_SERVICES, repo)
976
964 def test_only_requester_can_abort_git_repository_creation(self):977 def test_only_requester_can_abort_git_repository_creation(self):
965 requester = self.factory.makePerson()978 requester = self.factory.makePerson()
966 repo = removeSecurityProxy(self.factory.makeGitRepository())979 repo = removeSecurityProxy(self.factory.makeGitRepository())