diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cbdb2725712..c627c589dad 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -102,7 +102,7 @@ repos: stages: [pre-commit] - id: kw-commit-msg name: 'kw commit-msg' - entry: 'Utilities/Hooks/kw-commit-msg' + entry: 'Utilities/Hooks/kw-commit-msg.py' language: system stages: [commit-msg] exclude: "\\/ThirdParty\\/" diff --git a/Utilities/Hooks/kw-commit-msg b/Utilities/Hooks/kw-commit-msg deleted file mode 100755 index 723763d35f5..00000000000 --- a/Utilities/Hooks/kw-commit-msg +++ /dev/null @@ -1,123 +0,0 @@ -#!/usr/bin/env bash -#============================================================================= -# Copyright 2010-2012 Kitware, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -#============================================================================= - -# Ensure that the first arguement is a valid file for processing -if [ ! -f "$1" ]; then - echo "Missing input commit message file for the kw-commit-msg processing" - exit 1 -fi - -# Prepare a copy of the message: -# - strip comment lines -# - stop at "diff --git" (git commit -v) -commit_msg="$GIT_DIR/COMMIT_MSG" -sed -n -e '/^#/d' -e '/^diff --git/q' -e 'p;d' "$1" > "$commit_msg" - -die_advice=' -To continue editing, run the command - git commit -e -F '"$commit_msg"' -(assuming your working directory is at the top).' - -die() { - echo 'commit-msg hook failure' 1>&2 - echo '-----------------------' 1>&2 - echo "$@" 1>&2 - echo '-----------------------' 1>&2 - test -n "$die_advice" && echo "$die_advice" 1>&2 - exit 1 -} - -#----------------------------------------------------------------------------- -# Check the commit message layout with a simple state machine. - -msg_is_merge() { - echo "$line" | grep "^Merge " >/dev/null 2>&1 -} - -msg_is_revert() { - echo "$line" | grep "^Revert " >/dev/null 2>&1 -} - -msg_first() { - len=$(echo -n "$line" | wc -c) - # Use one of the following git commands to set the enforced subject maximum length. - # git config --global hooks.commit-msg.ITKCommitSubjectMaxLength 78 - # git config --local hooks.commit-msg.ITKCommitSubjectMaxLength 78 - max_len=$(git config --get hooks.commit-msg.ITKCommitSubjectMaxLength || echo 78) && - if test $len -eq 0; then - # not yet first line - return - elif test $len -lt 8; then - die 'The first line must be at least 8 characters: --------- -'"$line"' ---------' - elif test $len -gt "$max_len" && ! msg_is_merge && ! msg_is_revert; then - die 'The first line may be at most '"$max_len"' characters: ------------------------------------------------------------------------------- -'"$line"' -------------------------------------------------------------------------------' - elif echo "$line" | grep "^[ ]\|[ ]$" >/dev/null 2>&1; then - die 'The first line may not have leading or trailing space: -['"$line"']' - - # Look for valid prefixes. - elif ! echo "$line" | egrep -q '^(Merge|Revert|BUG:|COMP:|DOC:|ENH:|PERF:|STYLE:|WIP:) ' > /dev/null 2>&1; then - die 'Start ITK commit messages with a standard prefix (and a space): - BUG: - fix for runtime crash or incorrect result - COMP: - compiler error or warning fix - DOC: - documentation change - ENH: - new functionality - PERF: - performance improvement - STYLE: - no logic impact (indentation, comments) - WIP: - Work In Progress not ready for merge -To reference GitHub issue XXXX, add "Issue #XXXX" to the commit message. -If the issue addresses an open issue, add "Closes #XXXX" to the message.' - - # Reject bug number reference that CDash rejects. - elif echo "$line" | egrep -q '^BUG: [0-9][0-9]*\.'> /dev/null 2>&1; then - die 'Do not put a "." after the bug number: - - '"$line" - - else - # first line okay - state=second - fi -} - -msg_second() { - if test "x$line" != "x"; then - die 'The second line must be empty: -'"\"$line\"" - fi - state=third -} - -msg_third() { - #Do nothing for 3rd -> Nth line - state=third -} - -# Pipe commit message into the state machine. -state=first -cat "$commit_msg" | -while IFS='' read line; do - msg_$state || break -done && -#rm -f "$commit_msg" || exit 1 -die_advice='' # No more temporary message file. diff --git a/Utilities/Hooks/kw-commit-msg.py b/Utilities/Hooks/kw-commit-msg.py new file mode 100755 index 00000000000..38275c63752 --- /dev/null +++ b/Utilities/Hooks/kw-commit-msg.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python3 +import os +import sys +import subprocess +import re + +from pathlib import Path +import textwrap + +DEFAULT_LINE_LENGTH: int = 78 + + +def die(message, commit_msg_path): + print("commit-msg hook failure", file=sys.stderr) + print("-----------------------", file=sys.stderr) + print(message, file=sys.stderr) + print("-----------------------", file=sys.stderr) + print( + f""" +To continue editing, run the command + git commit -e -F "{commit_msg_path}" +(assuming your working directory is at the top).""", + file=sys.stderr, + ) + sys.exit(1) + + +def get_max_length(): + try: + result = subprocess.run( + ["git", "config", "--get", "hooks.commit-msg.ITKCommitSubjectMaxLength"], + capture_output=True, + text=True, + check=True, + ) + except subprocess.CalledProcessError as _: + return DEFAULT_LINE_LENGTH + + +def main(): + git_dir_path: Path = Path(os.environ.get("GIT_DIR", ".git")).resolve() + commit_msg_path: Path = git_dir_path / "COMMIT_MSG" + + if len(sys.argv) < 2: + die(f"Usage: {sys.argv[0]} ", commit_msg_path) + + input_file: Path = Path(sys.argv[1]) + if not input_file.exists(): + die( + f"Missing input_file {sys.argv[1]} for {sys.argv[0]} processing", + commit_msg_path, + ) + max_subjectline_length: int = get_max_length() + + original_input_file_lines: list[str] = [] + with open(input_file) as f_in: + original_input_file_lines = f_in.readlines() + + input_file_lines: list[str] = [] + for test_line in original_input_file_lines: + test_line = test_line.strip() + is_empty_line_before_subject: bool = ( + len(input_file_lines) == 0 and len(test_line) == 0 + ) + if test_line.startswith("#") or is_empty_line_before_subject: + continue + input_file_lines.append(f"{test_line}\n") + + with open(commit_msg_path, "w") as f_out: + f_out.writelines(input_file_lines) + + subject_line: str = input_file_lines[0] + + if len(subject_line) < 8: + die( + f"The first line must be at least 8 characters:\n--------\n{subject_line}\n--------", + commit_msg_path, + ) + if ( + len(subject_line) > max_subjectline_length + and not subject_line.startswith("Merge ") + and not subject_line.startswith("Revert ") + ): + die( + f"The first line may be at most {max_subjectline_length} characters:\n" + + "-" * max_subjectline_length + + f"\n{subject_line}\n" + + "-" * max_subjectline_length, + commit_msg_path, + ) + if re.match(r"^[ \t]|[ \t]$", subject_line): + die( + f"The first line may not have leading or trailing space:\n[{subject_line}]", + commit_msg_path, + ) + if not re.match( + r"^(Merge|Revert|BUG:|COMP:|DOC:|ENH:|PERF:|STYLE:|WIP:)\s", subject_line + ): + die( + f"""Start ITK commit messages with a standard prefix (and a space): + BUG: - fix for runtime crash or incorrect result + COMP: - compiler error or warning fix + DOC: - documentation change + ENH: - new functionality + PERF: - performance improvement + STYLE: - no logic impact (indentation, comments) + WIP: - Work In Progress not ready for merge +To reference GitHub issue XXXX, add "Issue #XXXX" to the commit message. +If the issue addresses an open issue, add "Closes #XXXX" to the message.""", + commit_msg_path, + ) + if re.match(r"^BUG: [0-9]+\.", subject_line): + die( + f'Do not put a "." after the bug number:\n\n {subject_line}', + commit_msg_path, + ) + del subject_line + + if len(input_file_lines) > 1: + second_line: str = input_file_lines[ + 1 + ].strip() # Remove whitespace at begining and end + if len(second_line) == 0: + input_file_lines[1] = "\n" # Replace line with only newline + else: + die( + f'The second line of the commit message must be empty:\n"{second_line}" with length {len(second_line)}', + commit_msg_path, + ) + del second_line + + +if __name__ == "__main__": + main()