-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy path.git-hook-commit-msg
More file actions
executable file
·119 lines (90 loc) · 3.2 KB
/
Copy path.git-hook-commit-msg
File metadata and controls
executable file
·119 lines (90 loc) · 3.2 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
#!/usr/bin/env python3
"""Git commit-msg hook.
Reads the activity (scielo-tools-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: [scielo-tools-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"scielo-tools-(\d+)", re.IGNORECASE)
EXISTING_PREFIX_RE = re.compile(r"^\s*\[scielo-tools-\d+\]\s*", re.IGNORECASE)
MESSAGE_FORMAT_RE = re.compile(
r"^(?:%s)(?:\([a-z0-9-]+\))?: .+" % "|".join(ALLOWED_TYPES)
)
AUTO_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(AUTO_COMMIT_PREFIXES):
return True
return bool(SQUASH_MERGE_RE.match(first_line))
def current_branch():
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")
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 scielo-tools-NNNN."
)
match = BRANCH_ACTIVITY_RE.search(branch)
if not match:
fail(
"Branch without an activity.\n"
"The branch name must contain scielo-tools-NNNN "
"(e.g. feature/scielo-tools-1234/my-feature).\n"
"Current branch: %s" % branch
)
activity = "scielo-tools-%s" % match.group(1)
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()