Add Chromium-only Blender WebEngine parity work
This commit is contained in:
286
blender-5.2.0/tools/triage/bug_fixes_per_module.py
Normal file
286
blender-5.2.0/tools/triage/bug_fixes_per_module.py
Normal file
@@ -0,0 +1,286 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-FileCopyrightText: 2025 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
r"""
|
||||
### What this script does
|
||||
This script looks for Fix #NUMBER commits in the current branch between
|
||||
a start and end date. It then iterates through each commit figuring
|
||||
out which issue it fixed and what module that issue belonged too.
|
||||
|
||||
Finally it prints the list of modules and the corresponding fix numbers
|
||||
to terminal.
|
||||
|
||||
The steps to use it as as follows:
|
||||
- Change the terminal to the Blender repository and make sure it's on
|
||||
the branch you're interested in (E.g. main) and up to date.
|
||||
- Run the script with `python bug_fixes_per_module.py -s YYYY-MM-DD -e YYYY-MM-DD`
|
||||
- `-s` and `-e` are the start and end dates you want to checkout.
|
||||
- Wait for the script to finish.
|
||||
|
||||
Limitation:
|
||||
Because the script is only looking at commits that contain `Fix #NUMBER`
|
||||
in them, this will not gather a full list of fix commits. It will just
|
||||
gather a list of commits that fixed reported issues.
|
||||
|
||||
Related to this, if the commit message contains the wrong issue number in
|
||||
`Fix #NUMBER`, then the commit will be sorted improperly.
|
||||
"""
|
||||
|
||||
__all__ = (
|
||||
"main",
|
||||
)
|
||||
|
||||
import re
|
||||
import sys
|
||||
import argparse
|
||||
import subprocess
|
||||
import multiprocessing
|
||||
|
||||
from typing import Any
|
||||
from time import time
|
||||
|
||||
from gitea_utils import url_json_get, BASE_API_URL
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Constant used throughout the script
|
||||
|
||||
UNKNOWN_MODULE = "UNKNOWN_MODULE"
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Commit Info Class
|
||||
|
||||
|
||||
class CommitInfo():
|
||||
def __init__(self, commit_line: str) -> None:
|
||||
split_message = commit_line.split()
|
||||
|
||||
# Commit line is in the format:
|
||||
# COMMIT_HASH Title of commit
|
||||
self.hash = split_message[0]
|
||||
|
||||
self.fixed_reports: list[str] = []
|
||||
self.check_full_commit_message_for_fixed_reports()
|
||||
|
||||
self.module = UNKNOWN_MODULE
|
||||
|
||||
def check_full_commit_message_for_fixed_reports(self) -> None:
|
||||
command = ['git', 'show', '-s', '--format=%B', self.hash]
|
||||
command_output = subprocess.run(command, capture_output=True, check=True).stdout.decode('utf-8')
|
||||
|
||||
if "revert" in command_output.lower():
|
||||
# If "revert" is the commit message, then it's probably a revert commit and didn't fix a issue.
|
||||
return
|
||||
|
||||
# Find every instance of #NUMBER. These are the report that the commit claims to fix.
|
||||
issue_match = re.findall(r'#(\d+)\b', command_output)
|
||||
if issue_match:
|
||||
self.fixed_reports = issue_match
|
||||
|
||||
def get_module(self, labels: list[dict[Any, Any]]) -> str:
|
||||
# Figures out what module the report that was fixed belongs too.
|
||||
for label in labels:
|
||||
if "module" in label['name'].lower():
|
||||
# Module labels are typically in the format Module/NAME.
|
||||
return " ".join(label['name'].split("/")[1:])
|
||||
|
||||
return UNKNOWN_MODULE
|
||||
|
||||
def classify(self) -> bool:
|
||||
commit_was_sorted = False
|
||||
for report_number in self.fixed_reports:
|
||||
report_information = url_json_get(f"{BASE_API_URL}/repos/blender/blender/issues/{report_number}")
|
||||
|
||||
if report_information is None:
|
||||
# It might be `None` if bug report has been deleted.
|
||||
continue
|
||||
|
||||
if isinstance(report_information, list):
|
||||
# List type is the wrong format.
|
||||
continue
|
||||
|
||||
if "pull" in report_information['html_url']:
|
||||
# Pull requests aren't bug reports. So skip processing it.
|
||||
continue
|
||||
|
||||
# The commit didn't exit early due to the criteria above, so it was correctly sorted.
|
||||
commit_was_sorted = True
|
||||
module = self.get_module(report_information['labels'])
|
||||
if module != UNKNOWN_MODULE:
|
||||
self.module = module
|
||||
break
|
||||
return commit_was_sorted
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Argument Parsing
|
||||
|
||||
|
||||
def argparse_create() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawTextHelpFormatter)
|
||||
parser.add_argument(
|
||||
"-s",
|
||||
"--start",
|
||||
required=True,
|
||||
help=(
|
||||
"Date to start checking commits from. Must be in the format YYYY-MM-DD."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"-e",
|
||||
"--end",
|
||||
required=True,
|
||||
help=(
|
||||
"Date to stop checking commits. Must be in the format YYYY-MM-DD."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"-j",
|
||||
"--jobs",
|
||||
type=int,
|
||||
default=0,
|
||||
help=(
|
||||
"Number of threads to use when processing commit messages "
|
||||
"(Only really useful for debugging)."
|
||||
),
|
||||
)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def validate_arguments(args: argparse.Namespace) -> bool:
|
||||
def valid_date(date_string: str) -> bool:
|
||||
if len(date_string) == 0:
|
||||
print("Date is missing")
|
||||
print(date_string)
|
||||
return False
|
||||
|
||||
split_date = date_string.split("-")
|
||||
if len(split_date) != 3:
|
||||
print("Date has too many or too few sections. It should be in the format YYYY-MM-DD")
|
||||
print(date_string)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
if not (valid_date(args.start) and valid_date(args.end)):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
def setup_commit_info(commit: str) -> CommitInfo | None:
|
||||
commit_information = CommitInfo(commit)
|
||||
if len(commit_information.fixed_reports) > 0:
|
||||
return commit_information
|
||||
return None
|
||||
|
||||
|
||||
def get_fix_commits(start_date: str, end_date: str, jobs: int) -> list[CommitInfo]:
|
||||
command = [
|
||||
'git',
|
||||
'--no-pager',
|
||||
'log',
|
||||
'--oneline',
|
||||
'--no-abbrev-commit',
|
||||
f'--since={start_date}',
|
||||
f'--until={end_date}',
|
||||
'-i',
|
||||
'-P',
|
||||
'--grep',
|
||||
r'Fix.*#+\d+',
|
||||
]
|
||||
git_log_command_output = subprocess.run(command, capture_output=True, check=True).stdout.decode('utf-8')
|
||||
git_log_output = git_log_command_output.splitlines()
|
||||
|
||||
# Gathering a list of commits is not compute intensive, it is time consuming due to hundreds of git log calls.
|
||||
# Multiprocessing can significantly reduce the time taken (E.g. 19s -> 4s on a 32 thread CPU).
|
||||
with multiprocessing.Pool(processes=jobs) as pool:
|
||||
list_of_commits = pool.map(setup_commit_info, git_log_output)
|
||||
|
||||
return [commit for commit in list_of_commits if commit is not None]
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
def classify_commits(list_of_commits: list[CommitInfo]) -> list[CommitInfo]:
|
||||
number_of_commits = len(list_of_commits)
|
||||
|
||||
print("Identifying which module the fix should be assigned too.")
|
||||
print("This requires querying information from Gitea which may take a while.\n")
|
||||
|
||||
start_time = time()
|
||||
new_list_of_commits: list[CommitInfo] = []
|
||||
for i, commit in enumerate(list_of_commits, 1):
|
||||
# Progress bar.
|
||||
print(
|
||||
f"{i}/{number_of_commits} - Estimated time remaining:",
|
||||
f"{(((time() - start_time) / i) * (number_of_commits - i)) / 60:.1f} minutes",
|
||||
end="\r",
|
||||
flush=True
|
||||
)
|
||||
|
||||
if commit.classify():
|
||||
# Only add commit to list if it was sorted.
|
||||
# If it wasn't sorted, then it probably means the commit "fixed" a pull request.
|
||||
new_list_of_commits.append(commit)
|
||||
|
||||
# Print so we're away from the progress bar.
|
||||
print("\n\n\n")
|
||||
|
||||
return new_list_of_commits
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
def print_info(list_of_commits: list[CommitInfo], start_date: str, end_date: str) -> None:
|
||||
print(f"Between {start_date} and {end_date}, there were a total of {len(list_of_commits)} Fix #NUMBER commits.")
|
||||
print("These are the numbers per module:\n")
|
||||
|
||||
dict_of_modules_and_commits: dict[str, list[CommitInfo]] = {}
|
||||
|
||||
for commit in list_of_commits:
|
||||
dict_of_modules_and_commits.setdefault(commit.module, []).append(commit)
|
||||
|
||||
dict_of_modules_and_commits = dict(sorted(dict_of_modules_and_commits.items()))
|
||||
|
||||
for module in dict_of_modules_and_commits:
|
||||
if module == UNKNOWN_MODULE:
|
||||
continue
|
||||
print(f"{module}: {len(dict_of_modules_and_commits[module])}")
|
||||
|
||||
if UNKNOWN_MODULE in dict_of_modules_and_commits:
|
||||
unknown_commits = dict_of_modules_and_commits[UNKNOWN_MODULE]
|
||||
print(f"\nUnknown: {len(unknown_commits)}")
|
||||
print("Here is a list of the commits with unknown modules.")
|
||||
print("Go through each of the commit messages, find the bug reports they fixed, then update the module label.")
|
||||
for commit in unknown_commits:
|
||||
print(f"https://projects.blender.org/blender/blender/commit/{commit.hash}")
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = argparse_create().parse_args()
|
||||
|
||||
if not validate_arguments(args):
|
||||
return 0
|
||||
|
||||
jobs = multiprocessing.cpu_count() if args.jobs < 1 else args.jobs
|
||||
|
||||
list_of_commits = get_fix_commits(args.start, args.end, jobs)
|
||||
|
||||
list_of_commits = classify_commits(list_of_commits)
|
||||
|
||||
print_info(list_of_commits, args.start, args.end)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
256
blender-5.2.0/tools/triage/gitea_utils.py
Normal file
256
blender-5.2.0/tools/triage/gitea_utils.py
Normal file
@@ -0,0 +1,256 @@
|
||||
#!/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 os
|
||||
import datetime
|
||||
import json
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
from typing import (
|
||||
Any,
|
||||
)
|
||||
|
||||
API_TOKEN_ENV = 'GITEA_API_TOKEN'
|
||||
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:
|
||||
request = urllib.request.Request(url)
|
||||
# If the token environment variable is set, add the `Authorization` header to the request
|
||||
token = os.environ.get(API_TOKEN_ENV)
|
||||
if token:
|
||||
request.add_header('Authorization', "token " + token)
|
||||
try:
|
||||
# Make the HTTP request and store the response in a 'response' object
|
||||
response = urllib.request.urlopen(request)
|
||||
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 = {}
|
||||
# Assume that if no timezone is provided, that it's UTC.
|
||||
# Without this, dates passed in that *do* have a timezone aren't handled properly.
|
||||
if date_start:
|
||||
if date_start.tzinfo is None:
|
||||
date_start = date_start.replace(tzinfo=datetime.timezone.utc)
|
||||
query_params["since"] = date_start.isoformat()
|
||||
if date_end:
|
||||
if date_end.tzinfo is None:
|
||||
date_end = date_end.replace(tzinfo=datetime.timezone.utc)
|
||||
query_params["before"] = date_end.isoformat()
|
||||
|
||||
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 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
|
||||
175
blender-5.2.0/tools/triage/issues_module_listing.py
Executable file
175
blender-5.2.0/tools/triage/issues_module_listing.py
Executable file
@@ -0,0 +1,175 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-FileCopyrightText: 2024 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
"""
|
||||
# This script prints the numbers of open issues per module.
|
||||
|
||||
Example usage:
|
||||
|
||||
python ./issues_module_listing.py --severity High
|
||||
"""
|
||||
__all__ = (
|
||||
"main",
|
||||
)
|
||||
|
||||
import argparse
|
||||
import dataclasses
|
||||
import sys
|
||||
|
||||
from datetime import date
|
||||
from gitea_utils import gitea_json_issues_search
|
||||
|
||||
IS_ATTY = sys.stdout.isatty()
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class ModuleInfo:
|
||||
name: str
|
||||
labelid: str
|
||||
buglist: list[str] = dataclasses.field(default_factory=list)
|
||||
buglist_full: list[str] = dataclasses.field(default_factory=list)
|
||||
|
||||
|
||||
# Label names and IDs are taken from https://projects.blender.org/blender/blender/labels.
|
||||
modules = {
|
||||
"Module/Animation & Rigging": ModuleInfo(name="Animation & Rigging", labelid="268"),
|
||||
"Module/Asset System": ModuleInfo(name="Asset System", labelid="1708"),
|
||||
"Module/Core": ModuleInfo(name="Core", labelid="269"),
|
||||
"Module/Development Management": ModuleInfo(name="Development Management", labelid="270"),
|
||||
"Module/Grease Pencil": ModuleInfo(name="Grease Pencil", labelid="273"),
|
||||
"Module/Modeling": ModuleInfo(name="Modeling", labelid="274"),
|
||||
"Module/Nodes & Physics": ModuleInfo(name="Nodes & Physics", labelid="275"),
|
||||
"Module/Pipeline & IO": ModuleInfo(name="Pipeline & I/O", labelid="276"),
|
||||
"Module/Platforms & Builds": ModuleInfo(name="Platforms & Builds", labelid="278"),
|
||||
"Module/Python API": ModuleInfo(name="Python API", labelid="279"),
|
||||
"Module/Render & Cycles": ModuleInfo(name="Render & Cycles", labelid="280"),
|
||||
"Module/Sculpt, Paint & Texture": ModuleInfo(name="Sculpt, Paint & Texture", labelid="281"),
|
||||
"Module/Triaging": ModuleInfo(name="Triaging", labelid="282"),
|
||||
"Module/User Interface": ModuleInfo(name="User Interface", labelid="283"),
|
||||
"Module/VFX & Video": ModuleInfo(name="VFX & Video", labelid="284"),
|
||||
"Module/Viewport & EEVEE": ModuleInfo(name="Viewport & EEVEE", labelid="272"),
|
||||
}
|
||||
|
||||
base_url = (
|
||||
"https://projects.blender.org/blender/blender/"
|
||||
"issues?q=&type=all&sort=&state=open&labels="
|
||||
)
|
||||
total_url = (
|
||||
"https://projects.blender.org/blender/blender/"
|
||||
"issues?q=&type=all&sort=&state=open&labels=285%2c-297%2c-298%2c-299%2c-301"
|
||||
)
|
||||
|
||||
severity_labelid = {
|
||||
"Low": "286",
|
||||
"Normal": "287",
|
||||
"High": "285",
|
||||
"Unbreak Now!": "288"
|
||||
}
|
||||
|
||||
|
||||
def compile_list(severity: str) -> None:
|
||||
|
||||
label = f"Severity/{severity}"
|
||||
issues_json = gitea_json_issues_search(
|
||||
type="issues",
|
||||
state="open",
|
||||
labels=label,
|
||||
verbose=True,
|
||||
)
|
||||
|
||||
# Create a dictionary of format {module_id: module_name}
|
||||
module_label_ids = {}
|
||||
for module_name in modules:
|
||||
module_label_ids[modules[module_name].labelid] = module_name
|
||||
|
||||
uncategorized_reports = []
|
||||
|
||||
issues_json_sorted = sorted(issues_json, key=lambda x: x["number"])
|
||||
|
||||
for issue in issues_json_sorted:
|
||||
html_url = issue["html_url"]
|
||||
number = issue["number"]
|
||||
created_at = issue["created_at"].rsplit('T', 1)[0]
|
||||
title = issue["title"]
|
||||
|
||||
# Check reports module assignment and fill in data.
|
||||
for label_iter in issue["labels"]:
|
||||
label_id = str(label_iter["id"])
|
||||
if label_id not in module_label_ids:
|
||||
continue
|
||||
|
||||
current_module_name = module_label_ids[label_id]
|
||||
if current_module_name != label_iter["name"]:
|
||||
new_label_name = label_iter["name"]
|
||||
print(f"ALERT: The name of label of '{current_module_name}' changed.")
|
||||
print(f"The new name is '{new_label_name}'.")
|
||||
if IS_ATTY:
|
||||
input("Press enter to continue: \n")
|
||||
|
||||
modules[current_module_name].buglist.append(f"[#{number}]({html_url})")
|
||||
modules[current_module_name].buglist_full.append(f"* [{title}]({html_url}) - {created_at}\n")
|
||||
break
|
||||
else:
|
||||
uncategorized_reports.append(f"[#{number}]({html_url})")
|
||||
|
||||
# Print statistics
|
||||
print(f"Open {severity} Severity bugs as of {date.today()}:\n")
|
||||
|
||||
# Module overview with numbers
|
||||
total = 0
|
||||
modules_with_no_bugs = []
|
||||
for module in modules.values():
|
||||
buglist_str = (", ".join(module.buglist))
|
||||
buglist_len = len(module.buglist)
|
||||
full_url = base_url + severity_labelid[severity] + "%2c" + module.labelid
|
||||
if buglist_len > 0:
|
||||
total += buglist_len
|
||||
if not module.buglist or severity != "High":
|
||||
print(f"- [{module.name}]({full_url}): *{buglist_len}*")
|
||||
else:
|
||||
print(f"- [{module.name}]({full_url}): *{buglist_len}* _{buglist_str}_")
|
||||
else:
|
||||
modules_with_no_bugs.append(f"[{module.name}]({full_url})")
|
||||
|
||||
print(f"- {', '.join(modules_with_no_bugs)}: *0*")
|
||||
print()
|
||||
print(f"[Total]({total_url}): {total}")
|
||||
print()
|
||||
print("Uncategorized:", ", ".join(uncategorized_reports))
|
||||
print()
|
||||
|
||||
# Module overview with titles and creation date
|
||||
for module in modules.values():
|
||||
buglist_full_str = ("".join(module.buglist_full))
|
||||
buglist_full_len = len(module.buglist_full)
|
||||
if buglist_full_len != 0:
|
||||
print(f"{module.name}:")
|
||||
print(f"{buglist_full_str}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Print statistics on open bug reports per module",
|
||||
epilog="This script is used to help module teams",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--severity",
|
||||
dest="severity",
|
||||
default="High",
|
||||
type=str,
|
||||
required=False,
|
||||
choices=severity_labelid.keys(),
|
||||
help="Severity level of reports",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
compile_list(args.severity)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
83
blender-5.2.0/tools/triage/issues_needing_info.py
Executable file
83
blender-5.2.0/tools/triage/issues_needing_info.py
Executable file
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
"""
|
||||
# This script prints the URLs of all opened issues labeled
|
||||
# "Status/Needs Information from User" by the specified user
|
||||
# and last updated more than 7 days ago.
|
||||
|
||||
Example usage:
|
||||
|
||||
python ./issues_needing_info.py --username mano-wii
|
||||
"""
|
||||
__all__ = (
|
||||
"main",
|
||||
)
|
||||
|
||||
import argparse
|
||||
import datetime
|
||||
from gitea_utils import (
|
||||
git_username_detect,
|
||||
gitea_json_issue_events_filter,
|
||||
gitea_json_issues_search,
|
||||
)
|
||||
|
||||
|
||||
def print_needing_info_urls(username: str, before: str) -> None:
|
||||
|
||||
print(f"Needs information from user before {before}:")
|
||||
|
||||
label = "Status/Needs Information from User"
|
||||
issues_json = gitea_json_issues_search(
|
||||
type="issues",
|
||||
state="open",
|
||||
before=before,
|
||||
labels=label,
|
||||
verbose=True,
|
||||
)
|
||||
|
||||
for issue in issues_json:
|
||||
fullname = issue["repository"]["full_name"]
|
||||
number = issue["number"]
|
||||
issue_events = gitea_json_issue_events_filter(
|
||||
f"{fullname}/issues/{number}",
|
||||
username=username,
|
||||
labels={label})
|
||||
|
||||
if issue_events:
|
||||
print(issue["html_url"])
|
||||
|
||||
print("concluded")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Print URL of Issues Needing Info",
|
||||
epilog="This script is typically used to help triaging")
|
||||
|
||||
parser.add_argument(
|
||||
"--username",
|
||||
dest="username",
|
||||
type=str,
|
||||
required=False,
|
||||
help="Username registered in Gitea")
|
||||
|
||||
args = parser.parse_args()
|
||||
username = args.username
|
||||
if not username:
|
||||
username = git_username_detect()
|
||||
if not username:
|
||||
return
|
||||
|
||||
before_date = datetime.datetime.now() - datetime.timedelta(7)
|
||||
print_needing_info_urls(username, f"{before_date.isoformat()}Z")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
# wait for input to close window
|
||||
input()
|
||||
510
blender-5.2.0/tools/triage/weekly_report.py
Executable file
510
blender-5.2.0/tools/triage/weekly_report.py
Executable file
@@ -0,0 +1,510 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
"""
|
||||
# Generates the weekly report containing information on:
|
||||
# - Pull Requests created,
|
||||
# - Pull Requests revised,
|
||||
# - Issues closed,
|
||||
# - Issues confirmed,
|
||||
# - Commits,
|
||||
|
||||
Example usage:
|
||||
|
||||
python ./weekly_report.py --username mano-wii
|
||||
"""
|
||||
__all__ = (
|
||||
"main",
|
||||
)
|
||||
|
||||
|
||||
import argparse
|
||||
import datetime
|
||||
import json
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from gitea_utils import (
|
||||
gitea_json_activities_get,
|
||||
gitea_json_pull_request_by_base_and_head_get,
|
||||
gitea_json_issue_events_filter,
|
||||
gitea_json_issue_get,
|
||||
gitea_user_get, git_username_detect,
|
||||
)
|
||||
|
||||
from typing import (
|
||||
Any,
|
||||
)
|
||||
from collections.abc import (
|
||||
Iterable,
|
||||
)
|
||||
|
||||
# Support piping the output to a file or process.
|
||||
IS_ATTY = sys.stdout.isatty()
|
||||
|
||||
|
||||
if IS_ATTY:
|
||||
def print_progress(text: str) -> None:
|
||||
# The trailing space clears the previous output.
|
||||
term_width = shutil.get_terminal_size(fallback=(80, 20))[0]
|
||||
|
||||
if (space := term_width - len(text)) > 0:
|
||||
text = text + (" " * space)
|
||||
print(text, end="\r", flush=True)
|
||||
else:
|
||||
def print_progress(text: str) -> None:
|
||||
del text
|
||||
|
||||
|
||||
def argparse_create() -> argparse.ArgumentParser:
|
||||
|
||||
def str_as_isodate(value: str) -> datetime.datetime:
|
||||
try:
|
||||
value_as_date = datetime.datetime.fromisoformat(value)
|
||||
except Exception as ex:
|
||||
raise argparse.ArgumentTypeError("Must be a valid ISO date (YYYY-MM-DD), failed: {!s}".format(ex))
|
||||
return value_as_date
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate Weekly Report",
|
||||
epilog="This script is typically used to help write weekly reports",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--username",
|
||||
dest="username",
|
||||
metavar='USERNAME',
|
||||
type=str,
|
||||
required=False,
|
||||
help="",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--weeks-ago",
|
||||
dest="weeks_ago",
|
||||
type=int,
|
||||
default=1,
|
||||
help=(
|
||||
"Determine which week the report should be generated for. 0 means the current week. "
|
||||
"The default is 1, to create a report for the previous week."
|
||||
),
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--date",
|
||||
dest="date",
|
||||
type=str_as_isodate,
|
||||
default=None,
|
||||
help="Show only for this day (YYYY-MM-DD), and not for an entire week."
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--hash-length",
|
||||
dest="hash_length",
|
||||
type=int,
|
||||
default=10,
|
||||
help="Number of characters to abbreviate the hash to (0 to disable).",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-v",
|
||||
"--verbose",
|
||||
action="store_true",
|
||||
help="increase output verbosity",
|
||||
)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def report_personal_weekly_get(
|
||||
username: str,
|
||||
start: datetime.datetime,
|
||||
num_days: int,
|
||||
*,
|
||||
hash_length: int,
|
||||
verbose: bool = True,
|
||||
) -> None:
|
||||
|
||||
data_cache: dict[str, dict[str, Any]] = {}
|
||||
|
||||
def gitea_json_issue_get_cached(issue_fullname: str) -> dict[str, Any]:
|
||||
if issue_fullname not in data_cache:
|
||||
issue = gitea_json_issue_get(issue_fullname)
|
||||
data_cache[issue_fullname] = issue
|
||||
|
||||
return data_cache[issue_fullname]
|
||||
|
||||
pulls_closed: set[str] = set()
|
||||
pulls_commented: set[str] = set()
|
||||
pulls_created: set[str] = set()
|
||||
|
||||
issues_closed: set[str] = set()
|
||||
issues_commented: set[str] = set()
|
||||
issues_created: set[str] = set()
|
||||
|
||||
pulls_reviewed: set[str] = set()
|
||||
|
||||
issues_confirmed: list[str] = []
|
||||
issues_needing_user_info: list[str] = []
|
||||
issues_needing_developer_info: list[str] = []
|
||||
issues_fixed: list[str] = []
|
||||
issues_duplicated: list[str] = []
|
||||
issues_archived: list[str] = []
|
||||
|
||||
@dataclass
|
||||
class Branch:
|
||||
# Name of the repository owning the branch (which can differ from the repository targeted by this branch!)
|
||||
repository_full_name: str
|
||||
commits: list[str]
|
||||
|
||||
@dataclass
|
||||
class PullRequest:
|
||||
title_str: str
|
||||
|
||||
@dataclass
|
||||
class Repository:
|
||||
name: str
|
||||
# Branches targeting this repository. Branch name is key.
|
||||
branches: dict[str, Branch] = field(default_factory=dict)
|
||||
# Pull requests targeting this repository. Key is repository of the branch and the branch name.
|
||||
prs: dict[tuple[str, str], PullRequest] = field(default_factory=dict)
|
||||
|
||||
# Repositories containing any commit activity, identified by full name (e.g. "blender/blender").
|
||||
repositories: dict[str, Repository] = {}
|
||||
|
||||
user_data: dict[str, Any] = gitea_user_get(username)
|
||||
|
||||
for i in range(num_days):
|
||||
date_curr = start + datetime.timedelta(days=i)
|
||||
date_curr_str = date_curr.strftime("%Y-%m-%d")
|
||||
print_progress(f"Requesting activity of {date_curr_str}")
|
||||
for activity in gitea_json_activities_get(username, date_curr_str):
|
||||
op_type = activity["op_type"]
|
||||
if op_type == "close_issue":
|
||||
fullname = activity["repo"]["full_name"] + "/issues/" + activity["content"].split('|')[0]
|
||||
issues_closed.add(fullname)
|
||||
elif op_type == "comment_issue":
|
||||
fullname = activity["repo"]["full_name"] + "/issues/" + activity["content"].split('|')[0]
|
||||
issues_commented.add(fullname)
|
||||
elif op_type == "create_issue":
|
||||
fullname = activity["repo"]["full_name"] + "/issues/" + activity["content"].split('|')[0]
|
||||
issues_created.add(fullname)
|
||||
elif op_type == "merge_pull_request":
|
||||
fullname = activity["repo"]["full_name"] + "/pulls/" + activity["content"].split('|')[0]
|
||||
pulls_closed.add(fullname)
|
||||
elif op_type == "comment_pull":
|
||||
fullname = activity["repo"]["full_name"] + "/pulls/" + activity["content"].split('|')[0]
|
||||
pulls_commented.add(fullname)
|
||||
elif op_type == "create_pull_request":
|
||||
fullname = activity["repo"]["full_name"] + "/pulls/" + activity["content"].split('|')[0]
|
||||
pulls_created.add(fullname)
|
||||
elif op_type in {"approve_pull_request", "reject_pull_request"}:
|
||||
fullname = activity["repo"]["full_name"] + "/pulls/" + activity["content"].split('|')[0]
|
||||
pulls_reviewed.add(fullname)
|
||||
elif op_type == "commit_repo":
|
||||
if (
|
||||
activity["content"] and
|
||||
activity["repo"]["name"] != ".profile"
|
||||
):
|
||||
content_json = json.loads(activity["content"])
|
||||
assert isinstance(content_json, dict)
|
||||
repo = activity["repo"]
|
||||
repo_fullname = repo["full_name"]
|
||||
content_json_commits: list[dict[str, Any]] = content_json["Commits"]
|
||||
for commit_json in content_json_commits:
|
||||
# Skip commits that were not made by this user. Using email doesn't seem to
|
||||
# be possible unfortunately.
|
||||
if commit_json["AuthorName"] != user_data["full_name"]:
|
||||
continue
|
||||
|
||||
title = commit_json["Message"].split('\n', 1)[0]
|
||||
|
||||
if title.startswith("Merge branch "):
|
||||
continue
|
||||
|
||||
hash_value = commit_json["Sha1"]
|
||||
if hash_length > 0:
|
||||
hash_value = hash_value[:hash_length]
|
||||
|
||||
branch_name = activity["ref_name"].removeprefix("refs/heads/")
|
||||
is_release_branch = re.match(r"^blender-v(?:\d+\.\d+)(?:\.\d+)?-release$", branch_name)
|
||||
|
||||
pr = None
|
||||
|
||||
# The PR workflow means branches and PRs are owned by a user's repository instead of the
|
||||
# repository they are made for. For weekly reports it makes more sense to keep all branches and
|
||||
# PRs related to a single repository together, regardless of who happens to own them.
|
||||
#
|
||||
# So the following adds branches and PRs to a "target" repository, not the owning one.
|
||||
target_repo_json = repo.get("parent", repo)
|
||||
target_repo_fullname = target_repo_json["full_name"] if target_repo_json else repo_fullname
|
||||
|
||||
# Substitute occurrences of "#\d+" with "repo#\d+"
|
||||
title = re.sub(r"#(\d+)", rf"{target_repo_fullname}#\1", title)
|
||||
|
||||
if target_repo_fullname not in repositories:
|
||||
repositories[target_repo_fullname] = Repository(target_repo_fullname)
|
||||
target_repo = repositories[target_repo_fullname]
|
||||
|
||||
if branch_name not in target_repo.branches:
|
||||
target_repo.branches[branch_name] = Branch(repo_fullname, [])
|
||||
# If we see this branch for the first time, try to find a PR for it. Only catches PRs made
|
||||
# against the default branch of the target repository.
|
||||
if not is_release_branch and target_repo_json:
|
||||
pr = gitea_json_pull_request_by_base_and_head_get(
|
||||
target_repo_fullname,
|
||||
target_repo_json["default_branch"],
|
||||
f"{repo_fullname}:{branch_name}",
|
||||
)
|
||||
branch = target_repo.branches[branch_name]
|
||||
|
||||
if pr:
|
||||
pr_title = pr["title"]
|
||||
pr_id = pr["number"]
|
||||
target_repo.prs[(repo_fullname, branch_name)
|
||||
] = PullRequest(f"{pr_title} ({target_repo_fullname}!{pr_id})")
|
||||
|
||||
branch.commits.append(f"{title} ({repo_fullname}@{hash_value})")
|
||||
|
||||
date_end = date_curr
|
||||
len_total = len(issues_closed) + len(issues_commented) + len(pulls_commented)
|
||||
process = 0
|
||||
for issue in issues_commented:
|
||||
print_progress("[{:d}%] Checking issue {:s}".format(int(100 * (process / len_total)), issue))
|
||||
process += 1
|
||||
|
||||
issue_events = gitea_json_issue_events_filter(
|
||||
issue,
|
||||
date_start=start,
|
||||
date_end=date_end,
|
||||
username=username,
|
||||
labels={
|
||||
"Status/Confirmed",
|
||||
"Status/Needs Information from User",
|
||||
"Status/Needs Info from Developers"
|
||||
}
|
||||
)
|
||||
|
||||
for event in issue_events:
|
||||
label_name = event["label"]["name"]
|
||||
if label_name == "Status/Confirmed":
|
||||
issues_confirmed.append(issue)
|
||||
elif label_name == "Status/Needs Information from User":
|
||||
issues_needing_user_info.append(issue)
|
||||
elif label_name == "Status/Needs Info from Developers":
|
||||
issues_needing_developer_info.append(issue)
|
||||
|
||||
for issue in issues_closed:
|
||||
print_progress("[{:d}%] Checking issue {:s}".format(int(100 * (process / len_total)), issue))
|
||||
process += 1
|
||||
|
||||
issue_events = gitea_json_issue_events_filter(
|
||||
issue,
|
||||
date_start=start,
|
||||
date_end=date_end,
|
||||
username=username,
|
||||
event_type={"close", "commit_ref"},
|
||||
labels={"Status/Duplicate"},
|
||||
)
|
||||
|
||||
for event in issue_events:
|
||||
event_type = event["type"]
|
||||
if event_type == "commit_ref":
|
||||
issues_fixed.append(issue)
|
||||
elif event_type == "label":
|
||||
issues_duplicated.append(issue)
|
||||
else:
|
||||
issues_archived.append(issue)
|
||||
|
||||
for pull in pulls_commented:
|
||||
print_progress("[{:d}%] Checking pull {:s}".format(int(100 * (process / len_total)), pull))
|
||||
process += 1
|
||||
|
||||
pull_events = gitea_json_issue_events_filter(
|
||||
pull.replace("pulls", "issues"),
|
||||
date_start=start,
|
||||
date_end=date_end,
|
||||
username=username,
|
||||
event_type={"comment"},
|
||||
)
|
||||
|
||||
if pull_events:
|
||||
pull_data = gitea_json_issue_get_cached(pull)
|
||||
if pull_data["user"]["login"] != username:
|
||||
pulls_reviewed.add(pull)
|
||||
|
||||
# Print triaging stats
|
||||
|
||||
issues_involved = issues_closed | issues_commented | issues_created
|
||||
|
||||
# Clear any progress.
|
||||
print_progress("")
|
||||
|
||||
print("**Involved in {:d} reports:**".format(len(issues_involved)))
|
||||
print("* Confirmed: {:d}".format(len(issues_confirmed)))
|
||||
print("* Closed as Resolved: {:d}".format(len(issues_fixed)))
|
||||
print("* Closed as Archived: {:d}".format(len(issues_archived)))
|
||||
print("* Closed as Duplicate: {:d}".format(len(issues_duplicated)))
|
||||
print("* Needs Info from User: {:d}".format(len(issues_needing_user_info)))
|
||||
print("* Needs Info from Developers: {:d}".format(len(issues_needing_developer_info)))
|
||||
print("* Actions total: {:d}".format(len(issues_closed) + len(issues_commented) + len(issues_created)))
|
||||
print()
|
||||
|
||||
# Print review stats
|
||||
def print_pulls(pulls: Iterable[str]) -> None:
|
||||
display_list = []
|
||||
|
||||
for pull in pulls:
|
||||
pull_data = gitea_json_issue_get_cached(pull)
|
||||
owner, repo, _, number = pull.split('/')
|
||||
|
||||
display_list.append({
|
||||
"title": pull_data["title"],
|
||||
"formatted_ref": f"{owner}/{repo}!{number}"
|
||||
})
|
||||
|
||||
# Sort the list by the "title" key. Use .lower() to ensure case insensitiveness.
|
||||
display_list.sort(key=lambda x: x["title"].lower())
|
||||
|
||||
for item in display_list:
|
||||
print(f"* {item['title']} ({item['formatted_ref']})")
|
||||
|
||||
print("**Review: {:d}**".format(len(pulls_reviewed)))
|
||||
print_pulls(pulls_reviewed)
|
||||
print()
|
||||
|
||||
# Print created diffs
|
||||
print("**Created Pull Requests: {:d}**".format(len(pulls_created)))
|
||||
print_pulls(pulls_created)
|
||||
print()
|
||||
|
||||
nice_repo_names = {
|
||||
"blender/blender-developer-docs": "Developer Documentation",
|
||||
"blender/blender-manual": "Blender Manual",
|
||||
}
|
||||
|
||||
def print_repo(repo: Repository, indent_level: int = 0) -> None:
|
||||
# Print main branch commits immediately, no need to add extra section.
|
||||
main_branch = repo.branches.get("main")
|
||||
if main_branch:
|
||||
for commit in main_branch.commits:
|
||||
print("{:s}* {:s}".format(" " * indent_level, commit))
|
||||
|
||||
for branch_name, branch in repo.branches.items():
|
||||
# Main branch already printed above.
|
||||
if branch_name == "main":
|
||||
continue
|
||||
|
||||
pr = repo.prs.get((branch.repository_full_name, branch_name))
|
||||
if pr:
|
||||
print("{:s}* {:s}".format(" " * indent_level, pr.title_str))
|
||||
else:
|
||||
print("{:s}* {:s}:{:s}".format(" " * indent_level, branch.repository_full_name, branch_name))
|
||||
|
||||
for commit in branch.commits:
|
||||
print(" {:s}* {:s}".format(" " * indent_level, commit))
|
||||
|
||||
# Print commits
|
||||
print("**Commits:**")
|
||||
# Print main branch commits from blender/blender first.
|
||||
blender_repo = repositories.get("blender/blender")
|
||||
if blender_repo:
|
||||
print_repo(blender_repo)
|
||||
|
||||
for repo in repositories.values():
|
||||
# Blender repo already handled above.
|
||||
if repo.name == "blender/blender":
|
||||
continue
|
||||
|
||||
# For some repositories we know a nicer name to display (e.g. "blender/blender-manual" -> "Blender Manual")
|
||||
nice_repo_name = nice_repo_names.get(repo.name, repo.name)
|
||||
print(f"* {nice_repo_name}:")
|
||||
print_repo(repo, indent_level=1)
|
||||
|
||||
if verbose:
|
||||
# Debug
|
||||
|
||||
def print_links(issues: Iterable[str]) -> None:
|
||||
for fullname in issues:
|
||||
print(f"https://projects.blender.org/{fullname}")
|
||||
|
||||
print("Debug:")
|
||||
print(f"Activities from {start.isoformat()} to {date_end.isoformat()}:")
|
||||
print()
|
||||
print("Pull Requests Created:")
|
||||
print_links(pulls_created)
|
||||
print("Pull Requests Reviewed:")
|
||||
print_links(pulls_reviewed)
|
||||
print("Issues Confirmed:")
|
||||
print_links(issues_confirmed)
|
||||
print("Issues Closed as Resolved:")
|
||||
print_links(issues_fixed)
|
||||
print("Issues Closed as Archived:")
|
||||
print_links(issues_closed)
|
||||
print("Issues Closed as Duplicate:")
|
||||
print_links(issues_duplicated)
|
||||
print("Issues Needing Info from User:")
|
||||
print_links(issues_needing_user_info)
|
||||
print("Issues Needing Info from Developers:")
|
||||
print_links(issues_needing_developer_info)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
# ----------
|
||||
# Parse Args
|
||||
args = argparse_create().parse_args()
|
||||
username = args.username
|
||||
if not username:
|
||||
username = git_username_detect()
|
||||
if not username:
|
||||
return
|
||||
|
||||
if args.date:
|
||||
num_days = 1 # Show only one day.
|
||||
start_date = args.date
|
||||
start_date_str = start_date.strftime('%B ') + str(start_date.day)
|
||||
|
||||
print(f"## {start_date_str}\n")
|
||||
else:
|
||||
num_days = 7 # Show an entire week.
|
||||
# end_date = datetime.datetime(2020, 3, 14)
|
||||
end_date = datetime.datetime.now() - datetime.timedelta(weeks=(args.weeks_ago - 1))
|
||||
weekday = end_date.weekday()
|
||||
|
||||
# Assuming I am lazy and making this at last moment or even later in worst case
|
||||
if weekday < 2:
|
||||
time_delta = 7 + weekday
|
||||
start_date = end_date - datetime.timedelta(days=time_delta, hours=end_date.hour)
|
||||
end_date -= datetime.timedelta(days=weekday, hours=end_date.hour)
|
||||
else:
|
||||
time_delta = weekday
|
||||
start_date = end_date - datetime.timedelta(days=time_delta, hours=end_date.hour)
|
||||
|
||||
sunday = start_date + datetime.timedelta(days=6)
|
||||
|
||||
# week = start_date.isocalendar()[1]
|
||||
start_date_str = start_date.strftime('%B ') + str(start_date.day)
|
||||
end_date_str = str(sunday.day) if start_date.month == sunday.month else sunday.strftime('%B ') + str(sunday.day)
|
||||
|
||||
print(f"## {start_date_str} - {end_date_str}\n")
|
||||
|
||||
report_personal_weekly_get(
|
||||
username,
|
||||
start_date,
|
||||
num_days,
|
||||
hash_length=args.hash_length,
|
||||
verbose=args.verbose,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
# Wait for input to close window.
|
||||
if IS_ATTY:
|
||||
input()
|
||||
Reference in New Issue
Block a user