Not all branches have a PR and code generally handled that case fine, just this assert didn't treat it as a valid result.
245 lines
8.5 KiB
Python
245 lines
8.5 KiB
Python
#!/usr/bin/env python3
|
|
# SPDX-FileCopyrightText: 2023 Blender Authors
|
|
#
|
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
# Simple module for inspecting GITEA users, pulls and issues.
|
|
|
|
__all__ = (
|
|
"git_username_detect",
|
|
"gitea_json_activities_get",
|
|
"gitea_json_pull_request_by_base_and_head_get",
|
|
"gitea_json_issue_events_filter",
|
|
"gitea_json_issue_get",
|
|
"gitea_json_issues_search",
|
|
"gitea_user_get",
|
|
)
|
|
|
|
import datetime
|
|
import json
|
|
import urllib.error
|
|
import urllib.parse
|
|
import urllib.request
|
|
|
|
from typing import (
|
|
Any,
|
|
)
|
|
|
|
BASE_API_URL = "https://projects.blender.org/api/v1"
|
|
|
|
|
|
def url_json_get(url: str, quiet: bool = False) -> dict[str, Any] | list[dict[str, Any]] | None:
|
|
try:
|
|
# Make the HTTP request and store the response in a 'response' object
|
|
response = urllib.request.urlopen(url)
|
|
except urllib.error.URLError as ex:
|
|
if not quiet:
|
|
print(url)
|
|
print("Error making HTTP request:", ex)
|
|
return None
|
|
|
|
# Convert the response content to a JSON object containing the user information.
|
|
result = json.loads(response.read())
|
|
assert result is None or isinstance(result, (dict, list))
|
|
return result
|
|
|
|
|
|
def url_json_get_all_pages(
|
|
url: str,
|
|
verbose: bool = False,
|
|
) -> list[dict[str, Any]]:
|
|
result: list[dict[str, Any]] = []
|
|
page = 1
|
|
while True:
|
|
if verbose:
|
|
print(f"Requesting page {page}", end="\r", flush=True)
|
|
|
|
if page == 1:
|
|
# XXX: In some cases, a bug prevents using the `page` and `limit` parameters if the page is 1
|
|
result_page = url_json_get(url)
|
|
else:
|
|
separator = '&' if urllib.parse.urlparse(url).query else '?'
|
|
result_page = url_json_get(f"{url}{separator}page={page}")
|
|
|
|
if not result_page:
|
|
break
|
|
assert isinstance(result_page, list)
|
|
result.extend(result_page)
|
|
|
|
if len(result_page) == 0:
|
|
break
|
|
|
|
page += 1
|
|
|
|
return result
|
|
|
|
|
|
def gitea_user_get(username: str) -> dict[str, Any]:
|
|
"""
|
|
Get the user data as JSON from the user name. https://docs.gitea.com/api/next/#tag/user/operation/userGet
|
|
"""
|
|
|
|
url = f"{BASE_API_URL}/users/{username}"
|
|
result = url_json_get(url)
|
|
assert isinstance(result, dict)
|
|
return result
|
|
|
|
|
|
def gitea_json_issue_get(issue_fullname: str) -> dict[str, Any]:
|
|
"""
|
|
Get issue/pull JSON data.
|
|
:param issue_fullname: string in the format "{owner}/{repo}/issues/{number}"
|
|
"""
|
|
url = f"{BASE_API_URL}/repos/{issue_fullname}"
|
|
result = url_json_get(url)
|
|
assert isinstance(result, dict)
|
|
return result
|
|
|
|
|
|
def gitea_json_activities_get(username: str, date: str) -> list[dict[str, Any]]:
|
|
"""
|
|
List a user's activity feeds.
|
|
:param username: username of user.
|
|
:param date: the date of the activities to be found.
|
|
"""
|
|
activity_url = f"{BASE_API_URL}/users/{username}/activities/feeds?only-performed-by=true&date={date}"
|
|
result = url_json_get_all_pages(activity_url)
|
|
assert isinstance(result, list)
|
|
return result
|
|
|
|
|
|
def gitea_json_pull_request_by_base_and_head_get(repo_name: str, base: str, head: str) -> dict[str, Any] | None:
|
|
"""
|
|
Get a pull request by base and head
|
|
:param repo_name: Full name of the repository, e.g. "blender/blender".
|
|
:param base: Target branch of the PR (branch it wants to merge into), e.g. "main".
|
|
:param head: Full identifier of the branch the PR is made from, e.g. "MyRepository:temp-feature-branch"
|
|
"""
|
|
url = f"{BASE_API_URL}/repos/{repo_name}/pulls/{base}/{head}"
|
|
result = url_json_get(url, quiet=True)
|
|
assert result is None or isinstance(result, dict)
|
|
return result
|
|
|
|
|
|
def gitea_json_issues_search(
|
|
type: str | None = None,
|
|
since: str | None = None,
|
|
before: str | None = None,
|
|
state: str = 'all',
|
|
labels: str | None = None,
|
|
created: bool = False,
|
|
reviewed: bool = False,
|
|
access_token: str | None = None,
|
|
verbose: bool = True,
|
|
) -> list[dict[str, Any]]:
|
|
"""
|
|
Search for issues across the repositories that the user has access to.
|
|
:param type: filter by type (issues / pulls) if set.
|
|
:param since: Only show notifications updated after the given time. This is a timestamp in RFC 3339 format.
|
|
:param before: Only show notifications updated before the given time. This is a timestamp in RFC 3339 format.
|
|
:param state: whether issue is open or closed.
|
|
:param labels: comma separated list of labels.
|
|
Fetch only issues that have any of this labels. Non existent labels are discarded.
|
|
:param created: filter (issues / pulls) created by you, default is false.
|
|
:param reviewed: filter pulls reviewed by you, default is false.
|
|
:param access_token: token generated by the GITEA API.
|
|
:return: List of issues or pulls.
|
|
"""
|
|
|
|
query_params = {k: v for k, v in locals().items() if v and k not in {"verbose"}}
|
|
for k, v in query_params.items():
|
|
if v is True:
|
|
query_params[k] = "true"
|
|
elif v is False:
|
|
query_params[k] = "false"
|
|
|
|
if verbose:
|
|
print("# Searching for {} #".format(
|
|
query_params["type"] if "type" in query_params else "issues and pulls"))
|
|
|
|
print("Query params:", {
|
|
k: v for k, v in query_params.items() if k not in {"type", "access_token"}})
|
|
|
|
base_url = f"{BASE_API_URL}/repos/issues/search"
|
|
encoded_query_params = urllib.parse.urlencode(query_params)
|
|
issues_url = f"{base_url}?{encoded_query_params}"
|
|
|
|
issues = url_json_get_all_pages(issues_url, verbose=verbose)
|
|
|
|
if verbose:
|
|
print(f"Total: {len(issues)} ", end="\n\n", flush=True)
|
|
|
|
return issues
|
|
|
|
|
|
def gitea_json_issue_events_filter(
|
|
issue_fullname: str,
|
|
date_start: datetime.datetime | None = None,
|
|
date_end: datetime.datetime | None = None,
|
|
username: str | None = None,
|
|
labels: set[str] | None = None,
|
|
event_type: set[str] | None = None,
|
|
) -> list[dict[str, Any]]:
|
|
"""
|
|
Filter all comments and events on the issue list. If both labels and event_type are provided,
|
|
an event is included if either the label or event type matches.
|
|
:param issue_fullname: string in the format "{owner}/{repo}/issues/{number}"
|
|
:param date_start: if provided, only comments updated since the specified time are returned.
|
|
:param date_end: if provided, only comments updated before the provided time are returned.
|
|
:param labels: list of labels. Fetch only events that have any of these labels (plus, events
|
|
passing the event_type check if set)
|
|
:param event_type: set of types of events in {"close", "commit_ref"...}.
|
|
:return: List of comments or events.
|
|
"""
|
|
issue_events_url = f"{BASE_API_URL}/repos/{issue_fullname}/timeline"
|
|
if date_start or date_end:
|
|
query_params = {}
|
|
if date_start:
|
|
query_params["since"] = f"{date_start.isoformat()}Z"
|
|
if date_end:
|
|
query_params["before"] = f"{date_end.isoformat()}Z"
|
|
|
|
encoded_query_params = urllib.parse.urlencode(query_params)
|
|
issue_events_url = f"{issue_events_url}?{encoded_query_params}"
|
|
|
|
result = []
|
|
for event in url_json_get_all_pages(issue_events_url):
|
|
if not event:
|
|
continue
|
|
|
|
if username and (not event["user"] or event["user"]["username"] != username):
|
|
continue
|
|
|
|
if labels and event["type"] == "label" and event["label"]["name"] in labels:
|
|
pass
|
|
elif event_type and event["type"] in event_type:
|
|
pass
|
|
elif labels or event_type:
|
|
continue
|
|
|
|
result.append(event)
|
|
|
|
return result
|
|
|
|
|
|
# WORKAROUND: This function doesn't involve GITEA, and the obtained username may not match the username used in GITEA.
|
|
# However, it provides an option to fetch the configured username from the local Git,
|
|
# in case the user does not explicitly supply the username.
|
|
def git_username_detect() -> str | None:
|
|
import os
|
|
import subprocess
|
|
|
|
# Get the repository directory
|
|
repo_dir = os.path.abspath(os.path.normpath(os.path.join(os.path.dirname(__file__), "..", "..")))
|
|
|
|
# Attempt to get the configured username from the local Git
|
|
try:
|
|
result = subprocess.run(["git", "config", "user.username"], stdout=subprocess.PIPE, cwd=repo_dir)
|
|
result.check_returncode() # Check if the command was executed successfully
|
|
username = result.stdout.decode().rstrip()
|
|
return username
|
|
except subprocess.CalledProcessError as ex:
|
|
# Handle errors if the git config command fails
|
|
print(f"Error fetching Git username: {ex}")
|
|
return None
|