| # 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, |
| ) |