mirror of
https://github.com/servo/servo.git
synced 2025-06-04 07:35:36 +00:00
Requires servo/servo#37045 for deps and config. Testing: No need for tests to test tests. Fixes: servo/servo#37041 --------- Signed-off-by: zefr0x <zer0-x.7ty50@aleeas.com>
169 lines
5.6 KiB
Python
169 lines
5.6 KiB
Python
# Copyright 2023 The Servo Project Developers. See the COPYRIGHT
|
|
# file at the top-level directory of this distribution.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
|
# http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
|
# <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
|
|
# option. This file may not be copied, modified, or distributed
|
|
# except according to those terms.
|
|
|
|
# pylint: disable=missing-docstring
|
|
|
|
"""This modules contains some abstractions of GitHub repositories. It could one
|
|
day be entirely replaced with something like PyGithub."""
|
|
|
|
# This allows using types that are defined later in the file.
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import urllib
|
|
|
|
from typing import Optional, TYPE_CHECKING
|
|
|
|
import requests
|
|
|
|
if TYPE_CHECKING:
|
|
from . import WPTSync
|
|
|
|
USER_AGENT = "Servo web-platform-test sync service"
|
|
TIMEOUT = 30 # 30 seconds
|
|
|
|
|
|
def authenticated(sync: WPTSync, method, url, json=None) -> requests.Response:
|
|
logging.info(" → Request: %s %s", method, url)
|
|
if json:
|
|
logging.info(" → Request JSON: %s", json)
|
|
|
|
headers = {
|
|
"Authorization": f"Bearer {sync.github_api_token}",
|
|
"User-Agent": USER_AGENT,
|
|
}
|
|
|
|
url = urllib.parse.urljoin(sync.github_api_url, url)
|
|
response = requests.request(method, url, headers=headers, json=json, timeout=TIMEOUT)
|
|
if int(response.status_code / 100) != 2:
|
|
raise ValueError(f"Got unexpected {response.status_code} response: {response.text}")
|
|
return response
|
|
|
|
|
|
class GithubRepository:
|
|
"""
|
|
This class allows interacting with a single GitHub repository.
|
|
"""
|
|
|
|
def __init__(self, sync: WPTSync, repo: str, default_branch: str):
|
|
self.sync = sync
|
|
self.repo = repo
|
|
self.default_branch = default_branch
|
|
self.org = repo.split("/")[0]
|
|
self.pulls_url = f"repos/{self.repo}/pulls"
|
|
|
|
def __str__(self):
|
|
return self.repo
|
|
|
|
def get_pull_request(self, number: int) -> PullRequest:
|
|
return PullRequest(self, number)
|
|
|
|
def get_branch(self, name: str) -> GithubBranch:
|
|
return GithubBranch(self, name)
|
|
|
|
def get_open_pull_request_for_branch(self, github_username: str, branch: GithubBranch) -> Optional[PullRequest]:
|
|
"""If this repository has an open pull request with the
|
|
given source head reference targeting the main branch,
|
|
return the first matching pull request, otherwise return None."""
|
|
|
|
params = "+".join(
|
|
[
|
|
"is:pr",
|
|
"state:open",
|
|
f"repo:{self.repo}",
|
|
f"author:{github_username}",
|
|
f"head:{branch.name}",
|
|
]
|
|
)
|
|
response = authenticated(self.sync, "GET", f"search/issues?q={params}")
|
|
if int(response.status_code / 100) != 2:
|
|
return None
|
|
|
|
json = response.json()
|
|
if not isinstance(json, dict) or "total_count" not in json or "items" not in json:
|
|
raise ValueError(f"Got unexpected response from GitHub search: {response.text}")
|
|
|
|
if json["total_count"] < 1:
|
|
return None
|
|
|
|
return self.get_pull_request(json["items"][0]["number"])
|
|
|
|
def open_pull_request(self, branch: GithubBranch, title: str, body: str):
|
|
data = {
|
|
"title": title,
|
|
"head": branch.get_pr_head_reference_for_repo(self),
|
|
"base": self.default_branch,
|
|
"body": body,
|
|
"maintainer_can_modify": False,
|
|
}
|
|
response = authenticated(self.sync, "POST", self.pulls_url, json=data)
|
|
return self.get_pull_request(response.json()["number"])
|
|
|
|
|
|
class GithubBranch:
|
|
def __init__(self, repo: GithubRepository, branch_name: str):
|
|
self.repo = repo
|
|
self.name = branch_name
|
|
|
|
def __str__(self):
|
|
return f"{self.repo}/{self.name}"
|
|
|
|
def get_pr_head_reference_for_repo(self, other_repo: GithubRepository) -> str:
|
|
"""Get the head reference to use in pull requests for the given repository.
|
|
If the organization is the same this is just `<branch>` otherwise
|
|
it will be `<org>:<branch>`."""
|
|
if self.repo.org == other_repo.org:
|
|
return self.name
|
|
return f"{self.repo.org}:{self.name}"
|
|
|
|
|
|
class PullRequest:
|
|
"""
|
|
This class allows interacting with a single pull request on GitHub.
|
|
"""
|
|
|
|
def __init__(self, repo: GithubRepository, number: int):
|
|
self.repo = repo
|
|
self.context = repo.sync
|
|
self.number = number
|
|
self.base_url = f"repos/{self.repo.repo}/pulls/{self.number}"
|
|
self.base_issues_url = f"repos/{self.repo.repo}/issues/{self.number}"
|
|
|
|
def __str__(self):
|
|
return f"{self.repo}#{self.number}"
|
|
|
|
def api(self, *args, **kwargs) -> requests.Response:
|
|
return authenticated(self.context, *args, **kwargs)
|
|
|
|
def leave_comment(self, comment: str):
|
|
return self.api("POST", f"{self.base_issues_url}/comments", json={"body": comment})
|
|
|
|
def change(
|
|
self,
|
|
state: Optional[str] = None,
|
|
title: Optional[str] = None,
|
|
body: Optional[str] = None,
|
|
):
|
|
data = {}
|
|
if title:
|
|
data["title"] = title
|
|
if body:
|
|
data["body"] = body
|
|
if state:
|
|
data["state"] = state
|
|
return self.api("PATCH", self.base_url, json=data)
|
|
|
|
def remove_label(self, label: str):
|
|
self.api("DELETE", f"{self.base_issues_url}/labels/{label}")
|
|
|
|
def add_labels(self, labels: list[str]):
|
|
self.api("POST", f"{self.base_issues_url}/labels", json=labels)
|
|
|
|
def merge(self):
|
|
self.api("PUT", f"{self.base_url}/merge", json={"merge_method": "rebase"})
|