xref: /aosp_15_r20/external/toolchain-utils/llvm_tools/nightly_revert_checker.py (revision 760c253c1ed00ce9abd48f8546f08516e57485fe)
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