-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy path.git-hook-commit-msg
More file actions
executable file
·126 lines (97 loc) · 3.63 KB
/
.git-hook-commit-msg
File metadata and controls
executable file
·126 lines (97 loc) · 3.63 KB
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
#!/usr/bin/env python3
"""Git commit-msg hook.
Reads the activity (MarkAPI-NNNN) from the branch name -- anywhere in the
name -- and:
1. validates the message format: <type>(<optional scope>): <message>
allowed types: feature, fix, chore, unittest
2. injects the activity prefix into the first line: [MarkAPI-NNNN] ...
Automatic Git commits (merge/revert/squash) are skipped.
Uses the standard library only for portability across Linux and macOS.
"""
import re
import subprocess
import sys
ALLOWED_TYPES = ("feature", "fix", "chore", "unittest")
BRANCH_ACTIVITY_RE = re.compile(r"MarkupAPI-(\d+)", re.IGNORECASE)
EXISTING_PREFIX_RE = re.compile(r"^\s*\[MarkupAPI-\d+\]\s*", re.IGNORECASE)
MESSAGE_FORMAT_RE = re.compile(
r"^(?:%s)(?:\([a-z0-9-]+\))?: .+" % "|".join(ALLOWED_TYPES)
)
# Automatic commits generated by Git that should pass without validation.
MarkAPI_COMMIT_PREFIXES = (
"Merge branch ",
"Merge pull request ",
"Merge remote-tracking ",
'Revert "',
)
SQUASH_MERGE_RE = re.compile(r"^Merge .+ into ")
def fail(message):
print(message, file=sys.stderr)
sys.exit(1)
def is_auto_commit(first_line):
if first_line.startswith(MarkAPI_COMMIT_PREFIXES):
return True
return bool(SQUASH_MERGE_RE.match(first_line))
def current_branch():
# symbolic-ref works even on the first commit (branch with no commits),
# where rev-parse --abbrev-ref HEAD fails. On a detached HEAD,
# symbolic-ref returns a non-zero code and we fall back to rev-parse
# (which returns "HEAD").
for cmd in (
["git", "symbolic-ref", "--short", "HEAD"],
["git", "rev-parse", "--abbrev-ref", "HEAD"],
):
try:
result = subprocess.run(cmd, capture_output=True, text=True)
except OSError:
return None
if result.returncode == 0:
branch = result.stdout.strip()
if branch:
return branch
return None
def main():
if len(sys.argv) < 2:
fail("⌠commit-msg hook: message file path not provided.")
msg_path = sys.argv[1]
with open(msg_path, "r", encoding="utf-8") as handle:
lines = handle.read().splitlines()
if not lines:
fail("⌠Empty commit message.")
first_line = lines[0].rstrip("\r")
# Let automatic Git commits pass through.
if is_auto_commit(first_line):
sys.exit(0)
branch = current_branch()
if not branch or branch == "HEAD":
fail(
"⌠Could not determine the current branch (detached HEAD?).\n"
"The branch must contain the activity MarkAPI-NNNN."
)
match = BRANCH_ACTIVITY_RE.search(branch)
if not match:
fail(
"⌠Branch without an activity.\n"
"The branch name must contain MarkAPI-NNNN "
"(e.g. feature/MarkAPI-1234/my-feature).\n"
"Current branch: %s" % branch
)
activity = "MarkAPI-%s" % match.group(1)
# Strip an existing prefix to avoid duplication (amend/rebase/reword).
body = EXISTING_PREFIX_RE.sub("", first_line).strip()
if not MESSAGE_FORMAT_RE.match(body):
fail(
"⌠Invalid commit message.\n"
"Expected format: <type>(<optional scope>): <message>\n"
"Allowed types: %s\n"
"Examples:\n"
" chore: format readme\n"
" fix(auth): fix authentication when token is invalid"
% ", ".join(ALLOWED_TYPES)
)
lines[0] = "[%s] %s" % (activity, body)
with open(msg_path, "w", encoding="utf-8") as handle:
handle.write("\n".join(lines) + "\n")
sys.exit(0)
if __name__ == "__main__":
main()