Repository: python-gitlab/python-gitlab

Issue:
How to revert a commit of a project
## Description of the problem, including code/CLI snippet
I need to remove an specific commit in a project

## Expected Behavior
https://docs.gitlab.com/ce/api/commits.html#revert-a-commit

## Actual Behavior


## Specifications

  - python-gitlab version: 
  - API version you are using (v3/v4): v4
  - Gitlab server version (or gitlab.com): 12.4



Discussion:
Not implemented yet?
yep. Seems like it. I would be happy to review any MRs, if you want to contribute :+1: 
> yep. Seems like it. I would be happy to review any MRs, if you want to contribute +1

Where are the functions to create commit? To check this as base

Resolving patch:
diff --git a/gitlab/exceptions.py b/gitlab/exceptions.py
--- 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/v4/objects.py b/gitlab/v4/objects.py
--- 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"


Test patch:
diff --git a/gitlab/tests/test_gitlab.py b/gitlab/tests/test_gitlab.py
--- 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/tools/cli_test_v4.sh b/tools/cli_test_v4.sh
--- 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
--- 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()