diff --git a/gitlab/exceptions.py b/gitlab/exceptions.py index 6b8647152..54f9b8cd0 100644 --- a/gitlab/exceptions.py +++ b/gitlab/exceptions.py @@ -107,6 +107,10 @@ class GitlabTransferProjectError(GitlabOperationError): pass +class GitlabGroupTransferError(GitlabOperationError): + pass + + class GitlabProjectDeployKeyError(GitlabOperationError): pass diff --git a/gitlab/v4/objects/groups.py b/gitlab/v4/objects/groups.py index 453548b94..933f48c64 100644 --- a/gitlab/v4/objects/groups.py +++ b/gitlab/v4/objects/groups.py @@ -95,12 +95,34 @@ def transfer_project(self, project_id: int, **kwargs: Any) -> None: path = f"/groups/{self.id}/projects/{project_id}" self.manager.gitlab.http_post(path, **kwargs) + @cli.register_custom_action("Group", tuple(), ("group_id",)) + @exc.on_http_error(exc.GitlabGroupTransferError) + def transfer(self, group_id: Optional[int] = None, **kwargs: Any) -> None: + """Transfer the group to a new parent group or make it a top-level group. + + Requires GitLab ≥14.6. + + Args: + group_id: ID of the new parent group. When not specified, + the group to transfer is instead turned into a top-level group. + **kwargs: Extra options to send to the server (e.g. sudo) + + Raises: + GitlabAuthenticationError: If authentication is not correct + GitlabGroupTransferError: If the group could not be transferred + """ + path = f"/groups/{self.encoded_id}/transfer" + post_data = {} + if group_id is not None: + post_data["group_id"] = group_id + self.manager.gitlab.http_post(path, post_data=post_data, **kwargs) + @cli.register_custom_action("Group", ("scope", "search")) @exc.on_http_error(exc.GitlabSearchError) def search( self, scope: str, search: str, **kwargs: Any ) -> Union[gitlab.GitlabList, List[Dict[str, Any]]]: - """Search the group resources matching the provided string.' + """Search the group resources matching the provided string. Args: scope: Scope of the search diff --git a/tests/functional/api/test_groups.py b/tests/functional/api/test_groups.py index 584ea8355..b61305569 100644 --- a/tests/functional/api/test_groups.py +++ b/tests/functional/api/test_groups.py @@ -233,17 +233,19 @@ def test_group_hooks(group): hook.delete() -@pytest.mark.skip(reason="Pending #1807") def test_group_transfer(gl, group): - transfer_group = gl.groups.create({"name": "transfer-test-group"}) - assert group.namespace["path"] != group.full_path + transfer_group = gl.groups.create( + {"name": "transfer-test-group", "path": "transfer-test-group"} + ) + transfer_group = gl.groups.get(transfer_group.id) + assert transfer_group.parent_id != group.id transfer_group.transfer(group.id) - transferred_group = gl.projects.get(transfer_group.id) - assert transferred_group.namespace["path"] == group.full_path + transferred_group = gl.groups.get(transfer_group.id) + assert transferred_group.parent_id == group.id transfer_group.transfer() - transferred_group = gl.projects.get(transfer_group.id) + transferred_group = gl.groups.get(transfer_group.id) assert transferred_group.path == transferred_group.full_path diff --git a/tests/functional/fixtures/.env b/tests/functional/fixtures/.env index 30abd5caf..bcfd35713 100644 --- a/tests/functional/fixtures/.env +++ b/tests/functional/fixtures/.env @@ -1,2 +1,2 @@ GITLAB_IMAGE=gitlab/gitlab-ce -GITLAB_TAG=14.5.2-ce.0 +GITLAB_TAG=14.6.2-ce.0 diff --git a/tests/unit/objects/test_groups.py b/tests/unit/objects/test_groups.py index b3e753e4b..2c91d38d8 100644 --- a/tests/unit/objects/test_groups.py +++ b/tests/unit/objects/test_groups.py @@ -99,13 +99,13 @@ def resp_create_import(accepted_content): def resp_transfer_group(): with responses.RequestsMock() as rsps: rsps.add( - method=responses.PUT, + method=responses.POST, url="http://localhost/api/v4/groups/1/transfer", json=content, content_type="application/json", status=200, match=[ - responses.matchers.json_params_matcher({"namespace": "test-namespace"}) + responses.matchers.json_params_matcher({"group_id": "test-namespace"}) ], ) yield rsps @@ -170,7 +170,6 @@ def test_refresh_group_import_status(group, resp_groups): assert group_import.import_status == "finished" -@pytest.mark.skip("Pending #1807") def test_transfer_group(gl, resp_transfer_group): group = gl.groups.get(1, lazy=True) group.transfer("test-namespace")