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