crosvm/tools/impl/vcs.py
Dennis Kempin 11a48d4cca tools: refactoring: Split common.py into multiple files
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>
2023-05-02 17:04:27 +00:00

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)