Skip to content

Commit e60f541

Browse files
committed
Adds support for prepare-commit-msg hooks
Adds a prepare-commit-msg hook stage which allows for hooks which add dynamic suggested/placeholder text to commit messages that an author can use as a starting point for writing a commit message
1 parent 809b748 commit e60f541

File tree

7 files changed

+142
-6
lines changed

7 files changed

+142
-6
lines changed

pre_commit/commands/run.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ def _compute_cols(hooks, verbose):
190190
def _all_filenames(args):
191191
if args.origin and args.source:
192192
return git.get_changed_files(args.origin, args.source)
193-
elif args.hook_stage == 'commit-msg':
193+
elif args.hook_stage in ['prepare-commit-msg', 'commit-msg']:
194194
return (args.commit_msg_filename,)
195195
elif args.files:
196196
return args.files

pre_commit/constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,6 @@
2121
VERSION = importlib_metadata.version('pre_commit')
2222

2323
# `manual` is not invoked by any installed git hook. See #719
24-
STAGES = ('commit', 'commit-msg', 'manual', 'push')
24+
STAGES = ('commit', 'prepare-commit-msg', 'commit-msg', 'manual', 'push')
2525

2626
DEFAULT = 'default'

pre_commit/main.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,9 @@ def _add_config_option(parser):
5252

5353
def _add_hook_type_option(parser):
5454
parser.add_argument(
55-
'-t', '--hook-type', choices=('pre-commit', 'pre-push', 'commit-msg'),
55+
'-t', '--hook-type', choices=(
56+
'pre-commit', 'pre-push', 'prepare-commit-msg', 'commit-msg',
57+
),
5658
default='pre-commit',
5759
)
5860

pre_commit/resources/hook-tmpl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ def _pre_push(stdin):
161161

162162
def _opts(stdin):
163163
fns = {
164+
'prepare-commit-msg': lambda _: ('--commit-msg-filename', sys.argv[1]),
164165
'commit-msg': lambda _: ('--commit-msg-filename', sys.argv[1]),
165166
'pre-commit': lambda _: (),
166167
'pre-push': _pre_push,

tests/commands/install_uninstall_test.py

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -655,7 +655,65 @@ def test_commit_msg_legacy(commit_msg_repo, tempdir_factory, store):
655655
assert second_line.startswith('Must have "Signed off by:"...')
656656

657657

658-
def test_install_disallow_mising_config(tempdir_factory, store):
658+
def test_prepare_commit_msg_integration_failing(
659+
failing_prepare_commit_msg_repo, tempdir_factory, store,
660+
):
661+
install(C.CONFIG_FILE, store, hook_type='prepare-commit-msg')
662+
retc, out = _get_commit_output(tempdir_factory)
663+
assert retc == 1
664+
assert out.startswith('Add "Signed off by:"...')
665+
assert out.strip().endswith('...Failed')
666+
667+
668+
def test_prepare_commit_msg_integration_passing(
669+
prepare_commit_msg_repo, tempdir_factory, store,
670+
):
671+
install(C.CONFIG_FILE, store, hook_type='prepare-commit-msg')
672+
msg = 'Hi'
673+
retc, out = _get_commit_output(tempdir_factory, msg=msg)
674+
assert retc == 0
675+
first_line = out.splitlines()[0]
676+
assert first_line.startswith('Add "Signed off by:"...')
677+
assert first_line.endswith('...Passed')
678+
commit_msg_path = os.path.join(
679+
prepare_commit_msg_repo, '.git/COMMIT_EDITMSG',
680+
)
681+
with io.open(commit_msg_path, 'rt') as f:
682+
assert 'Signed off by: ' in f.read()
683+
684+
685+
def test_prepare_commit_msg_legacy(
686+
prepare_commit_msg_repo, tempdir_factory, store,
687+
):
688+
hook_path = os.path.join(
689+
prepare_commit_msg_repo, '.git/hooks/prepare-commit-msg',
690+
)
691+
mkdirp(os.path.dirname(hook_path))
692+
with io.open(hook_path, 'w') as hook_file:
693+
hook_file.write(
694+
'#!/usr/bin/env bash\n'
695+
'set -eu\n'
696+
'test -e "$1"\n'
697+
'echo legacy\n',
698+
)
699+
make_executable(hook_path)
700+
701+
install(C.CONFIG_FILE, store, hook_type='prepare-commit-msg')
702+
703+
msg = 'Hi'
704+
retc, out = _get_commit_output(tempdir_factory, msg=msg)
705+
assert retc == 0
706+
first_line, second_line = out.splitlines()[:2]
707+
assert first_line == 'legacy'
708+
assert second_line.startswith('Add "Signed off by:"...')
709+
commit_msg_path = os.path.join(
710+
prepare_commit_msg_repo, '.git/COMMIT_EDITMSG',
711+
)
712+
with io.open(commit_msg_path, 'rt') as f:
713+
assert 'Signed off by: ' in f.read()
714+
715+
716+
def test_install_disallow_missing_config(tempdir_factory, store):
659717
path = make_consuming_repo(tempdir_factory, 'script_hooks_repo')
660718
with cwd(path):
661719
remove_config_from_repo(path)
@@ -668,7 +726,7 @@ def test_install_disallow_mising_config(tempdir_factory, store):
668726
assert ret == 1
669727

670728

671-
def test_install_allow_mising_config(tempdir_factory, store):
729+
def test_install_allow_missing_config(tempdir_factory, store):
672730
path = make_consuming_repo(tempdir_factory, 'script_hooks_repo')
673731
with cwd(path):
674732
remove_config_from_repo(path)

tests/commands/run_test.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -557,7 +557,12 @@ def test_stages(cap_out, store, repo_with_passing_hook):
557557
'language': 'pygrep',
558558
'stages': [stage],
559559
}
560-
for i, stage in enumerate(('commit', 'push', 'manual'), 1)
560+
for i, stage in enumerate(
561+
(
562+
'commit', 'push', 'manual', 'prepare-commit-msg',
563+
'commit-msg',
564+
), 1,
565+
)
561566
],
562567
}
563568
add_config_to_repo(repo_with_passing_hook, config)
@@ -575,6 +580,8 @@ def _run_for_stage(stage):
575580
assert _run_for_stage('commit').startswith(b'hook 1...')
576581
assert _run_for_stage('push').startswith(b'hook 2...')
577582
assert _run_for_stage('manual').startswith(b'hook 3...')
583+
assert _run_for_stage('prepare-commit-msg').startswith(b'hook 4...')
584+
assert _run_for_stage('commit-msg').startswith(b'hook 5...')
578585

