launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #29354
[Merge] ~cjwatson/launchpad:repository-fork-api into launchpad:master
Colin Watson has proposed merging ~cjwatson/launchpad:repository-fork-api into launchpad:master.
Commit message:
Allow forking Git repositories via the API
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/432118
This would be useful to the kernel team, in particular.
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:repository-fork-api into launchpad:master.
diff --git a/lib/lp/code/browser/gitrepository.py b/lib/lp/code/browser/gitrepository.py
index da7dc76..ac4e0db 100644
--- a/lib/lp/code/browser/gitrepository.py
+++ b/lib/lp/code/browser/gitrepository.py
@@ -88,7 +88,6 @@ from lp.code.interfaces.gitref import IGitRefBatchNavigator
from lp.code.interfaces.gitrepository import (
ContributorGitIdentity,
IGitRepository,
- IGitRepositorySet,
)
from lp.code.interfaces.revisionstatus import (
IRevisionStatusArtifactSet,
@@ -576,9 +575,7 @@ class GitRepositoryForkView(LaunchpadEditFormView):
@action("Fork it", name="fork")
def fork(self, action, data):
- forked = getUtility(IGitRepositorySet).fork(
- self.context, self.user, data.get("owner")
- )
+ forked = self.context.fork(self.user, data.get("owner"))
self.request.response.addNotification("Repository forked.")
self.next_url = canonical_url(forked)
diff --git a/lib/lp/code/interfaces/gitrepository.py b/lib/lp/code/interfaces/gitrepository.py
index 4b0521b..b8c75d1 100644
--- a/lib/lp/code/interfaces/gitrepository.py
+++ b/lib/lp/code/interfaces/gitrepository.py
@@ -905,6 +905,24 @@ class IGitRepositoryView(IHasRecipes):
:param commit_sha1: The commit sha1 for the report.
"""
+ @call_with(requester=REQUEST_USER)
+ @operation_parameters(
+ new_owner=Reference(
+ title=_("The person who will own the forked repository."),
+ schema=IPerson,
+ )
+ )
+ # Really IGitRepository, patched in lp.code.interfaces.webservice.
+ @operation_returns_entry(Interface)
+ @export_write_operation()
+ @operation_for_version("devel")
+ def fork(requester, new_owner):
+ """Fork this repository to the given user's account.
+
+ :param requester: The IPerson performing this fork.
+ :param new_owner: The IPerson that will own the forked repository.
+ :return: The newly created GitRepository."""
+
class IGitRepositoryModerateAttributes(Interface):
"""IGitRepository attributes that can be edited by more than one
@@ -1308,14 +1326,6 @@ class IGitRepositorySet(Interface):
:param with_hosting: Create the repository on the hosting service.
"""
- def fork(origin, requester, new_owner):
- """Fork a repository to the given user's account.
-
- :param origin: The original GitRepository.
- :param requester: The IPerson performing this fork.
- :param new_owner: The IPerson that will own the forked 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/interfaces/webservice.py b/lib/lp/code/interfaces/webservice.py
index 203d9de..43e8436 100644
--- a/lib/lp/code/interfaces/webservice.py
+++ b/lib/lp/code/interfaces/webservice.py
@@ -211,6 +211,7 @@ patch_collection_return_type(
patch_list_parameter_type(
IGitRepository, "setRules", "rules", InlineObject(schema=IGitNascentRule)
)
+patch_entry_return_type(IGitRepository, "fork", IGitRepository)
# IHasBranches
patch_collection_return_type(IHasBranches, "getBranches", IBranch)
diff --git a/lib/lp/code/model/gitrepository.py b/lib/lp/code/model/gitrepository.py
index 64197b3..3576601 100644
--- a/lib/lp/code/model/gitrepository.py
+++ b/lib/lp/code/model/gitrepository.py
@@ -515,6 +515,40 @@ class GitRepository(
self, commit_sha1
)
+ def fork(self, requester, new_owner):
+ if not requester.inTeam(new_owner):
+ raise Unauthorized(
+ "The owner of the new repository must be you or a team of "
+ "which you are a member."
+ )
+ namespace = get_git_namespace(self.target, new_owner)
+ name = namespace.findUnusedName(self.name)
+ repository = getUtility(IGitRepositorySet).new(
+ repository_type=GitRepositoryType.HOSTED,
+ registrant=requester,
+ owner=new_owner,
+ target=self.target,
+ name=name,
+ information_type=self.information_type,
+ date_created=UTC_NOW,
+ description=self.description,
+ with_hosting=True,
+ async_hosting=True,
+ status=GitRepositoryStatus.CREATING,
+ clone_from_repository=self,
+ )
+ if self.target_default or self.owner_default:
+ try:
+ # If the origin is the default for its target or for its
+ # owner and target, then try to set the new repo as
+ # owner-default.
+ repository.setOwnerDefault(True)
+ except GitDefaultConflict:
+ # If there is already a owner-default for this owner/target,
+ # just move on.
+ pass
+ return repository
+
@property
def namespace(self):
"""See `IGitRepository`."""
@@ -2189,35 +2223,6 @@ class GitRepositorySet:
clone_from_repository=clone_from_repository,
)
- def fork(self, origin, requester, new_owner):
- namespace = get_git_namespace(origin.target, new_owner)
- name = namespace.findUnusedName(origin.name)
- repository = self.new(
- repository_type=GitRepositoryType.HOSTED,
- registrant=requester,
- owner=new_owner,
- target=origin.target,
- name=name,
- information_type=origin.information_type,
- date_created=UTC_NOW,
- description=origin.description,
- with_hosting=True,
- async_hosting=True,
- status=GitRepositoryStatus.CREATING,
- clone_from_repository=origin,
- )
- if origin.target_default or origin.owner_default:
- try:
- # If the origin is the default for its target or for its
- # owner and target, then try to set the new repo as
- # owner-default.
- repository.setOwnerDefault(True)
- except GitDefaultConflict:
- # If there is already a owner-default for this owner/target,
- # just move on.
- pass
- return repository
-
def getByPath(self, user, path):
"""See `IGitRepositorySet`."""
repository, extra_path = getUtility(IGitLookup).getByPath(path)
diff --git a/lib/lp/code/model/tests/test_gitrepository.py b/lib/lp/code/model/tests/test_gitrepository.py
index 670b4d6..c9fa5d4 100644
--- a/lib/lp/code/model/tests/test_gitrepository.py
+++ b/lib/lp/code/model/tests/test_gitrepository.py
@@ -3730,9 +3730,7 @@ class TestGitRepositoryFork(TestCaseWithFactory):
another_person = self.factory.makePerson()
another_team = self.factory.makeTeam(members=[another_person])
- forked_repo = getUtility(IGitRepositorySet).fork(
- repo, another_person, another_team
- )
+ forked_repo = repo.fork(another_person, another_team)
self.assertThat(
forked_repo,
MatchesStructure(
@@ -3768,9 +3766,7 @@ class TestGitRepositoryFork(TestCaseWithFactory):
previous_repo = self.factory.makeGitRepository(target=repo.target)
previous_repo.setOwnerDefault(True)
- forked_repo = getUtility(IGitRepositorySet).fork(
- repo, previous_repo.owner, previous_repo.owner
- )
+ forked_repo = repo.fork(previous_repo.owner, previous_repo.owner)
self.assertThat(
forked_repo,
MatchesStructure(
@@ -3807,7 +3803,7 @@ class TestGitRepositoryFork(TestCaseWithFactory):
owner=person, registrant=person, name=repo.name, target=repo.target
)
- forked_repo = getUtility(IGitRepositorySet).fork(repo, person, person)
+ forked_repo = repo.fork(person, person)
self.assertThat(
forked_repo,
MatchesStructure(
@@ -3842,9 +3838,7 @@ class TestGitRepositoryFork(TestCaseWithFactory):
)
person = self.factory.makePerson()
- forked_repo = getUtility(IGitRepositorySet).fork(
- non_default_repo, person, person
- )
+ forked_repo = non_default_repo.fork(person, person)
self.assertThat(
forked_repo,
MatchesStructure(
@@ -6601,6 +6595,88 @@ class TestGitRepositoryWebservice(TestCaseWithFactory):
response.body,
)
+ def test_fork_to_self(self):
+ hosting_fixture = self.useFixture(GitHostingFixture())
+ repository = self.factory.makeGitRepository()
+ requester = self.factory.makePerson()
+ repository_url = api_url(repository)
+ requester_url = api_url(requester)
+ webservice = webservice_for_person(
+ requester,
+ permission=OAuthPermission.WRITE_PUBLIC,
+ default_api_version="devel",
+ )
+ response = webservice.named_post(
+ repository_url, "fork", new_owner=requester_url
+ )
+ self.assertEqual(200, response.status)
+ self.assertEndsWith(response.jsonBody()["owner_link"], requester_url)
+ self.assertEqual(1, len(hosting_fixture.create.calls))
+
+ def test_fork_to_team_as_member(self):
+ hosting_fixture = self.useFixture(GitHostingFixture())
+ repository = self.factory.makeGitRepository()
+ requester = self.factory.makePerson()
+ team = self.factory.makeTeam(members=[requester])
+ repository_url = api_url(repository)
+ team_url = api_url(team)
+ webservice = webservice_for_person(
+ requester,
+ permission=OAuthPermission.WRITE_PUBLIC,
+ default_api_version="devel",
+ )
+ response = webservice.named_post(
+ repository_url, "fork", new_owner=team_url
+ )
+ self.assertEqual(200, response.status)
+ self.assertEndsWith(response.jsonBody()["owner_link"], team_url)
+ self.assertEqual(1, len(hosting_fixture.create.calls))
+
+ def test_fork_to_team_as_non_member(self):
+ hosting_fixture = self.useFixture(GitHostingFixture())
+ repository = self.factory.makeGitRepository()
+ requester = self.factory.makePerson()
+ team = self.factory.makeTeam()
+ repository_url = api_url(repository)
+ team_url = api_url(team)
+ webservice = webservice_for_person(
+ requester,
+ permission=OAuthPermission.WRITE_PUBLIC,
+ default_api_version="devel",
+ )
+ response = webservice.named_post(
+ repository_url, "fork", new_owner=team_url
+ )
+ self.assertEqual(401, response.status)
+ self.assertEqual(
+ b"The owner of the new repository must be you or a team of which "
+ b"you are a member.",
+ response.body,
+ )
+ self.assertEqual(0, len(hosting_fixture.create.calls))
+
+ def test_fork_invisible(self):
+ hosting_fixture = self.useFixture(GitHostingFixture())
+ owner = self.factory.makePerson()
+ repository = self.factory.makeGitRepository(
+ owner=owner, information_type=InformationType.USERDATA
+ )
+ requester = self.factory.makePerson()
+ with person_logged_in(owner):
+ repository_url = api_url(repository)
+ requester_url = api_url(requester)
+ webservice = webservice_for_person(
+ requester,
+ permission=OAuthPermission.WRITE_PUBLIC,
+ default_api_version="devel",
+ )
+ response = webservice.named_post(
+ repository_url, "fork", new_owner=requester_url
+ )
+ self.assertEqual(401, response.status)
+ self.assertIn(b"launchpad.View", response.body)
+ self.assertEqual(0, len(hosting_fixture.create.calls))
+
class TestGitRepositoryMacaroonIssuer(MacaroonTestMixin, TestCaseWithFactory):
"""Test GitRepository macaroon issuing and verification."""