Created
January 29, 2025 11:02
-
-
Save bindreams/b977a6d6ea005457525ea1b1a329f189 to your computer and use it in GitHub Desktop.
Git hook: ensure that every commit message contains a ticket reference
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
"""Ensure that every commit starts with a ticket ref. | |
A ticket ref regex is defined in `RE_TICKET_REF`, and by default matches tickets like "ABC-123456". The ticket must be | |
the first text in the commit message, not counting punctuation like brackets or parenthesis. When a ticket ref is | |
missing, this hook tries to guess one from the branch name, or fails the commit. | |
You can bypass this commit hook using `git commit --no-verify`. | |
""" | |
import sys | |
import re | |
import subprocess as sp | |
RE_TICKET_REF = r"[A-Z]+-\d+" # Such as QD-123456 | |
RE_TAGGED_MESSAGE = rf"^\W*({RE_TICKET_REF})" # Non-letters (square bracket, etc.) followed by the ticket | |
def normalize_message(message: str): | |
"""Convert the contents of a commit message file, with comments and empty lines, into a commit message.""" | |
result_lines = [] | |
first_nonempty = None | |
last_nonempty = None | |
for line in message.splitlines(): | |
if line.startswith("#"): | |
continue # Skip comments | |
if not (line == "" or line.isspace()): | |
if first_nonempty is None: | |
first_nonempty = len(result_lines) | |
last_nonempty = len(result_lines) | |
result_lines.append(line) | |
# Strip empty lines bookending the commit message, if the commit is not empty | |
if first_nonempty is not None: | |
assert last_nonempty is not None | |
result_lines = result_lines[first_nonempty : last_nonempty + 1] | |
return "\n".join(result_lines) | |
def guess_ticket_from_branch(): | |
branch_name = sp.run(["git", "branch", "--show-current"], check=True, text=True, stdout=sp.PIPE).stdout.strip() | |
m = re.search(RE_TICKET_REF, branch_name) | |
if m: | |
return m[0] | |
return None | |
def main(): | |
with open(sys.argv[1], "r", encoding="utf-8") as fd: | |
text = fd.read() | |
message = normalize_message(text) | |
if re.search(RE_TAGGED_MESSAGE, message): | |
return 0 # Ticket present in message already | |
ticket = guess_ticket_from_branch() | |
if ticket is None: | |
print("Error: missing ticket reference at the start of the message.", file=sys.stderr) | |
return 1 | |
message = f"{ticket}: {message}" | |
assert re.search(RE_TAGGED_MESSAGE, message) | |
with open(sys.argv[1], "w", encoding="utf-8", newline="\n") as fd: | |
fd.write(f"{message}\n") | |
print(f'Warning: added ticket ref "{ticket}" to the commit message.', file=sys.stderr) | |
return 0 | |
if __name__ == "__main__": | |
sys.exit(main()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment