diff --git a/docs/gl_objects/commits.rst b/docs/gl_objects/commits.rst index 97cd1c48f..abfedc8a4 100644 --- a/docs/gl_objects/commits.rst +++ b/docs/gl_objects/commits.rst @@ -72,6 +72,10 @@ Cherry-pick a commit into another branch:: commit.cherry_pick(branch='target_branch') +Revert a commit on a given branch:: + + commit.revert(branch='target_branch') + Get the references the commit has been pushed to (branches and tags):: commit.refs() # all references diff --git a/gitlab/exceptions.py b/gitlab/exceptions.py index aff3c87d5..d6791f223 100644 --- a/gitlab/exceptions.py +++ b/gitlab/exceptions.py @@ -245,6 +245,10 @@ class GitlabRepairError(GitlabOperationError): pass +class GitlabRevertError(GitlabOperationError): + pass + + class GitlabLicenseError(GitlabOperationError): pass diff --git a/gitlab/tests/test_gitlab.py b/gitlab/tests/test_gitlab.py index 3eccf6e7d..b56889a91 100644 --- a/gitlab/tests/test_gitlab.py +++ b/gitlab/tests/test_gitlab.py @@ -794,6 +794,50 @@ def resp_deactivate(url, request): self.gl.users.get(1, lazy=True).activate() self.gl.users.get(1, lazy=True).deactivate() + def test_commit_revert(self): + @urlmatch( + scheme="http", + netloc="localhost", + path="/api/v4/projects/1/repository/commits/6b2257ea", + method="get", + ) + def resp_get_commit(url, request): + headers = {"content-type": "application/json"} + content = """{ + "id": "6b2257eabcec3db1f59dafbd84935e3caea04235", + "short_id": "6b2257ea", + "title": "Initial commit" + }""" + content = content.encode("utf-8") + return response(200, content, headers, None, 5, request) + + @urlmatch( + scheme="http", + netloc="localhost", + path="/api/v4/projects/1/repository/commits/6b2257ea", + method="post", + ) + def resp_revert_commit(url, request): + headers = {"content-type": "application/json"} + content = """{ + "id": "8b090c1b79a14f2bd9e8a738f717824ff53aebad", + "short_id": "8b090c1b", + "title":"Revert \\"Initial commit\\"" + }""" + content = content.encode("utf-8") + return response(200, content, headers, None, 5, request) + + with HTTMock(resp_get_commit): + project = self.gl.projects.get(1, lazy=True) + commit = project.commits.get("6b2257ea") + self.assertEqual(commit.short_id, "6b2257ea") + self.assertEqual(commit.title, "Initial commit") + + with HTTMock(resp_revert_commit): + revert_commit = commit.revert(branch="master") + self.assertEqual(revert_commit["short_id"], "8b090c1b") + self.assertEqual(revert_commit["title"], 'Revert "Initial commit"') + def test_update_submodule(self): @urlmatch( scheme="http", netloc="localhost", path="/api/v4/projects/1$", method="get" diff --git a/gitlab/v4/objects.py b/gitlab/v4/objects.py index b31870c2b..83f77d365 100644 --- a/gitlab/v4/objects.py +++ b/gitlab/v4/objects.py @@ -2136,6 +2136,26 @@ def merge_requests(self, **kwargs): path = "%s/%s/merge_requests" % (self.manager.path, self.get_id()) return self.manager.gitlab.http_get(path, **kwargs) + @cli.register_custom_action("ProjectCommit", ("branch",)) + @exc.on_http_error(exc.GitlabRevertError) + def revert(self, branch, **kwargs): + """Revert a commit on a given branch. + + Args: + branch (str): Name of target branch + **kwargs: Extra options to send to the server (e.g. sudo) + + Raises: + GitlabAuthenticationError: If authentication is not correct + GitlabRevertError: If the revert could not be performed + + Returns: + dict: The new commit data (*not* a RESTObject) + """ + path = "%s/%s/revert" % (self.manager.path, self.get_id()) + post_data = {"branch": branch} + return self.manager.gitlab.http_post(path, post_data=post_data, **kwargs) + class ProjectCommitManager(RetrieveMixin, CreateMixin, RESTManager): _path = "/projects/%(project_id)s/repository/commits" diff --git a/tools/cli_test_v4.sh b/tools/cli_test_v4.sh index dc6e0b278..b7ed708ed 100755 --- a/tools/cli_test_v4.sh +++ b/tools/cli_test_v4.sh @@ -100,6 +100,15 @@ testcase "merge request validation" ' --iid "$MR_ID" >/dev/null 2>&1 ' +# Test revert commit +COMMITS=$(GITLAB -v project-commit list --project-id "${PROJECT_ID}") +COMMIT_ID=$(pecho "${COMMITS}" | grep -m1 '^id:' | cut -d' ' -f2) + +testcase "revert commit" ' + GITLAB project-commit revert --project-id "$PROJECT_ID" \ + --id "$COMMIT_ID" --branch master +' + # Test project labels testcase "create project label" ' OUTPUT=$(GITLAB -v project-label create --project-id $PROJECT_ID \ diff --git a/tools/python_test_v4.py b/tools/python_test_v4.py index bffdd2a17..49f99e5ba 100644 --- a/tools/python_test_v4.py +++ b/tools/python_test_v4.py @@ -462,6 +462,21 @@ discussion = commit.discussions.get(discussion.id) # assert len(discussion.attributes["notes"]) == 1 +# Revert commit +revert_commit = commit.revert(branch="master") + +expected_message = 'Revert "{}"\n\nThis reverts commit {}'.format( + commit.message, commit.id +) +assert revert_commit["message"] == expected_message + +try: + commit.revert(branch="master") + # Only here to really ensure expected error without a full test framework + raise AssertionError("Two revert attempts should raise GitlabRevertError") +except gitlab.GitlabRevertError: + pass + # housekeeping admin_project.housekeeping()