Skip to content

Commit a8b19f3

Browse files
hjmjohnsonclaude
andcommitted
ENH: Add ITK commit message hooks (prepare-commit-msg and kw-commit-msg)
Add pre-commit hooks in Utilities/Hooks/ matching ITK's directory structure and commit message enforcement: - prepare-commit-msg: Inserts ITK prefix instructions into the commit message editor so developers see valid prefixes while composing. - kw-commit-msg.py: Validates commit messages with the same rules as upstream ITK: prefix check (BUG/COMP/DOC/ENH/PERF/STYLE/WIP), subject line max 78 chars, no leading/trailing whitespace, empty second line, and Merge/Revert exemptions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent b15a64e commit a8b19f3

3 files changed

Lines changed: 219 additions & 0 deletions

File tree

.pre-commit-config.yaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
repos:
2+
- repo: local
3+
hooks:
4+
- id: local-prepare-commit-msg
5+
name: 'local prepare-commit-msg'
6+
entry: 'Utilities/Hooks/prepare-commit-msg'
7+
language: system
8+
stages: [prepare-commit-msg]
9+
- id: kw-commit-msg
10+
name: 'kw commit-msg'
11+
entry: 'python3 Utilities/Hooks/kw-commit-msg.py'
12+
language: system
13+
stages: [commit-msg]

Utilities/Hooks/kw-commit-msg.py

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
#!/usr/bin/env python3
2+
# ==========================================================================
3+
#
4+
# Copyright NumFOCUS
5+
#
6+
# Licensed under the Apache License, Version 2.0 (the "License");
7+
# you may not use this file except in compliance with the License.
8+
# You may obtain a copy of the License at
9+
#
10+
# https://www.apache.org/licenses/LICENSE-2.0.txt
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS,
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
# See the License for the specific language governing permissions and
16+
# limitations under the License.
17+
#
18+
# ==========================================================================
19+
20+
import os
21+
import re
22+
import subprocess
23+
import sys
24+
25+
from pathlib import Path
26+
27+
28+
DEFAULT_LINE_LENGTH: int = 78
29+
30+
31+
def die(message, commit_msg_path):
32+
print("commit-msg hook failure", file=sys.stderr)
33+
print("-----------------------", file=sys.stderr)
34+
print(message, file=sys.stderr)
35+
print("-----------------------", file=sys.stderr)
36+
print(
37+
f"""
38+
To continue editing, run the command
39+
git commit -e -F "{commit_msg_path}"
40+
(assuming your working directory is at the top).""",
41+
file=sys.stderr,
42+
)
43+
sys.exit(1)
44+
45+
46+
def get_max_length():
47+
try:
48+
result = subprocess.run(
49+
["git", "config", "--get", "hooks.commit-msg.ITKCommitSubjectMaxLength"],
50+
capture_output=True,
51+
text=True,
52+
check=True,
53+
)
54+
return int(result.stdout.strip())
55+
except (subprocess.CalledProcessError, ValueError):
56+
return DEFAULT_LINE_LENGTH
57+
58+
59+
def main():
60+
git_dir_path: Path = Path(os.environ.get("GIT_DIR", ".git")).resolve()
61+
commit_msg_path: Path = git_dir_path / "COMMIT_MSG"
62+
63+
if len(sys.argv) < 2:
64+
die(f"Usage: {sys.argv[0]} <git_commit_message_file>", commit_msg_path)
65+
66+
input_file: Path = Path(sys.argv[1])
67+
if not input_file.exists():
68+
die(
69+
f"Missing input_file {sys.argv[1]} for {sys.argv[0]} processing",
70+
commit_msg_path,
71+
)
72+
max_subjectline_length: int = get_max_length()
73+
74+
original_input_file_lines: list[str] = []
75+
with open(input_file) as f_in:
76+
original_input_file_lines = f_in.readlines()
77+
78+
input_file_lines: list[str] = []
79+
for test_line in original_input_file_lines:
80+
test_line = test_line.strip()
81+
is_empty_line_before_subject: bool = (
82+
len(input_file_lines) == 0 and len(test_line) == 0
83+
)
84+
if test_line.startswith("#") or is_empty_line_before_subject:
85+
continue
86+
input_file_lines.append(f"{test_line}\n")
87+
88+
with open(commit_msg_path, "w") as f_out:
89+
f_out.writelines(input_file_lines)
90+
91+
subject_line: str = input_file_lines[0]
92+
93+
if len(subject_line) < 8:
94+
die(
95+
f"The first line must be at least 8 characters:\n--------\n{subject_line}\n--------",
96+
commit_msg_path,
97+
)
98+
if (
99+
len(subject_line) > max_subjectline_length
100+
and not subject_line.startswith("Merge ")
101+
and not subject_line.startswith("Revert ")
102+
):
103+
die(
104+
f"The first line may be at most {max_subjectline_length} characters:\n"
105+
+ "-" * max_subjectline_length
106+
+ f"\n{subject_line}\n"
107+
+ "-" * max_subjectline_length,
108+
commit_msg_path,
109+
)
110+
if re.match(r"^[ \t]|[ \t]$", subject_line):
111+
die(
112+
f"The first line may not have leading or trailing space:\n[{subject_line}]",
113+
commit_msg_path,
114+
)
115+
if not re.match(
116+
r"^(Merge|Revert|BUG:|COMP:|DOC:|ENH:|PERF:|STYLE:|WIP:)\s", subject_line
117+
):
118+
die(
119+
f"""Start ITK commit messages with a standard prefix (and a space):
120+
BUG: - fix for runtime crash or incorrect result
121+
COMP: - compiler error or warning fix
122+
DOC: - documentation change
123+
ENH: - new functionality
124+
PERF: - performance improvement
125+
STYLE: - no logic impact (indentation, comments)
126+
WIP: - Work In Progress not ready for merge
127+
To reference GitHub issue XXXX, add "Issue #XXXX" to the commit message.
128+
If the issue addresses an open issue, add "Closes #XXXX" to the message.""",
129+
commit_msg_path,
130+
)
131+
if re.match(r"^BUG: [0-9]+\.", subject_line):
132+
die(
133+
f'Do not put a "." after the bug number:\n\n {subject_line}',
134+
commit_msg_path,
135+
)
136+
del subject_line
137+
138+
if len(input_file_lines) > 1:
139+
second_line: str = input_file_lines[
140+
1
141+
].strip() # Remove whitespace at beginning and end
142+
if len(second_line) == 0:
143+
input_file_lines[1] = "\n" # Replace line with only newline
144+
else:
145+
die(
146+
f'The second line of the commit message must be empty:\n"{second_line}" with length {len(second_line)}',
147+
commit_msg_path,
148+
)
149+
del second_line
150+
151+
152+
if __name__ == "__main__":
153+
main()

