xref: /aosp_15_r20/external/toolchain-utils/llvm_tools/generate_llvm_revert_report.py (revision 760c253c1ed00ce9abd48f8546f08516e57485fe)
1#!/usr/bin/env python3
2# Copyright 2023 The ChromiumOS Authors
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Reports on all reverts applied and not applied to sys-devel/llvm.
7
8Note that this is primarily intended to produce output that can be easily
9pasted into a spreadsheet (read: the ChromeOS Mage's test matrix), so output is
10in CSV format.
11"""
12
13import argparse
14import csv
15import dataclasses
16import json
17import logging
18from pathlib import Path
19import re
20import subprocess
21import sys
22from typing import List, Set, TextIO
23
24import get_upstream_patch
25import revert_checker
26
27
28@dataclasses.dataclass(frozen=True)
29class RevertInfo:
30    """Information to write about a revert."""
31
32    revert: revert_checker.Revert
33    has_in_patches: bool
34    subject: str
35
36
37def list_upstream_cherrypicks(patches_json: Path) -> Set[str]:
38    with patches_json.open(encoding="utf-8") as f:
39        applicable_patches = [
40            x
41            for x in json.load(f)
42            if not x.get("platforms") or "chromiumos" in x["platforms"]
43        ]
44
45    # Allow for arbitrary suffixes for patches; some have `-v2`, `_fixed`, etc.
46    sha_re = re.compile(r"cherry/([a-fA-F0-9]{40})\b.*\.patch$")
47    sha_like_patches = set()
48    for p in applicable_patches:
49        m = sha_re.match(p["rel_patch_path"])
50        if m:
51            sha_like_patches.add(m.group(1))
52
53    return sha_like_patches
54
55
56def fetch_commit_subject(llvm_git_dir: Path, sha: str) -> str:
57    result = subprocess.run(
58        ["git", "log", "--format=%s", "-n1", sha],
59        check=True,
60        cwd=llvm_git_dir,
61        encoding="utf-8",
62        stdin=subprocess.DEVNULL,
63        stdout=subprocess.PIPE,
64        # Don't set stderr, since that should only be written to on error (and
65        # `check=True`).
66    )
67    return result.stdout.strip()
68
69
70def write_reverts_as_csv(write_to: TextIO, reverts: List[RevertInfo]):
71    writer = csv.writer(write_to, quoting=csv.QUOTE_ALL)
72    # Write the header.
73    writer.writerow(("SHA", "Reverted SHA", "Has Revert", "Subject"))
74    writer.writerows(
75        (x.revert.sha, x.revert.reverted_sha, x.has_in_patches, x.subject)
76        for x in reverts
77    )
78
79
80def main(argv: List[str]):
81    logging.basicConfig(
82        format=">> %(asctime)s: %(levelname)s: %(filename)s:%(lineno)d: "
83        "%(message)s",
84        level=logging.INFO,
85    )
86
87    my_dir = Path(__name__).resolve().parent
88
89    parser = argparse.ArgumentParser(
90        description=__doc__,
91        formatter_class=argparse.RawDescriptionHelpFormatter,
92    )
93    parser.add_argument(
94        "-C",
95        "--git-dir",
96        default=my_dir.parent.parent / "llvm-project",
97        help="LLVM git directory to use.",
98        # Note that this is left as `type=str` because that's what
99        # `revert_checker` expects.
100        type=str,
101    )
102    parser.add_argument(
103        "--llvm-next", action="store_true", help="Use the llvm-next hash"
104    )
105    parser.add_argument(
106        "--llvm-dir",
107        help="Directory containing LLVM ebuilds",
108        type=Path,
109        default=my_dir.parent.parent / "chromiumos-overlay/sys-devel/llvm",
110    )
111    parser.add_argument(
112        "--llvm-head",
113        default="cros/upstream/main",
114        help="ref to treat as 'origin/main' in the given LLVM dir.",
115    )
116    opts = parser.parse_args(argv)
117
118    symbolic_sha = "llvm-next" if opts.llvm_next else "llvm"
119    llvm_sha = get_upstream_patch.resolve_symbolic_sha(
120        symbolic_sha,
121        opts.llvm_dir,
122    )
123    logging.info("Resolved %r as the LLVM SHA to check.", llvm_sha)
124
125    in_tree_cherrypicks = list_upstream_cherrypicks(
126        opts.llvm_dir / "files/PATCHES.json"
127    )
128    logging.info("Identified %d local cherrypicks.", len(in_tree_cherrypicks))
129
130    raw_reverts = revert_checker.find_reverts(
131        opts.git_dir,
132        llvm_sha,
133        opts.llvm_head,
134    )
135
136    llvm_dir = Path(opts.git_dir)
137    # Sort by `has_in_patches`, since that ordering is easier to visually scan.
138    # Note that `sorted` is stable, so any ordering in `find_reverts` will be
139    # preserved secondary to the `has_in_patches` ordering. Reverts not in
140    # PATCHES.json will appear earlier than those that are.
141    reverts = sorted(
142        (
143            RevertInfo(
144                revert=revert,
145                subject=fetch_commit_subject(llvm_dir, revert.sha),
146                has_in_patches=revert.sha in in_tree_cherrypicks,
147            )
148            for revert in raw_reverts
149        ),
150        key=lambda x: x.has_in_patches,
151    )
152
153    print()
154    print("CSV summary of reverts:")
155    write_reverts_as_csv(sys.stdout, reverts)
156
157
158if __name__ == "__main__":
159    main(sys.argv[1:])
160