So far this would include commits committed by the given user, but authored by someone else. Unfotunately we can't use email addresses to filter these out, since we can't get the email addresses associated with an account from gitea, or do a user lookup by email. In my testing the commit author email and the publicly visible account email would mismatch in most cases.
297 lines
11 KiB
Python
297 lines
11 KiB
Python
#!/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
|
|
"""
|
|
|
|
import argparse
|
|
import datetime
|
|
import json
|
|
import re
|
|
from gitea_utils import gitea_json_activities_get, gitea_json_issue_get, gitea_json_issue_events_filter, gitea_user_get, git_username_detect
|
|
|
|
|
|
def argparse_create():
|
|
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(
|
|
"-v",
|
|
"--verbose",
|
|
action="store_true",
|
|
help="increase output verbosity")
|
|
|
|
return parser
|
|
|
|
|
|
def report_personal_weekly_get(username, start, verbose=True):
|
|
|
|
data_cache = {}
|
|
|
|
def gitea_json_issue_get_cached(issue_fullname):
|
|
if issue_fullname not in data_cache:
|
|
data_cache[issue_fullname] = gitea_json_issue_get(issue_fullname)
|
|
|
|
return data_cache[issue_fullname]
|
|
|
|
pulls_closed = set()
|
|
pulls_commented = set()
|
|
pulls_created = set()
|
|
|
|
issues_closed = set()
|
|
issues_commented = set()
|
|
issues_created = set()
|
|
|
|
pulls_reviewed = []
|
|
|
|
issues_confirmed = []
|
|
issues_needing_user_info = []
|
|
issues_needing_developer_info = []
|
|
issues_fixed = []
|
|
issues_duplicated = []
|
|
issues_archived = []
|
|
|
|
commits_main = []
|
|
|
|
user_data = gitea_user_get(username)
|
|
|
|
for i in range(7):
|
|
date_curr = start + datetime.timedelta(days=i)
|
|
date_curr_str = date_curr.strftime("%Y-%m-%d")
|
|
print(f"Requesting activity of {date_curr_str}", end="\r", flush=True)
|
|
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.append(fullname)
|
|
elif op_type == "commit_repo":
|
|
if activity["ref_name"] == "refs/heads/main" and activity["content"] and activity["repo"]["name"] != ".profile":
|
|
content_json = json.loads(activity["content"])
|
|
repo_fullname = activity["repo"]["full_name"]
|
|
for commits in content_json["Commits"]:
|
|
# Skip commits that were not made by this user. Using email doesn't seem to
|
|
# be possible unfortunately.
|
|
if commits["AuthorName"] != user_data["full_name"]:
|
|
continue
|
|
|
|
title = commits["Message"].split('\n', 1)[0]
|
|
|
|
if title.startswith("Merge branch "):
|
|
continue
|
|
|
|
# Substitute occurrences of "#\d+" with "repo#\d+"
|
|
title = re.sub(r"#(\d+)", rf"{repo_fullname}#\1", title)
|
|
|
|
hash_value = commits["Sha1"][:10]
|
|
commits_main.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(f"[{int(100 * (process / len_total))}%] Checking issue {issue} ", end="\r", flush=True)
|
|
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(f"[{int(100 * (process / len_total))}%] Checking issue {issue} ", end="\r", flush=True)
|
|
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(f"[{int(100 * (process / len_total))}%] Checking pull {pull} ", end="\r", flush=True)
|
|
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.append(pull)
|
|
|
|
# Print triaging stats
|
|
|
|
issues_involved = issues_closed | issues_commented | issues_created
|
|
|
|
print("**Involved in %s reports:** " % len(issues_involved))
|
|
print("* Confirmed: %s" % len(issues_confirmed))
|
|
print("* Closed as Resolved: %s" % len(issues_fixed))
|
|
print("* Closed as Archived: %s" % len(issues_archived))
|
|
print("* Closed as Duplicate: %s" % len(issues_duplicated))
|
|
print("* Needs Info from User: %s" % len(issues_needing_user_info))
|
|
print("* Needs Info from Developers: %s" % len(issues_needing_developer_info))
|
|
print("* Actions total: %s" % (len(issues_closed) + len(issues_commented) + len(issues_created)))
|
|
print()
|
|
|
|
# Print review stats
|
|
def print_pulls(pulls):
|
|
for pull in pulls:
|
|
pull_data = gitea_json_issue_get_cached(pull)
|
|
title = pull_data["title"]
|
|
owner, repo, _, number = pull.split('/')
|
|
print(f"* {title} ({owner}/{repo}!{number})")
|
|
|
|
print("**Review: %s**" % len(pulls_reviewed))
|
|
print_pulls(pulls_reviewed)
|
|
print()
|
|
|
|
# Print created diffs
|
|
print("**Created Pull Requests: %s**" % len(pulls_created))
|
|
print_pulls(pulls_created)
|
|
print()
|
|
|
|
# Print commits
|
|
print("**Commits:**")
|
|
for commit in commits_main:
|
|
print("*", commit)
|
|
print()
|
|
|
|
if verbose:
|
|
# Debug
|
|
|
|
def print_links(issues):
|
|
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
|
|
|
|
# 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, verbose=args.verbose)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
|
|
# wait for input to close window
|
|
input()
|