1*760c253cSXin Li#!/usr/bin/env python3 2*760c253cSXin Li# Copyright 2020 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"""Checks for new reverts in LLVM on a nightly basis. 7*760c253cSXin Li 8*760c253cSXin LiIf any reverts are found that were previously unknown, this cherry-picks them or 9*760c253cSXin Lifires off an email. All LLVM SHAs to monitor are autodetected. 10*760c253cSXin Li""" 11*760c253cSXin Li 12*760c253cSXin Liimport argparse 13*760c253cSXin Liimport dataclasses 14*760c253cSXin Liimport json 15*760c253cSXin Liimport logging 16*760c253cSXin Liimport os 17*760c253cSXin Lifrom pathlib import Path 18*760c253cSXin Liimport pprint 19*760c253cSXin Liimport subprocess 20*760c253cSXin Liimport sys 21*760c253cSXin Liimport time 22*760c253cSXin Lifrom typing import Any, Callable, Dict, List, NamedTuple, Set, Tuple 23*760c253cSXin Li 24*760c253cSXin Lifrom cros_utils import email_sender 25*760c253cSXin Lifrom cros_utils import tiny_render 26*760c253cSXin Liimport get_llvm_hash 27*760c253cSXin Liimport get_upstream_patch 28*760c253cSXin Liimport git_llvm_rev 29*760c253cSXin Liimport revert_checker 30*760c253cSXin Li 31*760c253cSXin Li 32*760c253cSXin LiONE_DAY_SECS = 24 * 60 * 60 33*760c253cSXin Li# How often to send an email about a HEAD not moving. 34*760c253cSXin LiHEAD_STALENESS_ALERT_INTERVAL_SECS = 21 * ONE_DAY_SECS 35*760c253cSXin Li# How long to wait after a HEAD changes for the first 'update' email to be sent. 36*760c253cSXin LiHEAD_STALENESS_ALERT_INITIAL_SECS = 60 * ONE_DAY_SECS 37*760c253cSXin Li 38*760c253cSXin Li 39*760c253cSXin Li# Not frozen, as `next_notification_timestamp` may be mutated. 40*760c253cSXin Li@dataclasses.dataclass(frozen=False, eq=True) 41*760c253cSXin Liclass HeadInfo: 42*760c253cSXin Li """Information about about a HEAD that's tracked by this script.""" 43*760c253cSXin Li 44*760c253cSXin Li # The most recent SHA observed for this HEAD. 45*760c253cSXin Li last_sha: str 46*760c253cSXin Li # The time at which the current value for this HEAD was first seen. 47*760c253cSXin Li first_seen_timestamp: int 48*760c253cSXin Li # The next timestamp to notify users if this HEAD doesn't move. 49*760c253cSXin Li next_notification_timestamp: int 50*760c253cSXin Li 51*760c253cSXin Li @classmethod 52*760c253cSXin Li def from_json(cls, json_object: Any) -> "HeadInfo": 53*760c253cSXin Li return cls(**json_object) 54*760c253cSXin Li 55*760c253cSXin Li def to_json(self) -> Any: 56*760c253cSXin Li return dataclasses.asdict(self) 57*760c253cSXin Li 58*760c253cSXin Li 59*760c253cSXin Li@dataclasses.dataclass(frozen=True, eq=True) 60*760c253cSXin Liclass State: 61*760c253cSXin Li """Persistent state for this script.""" 62*760c253cSXin Li 63*760c253cSXin Li # Mapping of LLVM SHA -> List of reverts that have been seen for it 64*760c253cSXin Li seen_reverts: Dict[str, List[str]] = dataclasses.field(default_factory=dict) 65*760c253cSXin Li # Mapping of friendly HEAD name (e.g., main-legacy) to last-known info 66*760c253cSXin Li # about it. 67*760c253cSXin Li heads: Dict[str, HeadInfo] = dataclasses.field(default_factory=dict) 68*760c253cSXin Li 69*760c253cSXin Li @classmethod 70*760c253cSXin Li def from_json(cls, json_object: Any) -> "State": 71*760c253cSXin Li # Autoupgrade old JSON files. 72*760c253cSXin Li if "heads" not in json_object: 73*760c253cSXin Li json_object = { 74*760c253cSXin Li "seen_reverts": json_object, 75*760c253cSXin Li "heads": {}, 76*760c253cSXin Li } 77*760c253cSXin Li 78*760c253cSXin Li return cls( 79*760c253cSXin Li seen_reverts=json_object["seen_reverts"], 80*760c253cSXin Li heads={ 81*760c253cSXin Li k: HeadInfo.from_json(v) 82*760c253cSXin Li for k, v in json_object["heads"].items() 83*760c253cSXin Li }, 84*760c253cSXin Li ) 85*760c253cSXin Li 86*760c253cSXin Li def to_json(self) -> Any: 87*760c253cSXin Li return { 88*760c253cSXin Li "seen_reverts": self.seen_reverts, 89*760c253cSXin Li "heads": {k: v.to_json() for k, v in self.heads.items()}, 90*760c253cSXin Li } 91*760c253cSXin Li 92*760c253cSXin Li 93*760c253cSXin Lidef _find_interesting_android_shas( 94*760c253cSXin Li android_llvm_toolchain_dir: str, 95*760c253cSXin Li) -> List[Tuple[str, str]]: 96*760c253cSXin Li llvm_project = os.path.join( 97*760c253cSXin Li android_llvm_toolchain_dir, "toolchain/llvm-project" 98*760c253cSXin Li ) 99*760c253cSXin Li 100*760c253cSXin Li def get_llvm_merge_base(branch: str) -> str: 101*760c253cSXin Li head_sha = subprocess.check_output( 102*760c253cSXin Li ["git", "rev-parse", branch], 103*760c253cSXin Li cwd=llvm_project, 104*760c253cSXin Li encoding="utf-8", 105*760c253cSXin Li ).strip() 106*760c253cSXin Li merge_base = subprocess.check_output( 107*760c253cSXin Li ["git", "merge-base", branch, "aosp/upstream-main"], 108*760c253cSXin Li cwd=llvm_project, 109*760c253cSXin Li encoding="utf-8", 110*760c253cSXin Li ).strip() 111*760c253cSXin Li logging.info( 112*760c253cSXin Li "Merge-base for %s (HEAD == %s) and upstream-main is %s", 113*760c253cSXin Li branch, 114*760c253cSXin Li head_sha, 115*760c253cSXin Li merge_base, 116*760c253cSXin Li ) 117*760c253cSXin Li return merge_base 118*760c253cSXin Li 119*760c253cSXin Li main_legacy = get_llvm_merge_base("aosp/master-legacy") # nocheck 120*760c253cSXin Li testing_upstream = get_llvm_merge_base("aosp/testing-upstream") 121*760c253cSXin Li result: List[Tuple[str, str]] = [("main-legacy", main_legacy)] 122*760c253cSXin Li 123*760c253cSXin Li # If these are the same SHA, there's no point in tracking both. 124*760c253cSXin Li if main_legacy != testing_upstream: 125*760c253cSXin Li result.append(("testing-upstream", testing_upstream)) 126*760c253cSXin Li else: 127*760c253cSXin Li logging.info( 128*760c253cSXin Li "main-legacy and testing-upstream are identical; ignoring " 129*760c253cSXin Li "the latter." 130*760c253cSXin Li ) 131*760c253cSXin Li return result 132*760c253cSXin Li 133*760c253cSXin Li 134*760c253cSXin Lidef _find_interesting_chromeos_shas( 135*760c253cSXin Li chromeos_base: str, 136*760c253cSXin Li) -> List[Tuple[str, str]]: 137*760c253cSXin Li chromeos_path = Path(chromeos_base) 138*760c253cSXin Li llvm_hash = get_llvm_hash.LLVMHash() 139*760c253cSXin Li 140*760c253cSXin Li current_llvm = llvm_hash.GetCrOSCurrentLLVMHash(chromeos_path) 141*760c253cSXin Li results = [("llvm", current_llvm)] 142*760c253cSXin Li next_llvm = llvm_hash.GetCrOSLLVMNextHash() 143*760c253cSXin Li if current_llvm != next_llvm: 144*760c253cSXin Li results.append(("llvm-next", next_llvm)) 145*760c253cSXin Li return results 146*760c253cSXin Li 147*760c253cSXin Li 148*760c253cSXin Li_Email = NamedTuple( 149*760c253cSXin Li "_Email", 150*760c253cSXin Li [ 151*760c253cSXin Li ("subject", str), 152*760c253cSXin Li ("body", tiny_render.Piece), 153*760c253cSXin Li ], 154*760c253cSXin Li) 155*760c253cSXin Li 156*760c253cSXin Li 157*760c253cSXin Lidef _generate_revert_email( 158*760c253cSXin Li repository_name: str, 159*760c253cSXin Li friendly_name: str, 160*760c253cSXin Li sha: str, 161*760c253cSXin Li prettify_sha: Callable[[str], tiny_render.Piece], 162*760c253cSXin Li get_sha_description: Callable[[str], tiny_render.Piece], 163*760c253cSXin Li new_reverts: List[revert_checker.Revert], 164*760c253cSXin Li) -> _Email: 165*760c253cSXin Li email_pieces = [ 166*760c253cSXin Li "It looks like there may be %s across %s (" 167*760c253cSXin Li % ( 168*760c253cSXin Li "a new revert" if len(new_reverts) == 1 else "new reverts", 169*760c253cSXin Li friendly_name, 170*760c253cSXin Li ), 171*760c253cSXin Li prettify_sha(sha), 172*760c253cSXin Li ").", 173*760c253cSXin Li tiny_render.line_break, 174*760c253cSXin Li tiny_render.line_break, 175*760c253cSXin Li "That is:" if len(new_reverts) == 1 else "These are:", 176*760c253cSXin Li ] 177*760c253cSXin Li 178*760c253cSXin Li revert_listing = [] 179*760c253cSXin Li for revert in sorted(new_reverts, key=lambda r: r.sha): 180*760c253cSXin Li revert_listing.append( 181*760c253cSXin Li [ 182*760c253cSXin Li prettify_sha(revert.sha), 183*760c253cSXin Li " (appears to revert ", 184*760c253cSXin Li prettify_sha(revert.reverted_sha), 185*760c253cSXin Li "): ", 186*760c253cSXin Li get_sha_description(revert.sha), 187*760c253cSXin Li ] 188*760c253cSXin Li ) 189*760c253cSXin Li 190*760c253cSXin Li email_pieces.append(tiny_render.UnorderedList(items=revert_listing)) 191*760c253cSXin Li email_pieces += [ 192*760c253cSXin Li tiny_render.line_break, 193*760c253cSXin Li "PTAL and consider reverting them locally.", 194*760c253cSXin Li ] 195*760c253cSXin Li return _Email( 196*760c253cSXin Li subject="[revert-checker/%s] new %s discovered across %s" 197*760c253cSXin Li % ( 198*760c253cSXin Li repository_name, 199*760c253cSXin Li "revert" if len(new_reverts) == 1 else "reverts", 200*760c253cSXin Li friendly_name, 201*760c253cSXin Li ), 202*760c253cSXin Li body=email_pieces, 203*760c253cSXin Li ) 204*760c253cSXin Li 205*760c253cSXin Li 206*760c253cSXin Li_EmailRecipients = NamedTuple( 207*760c253cSXin Li "_EmailRecipients", 208*760c253cSXin Li [ 209*760c253cSXin Li ("well_known", List[str]), 210*760c253cSXin Li ("direct", List[str]), 211*760c253cSXin Li ], 212*760c253cSXin Li) 213*760c253cSXin Li 214*760c253cSXin Li 215*760c253cSXin Lidef _send_revert_email(recipients: _EmailRecipients, email: _Email) -> None: 216*760c253cSXin Li email_sender.EmailSender().SendX20Email( 217*760c253cSXin Li subject=email.subject, 218*760c253cSXin Li identifier="revert-checker", 219*760c253cSXin Li well_known_recipients=recipients.well_known, 220*760c253cSXin Li direct_recipients=["[email protected]"] + recipients.direct, 221*760c253cSXin Li text_body=tiny_render.render_text_pieces(email.body), 222*760c253cSXin Li html_body=tiny_render.render_html_pieces(email.body), 223*760c253cSXin Li ) 224*760c253cSXin Li 225*760c253cSXin Li 226*760c253cSXin Lidef _write_state(state_file: str, new_state: State) -> None: 227*760c253cSXin Li tmp_file = state_file + ".new" 228*760c253cSXin Li try: 229*760c253cSXin Li with open(tmp_file, "w", encoding="utf-8") as f: 230*760c253cSXin Li json.dump( 231*760c253cSXin Li new_state.to_json(), 232*760c253cSXin Li f, 233*760c253cSXin Li sort_keys=True, 234*760c253cSXin Li indent=2, 235*760c253cSXin Li separators=(",", ": "), 236*760c253cSXin Li ) 237*760c253cSXin Li os.rename(tmp_file, state_file) 238*760c253cSXin Li except: 239*760c253cSXin Li try: 240*760c253cSXin Li os.remove(tmp_file) 241*760c253cSXin Li except FileNotFoundError: 242*760c253cSXin Li pass 243*760c253cSXin Li raise 244*760c253cSXin Li 245*760c253cSXin Li 246*760c253cSXin Lidef _read_state(state_file: str) -> State: 247*760c253cSXin Li try: 248*760c253cSXin Li with open(state_file, encoding="utf-8") as f: 249*760c253cSXin Li return State.from_json(json.load(f)) 250*760c253cSXin Li except FileNotFoundError: 251*760c253cSXin Li logging.info( 252*760c253cSXin Li "No state file found at %r; starting with an empty slate", 253*760c253cSXin Li state_file, 254*760c253cSXin Li ) 255*760c253cSXin Li return State() 256*760c253cSXin Li 257*760c253cSXin Li 258*760c253cSXin Li@dataclasses.dataclass(frozen=True) 259*760c253cSXin Liclass NewRevertInfo: 260*760c253cSXin Li """A list of new reverts for a given SHA.""" 261*760c253cSXin Li 262*760c253cSXin Li friendly_name: str 263*760c253cSXin Li sha: str 264*760c253cSXin Li new_reverts: List[revert_checker.Revert] 265*760c253cSXin Li 266*760c253cSXin Li 267*760c253cSXin Lidef locate_new_reverts_across_shas( 268*760c253cSXin Li llvm_dir: str, 269*760c253cSXin Li interesting_shas: List[Tuple[str, str]], 270*760c253cSXin Li state: State, 271*760c253cSXin Li) -> Tuple[State, List[NewRevertInfo]]: 272*760c253cSXin Li """Locates and returns yet-unseen reverts across `interesting_shas`.""" 273*760c253cSXin Li new_state = State() 274*760c253cSXin Li revert_infos = [] 275*760c253cSXin Li for friendly_name, sha in interesting_shas: 276*760c253cSXin Li logging.info("Finding reverts across %s (%s)", friendly_name, sha) 277*760c253cSXin Li all_reverts = revert_checker.find_reverts( 278*760c253cSXin Li llvm_dir, sha, root="origin/" + git_llvm_rev.MAIN_BRANCH 279*760c253cSXin Li ) 280*760c253cSXin Li logging.info( 281*760c253cSXin Li "Detected the following revert(s) across %s:\n%s", 282*760c253cSXin Li friendly_name, 283*760c253cSXin Li pprint.pformat(all_reverts), 284*760c253cSXin Li ) 285*760c253cSXin Li 286*760c253cSXin Li new_state.seen_reverts[sha] = [r.sha for r in all_reverts] 287*760c253cSXin Li 288*760c253cSXin Li if sha not in state.seen_reverts: 289*760c253cSXin Li logging.info("SHA %s is new to me", sha) 290*760c253cSXin Li existing_reverts = set() 291*760c253cSXin Li else: 292*760c253cSXin Li existing_reverts = set(state.seen_reverts[sha]) 293*760c253cSXin Li 294*760c253cSXin Li new_reverts = [r for r in all_reverts if r.sha not in existing_reverts] 295*760c253cSXin Li if not new_reverts: 296*760c253cSXin Li logging.info("...All of which have been reported.") 297*760c253cSXin Li continue 298*760c253cSXin Li 299*760c253cSXin Li new_head_info = None 300*760c253cSXin Li if old_head_info := state.heads.get(friendly_name): 301*760c253cSXin Li if old_head_info.last_sha == sha: 302*760c253cSXin Li new_head_info = old_head_info 303*760c253cSXin Li 304*760c253cSXin Li if new_head_info is None: 305*760c253cSXin Li now = int(time.time()) 306*760c253cSXin Li notify_at = HEAD_STALENESS_ALERT_INITIAL_SECS + now 307*760c253cSXin Li new_head_info = HeadInfo( 308*760c253cSXin Li last_sha=sha, 309*760c253cSXin Li first_seen_timestamp=now, 310*760c253cSXin Li next_notification_timestamp=notify_at, 311*760c253cSXin Li ) 312*760c253cSXin Li new_state.heads[friendly_name] = new_head_info 313*760c253cSXin Li 314*760c253cSXin Li revert_infos.append( 315*760c253cSXin Li NewRevertInfo( 316*760c253cSXin Li friendly_name=friendly_name, 317*760c253cSXin Li sha=sha, 318*760c253cSXin Li new_reverts=new_reverts, 319*760c253cSXin Li ) 320*760c253cSXin Li ) 321*760c253cSXin Li return new_state, revert_infos 322*760c253cSXin Li 323*760c253cSXin Li 324*760c253cSXin Lidef do_cherrypick( 325*760c253cSXin Li chroot_path: str, 326*760c253cSXin Li llvm_dir: str, 327*760c253cSXin Li repository: str, 328*760c253cSXin Li interesting_shas: List[Tuple[str, str]], 329*760c253cSXin Li state: State, 330*760c253cSXin Li reviewers: List[str], 331*760c253cSXin Li cc: List[str], 332*760c253cSXin Li) -> State: 333*760c253cSXin Li def prettify_sha(sha: str) -> tiny_render.Piece: 334*760c253cSXin Li rev = get_llvm_hash.GetVersionFrom(llvm_dir, sha) 335*760c253cSXin Li return prettify_sha_for_email(sha, rev) 336*760c253cSXin Li 337*760c253cSXin Li new_state = State() 338*760c253cSXin Li seen: Set[str] = set() 339*760c253cSXin Li 340*760c253cSXin Li new_state, new_reverts = locate_new_reverts_across_shas( 341*760c253cSXin Li llvm_dir, interesting_shas, state 342*760c253cSXin Li ) 343*760c253cSXin Li 344*760c253cSXin Li for revert_info in new_reverts: 345*760c253cSXin Li if revert_info.friendly_name in seen: 346*760c253cSXin Li continue 347*760c253cSXin Li seen.add(revert_info.friendly_name) 348*760c253cSXin Li for sha, reverted_sha in revert_info.new_reverts: 349*760c253cSXin Li try: 350*760c253cSXin Li # We upload reverts for all platforms by default, since there's 351*760c253cSXin Li # no real reason for them to be CrOS-specific. 352*760c253cSXin Li get_upstream_patch.get_from_upstream( 353*760c253cSXin Li chroot_path=chroot_path, 354*760c253cSXin Li create_cl=True, 355*760c253cSXin Li start_sha=reverted_sha, 356*760c253cSXin Li patches=[sha], 357*760c253cSXin Li reviewers=reviewers, 358*760c253cSXin Li cc=cc, 359*760c253cSXin Li platforms=(), 360*760c253cSXin Li ) 361*760c253cSXin Li except get_upstream_patch.CherrypickError as e: 362*760c253cSXin Li logging.info("%s, skipping...", str(e)) 363*760c253cSXin Li 364*760c253cSXin Li maybe_email_about_stale_heads( 365*760c253cSXin Li new_state, 366*760c253cSXin Li repository, 367*760c253cSXin Li recipients=_EmailRecipients( 368*760c253cSXin Li well_known=[], 369*760c253cSXin Li direct=reviewers + cc, 370*760c253cSXin Li ), 371*760c253cSXin Li prettify_sha=prettify_sha, 372*760c253cSXin Li is_dry_run=False, 373*760c253cSXin Li ) 374*760c253cSXin Li return new_state 375*760c253cSXin Li 376*760c253cSXin Li 377*760c253cSXin Lidef prettify_sha_for_email( 378*760c253cSXin Li sha: str, 379*760c253cSXin Li rev: int, 380*760c253cSXin Li) -> tiny_render.Piece: 381*760c253cSXin Li """Returns a piece of an email representing the given sha and its rev.""" 382*760c253cSXin Li # 12 is arbitrary, but should be unambiguous enough. 383*760c253cSXin Li short_sha = sha[:12] 384*760c253cSXin Li return tiny_render.Switch( 385*760c253cSXin Li text=f"r{rev} ({short_sha})", 386*760c253cSXin Li html=tiny_render.Link( 387*760c253cSXin Li href=f"https://github.com/llvm/llvm-project/commit/{sha}", 388*760c253cSXin Li inner=f"r{rev}", 389*760c253cSXin Li ), 390*760c253cSXin Li ) 391*760c253cSXin Li 392*760c253cSXin Li 393*760c253cSXin Lidef maybe_email_about_stale_heads( 394*760c253cSXin Li new_state: State, 395*760c253cSXin Li repository_name: str, 396*760c253cSXin Li recipients: _EmailRecipients, 397*760c253cSXin Li prettify_sha: Callable[[str], tiny_render.Piece], 398*760c253cSXin Li is_dry_run: bool, 399*760c253cSXin Li) -> bool: 400*760c253cSXin Li """Potentially send an email about stale HEADs in `new_state`. 401*760c253cSXin Li 402*760c253cSXin Li These emails are sent to notify users of the current HEADs detected by this 403*760c253cSXin Li script. They: 404*760c253cSXin Li - aren't meant to hurry LLVM rolls along, 405*760c253cSXin Li - are worded to avoid the implication that an LLVM roll is taking an 406*760c253cSXin Li excessive amount of time, and 407*760c253cSXin Li - are initially sent at the 2 month point of seeing the same HEAD. 408*760c253cSXin Li 409*760c253cSXin Li We've had multiple instances in the past of upstream changes (e.g., moving 410*760c253cSXin Li to other git branches or repos) leading to this revert checker silently 411*760c253cSXin Li checking a very old HEAD for months. The intent is to send emails when the 412*760c253cSXin Li correctness of the HEADs we're working with _might_ be wrong. 413*760c253cSXin Li """ 414*760c253cSXin Li logging.info("Checking HEAD freshness...") 415*760c253cSXin Li now = int(time.time()) 416*760c253cSXin Li stale = sorted( 417*760c253cSXin Li (name, info) 418*760c253cSXin Li for name, info in new_state.heads.items() 419*760c253cSXin Li if info.next_notification_timestamp <= now 420*760c253cSXin Li ) 421*760c253cSXin Li if not stale: 422*760c253cSXin Li logging.info("All HEADs are fresh-enough; no need to send an email.") 423*760c253cSXin Li return False 424*760c253cSXin Li 425*760c253cSXin Li stale_listings = [] 426*760c253cSXin Li 427*760c253cSXin Li for name, info in stale: 428*760c253cSXin Li days = (now - info.first_seen_timestamp) // ONE_DAY_SECS 429*760c253cSXin Li pretty_rev = prettify_sha(info.last_sha) 430*760c253cSXin Li stale_listings.append( 431*760c253cSXin Li f"{name} at {pretty_rev}, which was last updated ~{days} days ago." 432*760c253cSXin Li ) 433*760c253cSXin Li 434*760c253cSXin Li shas_are = "SHAs are" if len(stale_listings) > 1 else "SHA is" 435*760c253cSXin Li email_body = [ 436*760c253cSXin Li "Hi! This is a friendly notification that the current upstream LLVM " 437*760c253cSXin Li f"{shas_are} being tracked by the LLVM revert checker:", 438*760c253cSXin Li tiny_render.UnorderedList(stale_listings), 439*760c253cSXin Li tiny_render.line_break, 440*760c253cSXin Li "If that's still correct, great! If it looks wrong, the revert " 441*760c253cSXin Li "checker's SHA autodetection may need an update. Please file a bug " 442*760c253cSXin Li "at go/crostc-bug if an update is needed. Thanks!", 443*760c253cSXin Li ] 444*760c253cSXin Li 445*760c253cSXin Li email = _Email( 446*760c253cSXin Li subject=f"[revert-checker/{repository_name}] Tracked branch update", 447*760c253cSXin Li body=email_body, 448*760c253cSXin Li ) 449*760c253cSXin Li if is_dry_run: 450*760c253cSXin Li logging.info("Dry-run specified; would otherwise send email %s", email) 451*760c253cSXin Li else: 452*760c253cSXin Li _send_revert_email(recipients, email) 453*760c253cSXin Li 454*760c253cSXin Li next_notification = now + HEAD_STALENESS_ALERT_INTERVAL_SECS 455*760c253cSXin Li for _, info in stale: 456*760c253cSXin Li info.next_notification_timestamp = next_notification 457*760c253cSXin Li return True 458*760c253cSXin Li 459*760c253cSXin Li 460*760c253cSXin Lidef do_email( 461*760c253cSXin Li is_dry_run: bool, 462*760c253cSXin Li llvm_dir: str, 463*760c253cSXin Li repository: str, 464*760c253cSXin Li interesting_shas: List[Tuple[str, str]], 465*760c253cSXin Li state: State, 466*760c253cSXin Li recipients: _EmailRecipients, 467*760c253cSXin Li) -> State: 468*760c253cSXin Li def prettify_sha(sha: str) -> tiny_render.Piece: 469*760c253cSXin Li rev = get_llvm_hash.GetVersionFrom(llvm_dir, sha) 470*760c253cSXin Li return prettify_sha_for_email(sha, rev) 471*760c253cSXin Li 472*760c253cSXin Li def get_sha_description(sha: str) -> tiny_render.Piece: 473*760c253cSXin Li return subprocess.check_output( 474*760c253cSXin Li ["git", "log", "-n1", "--format=%s", sha], 475*760c253cSXin Li cwd=llvm_dir, 476*760c253cSXin Li encoding="utf-8", 477*760c253cSXin Li ).strip() 478*760c253cSXin Li 479*760c253cSXin Li new_state, new_reverts = locate_new_reverts_across_shas( 480*760c253cSXin Li llvm_dir, interesting_shas, state 481*760c253cSXin Li ) 482*760c253cSXin Li 483*760c253cSXin Li for revert_info in new_reverts: 484*760c253cSXin Li email = _generate_revert_email( 485*760c253cSXin Li repository, 486*760c253cSXin Li revert_info.friendly_name, 487*760c253cSXin Li revert_info.sha, 488*760c253cSXin Li prettify_sha, 489*760c253cSXin Li get_sha_description, 490*760c253cSXin Li revert_info.new_reverts, 491*760c253cSXin Li ) 492*760c253cSXin Li if is_dry_run: 493*760c253cSXin Li logging.info( 494*760c253cSXin Li "Would send email:\nSubject: %s\nBody:\n%s\n", 495*760c253cSXin Li email.subject, 496*760c253cSXin Li tiny_render.render_text_pieces(email.body), 497*760c253cSXin Li ) 498*760c253cSXin Li else: 499*760c253cSXin Li logging.info("Sending email with subject %r...", email.subject) 500*760c253cSXin Li _send_revert_email(recipients, email) 501*760c253cSXin Li logging.info("Email sent.") 502*760c253cSXin Li 503*760c253cSXin Li maybe_email_about_stale_heads( 504*760c253cSXin Li new_state, repository, recipients, prettify_sha, is_dry_run 505*760c253cSXin Li ) 506*760c253cSXin Li return new_state 507*760c253cSXin Li 508*760c253cSXin Li 509*760c253cSXin Lidef parse_args(argv: List[str]) -> argparse.Namespace: 510*760c253cSXin Li parser = argparse.ArgumentParser( 511*760c253cSXin Li description=__doc__, 512*760c253cSXin Li formatter_class=argparse.RawDescriptionHelpFormatter, 513*760c253cSXin Li ) 514*760c253cSXin Li parser.add_argument( 515*760c253cSXin Li "action", 516*760c253cSXin Li choices=["cherry-pick", "email", "dry-run"], 517*760c253cSXin Li help="Automatically cherry-pick upstream reverts, send an email, or " 518*760c253cSXin Li "write to stdout.", 519*760c253cSXin Li ) 520*760c253cSXin Li parser.add_argument( 521*760c253cSXin Li "--state_file", required=True, help="File to store persistent state in." 522*760c253cSXin Li ) 523*760c253cSXin Li parser.add_argument( 524*760c253cSXin Li "--llvm_dir", required=True, help="Up-to-date LLVM directory to use." 525*760c253cSXin Li ) 526*760c253cSXin Li parser.add_argument("--debug", action="store_true") 527*760c253cSXin Li parser.add_argument( 528*760c253cSXin Li "--reviewers", 529*760c253cSXin Li type=str, 530*760c253cSXin Li nargs="*", 531*760c253cSXin Li help=""" 532*760c253cSXin Li Requests reviews from REVIEWERS. All REVIEWERS must have existing 533*760c253cSXin Li accounts. 534*760c253cSXin Li """, 535*760c253cSXin Li ) 536*760c253cSXin Li parser.add_argument( 537*760c253cSXin Li "--cc", 538*760c253cSXin Li type=str, 539*760c253cSXin Li nargs="*", 540*760c253cSXin Li help=""" 541*760c253cSXin Li CCs the CL or email to the recipients. If in cherry-pick mode, all 542*760c253cSXin Li recipients must have Gerrit accounts. 543*760c253cSXin Li """, 544*760c253cSXin Li ) 545*760c253cSXin Li 546*760c253cSXin Li subparsers = parser.add_subparsers(dest="repository") 547*760c253cSXin Li subparsers.required = True 548*760c253cSXin Li 549*760c253cSXin Li chromeos_subparser = subparsers.add_parser("chromeos") 550*760c253cSXin Li chromeos_subparser.add_argument( 551*760c253cSXin Li "--chromeos_dir", 552*760c253cSXin Li required=True, 553*760c253cSXin Li help="Up-to-date CrOS directory to use.", 554*760c253cSXin Li ) 555*760c253cSXin Li 556*760c253cSXin Li android_subparser = subparsers.add_parser("android") 557*760c253cSXin Li android_subparser.add_argument( 558*760c253cSXin Li "--android_llvm_toolchain_dir", 559*760c253cSXin Li required=True, 560*760c253cSXin Li help="Up-to-date android-llvm-toolchain directory to use.", 561*760c253cSXin Li ) 562*760c253cSXin Li 563*760c253cSXin Li return parser.parse_args(argv) 564*760c253cSXin Li 565*760c253cSXin Li 566*760c253cSXin Lidef find_chroot( 567*760c253cSXin Li opts: argparse.Namespace, cc: List[str] 568*760c253cSXin Li) -> Tuple[str, List[Tuple[str, str]], _EmailRecipients]: 569*760c253cSXin Li if opts.repository == "chromeos": 570*760c253cSXin Li chroot_path = opts.chromeos_dir 571*760c253cSXin Li return ( 572*760c253cSXin Li chroot_path, 573*760c253cSXin Li _find_interesting_chromeos_shas(chroot_path), 574*760c253cSXin Li _EmailRecipients(well_known=["mage"], direct=cc), 575*760c253cSXin Li ) 576*760c253cSXin Li elif opts.repository == "android": 577*760c253cSXin Li if opts.action == "cherry-pick": 578*760c253cSXin Li raise RuntimeError( 579*760c253cSXin Li "android doesn't currently support automatic cherry-picking." 580*760c253cSXin Li ) 581*760c253cSXin Li 582*760c253cSXin Li chroot_path = opts.android_llvm_toolchain_dir 583*760c253cSXin Li return ( 584*760c253cSXin Li chroot_path, 585*760c253cSXin Li _find_interesting_android_shas(chroot_path), 586*760c253cSXin Li _EmailRecipients( 587*760c253cSXin Li well_known=[], 588*760c253cSXin Li direct=["[email protected]"] + cc, 589*760c253cSXin Li ), 590*760c253cSXin Li ) 591*760c253cSXin Li else: 592*760c253cSXin Li raise ValueError(f"Unknown repository {opts.repository}") 593*760c253cSXin Li 594*760c253cSXin Li 595*760c253cSXin Lidef main(argv: List[str]) -> int: 596*760c253cSXin Li opts = parse_args(argv) 597*760c253cSXin Li 598*760c253cSXin Li logging.basicConfig( 599*760c253cSXin Li format="%(asctime)s: %(levelname)s: " 600*760c253cSXin Li "%(filename)s:%(lineno)d: %(message)s", 601*760c253cSXin Li level=logging.DEBUG if opts.debug else logging.INFO, 602*760c253cSXin Li ) 603*760c253cSXin Li 604*760c253cSXin Li action = opts.action 605*760c253cSXin Li llvm_dir = opts.llvm_dir 606*760c253cSXin Li repository = opts.repository 607*760c253cSXin Li state_file = opts.state_file 608*760c253cSXin Li reviewers = opts.reviewers if opts.reviewers else [] 609*760c253cSXin Li cc = opts.cc if opts.cc else [] 610*760c253cSXin Li 611*760c253cSXin Li chroot_path, interesting_shas, recipients = find_chroot(opts, cc) 612*760c253cSXin Li logging.info("Interesting SHAs were %r", interesting_shas) 613*760c253cSXin Li 614*760c253cSXin Li state = _read_state(state_file) 615*760c253cSXin Li logging.info("Loaded state\n%s", pprint.pformat(state)) 616*760c253cSXin Li 617*760c253cSXin Li # We want to be as free of obvious side-effects as possible in case 618*760c253cSXin Li # something above breaks. Hence, action as late as possible. 619*760c253cSXin Li if action == "cherry-pick": 620*760c253cSXin Li new_state = do_cherrypick( 621*760c253cSXin Li chroot_path=chroot_path, 622*760c253cSXin Li llvm_dir=llvm_dir, 623*760c253cSXin Li repository=repository, 624*760c253cSXin Li interesting_shas=interesting_shas, 625*760c253cSXin Li state=state, 626*760c253cSXin Li reviewers=reviewers, 627*760c253cSXin Li cc=cc, 628*760c253cSXin Li ) 629*760c253cSXin Li else: 630*760c253cSXin Li new_state = do_email( 631*760c253cSXin Li is_dry_run=action == "dry-run", 632*760c253cSXin Li llvm_dir=llvm_dir, 633*760c253cSXin Li interesting_shas=interesting_shas, 634*760c253cSXin Li repository=repository, 635*760c253cSXin Li state=state, 636*760c253cSXin Li recipients=recipients, 637*760c253cSXin Li ) 638*760c253cSXin Li 639*760c253cSXin Li _write_state(state_file, new_state) 640*760c253cSXin Li return 0 641*760c253cSXin Li 642*760c253cSXin Li 643*760c253cSXin Liif __name__ == "__main__": 644*760c253cSXin Li sys.exit(main(sys.argv[1:])) 645