579586

580587
def test_commit_msg_hook(cap_out, store, commit_msg_repo):
@@ -593,6 +600,25 @@ def test_commit_msg_hook(cap_out, store, commit_msg_repo):
593600
)
594601

595602

603+
def test_prepare_commit_msg_hook(cap_out, store, prepare_commit_msg_repo):
604+
filename = '.git/COMMIT_EDITMSG'
605+
with io.open(filename, 'w') as f:
606+
f.write('This is the commit message')
607+
608+
_test_run(
609+
cap_out,
610+
store,
611+
prepare_commit_msg_repo,
612+
{'hook_stage': 'prepare-commit-msg', 'commit_msg_filename': filename},
613+
expected_outputs=[b'Add "Signed off by:"', b'Passed'],
614+
expected_ret=0,
615+
stage=False,
616+
)
617+
618+
with io.open(filename, 'rt') as f:
619+
assert 'Signed off by: ' in f.read()
620+
621+
596622
def test_local_hook_passes(cap_out, store, repo_with_passing_hook):
597623
config = {
598624
'repo': 'local',

tests/conftest.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from pre_commit.logging_handler import logging_handler
1515
from pre_commit.store import Store
1616
from pre_commit.util import cmd_output
17+
from pre_commit.util import make_executable
1718
from testing.fixtures import git_dir
1819
from testing.fixtures import make_consuming_repo
1920
from testing.fixtures import write_config
@@ -134,6 +135,54 @@ def commit_msg_repo(tempdir_factory):
134135
yield path
135136

136137

138+
@pytest.fixture
139+
def prepare_commit_msg_repo(tempdir_factory):
140+
path = git_dir(tempdir_factory)
141+
script_name = 'add_sign_off.sh'
142+
config = {
143+
'repo': 'local',
144+
'hooks': [{
145+
'id': 'add-signoff',
146+
'name': 'Add "Signed off by:"',
147+
'entry': './{}'.format(script_name),
148+
'language': 'script',
149+
'stages': ['prepare-commit-msg'],
150+
}],
151+
}
152+
write_config(path, config)
153+
with cwd(path):
154+
with io.open(script_name, 'w') as script_file:
155+
script_file.write(
156+
'#!/usr/bin/env bash\n'
157+
'set -eu\n'
158+
'echo "\nSigned off by: " >> "$1"\n',
159+
)
160+
make_executable(script_name)
161+
cmd_output('git', 'add', '.')
162+
git_commit(msg=prepare_commit_msg_repo.__name__)
163+
yield path
164+
165+
166+
@pytest.fixture
167+
def failing_prepare_commit_msg_repo(tempdir_factory):
168+
path = git_dir(tempdir_factory)
169+
config = {
170+
'repo': 'local',
171+
'hooks': [{
172+
'id': 'add-signoff',
173+
'name': 'Add "Signed off by:"',
174+
'entry': '/usr/bin/env bash -c "exit 1"',
175+
'language': 'system',
176+
'stages': ['prepare-commit-msg'],
177+
}],
178+
}
179+
write_config(path, config)
180+
with cwd(path):
181+
cmd_output('git', 'add', '.')
182+
git_commit(msg=failing_prepare_commit_msg_repo.__name__)
183+
yield path
184+
185+
137186
@pytest.fixture(autouse=True, scope='session')
138187
def dont_write_to_home_directory():
139188
"""pre_commit.store.Store will by default write to the home directory

0 commit comments

Comments
 (0)