12e5b6d6dSopenharmony_ci#!/usr/bin/env python3 22e5b6d6dSopenharmony_ci# Copyright (C) 2018 and later: Unicode, Inc. and others. 32e5b6d6dSopenharmony_ci# License & terms of use: http://www.unicode.org/copyright.html 42e5b6d6dSopenharmony_ci# Author: shane@unicode.org 52e5b6d6dSopenharmony_ci 62e5b6d6dSopenharmony_ciimport argparse 72e5b6d6dSopenharmony_ciimport itertools 82e5b6d6dSopenharmony_ciimport os 92e5b6d6dSopenharmony_ciimport re 102e5b6d6dSopenharmony_ciimport sys 112e5b6d6dSopenharmony_ciimport datetime 122e5b6d6dSopenharmony_ci 132e5b6d6dSopenharmony_cifrom enum import Enum 142e5b6d6dSopenharmony_cifrom collections import namedtuple 152e5b6d6dSopenharmony_cifrom git import Repo 162e5b6d6dSopenharmony_cifrom jira import JIRA 172e5b6d6dSopenharmony_ci 182e5b6d6dSopenharmony_ci# singleCount = 0 192e5b6d6dSopenharmony_ci 202e5b6d6dSopenharmony_ciICUCommit = namedtuple("ICUCommit", ["issue_id", "commit"]) 212e5b6d6dSopenharmony_ci 222e5b6d6dSopenharmony_ciclass CommitWanted(Enum): 232e5b6d6dSopenharmony_ci REQUIRED = 1 242e5b6d6dSopenharmony_ci OPTIONAL = 2 252e5b6d6dSopenharmony_ci FORBIDDEN = 3 262e5b6d6dSopenharmony_ci ERROR = 4 272e5b6d6dSopenharmony_ci 282e5b6d6dSopenharmony_ciICUIssue = namedtuple("ICUIssue", ["issue_id", "is_closed", "commit_wanted", "issue"]) 292e5b6d6dSopenharmony_ci 302e5b6d6dSopenharmony_ci# JIRA constants. 312e5b6d6dSopenharmony_ci 322e5b6d6dSopenharmony_ci# TODO: clearly these should move into a config file of some sort. 332e5b6d6dSopenharmony_ci# NB: you can fetch the resolution IDs by authenticating to JIRA and then viewing 342e5b6d6dSopenharmony_ci# the URL given. 352e5b6d6dSopenharmony_ci 362e5b6d6dSopenharmony_ci# constants for jira_issue.fields.resolution.id 372e5b6d6dSopenharmony_ci# <https://unicode-org.atlassian.net/rest/api/2/resolution> 382e5b6d6dSopenharmony_ciR_NEEDS_MOREINFO = "10003" 392e5b6d6dSopenharmony_ciR_FIXED = "10004" 402e5b6d6dSopenharmony_ciR_DUPLICATE = "10006" 412e5b6d6dSopenharmony_ciR_OUTOFSCOPE = "10008" 422e5b6d6dSopenharmony_ciR_ASDESIGNED = "10009" 432e5b6d6dSopenharmony_ciR_WONTFIX = "10010" # deprecated 442e5b6d6dSopenharmony_ciR_INVALID = "10012" 452e5b6d6dSopenharmony_ciR_FIXED_BY_OTHER_TICKET = "10015" 462e5b6d6dSopenharmony_ciR_NOTREPRO = "10024" 472e5b6d6dSopenharmony_ciR_FIXED_NON_REPO = "10025" 482e5b6d6dSopenharmony_ciR_FIX_SURVEY_TOOL = "10022" 492e5b6d6dSopenharmony_ciR_OBSOLETE = "10023" 502e5b6d6dSopenharmony_ci 512e5b6d6dSopenharmony_ci# constants for jira_issue.fields.issuetype.id 522e5b6d6dSopenharmony_ci# <https://unicode-org.atlassian.net/rest/api/2/issuetype> 532e5b6d6dSopenharmony_ciI_ICU_USERGUIDE = "10010" 542e5b6d6dSopenharmony_ciI_TASK = "10003" 552e5b6d6dSopenharmony_ci 562e5b6d6dSopenharmony_ci# constants for jira_issue.fields.status.id 572e5b6d6dSopenharmony_ci# <https://unicode-org.atlassian.net/rest/api/2/status> 582e5b6d6dSopenharmony_ciS_REVIEWING = "10001" 592e5b6d6dSopenharmony_ciS_DONE = "10002" 602e5b6d6dSopenharmony_ciS_REVIEW_FEEDBACK = "10003" 612e5b6d6dSopenharmony_ci 622e5b6d6dSopenharmony_cidef jira_issue_under_review(jira_issue): 632e5b6d6dSopenharmony_ci """ 642e5b6d6dSopenharmony_ci Yields True if ticket is considered "under review" 652e5b6d6dSopenharmony_ci """ 662e5b6d6dSopenharmony_ci # TODO: should be data driven from a config file. 672e5b6d6dSopenharmony_ci if jira_issue.issue.fields.status.id in [S_REVIEWING, S_REVIEW_FEEDBACK]: 682e5b6d6dSopenharmony_ci return True 692e5b6d6dSopenharmony_ci else: 702e5b6d6dSopenharmony_ci return False 712e5b6d6dSopenharmony_ci 722e5b6d6dSopenharmony_cidef make_commit_wanted(jira_issue): 732e5b6d6dSopenharmony_ci """Yields a CommitWanted enum with the policy decision for this particular issue""" 742e5b6d6dSopenharmony_ci # TODO: should be data driven from a config file. 752e5b6d6dSopenharmony_ci if not jira_issue.fields.resolution: 762e5b6d6dSopenharmony_ci commit_wanted = CommitWanted["OPTIONAL"] 772e5b6d6dSopenharmony_ci elif jira_issue.fields.resolution.id in [ R_DUPLICATE, R_ASDESIGNED, R_OUTOFSCOPE, R_NOTREPRO, R_INVALID, R_NEEDS_MOREINFO, R_OBSOLETE ]: 782e5b6d6dSopenharmony_ci commit_wanted = CommitWanted["FORBIDDEN"] 792e5b6d6dSopenharmony_ci elif jira_issue.fields.resolution.id in [ R_FIXED_NON_REPO, R_FIX_SURVEY_TOOL, R_FIXED_BY_OTHER_TICKET ]: 802e5b6d6dSopenharmony_ci commit_wanted = CommitWanted["FORBIDDEN"] 812e5b6d6dSopenharmony_ci elif jira_issue.fields.issuetype.id in [ I_ICU_USERGUIDE, I_TASK ]: 822e5b6d6dSopenharmony_ci commit_wanted = CommitWanted["OPTIONAL"] 832e5b6d6dSopenharmony_ci elif jira_issue.fields.resolution.id in [ R_FIXED ]: 842e5b6d6dSopenharmony_ci commit_wanted = CommitWanted["REQUIRED"] 852e5b6d6dSopenharmony_ci elif jira_issue.fields.resolution.id == R_FIXED_BY_OTHER_TICKET: 862e5b6d6dSopenharmony_ci commit_wanted = CommitWanted["FORBIDDEN"] 872e5b6d6dSopenharmony_ci elif jira_issue.fields.resolution.id != R_FIXED: 882e5b6d6dSopenharmony_ci commit_wanted = CommitWanted["ERROR"] 892e5b6d6dSopenharmony_ci else: 902e5b6d6dSopenharmony_ci commit_wanted = CommitWanted["REQUIRED"] 912e5b6d6dSopenharmony_ci return commit_wanted 922e5b6d6dSopenharmony_ci 932e5b6d6dSopenharmony_ci 942e5b6d6dSopenharmony_ciflag_parser = argparse.ArgumentParser( 952e5b6d6dSopenharmony_ci description = "Generates a Markdown report for commits on main since the 'latest' tag.", 962e5b6d6dSopenharmony_ci formatter_class = argparse.ArgumentDefaultsHelpFormatter 972e5b6d6dSopenharmony_ci) 982e5b6d6dSopenharmony_ciflag_parser.add_argument( 992e5b6d6dSopenharmony_ci "--rev-range", 1002e5b6d6dSopenharmony_ci help = "A git revision range; see https://git-scm.com/docs/gitrevisions. Should be the two-dot range between the previous release and the current tip.", 1012e5b6d6dSopenharmony_ci required = True 1022e5b6d6dSopenharmony_ci) 1032e5b6d6dSopenharmony_ciflag_parser.add_argument( 1042e5b6d6dSopenharmony_ci "--repo-root", 1052e5b6d6dSopenharmony_ci help = "Path to the repository to check", 1062e5b6d6dSopenharmony_ci default = os.path.join(os.path.dirname(__file__), "..", "..") 1072e5b6d6dSopenharmony_ci) 1082e5b6d6dSopenharmony_ciflag_parser.add_argument( 1092e5b6d6dSopenharmony_ci "--jira-hostname", 1102e5b6d6dSopenharmony_ci help = "Hostname of the Jira instance", 1112e5b6d6dSopenharmony_ci default = "unicode-org.atlassian.net" 1122e5b6d6dSopenharmony_ci) 1132e5b6d6dSopenharmony_ciflag_parser.add_argument( 1142e5b6d6dSopenharmony_ci "--jira-username", 1152e5b6d6dSopenharmony_ci help = "Username to use for authenticating to Jira", 1162e5b6d6dSopenharmony_ci default = os.environ.get("JIRA_USERNAME", None) 1172e5b6d6dSopenharmony_ci) 1182e5b6d6dSopenharmony_ciflag_parser.add_argument( 1192e5b6d6dSopenharmony_ci "--jira-password", 1202e5b6d6dSopenharmony_ci help = "Password to use for authenticating to Jira. Authentication is necessary to process sensitive tickets. Leave empty to skip authentication. Instead of passing your password on the command line, you can save your password in the JIRA_PASSWORD environment variable. You can also create a file in this directory named \".env\" with the contents \"JIRA_PASSWORD=xxxxx\".", 1212e5b6d6dSopenharmony_ci default = os.environ.get("JIRA_PASSWORD", None) 1222e5b6d6dSopenharmony_ci) 1232e5b6d6dSopenharmony_ciflag_parser.add_argument( 1242e5b6d6dSopenharmony_ci "--jira-query", 1252e5b6d6dSopenharmony_ci help = "JQL query load tickets; this should match tickets expected to correspond to the commits being checked. Example: 'project=ICU and fixVersion=63.1'; set fixVersion to the upcoming version.", 1262e5b6d6dSopenharmony_ci required = True 1272e5b6d6dSopenharmony_ci) 1282e5b6d6dSopenharmony_ciflag_parser.add_argument( 1292e5b6d6dSopenharmony_ci "--github-url", 1302e5b6d6dSopenharmony_ci help = "Base URL of the GitHub repo", 1312e5b6d6dSopenharmony_ci default = "https://github.com/unicode-org/icu" 1322e5b6d6dSopenharmony_ci) 1332e5b6d6dSopenharmony_ciflag_parser.add_argument( 1342e5b6d6dSopenharmony_ci "--nocopyright", 1352e5b6d6dSopenharmony_ci help = "Omit ICU copyright", 1362e5b6d6dSopenharmony_ci action = "store_true" 1372e5b6d6dSopenharmony_ci) 1382e5b6d6dSopenharmony_ci 1392e5b6d6dSopenharmony_ci 1402e5b6d6dSopenharmony_cidef issue_id_to_url(issue_id, jira_hostname, **kwargs): 1412e5b6d6dSopenharmony_ci return "https://%s/browse/%s" % (jira_hostname, issue_id) 1422e5b6d6dSopenharmony_ci 1432e5b6d6dSopenharmony_ci 1442e5b6d6dSopenharmony_cidef pretty_print_commit(commit, github_url, **kwargs): 1452e5b6d6dSopenharmony_ci print("- %s `%s`" % (commit.commit.hexsha[:7], commit.commit.summary)) 1462e5b6d6dSopenharmony_ci print("\t- Authored by %s <%s>" % (commit.commit.author.name, commit.commit.author.email)) 1472e5b6d6dSopenharmony_ci print("\t- Committed at %s" % commit.commit.committed_datetime.isoformat()) 1482e5b6d6dSopenharmony_ci print("\t- GitHub Link: %s" % "%s/commit/%s" % (github_url, commit.commit.hexsha)) 1492e5b6d6dSopenharmony_ci 1502e5b6d6dSopenharmony_ci 1512e5b6d6dSopenharmony_cidef pretty_print_issue(issue, type=None, **kwargs): 1522e5b6d6dSopenharmony_ci print("- %s: `%s`" % (issue.issue_id, issue.issue.fields.summary)) 1532e5b6d6dSopenharmony_ci if type: 1542e5b6d6dSopenharmony_ci print("\t- _%s_" % type) 1552e5b6d6dSopenharmony_ci if issue.issue.fields.assignee: 1562e5b6d6dSopenharmony_ci print("\t- Assigned to %s" % issue.issue.fields.assignee.displayName) 1572e5b6d6dSopenharmony_ci else: 1582e5b6d6dSopenharmony_ci print("\t- No assignee!") 1592e5b6d6dSopenharmony_ci # If actually under review, print reviewer 1602e5b6d6dSopenharmony_ci if jira_issue_under_review(issue) and issue.issue.fields.customfield_10031: 1612e5b6d6dSopenharmony_ci print("\t- Reviewer: %s" % issue.issue.fields.customfield_10031.displayName) 1622e5b6d6dSopenharmony_ci print("\t- Jira Link: %s" % issue_id_to_url(issue.issue_id, **kwargs)) 1632e5b6d6dSopenharmony_ci print("\t- Status: %s" % issue.issue.fields.status.name) 1642e5b6d6dSopenharmony_ci if(issue.issue.fields.resolution): 1652e5b6d6dSopenharmony_ci print("\t- Resolution: " + issue.issue.fields.resolution.name) 1662e5b6d6dSopenharmony_ci if(issue.issue.fields.fixVersions): 1672e5b6d6dSopenharmony_ci for version in issue.issue.fields.fixVersions: 1682e5b6d6dSopenharmony_ci print("\t- Fix Version: " + version.name) 1692e5b6d6dSopenharmony_ci else: 1702e5b6d6dSopenharmony_ci print("\t- Fix Version: _none_") 1712e5b6d6dSopenharmony_ci if issue.issue.fields.components and len(issue.issue.fields.components) > 0: 1722e5b6d6dSopenharmony_ci print("\t- Component(s): " + (' '.join(sorted([str(component.name) for component in issue.issue.fields.components])))) 1732e5b6d6dSopenharmony_ci 1742e5b6d6dSopenharmony_cidef get_commits(repo_root, rev_range, **kwargs): 1752e5b6d6dSopenharmony_ci """ 1762e5b6d6dSopenharmony_ci Yields an ICUCommit for each commit in the user-specified rev-range. 1772e5b6d6dSopenharmony_ci """ 1782e5b6d6dSopenharmony_ci repo = Repo(repo_root) 1792e5b6d6dSopenharmony_ci for commit in repo.iter_commits(rev_range): 1802e5b6d6dSopenharmony_ci match = re.search(r"^(\w+-\d+) ", commit.message) 1812e5b6d6dSopenharmony_ci if match: 1822e5b6d6dSopenharmony_ci issue_id = match.group(1) 1832e5b6d6dSopenharmony_ci # print("@@@ %s = %s / %s" % (issue_id, commit, commit.summary), file=sys.stderr) 1842e5b6d6dSopenharmony_ci yield ICUCommit(issue_id, commit) 1852e5b6d6dSopenharmony_ci else: 1862e5b6d6dSopenharmony_ci yield ICUCommit(None, commit) 1872e5b6d6dSopenharmony_ci 1882e5b6d6dSopenharmony_cidef get_cherrypicked_commits(repo_root, rev_range, **kwargs): 1892e5b6d6dSopenharmony_ci """ 1902e5b6d6dSopenharmony_ci Yields a set of commit SHAs (strings) that should be EXCLUDED from 1912e5b6d6dSopenharmony_ci "missing jira" consideration, because they have already been cherry-picked onto the maint branch. 1922e5b6d6dSopenharmony_ci """ 1932e5b6d6dSopenharmony_ci repo = Repo(repo_root) 1942e5b6d6dSopenharmony_ci [a, b] = splitRevRange(rev_range) 1952e5b6d6dSopenharmony_ci branchCut = get_branchcut_sha(repo_root, rev_range) 1962e5b6d6dSopenharmony_ci print ("## git cherry %s %s %s (branch cut)" % (a, b, branchCut), file=sys.stderr) 1972e5b6d6dSopenharmony_ci cherries = repo.git.cherry(a, b, branchCut) 1982e5b6d6dSopenharmony_ci lns = cherries.split('\n') 1992e5b6d6dSopenharmony_ci excludeThese = set() 2002e5b6d6dSopenharmony_ci for ln in lns: 2012e5b6d6dSopenharmony_ci [symbol, sha] = ln.split(' ') 2022e5b6d6dSopenharmony_ci if(symbol == '-'): 2032e5b6d6dSopenharmony_ci # print("Exclude: %s" % sha, file=sys.stderr) 2042e5b6d6dSopenharmony_ci excludeThese.add(sha) 2052e5b6d6dSopenharmony_ci print("## Collected %d commit(s) to exclude" % len(excludeThese)) 2062e5b6d6dSopenharmony_ci return excludeThese 2072e5b6d6dSopenharmony_ci 2082e5b6d6dSopenharmony_cidef splitRevRange(rev_range): 2092e5b6d6dSopenharmony_ci """ 2102e5b6d6dSopenharmony_ci Return the start and end of the revrange 2112e5b6d6dSopenharmony_ci """ 2122e5b6d6dSopenharmony_ci return rev_range.split('..') 2132e5b6d6dSopenharmony_ci 2142e5b6d6dSopenharmony_cidef get_branchcut_sha(repo_root, rev_range): 2152e5b6d6dSopenharmony_ci """ 2162e5b6d6dSopenharmony_ci Return the sha of the 'branch cut', that is, the merge-base. 2172e5b6d6dSopenharmony_ci Returns a git commit 2182e5b6d6dSopenharmony_ci """ 2192e5b6d6dSopenharmony_ci repo = Repo(repo_root) 2202e5b6d6dSopenharmony_ci [a, b] = splitRevRange(rev_range) 2212e5b6d6dSopenharmony_ci return repo.merge_base(a, b)[0] 2222e5b6d6dSopenharmony_ci 2232e5b6d6dSopenharmony_cidef get_jira_instance(jira_hostname, jira_username, jira_password, **kwargs): 2242e5b6d6dSopenharmony_ci jira_url = "https://%s" % jira_hostname 2252e5b6d6dSopenharmony_ci if jira_username and jira_password: 2262e5b6d6dSopenharmony_ci jira = JIRA(jira_url, basic_auth=(jira_username, jira_password)) 2272e5b6d6dSopenharmony_ci else: 2282e5b6d6dSopenharmony_ci jira = JIRA(jira_url) 2292e5b6d6dSopenharmony_ci return (jira_url, jira) 2302e5b6d6dSopenharmony_ci 2312e5b6d6dSopenharmony_cidef make_icu_issue(jira_issue): 2322e5b6d6dSopenharmony_ci """Yields an ICUIssue for the individual jira object""" 2332e5b6d6dSopenharmony_ci commit_wanted = make_commit_wanted(jira_issue) 2342e5b6d6dSopenharmony_ci return ICUIssue(jira_issue.key, jira_issue.fields.status.id == S_DONE, commit_wanted, jira_issue) 2352e5b6d6dSopenharmony_ci 2362e5b6d6dSopenharmony_ci 2372e5b6d6dSopenharmony_cidef get_jira_issues(jira_query, **kwargs): 2382e5b6d6dSopenharmony_ci """ 2392e5b6d6dSopenharmony_ci Yields an ICUIssue for each issue in the user-specified query. 2402e5b6d6dSopenharmony_ci """ 2412e5b6d6dSopenharmony_ci jira_url, jira = get_jira_instance(**kwargs) 2422e5b6d6dSopenharmony_ci # Jira limits us to query the API using a limited batch size. 2432e5b6d6dSopenharmony_ci start = 0 2442e5b6d6dSopenharmony_ci batch_size = 100 # https://jira.atlassian.com/browse/JRACLOUD-67570 2452e5b6d6dSopenharmony_ci while True: 2462e5b6d6dSopenharmony_ci issues = jira.search_issues(jira_query, startAt=start, maxResults=batch_size) 2472e5b6d6dSopenharmony_ci if len(issues) > 0: 2482e5b6d6dSopenharmony_ci print("Loaded issues %d-%d" % (start + 1, start + len(issues)), file=sys.stderr) 2492e5b6d6dSopenharmony_ci else: 2502e5b6d6dSopenharmony_ci print(":warning: No issues matched the query.") # leave this as a warning 2512e5b6d6dSopenharmony_ci for jira_issue in issues: 2522e5b6d6dSopenharmony_ci yield make_icu_issue(jira_issue) 2532e5b6d6dSopenharmony_ci if len(issues) < batch_size: 2542e5b6d6dSopenharmony_ci break 2552e5b6d6dSopenharmony_ci start += batch_size 2562e5b6d6dSopenharmony_ci 2572e5b6d6dSopenharmony_cijira_issue_map = dict() # loaded in main() 2582e5b6d6dSopenharmony_ci 2592e5b6d6dSopenharmony_cidef get_single_jira_issue(issue_id, **kwargs): 2602e5b6d6dSopenharmony_ci """ 2612e5b6d6dSopenharmony_ci Returns a single ICUIssue for the given issue ID. 2622e5b6d6dSopenharmony_ci This can always be used (in- or out- of query issues), because it 2632e5b6d6dSopenharmony_ci uses the jira_issue_map as the backing store. 2642e5b6d6dSopenharmony_ci """ 2652e5b6d6dSopenharmony_ci if issue_id in jira_issue_map: 2662e5b6d6dSopenharmony_ci # print("Cache hit: issue %s " % (issue_id), file=sys.stderr) 2672e5b6d6dSopenharmony_ci return jira_issue_map[issue_id] 2682e5b6d6dSopenharmony_ci jira_url, jira = get_jira_instance(**kwargs) 2692e5b6d6dSopenharmony_ci jira_issue = jira.issue(issue_id) 2702e5b6d6dSopenharmony_ci # singleCount = singleCount + 1 2712e5b6d6dSopenharmony_ci if jira_issue: 2722e5b6d6dSopenharmony_ci icu_issue = make_icu_issue(jira_issue) 2732e5b6d6dSopenharmony_ci else: 2742e5b6d6dSopenharmony_ci icu_issue = None 2752e5b6d6dSopenharmony_ci jira_issue_map[issue_id] = icu_issue 2762e5b6d6dSopenharmony_ci print("Loaded single issue %s (%d in cache) " % (issue_id, len(jira_issue_map)), file=sys.stderr) 2772e5b6d6dSopenharmony_ci return icu_issue 2782e5b6d6dSopenharmony_ci 2792e5b6d6dSopenharmony_cidef toplink(): 2802e5b6d6dSopenharmony_ci print("[Top](#table-of-contents)") 2812e5b6d6dSopenharmony_ci print() 2822e5b6d6dSopenharmony_ci 2832e5b6d6dSopenharmony_cidef sectionToFragment(section): 2842e5b6d6dSopenharmony_ci return re.sub(r' ', '-', section.lower()) 2852e5b6d6dSopenharmony_ci 2862e5b6d6dSopenharmony_ci# def aname(section): 2872e5b6d6dSopenharmony_ci# """convert section name to am anchor""" 2882e5b6d6dSopenharmony_ci# return "<a name=\"%s\"></a>" % sectionToFragment(section) 2892e5b6d6dSopenharmony_ci 2902e5b6d6dSopenharmony_cidef print_sectionheader(section): 2912e5b6d6dSopenharmony_ci """Print a section (###) header, including anchor""" 2922e5b6d6dSopenharmony_ci print("### %s" % (section)) 2932e5b6d6dSopenharmony_ci #print("### %s%s" % (aname(section), section)) 2942e5b6d6dSopenharmony_ci 2952e5b6d6dSopenharmony_cidef main(): 2962e5b6d6dSopenharmony_ci args = flag_parser.parse_args() 2972e5b6d6dSopenharmony_ci print("TIP: Have you pulled the latest main? This script only looks at local commits.", file=sys.stderr) 2982e5b6d6dSopenharmony_ci if not args.jira_username or not args.jira_password: 2992e5b6d6dSopenharmony_ci print("WARNING: Jira credentials not supplied. Sensitive tickets will not be found.", file=sys.stderr) 3002e5b6d6dSopenharmony_ci authenticated = False 3012e5b6d6dSopenharmony_ci else: 3022e5b6d6dSopenharmony_ci authenticated = True 3032e5b6d6dSopenharmony_ci 3042e5b6d6dSopenharmony_ci # exclude these, already merged to old maint 3052e5b6d6dSopenharmony_ci excludeAlreadyMergedToOldMaint = get_cherrypicked_commits(**vars(args)) 3062e5b6d6dSopenharmony_ci 3072e5b6d6dSopenharmony_ci commits = list(get_commits(**vars(args))) 3082e5b6d6dSopenharmony_ci issues = list(get_jira_issues(**vars(args))) 3092e5b6d6dSopenharmony_ci 3102e5b6d6dSopenharmony_ci # commit_issue_ids is all commits in the git query. Excluding cherry exclusions. 3112e5b6d6dSopenharmony_ci commit_issue_ids = set(commit.issue_id for commit in commits if commit.issue_id is not None and commit.commit.hexsha not in excludeAlreadyMergedToOldMaint) 3122e5b6d6dSopenharmony_ci # which issues have commits that were excluded 3132e5b6d6dSopenharmony_ci excluded_commit_issue_ids = set(commit.issue_id for commit in commits if commit.issue_id is not None and commit.commit.hexsha in excludeAlreadyMergedToOldMaint) 3142e5b6d6dSopenharmony_ci 3152e5b6d6dSopenharmony_ci # grouped_commits is all commits and issue_ids in the git query, regardless of issue status 3162e5b6d6dSopenharmony_ci # but NOT including cherry exclusions 3172e5b6d6dSopenharmony_ci grouped_commits = [ 3182e5b6d6dSopenharmony_ci (issue_id, [commit for commit in commits if commit.issue_id == issue_id and commit.commit.hexsha not in excludeAlreadyMergedToOldMaint]) 3192e5b6d6dSopenharmony_ci for issue_id in sorted(commit_issue_ids) 3202e5b6d6dSopenharmony_ci ] 3212e5b6d6dSopenharmony_ci # add all queried issues to the cache 3222e5b6d6dSopenharmony_ci for issue in issues: 3232e5b6d6dSopenharmony_ci jira_issue_map[issue.issue_id] = issue 3242e5b6d6dSopenharmony_ci # only the issue ids in-query 3252e5b6d6dSopenharmony_ci jira_issue_ids = set(issue.issue_id for issue in issues) 3262e5b6d6dSopenharmony_ci # only the closed issue ids in-query 3272e5b6d6dSopenharmony_ci closed_jira_issue_ids = set(issue.issue_id for issue in issues if issue.is_closed) 3282e5b6d6dSopenharmony_ci 3292e5b6d6dSopenharmony_ci # keep track of issues that we already said have no commit. 3302e5b6d6dSopenharmony_ci no_commit_ids = set() 3312e5b6d6dSopenharmony_ci 3322e5b6d6dSopenharmony_ci # constants for the section names. 3332e5b6d6dSopenharmony_ci CLOSED_NO_COMMIT = "Closed Issues with No Commit" 3342e5b6d6dSopenharmony_ci CLOSED_ILLEGAL_RESOLUTION = "Closed Issues with Illegal Resolution or Commit" 3352e5b6d6dSopenharmony_ci COMMIT_NO_JIRA = "Commits without Jira Issue Tag" 3362e5b6d6dSopenharmony_ci COMMIT_OPEN_JIRA = "Commits with Open Jira Issue" 3372e5b6d6dSopenharmony_ci COMMIT_JIRA_NOT_IN_QUERY = "Commits with Jira Issue Not Found" 3382e5b6d6dSopenharmony_ci ISSUE_UNDER_REVIEW = "Issue is under Review" 3392e5b6d6dSopenharmony_ci 3402e5b6d6dSopenharmony_ci total_problems = 0 3412e5b6d6dSopenharmony_ci if not args.nocopyright: 3422e5b6d6dSopenharmony_ci print("<!--") 3432e5b6d6dSopenharmony_ci print("Copyright (C) 2021 and later: Unicode, Inc. and others.") 3442e5b6d6dSopenharmony_ci print("License & terms of use: http://www.unicode.org/copyright.html") 3452e5b6d6dSopenharmony_ci print("-->") 3462e5b6d6dSopenharmony_ci 3472e5b6d6dSopenharmony_ci print("Commit Report") 3482e5b6d6dSopenharmony_ci print("=============") 3492e5b6d6dSopenharmony_ci print() 3502e5b6d6dSopenharmony_ci print("Environment:") 3512e5b6d6dSopenharmony_ci print("- Now: %s" % datetime.datetime.now().isoformat()) 3522e5b6d6dSopenharmony_ci print("- Latest Commit: %s/commit/%s" % (args.github_url, commits[0].commit.hexsha)) 3532e5b6d6dSopenharmony_ci print("- Jira Query: `%s`" % args.jira_query) 3542e5b6d6dSopenharmony_ci print("- Rev Range: `%s`" % args.rev_range) 3552e5b6d6dSopenharmony_ci print("- Authenticated: %s" % ("`Yes`" if authenticated else "`No` (sensitive tickets not shown)")) 3562e5b6d6dSopenharmony_ci print() 3572e5b6d6dSopenharmony_ci print("## Table Of Contents") 3582e5b6d6dSopenharmony_ci for section in [CLOSED_NO_COMMIT, CLOSED_ILLEGAL_RESOLUTION, COMMIT_NO_JIRA, COMMIT_JIRA_NOT_IN_QUERY, COMMIT_OPEN_JIRA, ISSUE_UNDER_REVIEW]: 3592e5b6d6dSopenharmony_ci print("- [%s](#%s)" % (section, sectionToFragment(section))) 3602e5b6d6dSopenharmony_ci print() 3612e5b6d6dSopenharmony_ci print("## Problem Categories") 3622e5b6d6dSopenharmony_ci print_sectionheader(CLOSED_NO_COMMIT) 3632e5b6d6dSopenharmony_ci toplink() 3642e5b6d6dSopenharmony_ci print("Tip: Tickets with type 'Task' or 'User Guide' or resolution 'Fixed by Other Ticket' are ignored.") 3652e5b6d6dSopenharmony_ci print() 3662e5b6d6dSopenharmony_ci found = False 3672e5b6d6dSopenharmony_ci for issue in issues: 3682e5b6d6dSopenharmony_ci if not issue.is_closed: 3692e5b6d6dSopenharmony_ci continue 3702e5b6d6dSopenharmony_ci if issue.issue_id in commit_issue_ids: 3712e5b6d6dSopenharmony_ci continue 3722e5b6d6dSopenharmony_ci if issue.commit_wanted == CommitWanted["OPTIONAL"] or issue.commit_wanted == CommitWanted["FORBIDDEN"]: 3732e5b6d6dSopenharmony_ci continue 3742e5b6d6dSopenharmony_ci found = True 3752e5b6d6dSopenharmony_ci total_problems += 1 3762e5b6d6dSopenharmony_ci no_commit_ids.add(issue.issue_id) 3772e5b6d6dSopenharmony_ci pretty_print_issue(issue, type=CLOSED_NO_COMMIT, **vars(args)) 3782e5b6d6dSopenharmony_ci if issue.issue_id in excluded_commit_issue_ids: 3792e5b6d6dSopenharmony_ci print("\t - **Note: Has cherry-picked commits. Fix Version may be wrong.**") 3802e5b6d6dSopenharmony_ci print() 3812e5b6d6dSopenharmony_ci if not found: 3822e5b6d6dSopenharmony_ci print("*Success: No problems in this category!*") 3832e5b6d6dSopenharmony_ci 3842e5b6d6dSopenharmony_ci print_sectionheader(CLOSED_ILLEGAL_RESOLUTION) 3852e5b6d6dSopenharmony_ci toplink() 3862e5b6d6dSopenharmony_ci print("Tip: Fixed tickets should have resolution 'Fixed by Other Ticket' or 'Fixed'.") 3872e5b6d6dSopenharmony_ci print("Duplicate tickets should have their fixVersion tag removed.") 3882e5b6d6dSopenharmony_ci print("Tickets with resolution 'Fixed by Other Ticket' are not allowed to have commits.") 3892e5b6d6dSopenharmony_ci print() 3902e5b6d6dSopenharmony_ci found = False 3912e5b6d6dSopenharmony_ci for issue in issues: 3922e5b6d6dSopenharmony_ci if not issue.is_closed: 3932e5b6d6dSopenharmony_ci continue 3942e5b6d6dSopenharmony_ci if issue.commit_wanted == CommitWanted["OPTIONAL"]: 3952e5b6d6dSopenharmony_ci continue 3962e5b6d6dSopenharmony_ci if issue.issue_id in commit_issue_ids and issue.commit_wanted == CommitWanted["REQUIRED"]: 3972e5b6d6dSopenharmony_ci continue 3982e5b6d6dSopenharmony_ci if issue.issue_id not in commit_issue_ids and issue.commit_wanted == CommitWanted["FORBIDDEN"]: 3992e5b6d6dSopenharmony_ci continue 4002e5b6d6dSopenharmony_ci if issue.issue_id in no_commit_ids: 4012e5b6d6dSopenharmony_ci continue # we already complained about it above. don't double count. 4022e5b6d6dSopenharmony_ci found = True 4032e5b6d6dSopenharmony_ci total_problems += 1 4042e5b6d6dSopenharmony_ci pretty_print_issue(issue, type=CLOSED_ILLEGAL_RESOLUTION, **vars(args)) 4052e5b6d6dSopenharmony_ci if issue.issue_id not in commit_issue_ids and issue.commit_wanted == CommitWanted["REQUIRED"]: 4062e5b6d6dSopenharmony_ci print("\t- No commits, and they are REQUIRED.") 4072e5b6d6dSopenharmony_ci if issue.issue_id in commit_issue_ids and issue.commit_wanted == CommitWanted["FORBIDDEN"]: 4082e5b6d6dSopenharmony_ci print("\t- Has commits, and they are FORBIDDEN.") 4092e5b6d6dSopenharmony_ci print() 4102e5b6d6dSopenharmony_ci if not found: 4112e5b6d6dSopenharmony_ci print("*Success: No problems in this category!*") 4122e5b6d6dSopenharmony_ci 4132e5b6d6dSopenharmony_ci # TODO: This section should usually be empty due to the PR checker. 4142e5b6d6dSopenharmony_ci # Pre-calculate the count and omit it. 4152e5b6d6dSopenharmony_ci print() 4162e5b6d6dSopenharmony_ci print_sectionheader(COMMIT_NO_JIRA) 4172e5b6d6dSopenharmony_ci toplink() 4182e5b6d6dSopenharmony_ci print("Tip: If you see your name here, make sure to label your commits correctly in the future.") 4192e5b6d6dSopenharmony_ci print() 4202e5b6d6dSopenharmony_ci found = False 4212e5b6d6dSopenharmony_ci for commit in commits: 4222e5b6d6dSopenharmony_ci if commit.issue_id is not None: 4232e5b6d6dSopenharmony_ci continue 4242e5b6d6dSopenharmony_ci found = True 4252e5b6d6dSopenharmony_ci total_problems += 1 4262e5b6d6dSopenharmony_ci pretty_print_commit(commit, type=COMMIT_NO_JIRA, **vars(args)) 4272e5b6d6dSopenharmony_ci print() 4282e5b6d6dSopenharmony_ci if not found: 4292e5b6d6dSopenharmony_ci print("*Success: No problems in this category!*") 4302e5b6d6dSopenharmony_ci 4312e5b6d6dSopenharmony_ci print() 4322e5b6d6dSopenharmony_ci print_sectionheader(COMMIT_JIRA_NOT_IN_QUERY) 4332e5b6d6dSopenharmony_ci toplink() 4342e5b6d6dSopenharmony_ci print("Tip: Check that these tickets have the correct fixVersion tag.") 4352e5b6d6dSopenharmony_ci print() 4362e5b6d6dSopenharmony_ci found = False 4372e5b6d6dSopenharmony_ci for issue_id, commits in grouped_commits: 4382e5b6d6dSopenharmony_ci if issue_id in jira_issue_ids: 4392e5b6d6dSopenharmony_ci continue 4402e5b6d6dSopenharmony_ci found = True 4412e5b6d6dSopenharmony_ci total_problems += 1 4422e5b6d6dSopenharmony_ci print("#### Issue %s" % issue_id) 4432e5b6d6dSopenharmony_ci print() 4442e5b6d6dSopenharmony_ci print("_issue was not found in `%s`_" % args.jira_query) # TODO: link to query? 4452e5b6d6dSopenharmony_ci jira_issue = get_single_jira_issue(issue_id, **vars(args)) 4462e5b6d6dSopenharmony_ci if jira_issue: 4472e5b6d6dSopenharmony_ci pretty_print_issue(jira_issue, **vars(args)) 4482e5b6d6dSopenharmony_ci else: 4492e5b6d6dSopenharmony_ci print("*Jira issue does not seem to exist*") 4502e5b6d6dSopenharmony_ci print() 4512e5b6d6dSopenharmony_ci print("##### Commits with Issue %s" % issue_id) 4522e5b6d6dSopenharmony_ci print() 4532e5b6d6dSopenharmony_ci for commit in commits: 4542e5b6d6dSopenharmony_ci if(commit.commit.hexsha in excludeAlreadyMergedToOldMaint): 4552e5b6d6dSopenharmony_ci print("@@@ ALREADY MERGED") 4562e5b6d6dSopenharmony_ci pretty_print_commit(commit, **vars(args)) 4572e5b6d6dSopenharmony_ci print() 4582e5b6d6dSopenharmony_ci if not found: 4592e5b6d6dSopenharmony_ci print("*Success: No problems in this category!*") 4602e5b6d6dSopenharmony_ci 4612e5b6d6dSopenharmony_ci print() 4622e5b6d6dSopenharmony_ci 4632e5b6d6dSopenharmony_ci # list of issues that are in review 4642e5b6d6dSopenharmony_ci issues_in_review = set() 4652e5b6d6dSopenharmony_ci 4662e5b6d6dSopenharmony_ci print_sectionheader(COMMIT_OPEN_JIRA) 4672e5b6d6dSopenharmony_ci toplink() 4682e5b6d6dSopenharmony_ci print("Tip: Consider closing the ticket if it is fixed.") 4692e5b6d6dSopenharmony_ci print() 4702e5b6d6dSopenharmony_ci found = False 4712e5b6d6dSopenharmony_ci componentToTicket = {} 4722e5b6d6dSopenharmony_ci def addToComponent(component, issue_id): 4732e5b6d6dSopenharmony_ci if component not in componentToTicket: 4742e5b6d6dSopenharmony_ci componentToTicket[component] = set() 4752e5b6d6dSopenharmony_ci componentToTicket[component].add(issue_id) 4762e5b6d6dSopenharmony_ci # first, scan ahead for the components 4772e5b6d6dSopenharmony_ci for issue_id, commits in grouped_commits: 4782e5b6d6dSopenharmony_ci if issue_id in closed_jira_issue_ids: 4792e5b6d6dSopenharmony_ci continue 4802e5b6d6dSopenharmony_ci jira_issue = get_single_jira_issue(issue_id, **vars(args)) 4812e5b6d6dSopenharmony_ci if jira_issue and jira_issue.is_closed: 4822e5b6d6dSopenharmony_ci # JIRA ticket was not in query, but was actually closed. 4832e5b6d6dSopenharmony_ci continue 4842e5b6d6dSopenharmony_ci if jira_issue_under_review(jira_issue): 4852e5b6d6dSopenharmony_ci print("skipping for now- %s is under review" % issue_id, file=sys.stderr) 4862e5b6d6dSopenharmony_ci issues_in_review.add(issue_id) 4872e5b6d6dSopenharmony_ci continue 4882e5b6d6dSopenharmony_ci # OK. Now, split it out by component 4892e5b6d6dSopenharmony_ci if jira_issue.issue.fields.components and len(jira_issue.issue.fields.components) > 0: 4902e5b6d6dSopenharmony_ci for component in jira_issue.issue.fields.components: 4912e5b6d6dSopenharmony_ci addToComponent(component.name, issue_id) 4922e5b6d6dSopenharmony_ci else: 4932e5b6d6dSopenharmony_ci addToComponent("(no component)", issue_id) 4942e5b6d6dSopenharmony_ci 4952e5b6d6dSopenharmony_ci print("#### Open Issues by Component") 4962e5b6d6dSopenharmony_ci print() 4972e5b6d6dSopenharmony_ci for component in sorted(componentToTicket.keys()): 4982e5b6d6dSopenharmony_ci print(" - **%s**: %s" % (component, ' '.join("[%s](#issue-%s)" % (issue_id, sectionToFragment(issue_id)) for issue_id in componentToTicket[component]))) 4992e5b6d6dSopenharmony_ci 5002e5b6d6dSopenharmony_ci print() 5012e5b6d6dSopenharmony_ci print() 5022e5b6d6dSopenharmony_ci 5032e5b6d6dSopenharmony_ci # now, actually show the ticket list. 5042e5b6d6dSopenharmony_ci for issue_id, commits in grouped_commits: 5052e5b6d6dSopenharmony_ci if issue_id in closed_jira_issue_ids: 5062e5b6d6dSopenharmony_ci continue 5072e5b6d6dSopenharmony_ci jira_issue = get_single_jira_issue(issue_id, **vars(args)) 5082e5b6d6dSopenharmony_ci if jira_issue and jira_issue.is_closed: 5092e5b6d6dSopenharmony_ci # JIRA ticket was not in query, but was actually closed. 5102e5b6d6dSopenharmony_ci continue 5112e5b6d6dSopenharmony_ci if jira_issue_under_review(jira_issue): 5122e5b6d6dSopenharmony_ci # We already added it to the review list above. 5132e5b6d6dSopenharmony_ci continue 5142e5b6d6dSopenharmony_ci print("#### Issue %s" % issue_id) 5152e5b6d6dSopenharmony_ci print() 5162e5b6d6dSopenharmony_ci print("_Jira issue is open_") 5172e5b6d6dSopenharmony_ci if jira_issue: 5182e5b6d6dSopenharmony_ci pretty_print_issue(jira_issue, **vars(args)) 5192e5b6d6dSopenharmony_ci else: 5202e5b6d6dSopenharmony_ci print("*Jira issue does not seem to exist*") 5212e5b6d6dSopenharmony_ci print() 5222e5b6d6dSopenharmony_ci print("##### Commits with Issue %s" % issue_id) 5232e5b6d6dSopenharmony_ci print() 5242e5b6d6dSopenharmony_ci found = True 5252e5b6d6dSopenharmony_ci total_problems += 1 5262e5b6d6dSopenharmony_ci for commit in commits: 5272e5b6d6dSopenharmony_ci # print("@@@@ %s = %s / %s" % (issue_id, commit, commit.commit.summary), file=sys.stderr) 5282e5b6d6dSopenharmony_ci pretty_print_commit(commit, **vars(args)) 5292e5b6d6dSopenharmony_ci print() 5302e5b6d6dSopenharmony_ci if not found: 5312e5b6d6dSopenharmony_ci print("*Success: No problems in this category!*") 5322e5b6d6dSopenharmony_ci 5332e5b6d6dSopenharmony_ci print_sectionheader(ISSUE_UNDER_REVIEW) 5342e5b6d6dSopenharmony_ci print() 5352e5b6d6dSopenharmony_ci toplink() 5362e5b6d6dSopenharmony_ci print("These issues are otherwise accounted for above, but are in review.") 5372e5b6d6dSopenharmony_ci for issue_id in sorted(issues_in_review): 5382e5b6d6dSopenharmony_ci jira_issue = get_single_jira_issue(issue_id, **vars(args)) 5392e5b6d6dSopenharmony_ci pretty_print_issue(jira_issue, type=ISSUE_UNDER_REVIEW, **vars(args)) 5402e5b6d6dSopenharmony_ci 5412e5b6d6dSopenharmony_ci print() 5422e5b6d6dSopenharmony_ci print("## Total Problems: %s" % total_problems) 5432e5b6d6dSopenharmony_ci print("## Issues under review: %s" % len(issues_in_review)) # not counted as a problem. 5442e5b6d6dSopenharmony_ci 5452e5b6d6dSopenharmony_ciif __name__ == "__main__": 5462e5b6d6dSopenharmony_ci main() 547