blob: 060207bcac211452452c717dd961483fef22e3de [file] [log] [blame]
# 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,
)