xref: /aosp_15_r20/external/toolchain-utils/auto_abandon_cls.py (revision 760c253c1ed00ce9abd48f8546f08516e57485fe)
1*760c253cSXin Li#!/usr/bin/env python3
2*760c253cSXin Li# Copyright 2024 The ChromiumOS Authors
3*760c253cSXin Li# Use of this source code is governed by a BSD-style license that can be
4*760c253cSXin Li# found in the LICENSE file.
5*760c253cSXin Li
6*760c253cSXin Li"""Abandons CLs from the current user that haven't been updated recently.
7*760c253cSXin Li
8*760c253cSXin LiNote that this needs to be run from inside a ChromeOS tree. Otherwise, the
9*760c253cSXin Li`gerrit` tool this depends on won't be found.
10*760c253cSXin Li"""
11*760c253cSXin Li
12*760c253cSXin Liimport argparse
13*760c253cSXin Liimport logging
14*760c253cSXin Liimport subprocess
15*760c253cSXin Liimport sys
16*760c253cSXin Lifrom typing import List
17*760c253cSXin Li
18*760c253cSXin Li
19*760c253cSXin Lidef gerrit_cmd(internal: bool) -> List[str]:
20*760c253cSXin Li    cmd = ["gerrit"]
21*760c253cSXin Li    if internal:
22*760c253cSXin Li        cmd.append("--internal")
23*760c253cSXin Li    return cmd
24*760c253cSXin Li
25*760c253cSXin Li
26*760c253cSXin Lidef enumerate_old_cls(old_days: int, internal: bool) -> List[int]:
27*760c253cSXin Li    """Returns CL numbers that haven't been updated in `old_days` days."""
28*760c253cSXin Li    stdout = subprocess.run(
29*760c253cSXin Li        gerrit_cmd(internal)
30*760c253cSXin Li        + ["--raw", "search", f"owner:me status:open age:{old_days}d"],
31*760c253cSXin Li        check=True,
32*760c253cSXin Li        stdin=subprocess.DEVNULL,
33*760c253cSXin Li        stdout=subprocess.PIPE,
34*760c253cSXin Li        encoding="utf-8",
35*760c253cSXin Li    ).stdout
36*760c253cSXin Li    # Sort for prettier output; it's unclear if Gerrit always sorts, and it's
37*760c253cSXin Li    # cheap.
38*760c253cSXin Li    lines = stdout.splitlines()
39*760c253cSXin Li    if internal:
40*760c253cSXin Li        # These are printed as `chrome-internal:NNNN`, rather than `NNNN`.
41*760c253cSXin Li        chrome_internal_prefix = "chrome-internal:"
42*760c253cSXin Li        assert all(x.startswith(chrome_internal_prefix) for x in lines), lines
43*760c253cSXin Li        lines = [x[len(chrome_internal_prefix) :] for x in lines]
44*760c253cSXin Li    return sorted(int(x) for x in lines)
45*760c253cSXin Li
46*760c253cSXin Li
47*760c253cSXin Lidef abandon_cls(cls: List[int], internal: bool) -> None:
48*760c253cSXin Li    subprocess.run(
49*760c253cSXin Li        gerrit_cmd(internal) + ["abandon"] + [str(x) for x in cls],
50*760c253cSXin Li        check=True,
51*760c253cSXin Li        stdin=subprocess.DEVNULL,
52*760c253cSXin Li    )
53*760c253cSXin Li
54*760c253cSXin Li
55*760c253cSXin Lidef detect_and_abandon_cls(
56*760c253cSXin Li    old_days: int, dry_run: bool, internal: bool
57*760c253cSXin Li) -> None:
58*760c253cSXin Li    old_cls = enumerate_old_cls(old_days, internal)
59*760c253cSXin Li    if not old_cls:
60*760c253cSXin Li        logging.info("No CLs less than %d days old found; quit", old_days)
61*760c253cSXin Li        return
62*760c253cSXin Li
63*760c253cSXin Li    cl_namespace = "i" if internal else "c"
64*760c253cSXin Li    logging.info(
65*760c253cSXin Li        "Abandoning CLs: %s", [f"crrev.com/{cl_namespace}/{x}" for x in old_cls]
66*760c253cSXin Li    )
67*760c253cSXin Li    if dry_run:
68*760c253cSXin Li        logging.info("--dry-run specified; skip the actual abandon part")
69*760c253cSXin Li        return
70*760c253cSXin Li
71*760c253cSXin Li    abandon_cls(old_cls, internal)
72*760c253cSXin Li
73*760c253cSXin Li
74*760c253cSXin Lidef main(argv: List[str]) -> None:
75*760c253cSXin Li    logging.basicConfig(
76*760c253cSXin Li        format=">> %(asctime)s: %(levelname)s: %(filename)s:%(lineno)d: "
77*760c253cSXin Li        "%(message)s",
78*760c253cSXin Li        level=logging.INFO,
79*760c253cSXin Li    )
80*760c253cSXin Li
81*760c253cSXin Li    parser = argparse.ArgumentParser(
82*760c253cSXin Li        description=__doc__,
83*760c253cSXin Li        formatter_class=argparse.RawDescriptionHelpFormatter,
84*760c253cSXin Li    )
85*760c253cSXin Li    parser.add_argument(
86*760c253cSXin Li        "--old-days",
87*760c253cSXin Li        default=14,
88*760c253cSXin Li        type=int,
89*760c253cSXin Li        help="""
90*760c253cSXin Li        How many days a CL needs to go without modification to be considered
91*760c253cSXin Li        'old'.
92*760c253cSXin Li        """,
93*760c253cSXin Li    )
94*760c253cSXin Li    parser.add_argument(
95*760c253cSXin Li        "--dry-run",
96*760c253cSXin Li        action="store_true",
97*760c253cSXin Li        help="Don't actually run the abandon command.",
98*760c253cSXin Li    )
99*760c253cSXin Li    opts = parser.parse_args(argv)
100*760c253cSXin Li
101*760c253cSXin Li    logging.info("Checking for external CLs...")
102*760c253cSXin Li    detect_and_abandon_cls(
103*760c253cSXin Li        old_days=opts.old_days,
104*760c253cSXin Li        dry_run=opts.dry_run,
105*760c253cSXin Li        internal=False,
106*760c253cSXin Li    )
107*760c253cSXin Li    logging.info("Checking for internal CLs...")
108*760c253cSXin Li    detect_and_abandon_cls(
109*760c253cSXin Li        old_days=opts.old_days,
110*760c253cSXin Li        dry_run=opts.dry_run,
111*760c253cSXin Li        internal=True,
112*760c253cSXin Li    )
113*760c253cSXin Li
114*760c253cSXin Li
115*760c253cSXin Liif __name__ == "__main__":
116*760c253cSXin Li    main(sys.argv[1:])
117