Utilities/Hooks/prepare-commit-msg

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
#!/usr/bin/env bash
2+
#==========================================================================
3+
#
4+
# Copyright NumFOCUS
5+
#
6+
# Licensed under the Apache License, Version 2.0 (the "License");
7+
# you may not use this file except in compliance with the License.
8+
# You may obtain a copy of the License at
9+
#
10+
# https://www.apache.org/licenses/LICENSE-2.0.txt
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS,
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
# See the License for the specific language governing permissions and
16+
# limitations under the License.
17+
#
18+
#==========================================================================
19+
20+
egrep-q() {
21+
egrep "$@" >/dev/null 2>/dev/null
22+
}
23+
24+
# First argument is file containing commit message.
25+
commit_msg="$1"
26+
27+
# Check for our extra instructions.
28+
egrep-q "^# Start ITK commit messages" -- "$commit_msg" && return 0
29+
30+
# Insert our extra instructions.
31+
commit_msg_tmp="$commit_msg.$$"
32+
instructions='#\
33+
# Start ITK commit messages with a standard prefix (and a space):\
34+
# BUG: - fix for runtime crash or incorrect result\
35+
# COMP: - compiler error or warning fix\
36+
# DOC: - documentation change\
37+
# ENH: - new functionality\
38+
# PERF: - performance improvement\
39+
# STYLE: - no logic impact (indentation, comments)\
40+
# WIP: - Work In Progress not ready for merge\
41+
#\
42+
# The first line of the commit message should preferably be 72 characters\
43+
# or less; the maximum allowed is 78 characters.\
44+
#\
45+
# Follow the first line commit summary with an empty line, then a detailed\
46+
# description in one or more paragraphs.\
47+
#' &&
48+
sed '/^# On branch.*$/ a\
49+
'"$instructions"'
50+
/^# Not currently on any branch.*$/ a\
51+
'"$instructions"'
52+
' "$commit_msg" > "$commit_msg_tmp" &&
53+
mv "$commit_msg_tmp" "$commit_msg"

0 commit comments

Comments
 (0)