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