From bdf3e0619e097a06b9d24ea8ebd06a5000cb2613 Mon Sep 17 00:00:00 2001 From: shenxianpeng Date: Sun, 27 Jul 2025 12:42:46 +0300 Subject: [PATCH] feat: support GPG signature check --- commit_check/__init__.py | 6 ++ commit_check/commit.py | 63 +++++++++++++++ commit_check/main.py | 10 +++ tests/commit_test.py | 167 ++++++++++++++++++++++++++++++++++++++- tests/main_test.py | 40 ++++++---- 5 files changed, 269 insertions(+), 17 deletions(-) diff --git a/commit_check/__init__.py b/commit_check/__init__.py index 8b78762..affb9bb 100644 --- a/commit_check/__init__.py +++ b/commit_check/__init__.py @@ -48,6 +48,12 @@ 'error': 'Signed-off-by not found in latest commit', 'suggest': 'run command `git commit -m "conventional commit message" --signoff`', }, + { + 'check': 'gpg_signature', + 'regex': r'', # Not used for GPG signature check + 'error': 'Commit does not have a valid GPG signature', + 'suggest': 'run command `git commit -S` to sign your commits with GPG', + }, { 'check': 'merge_base', 'regex': r'main', # it can be master, develop, devel etc based on your project. diff --git a/commit_check/commit.py b/commit_check/commit.py index fccc5da..a92e738 100644 --- a/commit_check/commit.py +++ b/commit_check/commit.py @@ -92,6 +92,69 @@ def check_commit_signoff(checks: list, commit_msg_file: str = "") -> int: return PASS +def check_commit_gpg_signature(checks: list) -> int: + """Check if commit has a valid GPG signature.""" + if has_commits() is False: + return PASS # pragma: no cover + + for check in checks: + if check['check'] == 'gpg_signature': + # Get GPG signature status using git log --pretty=format:"%G?" + try: + gpg_status = cmd_output(['git', 'log', '--pretty=format:%G?', '-1']).strip() + commit_hash = get_commit_info("H") + + # GPG status codes: + # G = good (valid) signature + # B = bad signature + # U = good signature with unknown validity + # X = good signature that has expired + # Y = good signature made by an expired key + # R = good signature made by a revoked key + # E = signature cannot be checked (e.g., missing public key) + # N = no signature + + if gpg_status not in ['G', 'U']: # Only accept good signatures + if not print_error_header.has_been_called: + print_error_header() # pragma: no cover + + error_msg = check['error'] + if gpg_status == 'N': + error_msg = 'Commit is not signed with GPG' + elif gpg_status == 'B': + error_msg = 'Commit has a bad GPG signature' + elif gpg_status == 'E': + error_msg = 'GPG signature cannot be verified (missing public key?)' + elif gpg_status == 'X': + error_msg = 'GPG signature has expired' + elif gpg_status == 'Y': + error_msg = 'GPG signature made by an expired key' + elif gpg_status == 'R': + error_msg = 'GPG signature made by a revoked key' + + print_error_message( + check['check'], f'GPG Status: {gpg_status}', + error_msg, commit_hash, + ) + if check['suggest']: + print_suggestion(check['suggest']) + return FAIL + + except Exception: + # If we can't check GPG status, treat as failure + if not print_error_header.has_been_called: + print_error_header() # pragma: no cover + print_error_message( + check['check'], 'Unknown', + 'Unable to check GPG signature status', get_commit_info("H"), + ) + if check['suggest']: + print_suggestion(check['suggest']) + return FAIL + + return PASS + + def check_imperative(checks: list, commit_msg_file: str = "") -> int: """Check if commit message uses imperative mood.""" if has_commits() is False: diff --git a/commit_check/main.py b/commit_check/main.py index f81af48..5d217c7 100644 --- a/commit_check/main.py +++ b/commit_check/main.py @@ -76,6 +76,14 @@ def get_parser() -> argparse.ArgumentParser: required=False, ) + parser.add_argument( + '-g', + '--gpg-signature', + help='check commit GPG signature', + action="store_true", + required=False, + ) + parser.add_argument( '-mb', '--merge-base', @@ -128,6 +136,8 @@ def main() -> int: check_results.append(branch.check_branch(checks)) if args.commit_signoff: check_results.append(commit.check_commit_signoff(checks)) + if args.gpg_signature: + check_results.append(commit.check_commit_gpg_signature(checks)) if args.merge_base: check_results.append(branch.check_merge_base(checks)) if args.imperative: diff --git a/tests/commit_test.py b/tests/commit_test.py index 9a1235d..dba96bf 100644 --- a/tests/commit_test.py +++ b/tests/commit_test.py @@ -1,6 +1,6 @@ import pytest from commit_check import PASS, FAIL -from commit_check.commit import check_commit_msg, get_default_commit_msg_file, read_commit_msg, check_commit_signoff, check_imperative +from commit_check.commit import check_commit_msg, get_default_commit_msg_file, read_commit_msg, check_commit_signoff, check_commit_gpg_signature, check_imperative # used by get_commit_info mock FAKE_BRANCH_NAME = "fake_commits_info" @@ -398,3 +398,168 @@ def test_is_imperative_invalid_cases(): for case in invalid_cases: assert not _is_imperative(case), f"'{case}' should not be imperative mood" + + +# GPG Signature tests +@pytest.mark.benchmark +def test_check_commit_gpg_signature_valid(mocker): + """Test GPG signature check passes for good signature.""" + checks = [{ + "check": "gpg_signature", + "regex": "", + "error": "Commit does not have a valid GPG signature", + "suggest": "Use git commit -S to sign commits" + }] + + mocker.patch("commit_check.util.cmd_output", return_value="G") + mocker.patch("commit_check.util.get_commit_info", return_value="abc123") + + retval = check_commit_gpg_signature(checks) + assert retval == PASS + + +@pytest.mark.benchmark +def test_check_commit_gpg_signature_valid_unknown(mocker): + """Test GPG signature check passes for good signature with unknown validity.""" + checks = [{ + "check": "gpg_signature", + "regex": "", + "error": "Commit does not have a valid GPG signature", + "suggest": "Use git commit -S to sign commits" + }] + + mocker.patch("commit_check.util.cmd_output", return_value="U") + mocker.patch("commit_check.util.get_commit_info", return_value="abc123") + + retval = check_commit_gpg_signature(checks) + assert retval == PASS + + +@pytest.mark.benchmark +def test_check_commit_gpg_signature_no_signature(mocker): + """Test GPG signature check fails for unsigned commit.""" + checks = [{ + "check": "gpg_signature", + "regex": "", + "error": "Commit does not have a valid GPG signature", + "suggest": "Use git commit -S to sign commits" + }] + + mocker.patch("commit_check.util.cmd_output", return_value="N") + mocker.patch("commit_check.util.get_commit_info", return_value="abc123") + m_print_error_message = mocker.patch(f"{LOCATION}.print_error_message") + m_print_suggestion = mocker.patch(f"{LOCATION}.print_suggestion") + + retval = check_commit_gpg_signature(checks) + assert retval == FAIL + assert m_print_error_message.call_count == 1 + assert m_print_suggestion.call_count == 1 + + +@pytest.mark.benchmark +def test_check_commit_gpg_signature_bad_signature(mocker): + """Test GPG signature check fails for bad signature.""" + checks = [{ + "check": "gpg_signature", + "regex": "", + "error": "Commit does not have a valid GPG signature", + "suggest": "Use git commit -S to sign commits" + }] + + mocker.patch("commit_check.util.cmd_output", return_value="B") + mocker.patch("commit_check.util.get_commit_info", return_value="abc123") + m_print_error_message = mocker.patch(f"{LOCATION}.print_error_message") + m_print_suggestion = mocker.patch(f"{LOCATION}.print_suggestion") + + retval = check_commit_gpg_signature(checks) + assert retval == FAIL + assert m_print_error_message.call_count == 1 + assert m_print_suggestion.call_count == 1 + + +@pytest.mark.benchmark +def test_check_commit_gpg_signature_expired(mocker): + """Test GPG signature check fails for expired signature.""" + checks = [{ + "check": "gpg_signature", + "regex": "", + "error": "Commit does not have a valid GPG signature", + "suggest": "Use git commit -S to sign commits" + }] + + mocker.patch("commit_check.util.cmd_output", return_value="X") + mocker.patch("commit_check.util.get_commit_info", return_value="abc123") + m_print_error_message = mocker.patch(f"{LOCATION}.print_error_message") + m_print_suggestion = mocker.patch(f"{LOCATION}.print_suggestion") + + retval = check_commit_gpg_signature(checks) + assert retval == FAIL + assert m_print_error_message.call_count == 1 + assert m_print_suggestion.call_count == 1 + + +@pytest.mark.benchmark +def test_check_commit_gpg_signature_cannot_check(mocker): + """Test GPG signature check fails when signature cannot be verified.""" + checks = [{ + "check": "gpg_signature", + "regex": "", + "error": "Commit does not have a valid GPG signature", + "suggest": "Use git commit -S to sign commits" + }] + + mocker.patch("commit_check.util.cmd_output", return_value="E") + mocker.patch("commit_check.util.get_commit_info", return_value="abc123") + m_print_error_message = mocker.patch(f"{LOCATION}.print_error_message") + m_print_suggestion = mocker.patch(f"{LOCATION}.print_suggestion") + + retval = check_commit_gpg_signature(checks) + assert retval == FAIL + assert m_print_error_message.call_count == 1 + assert m_print_suggestion.call_count == 1 + + +@pytest.mark.benchmark +def test_check_commit_gpg_signature_exception(mocker): + """Test GPG signature check fails when git command throws exception.""" + checks = [{ + "check": "gpg_signature", + "regex": "", + "error": "Commit does not have a valid GPG signature", + "suggest": "Use git commit -S to sign commits" + }] + + mocker.patch("commit_check.util.cmd_output", side_effect=Exception("Git command failed")) + mocker.patch("commit_check.util.get_commit_info", return_value="abc123") + m_print_error_message = mocker.patch(f"{LOCATION}.print_error_message") + m_print_suggestion = mocker.patch(f"{LOCATION}.print_suggestion") + + retval = check_commit_gpg_signature(checks) + assert retval == FAIL + assert m_print_error_message.call_count == 1 + assert m_print_suggestion.call_count == 1 + + +@pytest.mark.benchmark +def test_check_commit_gpg_signature_no_commits(mocker): + """Test GPG signature check passes when no commits exist.""" + checks = [{ + "check": "gpg_signature", + "regex": "", + "error": "Commit does not have a valid GPG signature", + "suggest": "Use git commit -S to sign commits" + }] + + mocker.patch("commit_check.commit.has_commits", return_value=False) + + retval = check_commit_gpg_signature(checks) + assert retval == PASS + + +@pytest.mark.benchmark +def test_check_commit_gpg_signature_empty_checks(mocker): + """Test GPG signature check with empty checks list.""" + checks = [] + + retval = check_commit_gpg_signature(checks) + assert retval == PASS diff --git a/tests/main_test.py b/tests/main_test.py index 4e277a6..67ebfc1 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -8,22 +8,25 @@ class TestMain: @pytest.mark.benchmark - @pytest.mark.parametrize("argv, check_commit_call_count, check_branch_call_count, check_author_call_count, check_commit_signoff_call_count, check_merge_base_call_count, check_imperative_call_count", [ - ([CMD, "--message"], 1, 0, 0, 0, 0, 0), - ([CMD, "--branch"], 0, 1, 0, 0, 0, 0), - ([CMD, "--author-name"], 0, 0, 1, 0, 0, 0), - ([CMD, "--author-email"], 0, 0, 1, 0, 0, 0), - ([CMD, "--commit-signoff"], 0, 0, 0, 1, 0, 0), - ([CMD, "--merge-base"], 0, 0, 0, 0, 1, 0), - ([CMD, "--imperative"], 0, 0, 0, 0, 0, 1), - ([CMD, "--message", "--author-email"], 1, 0, 1, 0, 0, 0), - ([CMD, "--branch", "--message"], 1, 1, 0, 0, 0, 0), - ([CMD, "--author-name", "--author-email"], 0, 0, 2, 0, 0, 0), - ([CMD, "--message", "--branch", "--author-email"], 1, 1, 1, 0, 0, 0), - ([CMD, "--branch", "--message", "--author-name", "--author-email"], 1, 1, 2, 0, 0, 0), - ([CMD, "--message", "--branch", "--author-name", "--author-email", "--commit-signoff", "--merge-base"], 1, 1, 2, 1, 1, 0), - ([CMD, "--message", "--imperative"], 1, 0, 0, 0, 0, 1), - ([CMD, "--dry-run"], 0, 0, 0, 0, 0, 0), + @pytest.mark.parametrize("argv, check_commit_call_count, check_branch_call_count, check_author_call_count, check_commit_signoff_call_count, check_gpg_signature_call_count, check_merge_base_call_count, check_imperative_call_count", [ + ([CMD, "--message"], 1, 0, 0, 0, 0, 0, 0), + ([CMD, "--branch"], 0, 1, 0, 0, 0, 0, 0), + ([CMD, "--author-name"], 0, 0, 1, 0, 0, 0, 0), + ([CMD, "--author-email"], 0, 0, 1, 0, 0, 0, 0), + ([CMD, "--commit-signoff"], 0, 0, 0, 1, 0, 0, 0), + ([CMD, "--gpg-signature"], 0, 0, 0, 0, 1, 0, 0), + ([CMD, "--merge-base"], 0, 0, 0, 0, 0, 1, 0), + ([CMD, "--imperative"], 0, 0, 0, 0, 0, 0, 1), + ([CMD, "--message", "--author-email"], 1, 0, 1, 0, 0, 0, 0), + ([CMD, "--branch", "--message"], 1, 1, 0, 0, 0, 0, 0), + ([CMD, "--author-name", "--author-email"], 0, 0, 2, 0, 0, 0, 0), + ([CMD, "--message", "--branch", "--author-email"], 1, 1, 1, 0, 0, 0, 0), + ([CMD, "--branch", "--message", "--author-name", "--author-email"], 1, 1, 2, 0, 0, 0, 0), + ([CMD, "--message", "--branch", "--author-name", "--author-email", "--commit-signoff", "--merge-base"], 1, 1, 2, 1, 0, 1, 0), + ([CMD, "--message", "--gpg-signature"], 1, 0, 0, 0, 1, 0, 0), + ([CMD, "--commit-signoff", "--gpg-signature"], 0, 0, 0, 1, 1, 0, 0), + ([CMD, "--message", "--imperative"], 1, 0, 0, 0, 0, 0, 1), + ([CMD, "--dry-run"], 0, 0, 0, 0, 0, 0, 0), ]) def test_main( self, @@ -33,6 +36,7 @@ def test_main( check_branch_call_count, check_author_call_count, check_commit_signoff_call_count, + check_gpg_signature_call_count, check_merge_base_call_count, check_imperative_call_count, ): @@ -48,6 +52,7 @@ def test_main( m_check_branch = mocker.patch("commit_check.branch.check_branch") m_check_author = mocker.patch("commit_check.author.check_author") m_check_commit_signoff = mocker.patch("commit_check.commit.check_commit_signoff") + m_check_gpg_signature = mocker.patch("commit_check.commit.check_commit_gpg_signature") m_check_merge_base = mocker.patch("commit_check.branch.check_merge_base") m_check_imperative = mocker.patch("commit_check.commit.check_imperative") sys.argv = argv @@ -56,6 +61,7 @@ def test_main( assert m_check_branch.call_count == check_branch_call_count assert m_check_author.call_count == check_author_call_count assert m_check_commit_signoff.call_count == check_commit_signoff_call_count + assert m_check_gpg_signature.call_count == check_gpg_signature_call_count assert m_check_merge_base.call_count == check_merge_base_call_count assert m_check_imperative.call_count == check_imperative_call_count @@ -73,6 +79,7 @@ def test_main_help(self, mocker, capfd): m_check_branch = mocker.patch("commit_check.branch.check_branch") m_check_author = mocker.patch("commit_check.author.check_author") m_check_commit_signoff = mocker.patch("commit_check.commit.check_commit_signoff") + m_check_gpg_signature = mocker.patch("commit_check.commit.check_commit_gpg_signature") m_check_merge_base = mocker.patch("commit_check.branch.check_merge_base") sys.argv = ["commit-check", "--h"] with pytest.raises(SystemExit): @@ -81,6 +88,7 @@ def test_main_help(self, mocker, capfd): assert m_check_branch.call_count == 0 assert m_check_author.call_count == 0 assert m_check_commit_signoff.call_count == 0 + assert m_check_gpg_signature.call_count == 0 assert m_check_merge_base.call_count == 0 stdout, _ = capfd.readouterr() assert "usage: " in stdout pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy