mirror of
https://chromium.googlesource.com/crosvm/crosvm
synced 2024-11-24 20:48:55 +00:00
11a48d4cca
This is a refactoring with no functionality change. The common.py file is split up so we will be able to use the parts that have no third party dependencies separately. For now, keep common.py and re-export everything, so we do not introduce issues in other tools. BUG=b:267499599 TEST=presubmit Change-Id: Idf6d45bd90f5cf448fb9dd88df540af3da0f7f88 Reviewed-on: https://chromium-review.googlesource.com/c/crosvm/crosvm/+/4482141 Reviewed-by: Zihan Chen <zihanchen@google.com> Commit-Queue: Dennis Kempin <denniskempin@google.com>
199 lines
6 KiB
Python
199 lines
6 KiB
Python
#!/usr/bin/env python3
|
|
# Copyright 2023 The ChromiumOS Authors
|
|
# Use of this source code is governed by a BSD-style license that can be
|
|
# found in the LICENSE file.
|
|
|
|
"""
|
|
Provides helpers for accessing gerrit and listing files under version control.
|
|
"""
|
|
|
|
import functools
|
|
import getpass
|
|
import json
|
|
import shutil
|
|
import sys
|
|
from pathlib import Path
|
|
from tempfile import gettempdir
|
|
from typing import (
|
|
Any,
|
|
Dict,
|
|
List,
|
|
cast,
|
|
)
|
|
|
|
from .command import quoted, cmd
|
|
from .util import very_verbose
|
|
|
|
# File where to store http headers for gcloud authentication
|
|
AUTH_HEADERS_FILE = Path(gettempdir()) / f"crosvm_gcloud_auth_headers_{getpass.getuser()}"
|
|
|
|
"Url of crosvm's gerrit review host"
|
|
GERRIT_URL = "https://chromium-review.googlesource.com"
|
|
|
|
|
|
def all_tracked_files():
|
|
for line in cmd("git ls-files").lines():
|
|
file = Path(line)
|
|
if file.is_file():
|
|
yield file
|
|
|
|
|
|
def find_source_files(extension: str, ignore: List[str] = []):
|
|
for file in all_tracked_files():
|
|
if file.suffix != f".{extension}":
|
|
continue
|
|
if file.is_relative_to("third_party"):
|
|
continue
|
|
if str(file) in ignore:
|
|
continue
|
|
yield file
|
|
|
|
|
|
def find_scripts(path: Path, shebang: str):
|
|
for file in path.glob("*"):
|
|
if file.is_file() and file.open(errors="ignore").read(512).startswith(f"#!{shebang}"):
|
|
yield file
|
|
|
|
|
|
def get_cookie_file():
|
|
path = cmd("git config http.cookiefile").stdout(check=False)
|
|
return Path(path) if path else None
|
|
|
|
|
|
def get_gcloud_access_token():
|
|
if not shutil.which("gcloud"):
|
|
return None
|
|
return cmd("gcloud auth print-access-token").stdout(check=False)
|
|
|
|
|
|
@functools.lru_cache(maxsize=None)
|
|
def curl_with_git_auth():
|
|
"""
|
|
Returns a curl `Command` instance set up to use the same HTTP credentials as git.
|
|
|
|
This currently supports two methods:
|
|
- git cookies (the default)
|
|
- gcloud
|
|
|
|
Most developers will use git cookies, which are passed to curl.
|
|
|
|
glloud for authorization can be enabled in git via `git config credential.helper gcloud.sh`.
|
|
If enabled in git, this command will also return a curl command using a gloud access token.
|
|
"""
|
|
helper = cmd("git config credential.helper").stdout(check=False)
|
|
|
|
if not helper:
|
|
cookie_file = get_cookie_file()
|
|
if not cookie_file or not cookie_file.is_file():
|
|
raise Exception("git http cookiefile is not available.")
|
|
return cmd("curl --cookie", cookie_file)
|
|
|
|
if helper.endswith("gcloud.sh"):
|
|
token = get_gcloud_access_token()
|
|
if not token:
|
|
raise Exception("Cannot get gcloud access token.")
|
|
# File where to store http headers for gcloud authentication
|
|
AUTH_HEADERS_FILE = Path(gettempdir()) / f"crosvm_gcloud_auth_headers_{getpass.getuser()}"
|
|
|
|
# Write token to a header file so it will not appear in logs or error messages.
|
|
AUTH_HEADERS_FILE.write_text(f"Authorization: Bearer {token}")
|
|
return cmd(f"curl -H @{AUTH_HEADERS_FILE}")
|
|
|
|
raise Exception(f"Unsupported git credentials.helper: {helper}")
|
|
|
|
|
|
def strip_xssi(response: str):
|
|
# See https://gerrit-review.googlesource.com/Documentation/rest-api.html#output
|
|
assert response.startswith(")]}'\n")
|
|
return response[5:]
|
|
|
|
|
|
def gerrit_api_get(path: str):
|
|
response = cmd(f"curl --silent --fail {GERRIT_URL}/{path}").stdout()
|
|
return json.loads(strip_xssi(response))
|
|
|
|
|
|
def gerrit_api_post(path: str, body: Any):
|
|
response = curl_with_git_auth()(
|
|
"--silent --fail",
|
|
"-X POST",
|
|
"-H",
|
|
quoted("Content-Type: application/json"),
|
|
"-d",
|
|
quoted(json.dumps(body)),
|
|
f"{GERRIT_URL}/a/{path}",
|
|
).stdout()
|
|
if very_verbose():
|
|
print("Response:", response)
|
|
return json.loads(strip_xssi(response))
|
|
|
|
|
|
class GerritChange(object):
|
|
"""
|
|
Class to interact with the gerrit /changes/ API.
|
|
|
|
For information on the data format returned by the API, see:
|
|
https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-info
|
|
"""
|
|
|
|
id: str
|
|
_data: Any
|
|
|
|
def __init__(self, data: Any):
|
|
self._data = data
|
|
self.id = data["id"]
|
|
|
|
@functools.cached_property
|
|
def _details(self) -> Any:
|
|
return gerrit_api_get(f"changes/{self.id}/detail")
|
|
|
|
@functools.cached_property
|
|
def _messages(self) -> List[Any]:
|
|
return gerrit_api_get(f"changes/{self.id}/messages")
|
|
|
|
@property
|
|
def status(self):
|
|
return cast(str, self._data["status"])
|
|
|
|
def get_votes(self, label_name: str) -> List[int]:
|
|
"Returns the list of votes on `label_name`"
|
|
label_info = self._details.get("labels", {}).get(label_name)
|
|
votes = label_info.get("all", [])
|
|
return [cast(int, v.get("value")) for v in votes]
|
|
|
|
def get_messages_by(self, email: str) -> List[str]:
|
|
"Returns all messages posted by the user with the specified `email`."
|
|
return [m["message"] for m in self._messages if m["author"].get("email") == email]
|
|
|
|
def review(self, message: str, labels: Dict[str, int]):
|
|
"Post review `message` and set the specified review `labels`"
|
|
print("Posting on", self, ":", message, labels)
|
|
gerrit_api_post(
|
|
f"changes/{self.id}/revisions/current/review",
|
|
{"message": message, "labels": labels},
|
|
)
|
|
|
|
def abandon(self, message: str):
|
|
print("Abandoning", self, ":", message)
|
|
gerrit_api_post(f"changes/{self.id}/abandon", {"message": message})
|
|
|
|
@classmethod
|
|
def query(cls, *queries: str):
|
|
"Returns a list of gerrit changes matching the provided list of queries."
|
|
return [cls(c) for c in gerrit_api_get(f"changes/?q={'+'.join(queries)}")]
|
|
|
|
def short_url(self):
|
|
return f"http://crrev.com/c/{self._data['_number']}"
|
|
|
|
def __str__(self):
|
|
return self.short_url()
|
|
|
|
def pretty_info(self):
|
|
return f"{self} - {self._data['subject']}"
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import doctest
|
|
|
|
(failures, num_tests) = doctest.testmod(optionflags=doctest.ELLIPSIS)
|
|
sys.exit(1 if failures > 0 else 0)
|