Add prepare-commit-msg hook

Added new hook for templating prepare-commit-msg hook. Added 2
templates to use keepgin `prepare_commit_msg_append.j2`as default.

The idea is that developers could create their own templates accordingly
to teams commit conventions and override them in the hook arguments when
configuring it.
This commit is contained in:
David Paz 2022-07-15 01:00:16 +02:00
parent 18c2496e4c
commit e01e1717b1
No known key found for this signature in database
GPG key ID: 6600CC47B1C1B063
8 changed files with 236 additions and 0 deletions

View file

@ -174,6 +174,12 @@
language: python
pass_filenames: false
always_run: true
- id: prepare-commit-msg
name: prepare commit message
description: Formatter Helper for your commit message
entry: prepare-commit-msg
language: python
stages: [prepare-commit-msg]
- id: requirements-txt-fixer
name: fix requirements.txt
description: sorts entries in requirements.txt.

View file

@ -0,0 +1,123 @@
from __future__ import annotations
import argparse
import re
from typing import Sequence
from jinja2 import Environment
from jinja2 import PackageLoader
from jinja2 import select_autoescape
from pre_commit_hooks.util import CalledProcessError
from pre_commit_hooks.util import cmd_output
def get_current_branch() -> str:
try:
ref_name = cmd_output('git', 'symbolic-ref', '--short', 'HEAD')
except CalledProcessError:
return ''
return ref_name.strip()
def _configure_args(
parser: argparse.ArgumentParser,
) -> argparse.ArgumentParser:
parser.add_argument(
'-t', '--template', default='prepare_commit_msg_append.j2',
help='Template to use for the commit message.',
)
parser.add_argument(
'-b', '--branch', action='append', default=['main', 'master'],
help='Branch to skip, may be specified multiple times.',
)
parser.add_argument(
'-p', '--pattern', action='append', default=['(?<=feature/).*'],
help='RegEx Pattern for recognising Ticket Numbers in branch, '
'may be specified multiple times.',
)
parser.add_argument('COMMIT_MSG_FILE', nargs=argparse.REMAINDER)
return parser
def get_jinja_env() -> Environment:
return Environment(
loader=PackageLoader('pre_commit_hooks'),
autoescape=select_autoescape(),
)
def get_rendered_template(
jinja: Environment,
template_file: str,
variables: dict[str, str],
) -> str:
template = jinja.get_template(template_file)
return template.render(variables)
def update_commit_file(
jinja: Environment,
commit_msg_file: str,
template: str,
ticket: str,
) -> int:
try:
with open(commit_msg_file) as f:
data = f.readlines()
data_as_str = ''.join([item for item in data])
# if message already contain ticket number means
# it is under git commit --amend or rebase or alike
# where message was already set in the past
if ticket in data_as_str:
return 0
variables = {
'ticket': ticket,
'content': data_as_str,
}
content = get_rendered_template(
jinja=jinja,
template_file=template,
variables=variables,
)
with open(commit_msg_file, 'w') as f:
f.write(content)
return 0
except OSError as err:
print(f'OS error: {err}')
return 1
def main(argv: Sequence[str] | None = None) -> int:
parser = _configure_args(argparse.ArgumentParser())
args = parser.parse_args(argv)
current = get_current_branch()
branches = frozenset(args.branch)
if current in branches:
# checked black listed branches
return 0
patterns = frozenset(args.pattern)
matches = [
match.group(0)
for match in (re.search(pattern, current) for pattern in patterns)
if match
]
if len(matches) == 0:
# checked white listed branches
return 0
jinja = get_jinja_env()
commit_file = args.COMMIT_MSG_FILE[0]
return update_commit_file(jinja, commit_file, args.template, matches[0])
if __name__ == '__main__':
raise SystemExit(main())

View file

@ -0,0 +1,3 @@
{{ content }}
Relates: {{ ticket }}

View file

@ -0,0 +1 @@
[{{ ticket }}] {{ content }}

