Repository: python-gitlab/python-gitlab

Issue:
Support for signature endpoint for commits
## Description of the problem, including code/CLI snippet

Commit object does not seems to provide information about the signature of the commit.

## Expected Behavior

Be able to get the signature status of a commit, as per https://docs.gitlab.com/ee/api/commits.html#get-gpg-signature-of-a-commit

## Actual Behavior

Not able to get the signature status of a commit

## Specifications

  - python-gitlab version: 2.1.2
  - API version you are using (v3/v4): v4
  - Gitlab server version (or gitlab.com): 12.7.6



Discussion:

Resolving patch:
diff --git a/gitlab/v4/objects.py b/gitlab/v4/objects.py
--- a/gitlab/v4/objects.py
+++ b/gitlab/v4/objects.py
@@ -2172,6 +2172,24 @@ def revert(self, branch, **kwargs):
         post_data = {"branch": branch}
         return self.manager.gitlab.http_post(path, post_data=post_data, **kwargs)
 
+    @cli.register_custom_action("ProjectCommit")
+    @exc.on_http_error(exc.GitlabGetError)
+    def signature(self, **kwargs):
+        """Get the GPG signature of the commit.
+
+        Args:
+            **kwargs: Extra options to send to the server (e.g. sudo)
+
+        Raises:
+            GitlabAuthenticationError: If authentication is not correct
+            GitlabGetError: If the signature could not be retrieved
+
+        Returns:
+            dict: The commit's GPG signature data
+        """
+        path = "%s/%s/signature" % (self.manager.path, self.get_id())
+        return self.manager.gitlab.http_get(path, **kwargs)
+
 
 class ProjectCommitManager(RetrieveMixin, CreateMixin, RESTManager):
     _path = "/projects/%(project_id)s/repository/commits"


Test patch:
diff --git a/gitlab/tests/objects/test_commits.py b/gitlab/tests/objects/test_commits.py
new file mode 100644
--- /dev/null
+++ b/gitlab/tests/objects/test_commits.py
@@ -0,0 +1,106 @@
+from httmock import urlmatch, response, with_httmock
+
+from .test_projects import headers, TestProject
+
+
+@urlmatch(
+    scheme="http",
+    netloc="localhost",
+    path="/api/v4/projects/1/repository/commits/6b2257ea",
+    method="get",
+)
+def resp_get_commit(url, request):
+    """Mock for commit GET response."""
+    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", path="/api/v4/projects/1/repository/commits", method="post",
+)
+def resp_create_commit(url, request):
+    """Mock for commit create POST response."""
+    content = """{
+    "id": "ed899a2f4b50b4370feeea94676502b42383c746",
+    "short_id": "ed899a2f",
+    "title": "Commit message"
+    }"""
+    content = content.encode("utf-8")
+    return response(200, content, headers, None, 5, request)
+
+
+@urlmatch(
+    scheme="http", path="/api/v4/projects/1/repository/commits/6b2257ea", method="post",
+)
+def resp_revert_commit(url, request):
+    """Mock for commit revert POST response."""
+    content = """{
+    "id": "8b090c1b79a14f2bd9e8a738f717824ff53aebad",
+    "short_id": "8b090c1b",
+    "title":"Revert \\"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/signature",
+    method="get",
+)
+def resp_get_commit_gpg_signature(url, request):
+    """Mock for commit GPG signature GET response."""
+    content = """{
+    "gpg_key_id": 1,
+    "gpg_key_primary_keyid": "8254AAB3FBD54AC9",
+    "gpg_key_user_name": "John Doe",
+    "gpg_key_user_email": "johndoe@example.com",
+    "verification_status": "verified",
+    "gpg_key_subkey_id": null
+    }"""
+    content = content.encode("utf-8")
+    return response(200, content, headers, None, 5, request)
+
+
+class TestCommit(TestProject):
+    """
+    Base class for commit tests. Inherits from TestProject,
+    since currently all commit methods are under projects.
+    """
+
+    @with_httmock(resp_get_commit)
+    def test_get_commit(self):
+        commit = self.project.commits.get("6b2257ea")
+        self.assertEqual(commit.short_id, "6b2257ea")
+        self.assertEqual(commit.title, "Initial commit")
+
+    @with_httmock(resp_create_commit)
+    def test_create_commit(self):
+        data = {
+            "branch": "master",
+            "commit_message": "Commit message",
+            "actions": [{"action": "create", "file_path": "README", "content": "",}],
+        }
+        commit = self.project.commits.create(data)
+        self.assertEqual(commit.short_id, "ed899a2f")
+        self.assertEqual(commit.title, data["commit_message"])
+
+    @with_httmock(resp_revert_commit)
+    def test_revert_commit(self):
+        commit = self.project.commits.get("6b2257ea", lazy=True)
+        revert_commit = commit.revert(branch="master")
+        self.assertEqual(revert_commit["short_id"], "8b090c1b")
+        self.assertEqual(revert_commit["title"], 'Revert "Initial commit"')
+
+    @with_httmock(resp_get_commit_gpg_signature)
+    def test_get_commit_gpg_signature(self):
+        commit = self.project.commits.get("6b2257ea", lazy=True)
+        signature = commit.signature()
+        self.assertEqual(signature["gpg_key_primary_keyid"], "8254AAB3FBD54AC9")
+        self.assertEqual(signature["verification_status"], "verified")
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
@@ -843,50 +843,6 @@ 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
@@ -113,6 +113,13 @@ testcase "revert commit" '
         --id "$COMMIT_ID" --branch master
 '
 
+# Test commit GPG signature
+testcase "attempt to get GPG signature of unsigned commit" '
+    OUTPUT=$(GITLAB project-commit signature --project-id "$PROJECT_ID" \
+        --id "$COMMIT_ID" 2>&1 || exit 0)
+    echo "$OUTPUT" | grep -q "404 GPG Signature Not Found"
+'
+
 # 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
@@ -473,6 +473,14 @@
 # assert commit.refs()
 # assert commit.merge_requests()
 
+# commit GPG signature (for unsigned commits)
+# TODO: reasonable tests for signed commits?
+try:
+    signature = commit.signature()
+except gitlab.GitlabGetError as e:
+    error_message = e.error_message
+assert error_message == "404 GPG Signature Not Found"
+
 # commit comment
 commit.comments.create({"note": "This is a commit comment"})
 # assert len(commit.comments.list()) == 1