#!/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 import shutil import sys from gitea_utils import ( gitea_json_activities_get, gitea_json_issue_events_filter, gitea_json_issue_get, gitea_user_get, git_username_detect, ) from typing import ( Any, Dict, List, Set, 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: pass def argparse_create() -> argparse.ArgumentParser: 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( "--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, *, 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: List[str] = [] 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] = [] commits_main: List[str] = [] user_data: Dict[str, Any] = 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_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.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"]) assert isinstance(content_json, dict) repo_fullname = activity["repo"]["full_name"] content_json_commits: List[Dict[str, Any]] = content_json["Commits"] 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"] if hash_length > 0: hash_value[:hash_length] 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_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.append(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: 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: {: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() # Print commits print("**Commits:**") for commit in commits_main: print("*", commit) print() 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 # 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, hash_length=args.hash_length, verbose=args.verbose, ) if __name__ == "__main__": main() # Wait for input to close window. if IS_ATTY: input()