View file

@ -1,3 +1,4 @@
covdefaults
coverage
Jinja2
pytest

View file

@ -23,6 +23,7 @@ classifiers =
[options]
packages = find:
install_requires =
Jinja2>=3.1.2
ruamel.yaml>=0.15
tomli>=1.1.0;python_version<"3.11"
python_requires = >=3.7
@ -62,6 +63,7 @@ console_scripts =
mixed-line-ending = pre_commit_hooks.mixed_line_ending:main
name-tests-test = pre_commit_hooks.tests_should_end_in_test:main
no-commit-to-branch = pre_commit_hooks.no_commit_to_branch:main
prepare-commit-msg = pre_commit_hooks.prepare_commit_msg:main
pre-commit-hooks-removed = pre_commit_hooks.removed:main
pretty-format-json = pre_commit_hooks.pretty_format_json:main
requirements-txt-fixer = pre_commit_hooks.requirements_txt_fixer:main

View file

@ -14,3 +14,8 @@ def get_resource_path(path):
def git_commit(*args, **kwargs):
cmd = ('git', 'commit', '--no-gpg-sign', '--no-verify', '--no-edit', *args)
subprocess.check_call(cmd, **kwargs)
def get_template_path(path):
parent_dir = os.path.abspath(os.path.join(TESTING_DIR, os.pardir))
return os.path.join(parent_dir, 'templates', path)

View file

@ -0,0 +1,95 @@
from __future__ import annotations
import pytest
from pre_commit_hooks.prepare_commit_msg import get_current_branch
from pre_commit_hooks.prepare_commit_msg import get_jinja_env
from pre_commit_hooks.prepare_commit_msg import main
from pre_commit_hooks.prepare_commit_msg import update_commit_file
from pre_commit_hooks.util import cmd_output
def test_current_branch(temp_git_dir):
with temp_git_dir.as_cwd():
cmd_output('git', 'checkout', '-b', 'feature')
assert get_current_branch() == 'feature'
cmd_output('git', 'checkout', '-b', 'feature/branch')
assert get_current_branch() == 'feature/branch'
# Input, expected value, branch, template
TESTS = (
(
b'',
b'[TT-01] ',
'feature/TT-01',
'prepare_commit_msg_prepend.j2',
),
(
b'[TT-02] Some message',
b'[TT-02] Some message',
'feature/TT-02',
'prepare_commit_msg_prepend.j2',
),
(
b'Initial message',
b'[TT-03] Initial message',
'feature/TT-03',
'prepare_commit_msg_prepend.j2',
),
(
b'',
b'\n\nRelates: AA-01',
'feature/AA-01',
'prepare_commit_msg_append.j2',
),
(
b'Initial message',
b'Initial message\n\nRelates: AA-02',
'feature/AA-02',
'prepare_commit_msg_append.j2',
),
)
@pytest.mark.parametrize(
('input_s', 'expected_val', 'branch_name', 'template'),
TESTS,
)
def test_update_commit_file(
input_s, expected_val, branch_name, template,
temp_git_dir,
):
with temp_git_dir.as_cwd():
path = temp_git_dir.join('COMMIT_EDITMSG')
path.write_binary(input_s)
ticket = branch_name.split('/')[1]
jinja = get_jinja_env()
update_commit_file(jinja, path, template, ticket)
assert path.read_binary() == expected_val
@pytest.mark.parametrize(
('input_s', 'expected_val', 'branch_name', 'template'),
TESTS,
)
def test_main(
input_s, expected_val, branch_name, template,
temp_git_dir,
):
with temp_git_dir.as_cwd():
path = temp_git_dir.join('file.txt')
path.write_binary(input_s)
assert path.read_binary() == input_s
cmd_output('git', 'checkout', '-b', branch_name)
assert main(
argv=[
'-t', template,
'-p', '(?<=feature/).*',
str(path),
],
) == 0
assert path.read_binary() == expected_val