diff --git a/.github/labeler.yml b/.github/labeler.yml index ccd5268..6f620c3 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -1,4 +1,4 @@ # Add 'dependencies' label to pre-commit config files within the entire repository dependencies: - changed-files: - - any-glob-to-any-file: '.pre-commit*.yml' + - any-glob-to-any-file: '.pre-commit-\w*.yaml' diff --git a/.github/release.yml b/.github/release.yml deleted file mode 100644 index d82f279..0000000 --- a/.github/release.yml +++ /dev/null @@ -1,25 +0,0 @@ -# https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes#configuration-options - -changelog: - exclude: - labels: - - ignore-for-release - categories: - - title: '🔥 Breaking Changes' - labels: - - 'breaking' - - title: 🏕 Features - labels: - - 'enhancement' - - title: '🐛 Bug Fixes' - labels: - - 'bug' - - title: '👋 Deprecated' - labels: - - 'deprecation' - - title: 📦 Dependencies - labels: - - dependencies - - title: Other Changes - labels: - - "*" diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index e57cd86..399105d 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -1,6 +1,7 @@ name: "Pull Request Labeler" on: -- pull_request_target + pull_request_target: + workflow_dispatch: jobs: labeler: @@ -9,4 +10,8 @@ jobs: pull-requests: write runs-on: ubuntu-latest steps: + - uses: actions/checkout@v4 - uses: actions/labeler@v5 + with: + sync-labels: true + configuration-path: .github/labeler.yml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 33c6308..4efa956 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -61,7 +61,7 @@ jobs: fail-fast: false matrix: py: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12'] - os: ['windows-latest', ubuntu-latest] + os: ['windows-latest', 'ubuntu-latest', 'macos-latest'] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index d703049..7c8d6e7 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -1,9 +1,14 @@ +# https://github.com/release-drafter/release-drafter name: Release Drafter on: push: branches: - "main" + pull_request: + types: [opened, reopened, synchronize] + pull_request_target: + types: [opened, reopened, synchronize] workflow_dispatch: jobs: @@ -11,6 +16,6 @@ jobs: runs-on: ubuntu-latest steps: # Drafts your next Release notes as Pull Requests are merged into the default branch - - uses: release-drafter/release-drafter@v5 + - uses: release-drafter/release-drafter@v6 env: - GITHUB_TOKEN: ${{ secrets.COMMIT_CHECK_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4a2244b..ed342a5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,8 +2,9 @@ ci: autofix_commit_msg: 'ci: auto fixes from pre-commit.com hooks' autoupdate_commit_msg: 'ci: pre-commit autoupdate' - skip: [pytest] +# prepare-commit-msg is used by hook id: check-message +default_install_hook_types: [pre-commit, prepare-commit-msg] repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.5.0 @@ -31,17 +32,10 @@ repos: hooks: - id: codespell - repo: https://github.com/commit-check/commit-check - rev: v0.7.0 + rev: v0.7.1 hooks: - id: check-message - - id: check-branch - # - id: check-author-email # uncomment if you need. - # - id: commit-signoff # uncomment if you need. -- repo: local - hooks: - - id: pytest - name: pytest - entry: pytest - language: system - pass_filenames: false - always_run: true + # - id: check-branch # uncomment if you need. + # - id: check-author-name # uncomment if you need. + # - id: check-author-email # uncomment if you need. + # - id: commit-signoff # uncomment if you need. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..92cd278 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,17 @@ +# Contributing + +Thank you for investing your time in contributing to our project! We welcome feedback, bug reports, and pull requests! + +## New contributor guide + +Our development branch is `main`. When submitting pull requests, please adhere to the following guidelines: + +* Add tests for any new features and bug fixes. +* Put a reasonable amount of comments into the code. +* Fork [commit-check](https://github.com/commit-check/commit-check) on your GitHub user account. +* Create branch from `main`, make your changes on the new branch and then create a PR against `main` branch of commit-check repository. +* Separate unrelated changes into multiple pull requests for better review and management. + +By contributing any code or documentation to this repository (by raising pull requests or otherwise), you explicitly agree to the [License Agreement](https://github.com/commit-check/commit-check/blob/main/LICENSE). + +We appreciate your contributions to make Commit Check even better! diff --git a/README.rst b/README.rst index f47acbd..4ec86be 100644 --- a/README.rst +++ b/README.rst @@ -28,39 +28,37 @@ Commit Check Overview -------- -Commit Check is open source alternative to Yet Another Commit Checker. +Commit Check supports checking commit messages, branch naming, committer name/email, commit signoff, customizing error messages, suggested commands and more. -It supports checking commit message, branch naming, committer name/email, commit signoff and customizing error message and suggest command (tell me more, please). - -commit-check is a tool designed for teams. Its main purpose is to standardize the format of commit message, branch naming, etc, and makes it possible to: +It is a powerful, free solution for individuals and teams aiming to standardize commit message formatting and branch naming, including - writing descriptive commit is easy to read - identify branch according to the branch type - triggering the specific types of commit/branch CI build - automatically generate changelogs +If you're using Bitbucket, it's an open source alternative to Yet Another Commit Checker. + Configuration ------------- -Use custom configuration +Use Custom Configuration ~~~~~~~~~~~~~~~~~~~~~~~~ -Create a config file ``.commit-check.yml`` under your repository root directory, e.g. `.commit-check.yml `_ +Create a config file ``.commit-check.yml`` under your repository's root directory, e.g., `.commit-check.yml `_ -Use default configuration +Use Default Configuration ~~~~~~~~~~~~~~~~~~~~~~~~~ -- If you did't set ``.commit-check.yml``, ``commit-check`` will use the `default configuration `_. +- If you don't set ``.commit-check.yml``, Commit Check will use the `default configuration `_. -- i.e. the commit message will follow the rules of `conventional commits `_, - branch naming follow bitbucket `branching model `_. +- The commit message will follow the rules of `conventional commits `_, + branch naming follow Bitbucket's `branching model `_. Usage ----- -There are a variety of ways you can use commit-check as follows. - Running as GitHub Action ~~~~~~~~~~~~~~~~~~~~~~~~ @@ -78,7 +76,7 @@ Running as pre-commit hook - repo: https://github.com/commit-check/commit-check rev: the tag or revision hooks: # support hooks - - id: check-message + - id: check-message # it requires hook prepare-commit-msg - id: check-branch - id: check-author-name - id: check-author-email @@ -87,50 +85,48 @@ Running as pre-commit hook Running as CLI ~~~~~~~~~~~~~~ -Global installation +Global Installation .. code-block:: bash sudo pip3 install -U commit-check -User installation +User Installation .. code-block:: bash pip install -U commit-check -Install from git repo +Install from Git Repo .. code-block:: bash pip install git+https://github.com/commit-check/commit-check.git@main -Then you can run ``commit-check`` command line. More about ``commit-check --help`` please see `docs `_. +Then, run ``commit-check`` from the command line. For more information, see the `docs `_. Running as Git Hooks ~~~~~~~~~~~~~~~~~~~~ -To configure the hook, you need to create a new script file in the ``.git/hooks/`` directory of your Git repository. - -Here is an example script that you can use to set up the hook: +To configure the hook, create a script file in the ``.git/hooks/`` directory. .. code-block:: bash #!/bin/sh commit-check --message --branch --author-name --author-email -Save the script file to ``pre-push`` and make it executable by running the following command: +Save the script file as ``pre-push`` and make it executable: .. code-block:: bash chmod +x .git/hooks/pre-push -Then when you run ``git push`` command, this push hook will be run automatically. +Now, ``git push`` will trigger this hook automatically. Example ------- -Check commit message failed +Check Commit Message Failed .. code-block:: text @@ -158,7 +154,7 @@ Check commit message failed Suggest: please check your commit message whether matches above regex -Check branch naming failed +Check Branch Naming Failed .. code-block:: text @@ -185,7 +181,7 @@ Check branch naming failed Badging your repository ----------------------- -You can add a badge to your repository to show your contributors / users that you use commit-check! +You can add a badge to your repository to show that you use commit-check! .. image:: https://img.shields.io/badge/commit--check-enabled-brightgreen?logo=Git&logoColor=white :target: https://github.com/commit-check/commit-check @@ -214,7 +210,7 @@ Versioning follows `Semantic Versioning `_. Have question or feedback? -------------------------- -To provide feedback (requesting a feature or reporting a bug) please post to `issues `_. +Please post to `issues `_ for feedback, feature requests, or bug reports. License ------- diff --git a/commit_check/__init__.py b/commit_check/__init__.py index 8d745fb..1c5982f 100644 --- a/commit_check/__init__.py +++ b/commit_check/__init__.py @@ -44,7 +44,7 @@ }, { 'check': 'commit_signoff', - 'regex': 'Signed-off-by', + 'regex': r'Signed-off-by:.*[A-Za-z0-9]\s+<[\w\.]+@([\w-]+\.)+[\w-]{2,4}>', 'error': 'Signed-off-by not found in latest commit', 'suggest': 'run command `git commit -m "conventional commit message" --signoff`', }, diff --git a/commit_check/author.py b/commit_check/author.py index 94a0b59..1ab3ba1 100644 --- a/commit_check/author.py +++ b/commit_check/author.py @@ -1,7 +1,7 @@ """Check git author name and email""" import re from commit_check import YELLOW, RESET_COLOR, PASS, FAIL -from commit_check.util import get_commits_info, print_error_message, print_suggestion +from commit_check.util import get_commit_info, print_error_message, print_suggestion def check_author(checks: list, check_type: str) -> int: @@ -16,7 +16,7 @@ def check_author(checks: list, check_type: str) -> int: format_str = "an" if check_type == 'author_email': format_str = "ae" - config_value = str(get_commits_info(format_str)) + config_value = str(get_commit_info(format_str)) result = re.match(check['regex'], config_value) if result is None: print_error_message( diff --git a/commit_check/commit.py b/commit_check/commit.py index 6fb0010..30b4dfe 100644 --- a/commit_check/commit.py +++ b/commit_check/commit.py @@ -2,7 +2,7 @@ import re from pathlib import PurePath from commit_check import YELLOW, RESET_COLOR, PASS, FAIL -from commit_check.util import cmd_output, get_commits_info, print_error_message, print_suggestion +from commit_check.util import cmd_output, get_commit_info, print_error_message, print_suggestion def get_default_commit_msg_file() -> str: @@ -17,11 +17,12 @@ def read_commit_msg(commit_msg_file) -> str: with open(commit_msg_file, 'r') as f: return f.read() except FileNotFoundError: - return str(get_commits_info("s")) + # Commit message is composed by subject and body + return str(get_commit_info("s") + "\n\n" + get_commit_info("b")) -def check_commit_msg(checks: list, commit_msg_file: str) -> int: - if not commit_msg_file: +def check_commit_msg(checks: list, commit_msg_file: str = "") -> int: + if commit_msg_file is None or commit_msg_file == "": commit_msg_file = get_default_commit_msg_file() commit_msg = read_commit_msg(commit_msg_file) @@ -47,7 +48,10 @@ def check_commit_msg(checks: list, commit_msg_file: str) -> int: return PASS -def check_commit_signoff(checks: list) -> int: +def check_commit_signoff(checks: list, commit_msg_file: str = "") -> int: + if commit_msg_file is None or commit_msg_file == "": + commit_msg_file = get_default_commit_msg_file() + for check in checks: if check['check'] == 'commit_signoff': if check['regex'] == "": @@ -56,9 +60,9 @@ def check_commit_signoff(checks: list) -> int: ) return PASS - commit_msg = get_commits_info("s") - commit_hash = get_commits_info("H") - result = re.match(check['regex'], commit_msg) + commit_msg = read_commit_msg(commit_msg_file) + commit_hash = get_commit_info("H") + result = re.search(check['regex'], commit_msg) if result is None: print_error_message( check['check'], check['regex'], diff --git a/commit_check/util.py b/commit_check/util.py index 6c9bfd7..ef8f89a 100644 --- a/commit_check/util.py +++ b/commit_check/util.py @@ -29,7 +29,7 @@ def get_branch_name() -> str: return branch_name.strip() -def get_commits_info(format_string: str) -> str: +def get_commit_info(format_string: str, sha: str = "HEAD") -> str: """Get latest commits information :param format_string: could be - s - subject @@ -43,7 +43,7 @@ def get_commits_info(format_string: str) -> str: """ try: commands = [ - 'git', 'log', '-n', '1', f"--pretty=format:%{format_string}", + 'git', 'log', '-n', '1', f"--pretty=format:%{format_string}", f"{sha}", ] output = cmd_output(commands) except CalledProcessError: diff --git a/tests/author_test.py b/tests/author_test.py index 04df99c..21f637d 100644 --- a/tests/author_test.py +++ b/tests/author_test.py @@ -7,17 +7,17 @@ class TestAuthor: class TestAuthorName: - # used by get_commits_info mock + # used by get_commit_info mock fake_author_value_an = "fake_author_name" def test_check_author(self, mocker): - # Must call get_commits_info, re.match. + # Must call get_commit_info, re.match. checks = [{ "check": "author_name", "regex": "dummy_regex" }] - m_get_commits_info = mocker.patch( - f"{LOCATION}.get_commits_info", + m_get_commit_info = mocker.patch( + f"{LOCATION}.get_commit_info", return_value=self.fake_author_value_an ) m_re_match = mocker.patch( @@ -26,14 +26,14 @@ def test_check_author(self, mocker): ) retval = check_author(checks, "author_name") assert retval == PASS - assert m_get_commits_info.call_count == 1 + assert m_get_commit_info.call_count == 1 assert m_re_match.call_count == 1 def test_check_author_with_empty_checks(self, mocker): - # Must NOT call get_commits_info, re.match. with `checks` param with length 0. + # Must NOT call get_commit_info, re.match. with `checks` param with length 0. checks = [] - m_get_commits_info = mocker.patch( - f"{LOCATION}.get_commits_info", + m_get_commit_info = mocker.patch( + f"{LOCATION}.get_commit_info", return_value=self.fake_author_value_an ) m_re_match = mocker.patch( @@ -42,7 +42,7 @@ def test_check_author_with_empty_checks(self, mocker): ) retval = check_author(checks, "author_name") assert retval == PASS - assert m_get_commits_info.call_count == 0 + assert m_get_commit_info.call_count == 0 assert m_re_match.call_count == 0 def test_check_author_with_different_check(self, mocker): @@ -51,8 +51,8 @@ def test_check_author_with_different_check(self, mocker): "check": "message", "regex": "dummy_regex" }] - m_get_commits_info = mocker.patch( - f"{LOCATION}.get_commits_info", + m_get_commit_info = mocker.patch( + f"{LOCATION}.get_commit_info", return_value=self.fake_author_value_an ) m_re_match = mocker.patch( @@ -61,19 +61,19 @@ def test_check_author_with_different_check(self, mocker): ) retval = check_author(checks, "author_name") assert retval == PASS - assert m_get_commits_info.call_count == 0 + assert m_get_commit_info.call_count == 0 assert m_re_match.call_count == 0 def test_check_author_with_len0_regex(self, mocker, capfd): - # Must NOT call get_commits_info, re.match with `regex` with length 0. + # Must NOT call get_commit_info, re.match with `regex` with length 0. checks = [ { "check": "author_name", "regex": "" } ] - m_get_commits_info = mocker.patch( - f"{LOCATION}.get_commits_info", + m_get_commit_info = mocker.patch( + f"{LOCATION}.get_commit_info", return_value=self.fake_author_value_an ) m_re_match = mocker.patch( @@ -82,7 +82,7 @@ def test_check_author_with_len0_regex(self, mocker, capfd): ) retval = check_author(checks, "author_name") assert retval == PASS - assert m_get_commits_info.call_count == 0 + assert m_get_commit_info.call_count == 0 assert m_re_match.call_count == 0 out, _ = capfd.readouterr() assert "Not found regex for author_name." in out @@ -95,8 +95,8 @@ def test_check_author_with_result_none(self, mocker): "error": "error", "suggest": "suggest" }] - m_get_commits_info = mocker.patch( - f"{LOCATION}.get_commits_info", + m_get_commit_info = mocker.patch( + f"{LOCATION}.get_commit_info", return_value=self.fake_author_value_an ) m_re_match = mocker.patch( @@ -111,23 +111,23 @@ def test_check_author_with_result_none(self, mocker): ) retval = check_author(checks, "author_name") assert retval == FAIL - assert m_get_commits_info.call_count == 1 + assert m_get_commit_info.call_count == 1 assert m_re_match.call_count == 1 assert m_print_error_message.call_count == 1 assert m_print_suggestion.call_count == 1 class TestAuthorEmail: - # used by get_commits_info mock + # used by get_commit_info mock fake_author_value_ae = "fake_author_email" def test_check_author(self, mocker): - # Must call get_commits_info, re.match. + # Must call get_commit_info, re.match. checks = [{ "check": "author_email", "regex": "dummy_regex" }] - m_get_commits_info = mocker.patch( - f"{LOCATION}.get_commits_info", + m_get_commit_info = mocker.patch( + f"{LOCATION}.get_commit_info", return_value=self.fake_author_value_ae ) m_re_match = mocker.patch( @@ -136,14 +136,14 @@ def test_check_author(self, mocker): ) retval = check_author(checks, "author_email") assert retval == PASS - assert m_get_commits_info.call_count == 1 + assert m_get_commit_info.call_count == 1 assert m_re_match.call_count == 1 def test_check_author_with_empty_checks(self, mocker): - # Must NOT call get_commits_info, re.match. with `checks` param with length 0. + # Must NOT call get_commit_info, re.match. with `checks` param with length 0. checks = [] - m_get_commits_info = mocker.patch( - f"{LOCATION}.get_commits_info", + m_get_commit_info = mocker.patch( + f"{LOCATION}.get_commit_info", return_value=self.fake_author_value_ae ) m_re_match = mocker.patch( @@ -152,7 +152,7 @@ def test_check_author_with_empty_checks(self, mocker): ) retval = check_author(checks, "author_email") assert retval == PASS - assert m_get_commits_info.call_count == 0 + assert m_get_commit_info.call_count == 0 assert m_re_match.call_count == 0 def test_check_author_with_different_check(self, mocker): @@ -161,8 +161,8 @@ def test_check_author_with_different_check(self, mocker): "check": "message", "regex": "dummy_regex" }] - m_get_commits_info = mocker.patch( - f"{LOCATION}.get_commits_info", + m_get_commit_info = mocker.patch( + f"{LOCATION}.get_commit_info", return_value=self.fake_author_value_ae ) m_re_match = mocker.patch( @@ -171,19 +171,19 @@ def test_check_author_with_different_check(self, mocker): ) retval = check_author(checks, "author_email") assert retval == PASS - assert m_get_commits_info.call_count == 0 + assert m_get_commit_info.call_count == 0 assert m_re_match.call_count == 0 def test_check_author_with_len0_regex(self, mocker, capfd): - # Must NOT call get_commits_info, re.match with `regex` with length 0. + # Must NOT call get_commit_info, re.match with `regex` with length 0. checks = [ { "check": "author_email", "regex": "" } ] - m_get_commits_info = mocker.patch( - f"{LOCATION}.get_commits_info", + m_get_commit_info = mocker.patch( + f"{LOCATION}.get_commit_info", return_value=self.fake_author_value_ae ) m_re_match = mocker.patch( @@ -192,7 +192,7 @@ def test_check_author_with_len0_regex(self, mocker, capfd): ) retval = check_author(checks, "author_email") assert retval == PASS - assert m_get_commits_info.call_count == 0 + assert m_get_commit_info.call_count == 0 assert m_re_match.call_count == 0 out, _ = capfd.readouterr() assert "Not found regex for author_email." in out @@ -205,8 +205,8 @@ def test_check_author_with_result_none(self, mocker): "error": "error", "suggest": "suggest" }] - m_get_commits_info = mocker.patch( - f"{LOCATION}.get_commits_info", + m_get_commit_info = mocker.patch( + f"{LOCATION}.get_commit_info", return_value=self.fake_author_value_ae ) m_re_match = mocker.patch( @@ -221,7 +221,7 @@ def test_check_author_with_result_none(self, mocker): ) retval = check_author(checks, "author_email") assert retval == FAIL - assert m_get_commits_info.call_count == 1 + assert m_get_commit_info.call_count == 1 assert m_re_match.call_count == 1 assert m_print_error_message.call_count == 1 assert m_print_suggestion.call_count == 1 diff --git a/tests/commit_test.py b/tests/commit_test.py index 4666828..5642a52 100644 --- a/tests/commit_test.py +++ b/tests/commit_test.py @@ -1,7 +1,7 @@ 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 -# used by get_commits_info mock +# used by get_commit_info mock FAKE_BRANCH_NAME = "fake_commits_info" # The location of check_commit_msg() LOCATION = "commit_check.commit" @@ -25,7 +25,7 @@ def test_read_commit_msg_from_existing_file(tmp_path): def test_read_commit_msg_file_not_found(mocker): - m_commits_info = mocker.patch('commit_check.util.get_commits_info', return_value='mocked_commits_info') + m_commits_info = mocker.patch('commit_check.util.get_commit_info', return_value='mocked_commits_info') read_commit_msg("non_existent_file.txt") assert m_commits_info.call_count == 0 @@ -104,8 +104,8 @@ def test_check_commit_signoff(mocker): "error": "error", "suggest": "suggest" }] - m_re_match = mocker.patch( - "re.match", + m_re_search = mocker.patch( + "re.search", return_value=None ) m_print_error_message = mocker.patch( @@ -116,7 +116,7 @@ def test_check_commit_signoff(mocker): ) retval = check_commit_signoff(checks) assert retval == FAIL - assert m_re_match.call_count == 1 + assert m_re_search.call_count == 1 assert m_print_error_message.call_count == 1 assert m_print_suggestion.call_count == 1 diff --git a/tests/util_test.py b/tests/util_test.py index 1e3dd66..70ae5b7 100644 --- a/tests/util_test.py +++ b/tests/util_test.py @@ -1,6 +1,6 @@ import pytest from commit_check.util import get_branch_name -from commit_check.util import get_commits_info +from commit_check.util import get_commit_info from commit_check.util import cmd_output from commit_check.util import validate_config from commit_check.util import print_error_message @@ -49,20 +49,20 @@ class TestGetCommitInfo: ("ae"), ] ) - def test_get_commits_info(self, mocker, format_string): - # Must call get_commits_info with given argument. + def test_get_commit_info(self, mocker, format_string): + # Must call get_commit_info with given argument. m_cmd_output = mocker.patch( "commit_check.util.cmd_output", return_value=" fake commit message " ) - retval = get_commits_info(format_string) + retval = get_commit_info(format_string) assert m_cmd_output.call_count == 1 assert m_cmd_output.call_args[0][0] == [ - "git", "log", "-n", "1", f"--pretty=format:%{format_string}" + "git", "log", "-n", "1", f"--pretty=format:%{format_string}", "HEAD" ] assert retval == " fake commit message " - def test_get_commits_info_with_exception(self, mocker): + def test_get_commit_info_with_exception(self, mocker): # Must return empty string when exception raises in cmd_output. m_cmd_output = mocker.patch( "commit_check.util.cmd_output", @@ -75,10 +75,10 @@ def test_get_commits_info_with_exception(self, mocker): dummy_cmd_name ) format_string = "s" - retval = get_commits_info(format_string) + retval = get_commit_info(format_string) assert m_cmd_output.call_count == 1 assert m_cmd_output.call_args[0][0] == [ - "git", "log", "-n", "1", f"--pretty=format:%{format_string}" + "git", "log", "-n", "1", f"--pretty=format:%{format_string}", "HEAD" ] assert retval == ""