aboutsummaryrefslogtreecommitdiff
path: root/auto_abandon_cls.py
blob: ae78bfa59ab96cec4a5aba076df34ae7c8bd7879 (plain)
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
#!/usr/bin/env python3
# Copyright 2024 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""Abandons CLs from the current user that haven't been updated recently.

Note that this needs to be run from inside a ChromeOS tree. Otherwise, the
`gerrit` tool this depends on won't be found.
"""

import argparse
import logging
import subprocess
import sys
from typing import List


def gerrit_cmd(internal: bool) -> List[str]:
    cmd = ["gerrit"]
    if internal:
        cmd.append("--internal")
    return cmd


def enumerate_old_cls(old_days: int, internal: bool) -> List[int]:
    """Returns CL numbers that haven't been updated in `old_days` days."""
    stdout = subprocess.run(
        gerrit_cmd(internal)
        + ["--raw", "search", f"owner:me status:open age:{old_days}d"],
        check=True,
        stdin=subprocess.DEVNULL,
        stdout=subprocess.PIPE,
        encoding="utf-8",
    ).stdout
    # Sort for prettier output; it's unclear if Gerrit always sorts, and it's
    # cheap.
    lines = stdout.splitlines()
    if internal:
        # These are printed as `chrome-internal:NNNN`, rather than `NNNN`.
        chrome_internal_prefix = "chrome-internal:"
        assert all(x.startswith(chrome_internal_prefix) for x in lines), lines
        lines = [x[len(chrome_internal_prefix) :] for x in lines]
    return sorted(int(x) for x in lines)


def abandon_cls(cls: List[int], internal: bool) -> None:
    subprocess.run(
        gerrit_cmd(internal) + ["abandon"] + [str(x) for x in cls],
        check=True,
        stdin=subprocess.DEVNULL,
    )


def detect_and_abandon_cls(
    old_days: int, dry_run: bool, internal: bool
) -> None:
    old_cls = enumerate_old_cls(old_days, internal)
    if not old_cls:
        logging.info("No CLs less than %d days old found; quit", old_days)
        return

    cl_namespace = "i" if internal else "c"
    logging.info(
        "Abandoning CLs: %s", [f"crrev.com/{cl_namespace}/{x}" for x in old_cls]
    )
    if dry_run:
        logging.info("--dry-run specified; skip the actual abandon part")
        return

    abandon_cls(old_cls, internal)


def main(argv: List[str]) -> None:
    logging.basicConfig(
        format=">> %(asctime)s: %(levelname)s: %(filename)s:%(lineno)d: "
        "%(message)s",
        level=logging.INFO,
    )

    parser = argparse.ArgumentParser(
        description=__doc__,
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    parser.add_argument(
        "--old-days",
        default=14,
        type=int,
        help="""
        How many days a CL needs to go without modification to be considered
        'old'.
        """,
    )
    parser.add_argument(
        "--dry-run",
        action="store_true",
        help="Don't actually run the abandon command.",
    )
    opts = parser.parse_args(argv)

    logging.info("Checking for external CLs...")
    detect_and_abandon_cls(
        old_days=opts.old_days,
        dry_run=opts.dry_run,
        internal=False,
    )
    logging.info("Checking for internal CLs...")
    detect_and_abandon_cls(
        old_days=opts.old_days,
        dry_run=opts.dry_run,
        internal=True,
    )


if __name__ == "__main__":
    main(sys.argv[1:])