xref: /aosp_15_r20/external/crosvm/tools/impl/vcs.py (revision bb4ee6a4ae7042d18b07a98463b9c8b875e44b39)
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