|
| 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