Skip to content

Commit d0b268c

Browse files
committed
Add support for commit-msg git hook
1 parent a6a4762 commit d0b268c

9 files changed

Lines changed: 107 additions & 57 deletions

File tree

pre_commit/commands/install_uninstall.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,16 +56,21 @@ def install(
5656

5757
with io.open(hook_path, 'w') as pre_commit_file_obj:
5858
if hook_type == 'pre-push':
59-
with io.open(resource_filename('pre-push-tmpl')) as fp:
60-
pre_push_contents = fp.read()
59+
with io.open(resource_filename('pre-push-tmpl')) as f:
60+
hook_specific_contents = f.read()
61+
elif hook_type == 'commit-msg':
62+
with io.open(resource_filename('commit-msg-tmpl')) as f:
63+
hook_specific_contents = f.read()
64+
elif hook_type == 'pre-commit':
65+
hook_specific_contents = ''
6166
else:
62-
pre_push_contents = ''
67+
raise AssertionError('Unknown hook type: {}'.format(hook_type))
6368

6469
skip_on_missing_conf = 'true' if skip_on_missing_conf else 'false'
6570
contents = io.open(resource_filename('hook-tmpl')).read().format(
6671
sys_executable=sys.executable,
6772
hook_type=hook_type,
68-
pre_push=pre_push_contents,
73+
hook_specific=hook_specific_contents,
6974
skip_on_missing_conf=skip_on_missing_conf,
7075
)
7176
pre_commit_file_obj.write(contents)

pre_commit/commands/run.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ def get_filenames(args, include_expr, exclude_expr):
5858
getter = git.get_files_matching(
5959
lambda: get_changed_files(args.origin, args.source),
6060
)
61+
elif args.hook_stage == 'commit-msg':
62+
def getter(*_):
63+
return (args.commit_msg_filename,)
6164
elif args.files:
6265
getter = git.get_files_matching(lambda: args.files)
6366
elif args.all_files:

pre_commit/main.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ def main(argv=None):
7676
),
7777
)
7878
install_parser.add_argument(
79-
'-t', '--hook-type', choices=('pre-commit', 'pre-push'),
79+
'-t', '--hook-type', choices=('pre-commit', 'pre-push', 'commit-msg'),
8080
default='pre-commit',
8181
)
8282
install_parser.add_argument(
@@ -149,6 +149,10 @@ def main(argv=None):
149149
'--source', '-s',
150150
help="The remote branch's commit_id when using `git push`.",
151151
)
152+
run_parser.add_argument(
153+
'--commit-msg-filename',
154+
help='Filename to check when running during `commit-msg`',
155+
)
152156
run_parser.add_argument(
153157
'--allow-unstaged-config', default=False, action='store_true',
154158
help=(
@@ -157,7 +161,8 @@ def main(argv=None):
157161
),
158162
)
159163
run_parser.add_argument(
160-
'--hook-stage', choices=('commit', 'push'), default='commit',
164+
'--hook-stage', choices=('commit', 'push', 'commit-msg'),
165+
default='commit',
161166
help='The stage during which the hook is fired e.g. commit or push.',
162167
)
163168
run_parser.add_argument(
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
args="run --hook-stage=commit-msg --commit-msg-filename=$1"

pre_commit/resources/hook-tmpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ if [ ! -f $CONF_FILE ]; then
5252
fi
5353
fi
5454

55-
{pre_push}
55+
{hook_specific}
5656

5757
# Run pre-commit
5858
if ((WHICH_RETV == 0)); then

testing/fixtures.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,7 @@
2323

2424
def git_dir(tempdir_factory):
2525
path = tempdir_factory.get()
26-
with cwd(path):
27-
cmd_output('git', 'init')
26+
cmd_output('git', 'init', path)
2827
return path
2928

3029

tests/commands/install_uninstall_test.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ def test_install_pre_commit(tempdir_factory):
5656
expected_contents = io.open(pre_commit_script).read().format(
5757
sys_executable=sys.executable,
5858
hook_type='pre-commit',
59-
pre_push='',
59+
hook_specific='',
6060
skip_on_missing_conf='false',
6161
)
6262
assert pre_commit_contents == expected_contents
@@ -71,7 +71,7 @@ def test_install_pre_commit(tempdir_factory):
7171
expected_contents = io.open(pre_commit_script).read().format(
7272
sys_executable=sys.executable,
7373
hook_type='pre-push',
74-
pre_push=pre_push_template_contents,
74+
hook_specific=pre_push_template_contents,
7575
skip_on_missing_conf='false',
7676
)
7777
assert pre_push_contents == expected_contents
@@ -118,10 +118,11 @@ def test_uninstall(tempdir_factory):
118118

119119

120120
def _get_commit_output(tempdir_factory, touch_file='foo', **kwargs):
121+
commit_msg = kwargs.pop('commit_msg', 'Commit!')
121122
open(touch_file, 'a').close()
122123
cmd_output('git', 'add', touch_file)
123124
return cmd_output_mocked_pre_commit_home(
124-
'git', 'commit', '-am', 'Commit!', '--allow-empty',
125+
'git', 'commit', '-am', commit_msg, '--allow-empty',
125126
# git commit puts pre-commit to stderr
126127
stderr=subprocess.STDOUT,
127128
retcode=None,
@@ -560,6 +561,24 @@ def test_pre_push_integration_empty_push(tempdir_factory):
560561
assert retc == 0
561562

562563

564+
def test_commit_msg_integration_failing(commit_msg_repo, tempdir_factory):
565+
install(Runner(commit_msg_repo, C.CONFIG_FILE), hook_type='commit-msg')
566+
retc, out = _get_commit_output(tempdir_factory)
567+
assert retc == 1
568+
assert out.startswith('Must have "Signed off by:"...')
569+
assert out.strip().endswith('...Failed')
570+
571+
572+
def test_commit_msg_integration_passing(commit_msg_repo, tempdir_factory):
573+
install(Runner(commit_msg_repo, C.CONFIG_FILE), hook_type='commit-msg')
574+
msg = 'Hi\nSigned off by: me, lol'
575+
retc, out = _get_commit_output(tempdir_factory, commit_msg=msg)
576+
assert retc == 0
577+
first_line = out.splitlines()[0]
578+
assert first_line.startswith('Must have "Signed off by:"...')
579+
assert first_line.endswith('...Passed')
580+
581+
563582
def test_install_disallow_mising_config(tempdir_factory):
564583
path = make_consuming_repo(tempdir_factory, 'script_hooks_repo')
565584
with cwd(path):

tests/commands/run_test.py

Lines changed: 38 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ def _get_opts(
6060
allow_unstaged_config=False,
6161
hook_stage='commit',
6262
show_diff_on_failure=False,
63+
commit_msg_filename='',
6364
):
6465
# These are mutually exclusive
6566
assert not (all_files and files)
@@ -75,6 +76,7 @@ def _get_opts(
7576
allow_unstaged_config=allow_unstaged_config,
7677
hook_stage=hook_stage,
7778
show_diff_on_failure=show_diff_on_failure,
79+
commit_msg_filename=commit_msg_filename,
7880
)
7981

8082

@@ -572,78 +574,69 @@ def test_lots_of_files(mock_out_store_directory, tempdir_factory):
572574
)
573575

574576

575-
@pytest.mark.parametrize(
576-
(
577-
'hook_stage', 'stage_for_first_hook', 'stage_for_second_hook',
578-
'expected_output',
579-
),
580-
(
581-
('push', ['commit'], ['commit'], [b'', b'']),
582-
(
583-
'push', ['commit', 'push'], ['commit', 'push'],
584-
[b'hook 1', b'hook 2'],
585-
),
586-
('push', [], [], [b'hook 1', b'hook 2']),
587-
('push', [], ['commit'], [b'hook 1', b'']),
588-
('push', ['push'], ['commit'], [b'hook 1', b'']),
589-
('push', ['commit'], ['push'], [b'', b'hook 2']),
590-
(
591-
'commit', ['commit', 'push'], ['commit', 'push'],
592-
[b'hook 1', b'hook 2'],
593-
),
594-
('commit', ['commit'], ['commit'], [b'hook 1', b'hook 2']),
595-
('commit', [], [], [b'hook 1', b'hook 2']),
596-
('commit', [], ['commit'], [b'hook 1', b'hook 2']),
597-
('commit', ['push'], ['commit'], [b'', b'hook 2']),
598-
('commit', ['commit'], ['push'], [b'hook 1', b'']),
599-
),
600-
)
601-
def test_local_hook_for_stages(
602-
cap_out,
603-
repo_with_passing_hook, mock_out_store_directory,
604-
stage_for_first_hook,
605-
stage_for_second_hook,
606-
hook_stage,
607-
expected_output,
608-
):
577+
def test_push_hook(cap_out, repo_with_passing_hook, mock_out_store_directory):
609578
config = OrderedDict((
610579
('repo', 'local'),
611580
(
612581
'hooks', (
613582
OrderedDict((
614583
('id', 'flake8'),
615584
('name', 'hook 1'),
616-
('entry', 'python -m flake8.__main__'),
585+
('entry', "'{}' -m flake8".format(sys.executable)),
617586
('language', 'system'),
618-
('files', r'\.py$'),
619-
('stages', stage_for_first_hook),
620-
)), OrderedDict((
587+
('types', ['python']),
588+
('stages', ['commit']),
589+
)),
590+
OrderedDict((
621591
('id', 'do_not_commit'),
622592
('name', 'hook 2'),
623593
('entry', 'DO NOT COMMIT'),
624594
('language', 'pcre'),
625-
('files', '^(.*)$'),
626-
('stages', stage_for_second_hook),
595+
('types', ['text']),
596+
('stages', ['push']),
627597
)),
628598
),
629599
),
630600
))
631601
add_config_to_repo(repo_with_passing_hook, config)
632602

633-
with io.open('dummy.py', 'w') as staged_file:
634-
staged_file.write('"""TODO: something"""\n')
603+
open('dummy.py', 'a').close()
635604
cmd_output('git', 'add', 'dummy.py')
636605

637606
_test_run(
638607
cap_out,
639608
repo_with_passing_hook,
640-
{'hook_stage': hook_stage},
641-
expected_outputs=expected_output,
609+
{'hook_stage': 'commit'},
610+
expected_outputs=[b'hook 1'],
611+
expected_ret=0,
612+
stage=False,
613+
)
614+
615+
_test_run(
616+
cap_out,
617+
repo_with_passing_hook,
618+
{'hook_stage': 'push'},
619+
expected_outputs=[b'hook 2'],
642620
expected_ret=0,
643621
stage=False,
644622
)
645623

646624

625+
def test_commit_msg_hook(cap_out, commit_msg_repo, mock_out_store_directory):
626+
filename = '.git/COMMIT_EDITMSG'
627+
with io.open(filename, 'w') as f:
628+
f.write('This is the commit message')
629+
630+
_test_run(
631+
cap_out,
632+
commit_msg_repo,
633+
{'hook_stage': 'commit-msg', 'commit_msg_filename': filename},
634+
expected_outputs=[b'Must have "Signed off by:"', b'Failed'],
635+
expected_ret=1,
636+
stage=False,
637+
)
638+
639+
647640
def test_local_hook_passes(
648641
cap_out, repo_with_passing_hook, mock_out_store_directory,
649642
):
@@ -654,7 +647,7 @@ def test_local_hook_passes(
654647
OrderedDict((
655648
('id', 'flake8'),
656649
('name', 'flake8'),
657-
('entry', 'python -m flake8.__main__'),
650+
('entry', "'{}' -m flake8".format(sys.executable)),
658651
('language', 'system'),
659652
('files', r'\.py$'),
660653
)), OrderedDict((

tests/conftest.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import absolute_import
22
from __future__ import unicode_literals
33

4+
import collections
45
import functools
56
import io
67
import logging
@@ -20,6 +21,7 @@
2021
from pre_commit.util import cwd
2122
from testing.fixtures import git_dir
2223
from testing.fixtures import make_consuming_repo
24+
from testing.fixtures import write_config
2325

2426

2527
@pytest.yield_fixture
@@ -92,6 +94,29 @@ def in_conflicting_submodule(tempdir_factory):
9294
yield
9395

9496

97+
@pytest.fixture
98+
def commit_msg_repo(tempdir_factory):
99+
path = git_dir(tempdir_factory)
100+
config = collections.OrderedDict((
101+
('repo', 'local'),
102+
(
103+
'hooks',
104+
[collections.OrderedDict((
105+
('id', 'must-have-signoff'),
106+
('name', 'Must have "Signed off by:"'),
107+
('entry', 'grep -q "Signed off by:"'),
108+
('language', 'system'),
109+
('stages', ['commit-msg']),
110+
))],
111+
),
112+
))
113+
write_config(path, config)
114+
with cwd(path):
115+
cmd_output('git', 'add', '.')
116+
cmd_output('git', 'commit', '-m', 'add hooks')
117+
yield path
118+
119+
95120
@pytest.yield_fixture(autouse=True, scope='session')
96121
def dont_write_to_home_directory():
97122
"""pre_commit.store.Store will by default write to the home directory

0 commit comments

Comments
 (0)