Skip to content

Commit ca22d0f

Browse files
committed
patchoulene: ruyi-change-id: Init
Add an initial part of patchoulene, namely a Git hook to insert Change-Id trailers to Git commit messages. Yes, this fails checkpatch. If you don't like that you can use a prefix like Ruyi-Change-Id. Signed-off-by: Vivian Wang <wangruikang@iscas.ac.cn>
1 parent 6d6db9a commit ca22d0f

2 files changed

Lines changed: 160 additions & 0 deletions

File tree

patchoulene/README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# patchoulene
2+
3+
Here lies some openRuyi patch tracking scripts.
4+
5+
## `ruyi-change-id.py`
6+
7+
A Git `prepare-commit-msg` hook for inserting Change-Id trailers on otherwise unidentified commit messages.
8+
This hook checks the commit message on `git commit --amend [--signoff]` or `git rebase` "reword".
9+
If it has no recognized identifier, a Change-Id trailer is added.
10+
11+
Usage:
12+
13+
```console
14+
$ ln -s /path/to/patchoulene/ruyi-change-id.py .git/hooks/prepare-commit-msg
15+
```

patchoulene/ruyi-change-id.py

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
#!/usr/bin/env python
2+
3+
import datetime
4+
import os
5+
import re
6+
import shlex
7+
import subprocess
8+
import sys
9+
10+
RE_CHERRY = re.compile(
11+
r"^\(cherry picked from commit (?P<commit>[0-9a-f]+)\)$", re.MULTILINE
12+
)
13+
RE_CHERRY_TREE = re.compile(
14+
r"^\(cherry picked from commit (?P<commit>[0-9a-f]+)[ \t]*\n[ \t]+(?P<tree>\S+)[ \t]+(?P<branch>\S+)\)$",
15+
re.MULTILINE,
16+
)
17+
RE_LINK = re.compile(
18+
r"^Link:[ \t]+(?:https://lore\.kernel\.org/r/|https://patch\.msgid\.link/)(?P<msgid>[^\s/]+)/?(?:[ \t]*$|[ \t]+#)",
19+
re.MULTILINE,
20+
)
21+
RE_CHANGE_ID = re.compile(r"^(\w+-)*Change-Id:[ \t]+(\w+:\S+)$", re.MULTILINE)
22+
23+
24+
def message_subject(message: str) -> str:
25+
"""
26+
The first line of a message, or the empty string if the message is empty.
27+
"""
28+
return (message.split("\n", 1) + [""])[0]
29+
30+
31+
def clean_subject(subject: str) -> str:
32+
"""
33+
Remove prefixes from commit message subject.
34+
Vendor prefixes (except RUYI) are *not* removed.
35+
"""
36+
return re.sub(r"^(?:(?:FROMLIST|FROMGIT|UPSTREAM|BACKPORT|RUYI): )*", "", subject)
37+
38+
39+
def sanitize_subject(subject: str) -> str:
40+
"""
41+
Convert subject to a filename safe format.
42+
This is a reimplementation of Git's log format %f.
43+
"""
44+
# https://github.com/git/git/blob/v2.55.0/pretty.c#L947
45+
# Safe chars: ASCII alphanumeric, '.', '_'
46+
47+
# Replace runs of other chars with one dash
48+
subject = re.sub(r"[^a-zA-Z0-9._]+", "-", subject)
49+
# Replace runs of multiple dots with one
50+
subject = re.sub(r"\.+", ".", subject)
51+
# Remove dashes at start or end
52+
return re.sub("^-+|-+$", "", subject)
53+
54+
55+
def gen_change_id(message):
56+
base = sanitize_subject(clean_subject(message_subject(message)))
57+
datepart = datetime.datetime.now().strftime("%Y%m%d")
58+
return f"{base}-{datepart}"
59+
60+
61+
def cur_branch() -> str | None:
62+
"""
63+
Get the current branch or current rebasing in the Git repository in the current directory.
64+
Returns the branch name, or None if none could be determined.
65+
"""
66+
cur_branch = subprocess.check_output(
67+
shlex.split("git rev-parse --abbrev-ref HEAD"), encoding="utf-8"
68+
).rstrip("\n")
69+
if cur_branch != "HEAD" and cur_branch != "":
70+
return cur_branch
71+
72+
rebase_head_file = subprocess.check_output(
73+
shlex.split("git rev-parse --git-path rebase-merge/head-name"), encoding="utf-8"
74+
).rstrip("\n")
75+
try:
76+
with open(rebase_head_file, "r") as f:
77+
rebase_head = f.read().rstrip("\n")
78+
except FileNotFoundError:
79+
return None
80+
81+
if not rebase_head.startswith("refs/"):
82+
return None
83+
84+
rebase_head = subprocess.check_output(
85+
shlex.split("git rev-parse --abbrev-ref") + [rebase_head], encoding="utf-8"
86+
).rstrip("\n")
87+
88+
return rebase_head
89+
90+
91+
def main():
92+
msg_file, msg_source = sys.argv[1], sys.argv[2]
93+
94+
if msg_source != "commit":
95+
return
96+
97+
if os.getenv("RUYI_FORCE_CHANGE_ID", "") == "":
98+
b = cur_branch()
99+
if b is None or not b.startswith("ruyi/"):
100+
return
101+
102+
with open(msg_file, "r") as f:
103+
commit_msg = f.read()
104+
105+
if message_subject(commit_msg).strip() == "":
106+
return
107+
108+
# Skip if we have a recognized identifier
109+
if any(
110+
r.search(commit_msg) for r in [RE_CHERRY, RE_CHERRY_TREE, RE_LINK, RE_CHANGE_ID]
111+
):
112+
return
113+
114+
# Okay, we need to insert an identifier.
115+
116+
# Find insertion point before last Signed-off-by and bracketed note.
117+
#
118+
# (insert here)
119+
# [ Author: Something, something
120+
# something more. ]
121+
# Signed-off-by: Author <author@example.com>
122+
matches = list(
123+
re.finditer(r"^(?:\[.*\n(?:\s.*\n)*)?Signed-off-by:", commit_msg, re.MULTILINE)
124+
)
125+
if not matches:
126+
return
127+
match = matches[-1]
128+
129+
change_id = gen_change_id(commit_msg)
130+
131+
# This is so the user still knows what happens after exiting the editor
132+
print(f"(ruyi) Generated Change-Id: {change_id}", file=sys.stderr)
133+
134+
insert = f"""# !!! Automatically generated Change-Id
135+
Change-Id: ruyi:{change_id}
136+
# !!! ^^^^^^
137+
"""
138+
new_msg = commit_msg[: match.span()[0]] + insert + commit_msg[match.span()[0] :]
139+
140+
with open(msg_file, "w") as f:
141+
f.write(new_msg)
142+
143+
144+
if __name__ == "__main__":
145+
main()

0 commit comments

Comments
 (0)