1# Copyright 2024 The ChromiumOS Authors 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5"""Shared utilities for working with git.""" 6 7import contextlib 8import logging 9from pathlib import Path 10import re 11import shlex 12import subprocess 13import tempfile 14from typing import Generator, Iterable, List 15 16 17# Email address used to tag the detective as a reviewer. 18REVIEWER_DETECTIVE = "[email protected]" 19 20 21def _parse_cls_from_upload_output(upload_output: str) -> List[int]: 22 """Returns the CL number in the given upload output.""" 23 id_regex = re.compile( 24 r"^remote:\s+https://" 25 r"(?:chromium|chrome-internal)" 26 r"-review\S+/\+/(\d+)\s", 27 re.MULTILINE, 28 ) 29 30 results = id_regex.findall(upload_output) 31 if not results: 32 raise ValueError( 33 f"Wanted at least one match for {id_regex} in {upload_output!r}; " 34 "found 0" 35 ) 36 return [int(x) for x in results] 37 38 39def upload_to_gerrit( 40 git_repo: Path, 41 remote: str, 42 branch: str, 43 reviewers: Iterable[str] = (), 44 cc: Iterable[str] = (), 45 ref: str = "HEAD", 46) -> List[int]: 47 """Uploads `ref` to gerrit, optionally adding reviewers/CCs.""" 48 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#reviewers 49 # for more info on the `%` params. 50 option_list = [f"r={x}" for x in reviewers] 51 option_list += (f"cc={x}" for x in cc) 52 if option_list: 53 trailing_options = "%" + ",".join(option_list) 54 else: 55 trailing_options = "" 56 57 run_result = subprocess.run( 58 [ 59 "git", 60 "push", 61 remote, 62 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#reviewers 63 # for more info on the `%` params. 64 f"{ref}:refs/for/{branch}{trailing_options}", 65 ], 66 cwd=git_repo, 67 check=False, 68 stdin=subprocess.DEVNULL, 69 stdout=subprocess.PIPE, 70 stderr=subprocess.STDOUT, 71 encoding="utf-8", 72 ) 73 74 logging.info( 75 "`git push`ing %s to %s/%s had this output:\n%s", 76 ref, 77 remote, 78 branch, 79 run_result.stdout, 80 ) 81 run_result.check_returncode() 82 return _parse_cls_from_upload_output(run_result.stdout) 83 84 85def try_set_autosubmit_labels(cwd: Path, cl_id: int) -> None: 86 """Sets autosubmit on a CL. Logs - not raises - on failure. 87 88 This sets a series of convenience labels on the given cl_number, so landing 89 it (e.g., for the detective) is as easy as possible. 90 91 Args: 92 cwd: the directory that the `gerrit` tool should be run in. Anywhere in 93 a ChromeOS tree will do. The `gerrit` command fails if it isn't run 94 from within a ChromeOS tree. 95 cl_id: The CL number to apply labels to. 96 """ 97 gerrit_cl_id = str(cl_id) 98 gerrit_commands = ( 99 ["gerrit", "label-as", gerrit_cl_id, "1"], 100 ["gerrit", "label-cq", gerrit_cl_id, "1"], 101 ["gerrit", "label-v", gerrit_cl_id, "1"], 102 ) 103 for cmd in gerrit_commands: 104 # Run the gerrit commands inside of toolchain_utils, since `gerrit` 105 # needs to be run inside of a ChromeOS tree to work. While 106 # `toolchain-utils` can be checked out on its own, that's not how this 107 # script is expeted to be used. 108 return_code = subprocess.run( 109 cmd, 110 cwd=cwd, 111 check=False, 112 stdin=subprocess.DEVNULL, 113 ).returncode 114 if return_code: 115 logging.warning( 116 "Failed to run gerrit command %s. Ignoring.", 117 shlex.join(cmd), 118 ) 119 120 121@contextlib.contextmanager 122def create_worktree(git_directory: Path) -> Generator[Path, None, None]: 123 """Creates a temp worktree of `git_directory`, yielding the result.""" 124 with tempfile.TemporaryDirectory(prefix="update_kernel_afdo_") as t: 125 tempdir = Path(t) 126 logging.info( 127 "Establishing worktree of %s in %s", git_directory, tempdir 128 ) 129 subprocess.run( 130 [ 131 "git", 132 "worktree", 133 "add", 134 "--detach", 135 "--force", 136 tempdir, 137 ], 138 cwd=git_directory, 139 check=True, 140 stdin=subprocess.DEVNULL, 141 ) 142 143 try: 144 yield tempdir 145 finally: 146 # Explicitly `git worktree remove` here, so the parent worktree's 147 # metadata is cleaned up promptly. 148 subprocess.run( 149 [ 150 "git", 151 "worktree", 152 "remove", 153 "--force", 154 tempdir, 155 ], 156 cwd=git_directory, 157 check=False, 158 stdin=subprocess.DEVNULL, 159 ) 160