1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
|
"""Tests for hooks/git-commit-confirm.py — file-backed commit messages."""
from conftest import load_hook
hook = load_hook("git-commit-confirm.py")
# --- existing forms still parse (regression guard) -------------------------
def test_extract_dash_m_simple():
msg = hook.extract_commit_message('git commit -m "fix: tidy parser"')
assert msg == "fix: tidy parser"
def test_extract_heredoc():
cmd = (
"git commit -m \"$(cat <<'EOF'\n"
"feat: add thing\n"
"\n"
"body line\n"
"EOF\n"
")\""
)
msg = hook.extract_commit_message(cmd)
assert msg.startswith("feat: add thing")
assert "body line" in msg
def test_extract_unparseable_falls_through():
# Bare `git commit` would drop into $EDITOR.
assert hook.extract_commit_message("git commit") == hook.UNPARSEABLE_MESSAGE
# --- new: -F / --file / --file= forms read the file ------------------------
def test_extract_dash_F_reads_file(tmp_path):
f = tmp_path / "msg.txt"
f.write_text("fix: from a file\n\nsome body\n")
assert hook.extract_commit_message(f"git commit -F {f}") == "fix: from a file\n\nsome body"
def test_extract_long_file_flag_reads_file(tmp_path):
f = tmp_path / "msg.txt"
f.write_text("docs: long form file flag\n")
assert hook.extract_commit_message(f"git commit --file {f}") == "docs: long form file flag"
def test_extract_file_equals_form_reads_file(tmp_path):
f = tmp_path / "msg.txt"
f.write_text("chore: equals form\n")
assert hook.extract_commit_message(f"git commit --file={f}") == "chore: equals form"
def test_extract_F_strips_quotes_around_path(tmp_path):
f = tmp_path / "my msg.txt"
f.write_text("feat: quoted path\n")
assert hook.extract_commit_message(f'git commit -F "{f}"') == "feat: quoted path"
# --- the audit-item bug: attribution in a file-backed message is now caught -
def test_file_backed_attribution_is_caught(tmp_path):
f = tmp_path / "msg.txt"
f.write_text("feat: add widget\n\nCo-Authored-By: Claude <noreply@anthropic.com>\n")
msg = hook.extract_commit_message(f"git commit -F {f}")
issues = hook.collect_issues(msg, staged=["a.py"], author="Real Dev <dev@example.com>")
assert any(i.startswith("AI-attribution") for i in issues)
def test_inline_message_without_attribution_is_clean(tmp_path):
# Sanity: a clean file-backed message produces no attribution issue.
f = tmp_path / "msg.txt"
f.write_text("fix: handle empty input\n")
msg = hook.extract_commit_message(f"git commit -F {f}")
issues = hook.collect_issues(msg, staged=["a.py"], author="Real Dev <dev@example.com>")
assert not any(i.startswith("AI-attribution") for i in issues)
# --- unreadable file falls through to UNPARSEABLE (fail-safe: ask) ---------
def test_missing_file_falls_through_to_unparseable(tmp_path):
missing = tmp_path / "nope.txt"
assert hook.extract_commit_message(f"git commit -F {missing}") == hook.UNPARSEABLE_MESSAGE
def test_oversized_file_falls_through_and_hook_asks(tmp_path, monkeypatch):
f = tmp_path / "big.txt"
f.write_text("x" * 5000)
# Force the read to refuse via a tiny limit (simulates oversize).
monkeypatch.setattr(
hook, "read_referenced_file", lambda p, max_bytes=10: None
)
msg = hook.extract_commit_message(f"git commit -F {f}")
assert msg == hook.UNPARSEABLE_MESSAGE
# And the hook would ask, because UNPARSEABLE_MESSAGE is a flagged issue.
issues = hook.collect_issues(msg, staged=["a.py"], author="Dev <d@e.com>")
assert any("not parseable" in i for i in issues)
# --- bundled test-run + commit: the hard gate ------------------------------
def test_bundle_semicolon_make_test_is_flagged():
assert hook.detect_bundled_test_run('make test; git commit -m "x"')
def test_bundle_ampersand_gated_is_allowed():
# `&&` runs the commit only on a green suite — safe, not flagged.
assert hook.detect_bundled_test_run('make test && git commit -m "x"') is None
def test_bundle_pytest_semicolon_is_flagged():
assert hook.detect_bundled_test_run('pytest ; git commit -m "x"')
def test_bundle_npm_test_is_flagged():
assert hook.detect_bundled_test_run('npm test; git commit -m "x"')
def test_bundle_go_test_is_flagged():
assert hook.detect_bundled_test_run('go test ./...; git commit -m "x"')
def test_bundle_cargo_test_is_flagged():
assert hook.detect_bundled_test_run('cargo test ; git commit -m "x"')
def test_bundle_bats_is_flagged():
assert hook.detect_bundled_test_run('bats tests/ ; git commit -m "x"')
def test_bundle_pipe_masks_exit_is_flagged():
# `make test | tee log` exits with tee's status, so && gates on tee, not
# the suite — a red suite would still commit. Flag it.
assert hook.detect_bundled_test_run('make test | tee log && git commit -m "x"')
def test_bundle_or_connector_is_flagged():
assert hook.detect_bundled_test_run('make test || git commit -m "x"')
def test_runner_only_in_message_is_not_flagged():
# "make test" inside the commit message must not trip the detector.
assert hook.detect_bundled_test_run('git commit -m "remember to make test"') is None
def test_plain_commit_is_not_flagged():
assert hook.detect_bundled_test_run('git commit -m "fix: thing"') is None
def test_gated_chain_before_commit_is_allowed():
assert hook.detect_bundled_test_run('cd proj && pytest && git commit -m "x"') is None
def test_empty_command_is_not_flagged():
assert hook.detect_bundled_test_run("") is None
|