1*bb4ee6a4SAndroid Build Coastguard Worker#!/usr/bin/env python3 2*bb4ee6a4SAndroid Build Coastguard Worker# Copyright 2023 The ChromiumOS Authors 3*bb4ee6a4SAndroid Build Coastguard Worker# Use of this source code is governed by a BSD-style license that can be 4*bb4ee6a4SAndroid Build Coastguard Worker# found in the LICENSE file. 5*bb4ee6a4SAndroid Build Coastguard Worker 6*bb4ee6a4SAndroid Build Coastguard Worker""" 7*bb4ee6a4SAndroid Build Coastguard WorkerProvides helpers for accessing gerrit and listing files under version control. 8*bb4ee6a4SAndroid Build Coastguard Worker""" 9*bb4ee6a4SAndroid Build Coastguard Worker 10*bb4ee6a4SAndroid Build Coastguard Workerimport functools 11*bb4ee6a4SAndroid Build Coastguard Workerimport getpass 12*bb4ee6a4SAndroid Build Coastguard Workerimport json 13*bb4ee6a4SAndroid Build Coastguard Workerimport shutil 14*bb4ee6a4SAndroid Build Coastguard Workerimport sys 15*bb4ee6a4SAndroid Build Coastguard Workerfrom pathlib import Path 16*bb4ee6a4SAndroid Build Coastguard Workerfrom tempfile import gettempdir 17*bb4ee6a4SAndroid Build Coastguard Workerfrom typing import ( 18*bb4ee6a4SAndroid Build Coastguard Worker Any, 19*bb4ee6a4SAndroid Build Coastguard Worker Dict, 20*bb4ee6a4SAndroid Build Coastguard Worker List, 21*bb4ee6a4SAndroid Build Coastguard Worker cast, 22*bb4ee6a4SAndroid Build Coastguard Worker) 23*bb4ee6a4SAndroid Build Coastguard Worker 24*bb4ee6a4SAndroid Build Coastguard Workerfrom .command import quoted, cmd 25*bb4ee6a4SAndroid Build Coastguard Workerfrom .util import very_verbose 26*bb4ee6a4SAndroid Build Coastguard Worker 27*bb4ee6a4SAndroid Build Coastguard Worker# File where to store http headers for gcloud authentication 28*bb4ee6a4SAndroid Build Coastguard WorkerAUTH_HEADERS_FILE = Path(gettempdir()) / f"crosvm_gcloud_auth_headers_{getpass.getuser()}" 29*bb4ee6a4SAndroid Build Coastguard Worker 30*bb4ee6a4SAndroid Build Coastguard Worker"Url of crosvm's gerrit review host" 31*bb4ee6a4SAndroid Build Coastguard WorkerGERRIT_URL = "https://chromium-review.googlesource.com" 32*bb4ee6a4SAndroid Build Coastguard Worker 33*bb4ee6a4SAndroid Build Coastguard Worker 34*bb4ee6a4SAndroid Build Coastguard Workerdef all_tracked_files(): 35*bb4ee6a4SAndroid Build Coastguard Worker for line in cmd("git ls-files").lines(): 36*bb4ee6a4SAndroid Build Coastguard Worker file = Path(line) 37*bb4ee6a4SAndroid Build Coastguard Worker if file.is_file(): 38*bb4ee6a4SAndroid Build Coastguard Worker yield file 39*bb4ee6a4SAndroid Build Coastguard Worker 40*bb4ee6a4SAndroid Build Coastguard Worker 41*bb4ee6a4SAndroid Build Coastguard Workerdef find_source_files(extension: str, ignore: List[str] = []): 42*bb4ee6a4SAndroid Build Coastguard Worker for file in all_tracked_files(): 43*bb4ee6a4SAndroid Build Coastguard Worker if file.suffix != f".{extension}": 44*bb4ee6a4SAndroid Build Coastguard Worker continue 45*bb4ee6a4SAndroid Build Coastguard Worker if file.is_relative_to("third_party"): 46*bb4ee6a4SAndroid Build Coastguard Worker continue 47*bb4ee6a4SAndroid Build Coastguard Worker if str(file) in ignore: 48*bb4ee6a4SAndroid Build Coastguard Worker continue 49*bb4ee6a4SAndroid Build Coastguard Worker yield file 50*bb4ee6a4SAndroid Build Coastguard Worker 51*bb4ee6a4SAndroid Build Coastguard Worker 52*bb4ee6a4SAndroid Build Coastguard Workerdef find_scripts(path: Path, shebang: str): 53*bb4ee6a4SAndroid Build Coastguard Worker for file in path.glob("*"): 54*bb4ee6a4SAndroid Build Coastguard Worker if file.is_file() and file.open(errors="ignore").read(512).startswith(f"#!{shebang}"): 55*bb4ee6a4SAndroid Build Coastguard Worker yield file 56*bb4ee6a4SAndroid Build Coastguard Worker 57*bb4ee6a4SAndroid Build Coastguard Worker 58*bb4ee6a4SAndroid Build Coastguard Workerdef get_cookie_file(): 59*bb4ee6a4SAndroid Build Coastguard Worker path = cmd("git config http.cookiefile").stdout(check=False) 60*bb4ee6a4SAndroid Build Coastguard Worker return Path(path) if path else None 61*bb4ee6a4SAndroid Build Coastguard Worker 62*bb4ee6a4SAndroid Build Coastguard Worker 63*bb4ee6a4SAndroid Build Coastguard Workerdef get_gcloud_access_token(): 64*bb4ee6a4SAndroid Build Coastguard Worker if not shutil.which("gcloud"): 65*bb4ee6a4SAndroid Build Coastguard Worker return None 66*bb4ee6a4SAndroid Build Coastguard Worker return cmd("gcloud auth print-access-token").stdout(check=False) 67*bb4ee6a4SAndroid Build Coastguard Worker 68*bb4ee6a4SAndroid Build Coastguard Worker 69*bb4ee6a4SAndroid Build Coastguard Worker@functools.lru_cache(maxsize=None) 70*bb4ee6a4SAndroid Build Coastguard Workerdef curl_with_git_auth(): 71*bb4ee6a4SAndroid Build Coastguard Worker """ 72*bb4ee6a4SAndroid Build Coastguard Worker Returns a curl `Command` instance set up to use the same HTTP credentials as git. 73*bb4ee6a4SAndroid Build Coastguard Worker 74*bb4ee6a4SAndroid Build Coastguard Worker This currently supports two methods: 75*bb4ee6a4SAndroid Build Coastguard Worker - git cookies (the default) 76*bb4ee6a4SAndroid Build Coastguard Worker - gcloud 77*bb4ee6a4SAndroid Build Coastguard Worker 78*bb4ee6a4SAndroid Build Coastguard Worker Most developers will use git cookies, which are passed to curl. 79*bb4ee6a4SAndroid Build Coastguard Worker 80*bb4ee6a4SAndroid Build Coastguard Worker glloud for authorization can be enabled in git via `git config credential.helper gcloud.sh`. 81*bb4ee6a4SAndroid Build Coastguard Worker If enabled in git, this command will also return a curl command using a gloud access token. 82*bb4ee6a4SAndroid Build Coastguard Worker """ 83*bb4ee6a4SAndroid Build Coastguard Worker helper = cmd("git config credential.helper").stdout(check=False) 84*bb4ee6a4SAndroid Build Coastguard Worker 85*bb4ee6a4SAndroid Build Coastguard Worker if not helper: 86*bb4ee6a4SAndroid Build Coastguard Worker cookie_file = get_cookie_file() 87*bb4ee6a4SAndroid Build Coastguard Worker if not cookie_file or not cookie_file.is_file(): 88*bb4ee6a4SAndroid Build Coastguard Worker raise Exception("git http cookiefile is not available.") 89*bb4ee6a4SAndroid Build Coastguard Worker return cmd("curl --cookie", cookie_file) 90*bb4ee6a4SAndroid Build Coastguard Worker 91*bb4ee6a4SAndroid Build Coastguard Worker if helper.endswith("gcloud.sh"): 92*bb4ee6a4SAndroid Build Coastguard Worker token = get_gcloud_access_token() 93*bb4ee6a4SAndroid Build Coastguard Worker if not token: 94*bb4ee6a4SAndroid Build Coastguard Worker raise Exception("Cannot get gcloud access token.") 95*bb4ee6a4SAndroid Build Coastguard Worker # File where to store http headers for gcloud authentication 96*bb4ee6a4SAndroid Build Coastguard Worker AUTH_HEADERS_FILE = Path(gettempdir()) / f"crosvm_gcloud_auth_headers_{getpass.getuser()}" 97*bb4ee6a4SAndroid Build Coastguard Worker 98*bb4ee6a4SAndroid Build Coastguard Worker # Write token to a header file so it will not appear in logs or error messages. 99*bb4ee6a4SAndroid Build Coastguard Worker AUTH_HEADERS_FILE.write_text(f"Authorization: Bearer {token}") 100*bb4ee6a4SAndroid Build Coastguard Worker return cmd(f"curl -H @{AUTH_HEADERS_FILE}") 101*bb4ee6a4SAndroid Build Coastguard Worker 102*bb4ee6a4SAndroid Build Coastguard Worker raise Exception(f"Unsupported git credentials.helper: {helper}") 103*bb4ee6a4SAndroid Build Coastguard Worker 104*bb4ee6a4SAndroid Build Coastguard Worker 105*bb4ee6a4SAndroid Build Coastguard Workerdef strip_xssi(response: str): 106*bb4ee6a4SAndroid Build Coastguard Worker # See https://gerrit-review.googlesource.com/Documentation/rest-api.html#output 107*bb4ee6a4SAndroid Build Coastguard Worker assert response.startswith(")]}'\n") 108*bb4ee6a4SAndroid Build Coastguard Worker return response[5:] 109*bb4ee6a4SAndroid Build Coastguard Worker 110*bb4ee6a4SAndroid Build Coastguard Worker 111*bb4ee6a4SAndroid Build Coastguard Workerdef gerrit_api_get(path: str): 112*bb4ee6a4SAndroid Build Coastguard Worker response = cmd(f"curl --silent --fail {GERRIT_URL}/{path}").stdout() 113*bb4ee6a4SAndroid Build Coastguard Worker return json.loads(strip_xssi(response)) 114*bb4ee6a4SAndroid Build Coastguard Worker 115*bb4ee6a4SAndroid Build Coastguard Worker 116*bb4ee6a4SAndroid Build Coastguard Workerdef gerrit_api_post(path: str, body: Any): 117*bb4ee6a4SAndroid Build Coastguard Worker response = curl_with_git_auth()( 118*bb4ee6a4SAndroid Build Coastguard Worker "--silent --fail", 119*bb4ee6a4SAndroid Build Coastguard Worker "-X POST", 120*bb4ee6a4SAndroid Build Coastguard Worker "-H", 121*bb4ee6a4SAndroid Build Coastguard Worker quoted("Content-Type: application/json"), 122*bb4ee6a4SAndroid Build Coastguard Worker "-d", 123*bb4ee6a4SAndroid Build Coastguard Worker quoted(json.dumps(body)), 124*bb4ee6a4SAndroid Build Coastguard Worker f"{GERRIT_URL}/a/{path}", 125*bb4ee6a4SAndroid Build Coastguard Worker ).stdout() 126*bb4ee6a4SAndroid Build Coastguard Worker if very_verbose(): 127*bb4ee6a4SAndroid Build Coastguard Worker print("Response:", response) 128*bb4ee6a4SAndroid Build Coastguard Worker return json.loads(strip_xssi(response)) 129*bb4ee6a4SAndroid Build Coastguard Worker 130*bb4ee6a4SAndroid Build Coastguard Worker 131*bb4ee6a4SAndroid Build Coastguard Workerclass GerritChange(object): 132*bb4ee6a4SAndroid Build Coastguard Worker """ 133*bb4ee6a4SAndroid Build Coastguard Worker Class to interact with the gerrit /changes/ API. 134*bb4ee6a4SAndroid Build Coastguard Worker 135*bb4ee6a4SAndroid Build Coastguard Worker For information on the data format returned by the API, see: 136*bb4ee6a4SAndroid Build Coastguard Worker https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-info 137*bb4ee6a4SAndroid Build Coastguard Worker """ 138*bb4ee6a4SAndroid Build Coastguard Worker 139*bb4ee6a4SAndroid Build Coastguard Worker id: str 140*bb4ee6a4SAndroid Build Coastguard Worker _data: Any 141*bb4ee6a4SAndroid Build Coastguard Worker 142*bb4ee6a4SAndroid Build Coastguard Worker def __init__(self, data: Any): 143*bb4ee6a4SAndroid Build Coastguard Worker self._data = data 144*bb4ee6a4SAndroid Build Coastguard Worker self.id = data["id"] 145*bb4ee6a4SAndroid Build Coastguard Worker 146*bb4ee6a4SAndroid Build Coastguard Worker @functools.cached_property 147*bb4ee6a4SAndroid Build Coastguard Worker def _details(self) -> Any: 148*bb4ee6a4SAndroid Build Coastguard Worker return gerrit_api_get(f"changes/{self.id}/detail") 149*bb4ee6a4SAndroid Build Coastguard Worker 150*bb4ee6a4SAndroid Build Coastguard Worker @functools.cached_property 151*bb4ee6a4SAndroid Build Coastguard Worker def _messages(self) -> List[Any]: 152*bb4ee6a4SAndroid Build Coastguard Worker return gerrit_api_get(f"changes/{self.id}/messages") 153*bb4ee6a4SAndroid Build Coastguard Worker 154*bb4ee6a4SAndroid Build Coastguard Worker @property 155*bb4ee6a4SAndroid Build Coastguard Worker def status(self): 156*bb4ee6a4SAndroid Build Coastguard Worker return cast(str, self._data["status"]) 157*bb4ee6a4SAndroid Build Coastguard Worker 158*bb4ee6a4SAndroid Build Coastguard Worker def get_votes(self, label_name: str) -> List[int]: 159*bb4ee6a4SAndroid Build Coastguard Worker "Returns the list of votes on `label_name`" 160*bb4ee6a4SAndroid Build Coastguard Worker label_info = self._details.get("labels", {}).get(label_name) 161*bb4ee6a4SAndroid Build Coastguard Worker votes = label_info.get("all", []) 162*bb4ee6a4SAndroid Build Coastguard Worker return [cast(int, v.get("value")) for v in votes] 163*bb4ee6a4SAndroid Build Coastguard Worker 164*bb4ee6a4SAndroid Build Coastguard Worker def get_messages_by(self, email: str) -> List[str]: 165*bb4ee6a4SAndroid Build Coastguard Worker "Returns all messages posted by the user with the specified `email`." 166*bb4ee6a4SAndroid Build Coastguard Worker return [m["message"] for m in self._messages if m["author"].get("email") == email] 167*bb4ee6a4SAndroid Build Coastguard Worker 168*bb4ee6a4SAndroid Build Coastguard Worker def review(self, message: str, labels: Dict[str, int]): 169*bb4ee6a4SAndroid Build Coastguard Worker "Post review `message` and set the specified review `labels`" 170*bb4ee6a4SAndroid Build Coastguard Worker print("Posting on", self, ":", message, labels) 171*bb4ee6a4SAndroid Build Coastguard Worker gerrit_api_post( 172*bb4ee6a4SAndroid Build Coastguard Worker f"changes/{self.id}/revisions/current/review", 173*bb4ee6a4SAndroid Build Coastguard Worker {"message": message, "labels": labels}, 174*bb4ee6a4SAndroid Build Coastguard Worker ) 175*bb4ee6a4SAndroid Build Coastguard Worker 176*bb4ee6a4SAndroid Build Coastguard Worker def abandon(self, message: str): 177*bb4ee6a4SAndroid Build Coastguard Worker print("Abandoning", self, ":", message) 178*bb4ee6a4SAndroid Build Coastguard Worker gerrit_api_post(f"changes/{self.id}/abandon", {"message": message}) 179*bb4ee6a4SAndroid Build Coastguard Worker 180*bb4ee6a4SAndroid Build Coastguard Worker @classmethod 181*bb4ee6a4SAndroid Build Coastguard Worker def query(cls, *queries: str): 182*bb4ee6a4SAndroid Build Coastguard Worker "Returns a list of gerrit changes matching the provided list of queries." 183*bb4ee6a4SAndroid Build Coastguard Worker return [cls(c) for c in gerrit_api_get(f"changes/?q={'+'.join(queries)}")] 184*bb4ee6a4SAndroid Build Coastguard Worker 185*bb4ee6a4SAndroid Build Coastguard Worker def short_url(self): 186*bb4ee6a4SAndroid Build Coastguard Worker return f"http://crrev.com/c/{self._data['_number']}" 187*bb4ee6a4SAndroid Build Coastguard Worker 188*bb4ee6a4SAndroid Build Coastguard Worker def __str__(self): 189*bb4ee6a4SAndroid Build Coastguard Worker return self.short_url() 190*bb4ee6a4SAndroid Build Coastguard Worker 191*bb4ee6a4SAndroid Build Coastguard Worker def pretty_info(self): 192*bb4ee6a4SAndroid Build Coastguard Worker return f"{self} - {self._data['subject']}" 193*bb4ee6a4SAndroid Build Coastguard Worker 194*bb4ee6a4SAndroid Build Coastguard Worker 195*bb4ee6a4SAndroid Build Coastguard Workerif __name__ == "__main__": 196*bb4ee6a4SAndroid Build Coastguard Worker import doctest 197*bb4ee6a4SAndroid Build Coastguard Worker 198*bb4ee6a4SAndroid Build Coastguard Worker (failures, num_tests) = doctest.testmod(optionflags=doctest.ELLIPSIS) 199*bb4ee6a4SAndroid Build Coastguard Worker sys.exit(1 if failures > 0 else 0) 200