16cef255aSPatrick Williams#!/bin/env python3 26cef255aSPatrick Williamsimport argparse 36cef255aSPatrick Williamsimport json 4*ca1b89ebSPatrick Williamsimport os 5*ca1b89ebSPatrick Williamsimport re 66cef255aSPatrick Williamsimport yaml 76cef255aSPatrick Williams 8*ca1b89ebSPatrick Williamsfrom sh import git # type: ignore 9*ca1b89ebSPatrick Williamsfrom typing import List, Set, TypedDict, Optional 106cef255aSPatrick Williamsfrom yaml.loader import SafeLoader 116cef255aSPatrick Williams 126cef255aSPatrick Williams# A list of Gerrit users (email addresses). 13*ca1b89ebSPatrick Williams# Some OWNERS files have empty lists for 'owners' or 'reviewers', which 14*ca1b89ebSPatrick Williams# results in a None type for the value. 15*ca1b89ebSPatrick WilliamsUsersList = Optional[List[str]] 166cef255aSPatrick Williams 176cef255aSPatrick Williams# A YAML node with an extra line number. 186cef255aSPatrick Williamsclass NumberedNode(TypedDict): 196cef255aSPatrick Williams line_number: int 206cef255aSPatrick Williams 216cef255aSPatrick Williams 22*ca1b89ebSPatrick Williamsclass MatchEntry(TypedDict, total=False): 23*ca1b89ebSPatrick Williams suffix: str 24*ca1b89ebSPatrick Williams regex: str 25*ca1b89ebSPatrick Williams partial_regex: str 26*ca1b89ebSPatrick Williams exact: str 27*ca1b89ebSPatrick Williams owners: UsersList 28*ca1b89ebSPatrick Williams reviewers: UsersList 29*ca1b89ebSPatrick Williams 30*ca1b89ebSPatrick Williams 316cef255aSPatrick Williams# The root YAML node of an OWNERS file 326cef255aSPatrick Williamsclass OwnersData(NumberedNode, TypedDict, total=False): 336cef255aSPatrick Williams owners: UsersList 346cef255aSPatrick Williams reviewers: UsersList 35*ca1b89ebSPatrick Williams matchers: List[MatchEntry] 366cef255aSPatrick Williams 376cef255aSPatrick Williams 386cef255aSPatrick Williams# A YAML loader that adds the start line number onto each node (for 396cef255aSPatrick Williams# later linting support) 406cef255aSPatrick Williamsclass YamlLoader(SafeLoader): 416cef255aSPatrick Williams def construct_mapping( 426cef255aSPatrick Williams self, node: yaml.nodes.Node, deep: bool = False 436cef255aSPatrick Williams ) -> NumberedNode: 446cef255aSPatrick Williams mapping: NumberedNode = super(YamlLoader, self).construct_mapping( 456cef255aSPatrick Williams node, deep=deep 466cef255aSPatrick Williams ) # type: ignore 476cef255aSPatrick Williams mapping["line_number"] = node.start_mark.line + 1 486cef255aSPatrick Williams return mapping 496cef255aSPatrick Williams 506cef255aSPatrick Williams # Load a file and return the OwnersData. 516cef255aSPatrick Williams @staticmethod 526cef255aSPatrick Williams def load(file: str) -> OwnersData: 536cef255aSPatrick Williams data: OwnersData 546cef255aSPatrick Williams with open(file, "r") as f: 556cef255aSPatrick Williams data = yaml.load(f, Loader=YamlLoader) 566cef255aSPatrick Williams return data 576cef255aSPatrick Williams 586cef255aSPatrick Williams 596cef255aSPatrick Williams# Class to match commit information with OWNERS files. 606cef255aSPatrick Williamsclass CommitMatch: 61*ca1b89ebSPatrick Williams def __init__(self, args: argparse.Namespace, owners: OwnersData): 62*ca1b89ebSPatrick Williams files: Set[str] = set( 63*ca1b89ebSPatrick Williams git.bake("-C", args.path) 64*ca1b89ebSPatrick Williams .show(args.commit, pretty="", name_only=True, _tty_out=False) 65*ca1b89ebSPatrick Williams .splitlines() 66*ca1b89ebSPatrick Williams ) 676cef255aSPatrick Williams 68*ca1b89ebSPatrick Williams self.owners: Set[str] = set(owners.get("owners") or []) 69*ca1b89ebSPatrick Williams self.reviewers: Set[str] = set(owners.get("reviewers") or []) 706cef255aSPatrick Williams 71*ca1b89ebSPatrick Williams for e in owners.get("matchers", []): 72*ca1b89ebSPatrick Williams if "exact" in e: 73*ca1b89ebSPatrick Williams self.__exact(files, e) 74*ca1b89ebSPatrick Williams elif "partial_regex" in e: 75*ca1b89ebSPatrick Williams self.__partial_regex(files, e) 76*ca1b89ebSPatrick Williams elif "regex" in e: 77*ca1b89ebSPatrick Williams self.__regex(files, e) 78*ca1b89ebSPatrick Williams elif "suffix" in e: 79*ca1b89ebSPatrick Williams self.__suffix(files, e) 80*ca1b89ebSPatrick Williams 81*ca1b89ebSPatrick Williams self.reviewers = self.reviewers.difference(self.owners) 82*ca1b89ebSPatrick Williams 83*ca1b89ebSPatrick Williams def __add_entry(self, entry: MatchEntry) -> None: 84*ca1b89ebSPatrick Williams self.owners = self.owners.union(entry.get("owners") or []) 85*ca1b89ebSPatrick Williams self.reviewers = self.reviewers.union(entry.get("reviewers") or []) 86*ca1b89ebSPatrick Williams 87*ca1b89ebSPatrick Williams def __exact(self, files: Set[str], entry: MatchEntry) -> None: 88*ca1b89ebSPatrick Williams for f in files: 89*ca1b89ebSPatrick Williams if f == entry["exact"]: 90*ca1b89ebSPatrick Williams self.__add_entry(entry) 91*ca1b89ebSPatrick Williams 92*ca1b89ebSPatrick Williams def __partial_regex(self, files: Set[str], entry: MatchEntry) -> None: 93*ca1b89ebSPatrick Williams for f in files: 94*ca1b89ebSPatrick Williams if re.search(entry["partial_regex"], f): 95*ca1b89ebSPatrick Williams self.__add_entry(entry) 96*ca1b89ebSPatrick Williams 97*ca1b89ebSPatrick Williams def __regex(self, files: Set[str], entry: MatchEntry) -> None: 98*ca1b89ebSPatrick Williams for f in files: 99*ca1b89ebSPatrick Williams if re.fullmatch(entry["regex"], f): 100*ca1b89ebSPatrick Williams self.__add_entry(entry) 101*ca1b89ebSPatrick Williams 102*ca1b89ebSPatrick Williams def __suffix(self, files: Set[str], entry: MatchEntry) -> None: 103*ca1b89ebSPatrick Williams for f in files: 104*ca1b89ebSPatrick Williams if os.path.splitext(f)[1] == entry["suffix"]: 105*ca1b89ebSPatrick Williams self.__add_entry(entry) 1066cef255aSPatrick Williams 1076cef255aSPatrick Williams 1086cef255aSPatrick Williams# The subcommand to get the reviewers. 1096cef255aSPatrick Williamsdef subcmd_reviewers(args: argparse.Namespace, data: OwnersData) -> None: 110*ca1b89ebSPatrick Williams matcher = CommitMatch(args, data) 1116cef255aSPatrick Williams 1126cef255aSPatrick Williams # Print in `git push refs/for/branch%<reviewers>` format. 1136cef255aSPatrick Williams if args.push_args: 1146cef255aSPatrick Williams result = [] 115*ca1b89ebSPatrick Williams for o in sorted(matcher.owners): 1166cef255aSPatrick Williams # Gerrit uses 'r' for the required reviewers (owners). 1176cef255aSPatrick Williams result.append(f"r={o}") 118*ca1b89ebSPatrick Williams for r in sorted(matcher.reviewers): 1196cef255aSPatrick Williams # Gerrit uses 'cc' for the optional reviewers. 1206cef255aSPatrick Williams result.append(f"cc={r}") 1216cef255aSPatrick Williams print(",".join(result)) 1226cef255aSPatrick Williams # Print as Gerrit Add Reviewers POST format. 1236cef255aSPatrick Williams # https://gerrit.openbmc.org/Documentation/rest-api-changes.html#add-reviewer 1246cef255aSPatrick Williams else: 125*ca1b89ebSPatrick Williams for o in sorted(matcher.owners): 1266cef255aSPatrick Williams print(json.dumps({"reviewer": o, "state": "REVIEWER"})) 127*ca1b89ebSPatrick Williams for r in sorted(matcher.reviewers): 1286cef255aSPatrick Williams print(json.dumps({"reviewer": r, "state": "CC"})) 1296cef255aSPatrick Williams 1306cef255aSPatrick Williams 1316cef255aSPatrick Williamsdef main() -> None: 1326cef255aSPatrick Williams parser = argparse.ArgumentParser() 1336cef255aSPatrick Williams parser.add_argument( 1346cef255aSPatrick Williams "-p", "--path", default=".", help="Root path to analyse" 1356cef255aSPatrick Williams ) 1366cef255aSPatrick Williams subparsers = parser.add_subparsers() 1376cef255aSPatrick Williams 1386cef255aSPatrick Williams parser_reviewers = subparsers.add_parser( 1396cef255aSPatrick Williams "reviewers", help="Generate List of Reviewers" 1406cef255aSPatrick Williams ) 1416cef255aSPatrick Williams parser_reviewers.add_argument( 1426cef255aSPatrick Williams "--push-args", 1436cef255aSPatrick Williams action=argparse.BooleanOptionalAction, 1446cef255aSPatrick Williams help="Format as git push options", 1456cef255aSPatrick Williams ) 146*ca1b89ebSPatrick Williams parser_reviewers.add_argument( 147*ca1b89ebSPatrick Williams "--commit", 148*ca1b89ebSPatrick Williams default="HEAD", 149*ca1b89ebSPatrick Williams help="Commit(s) to match against", 150*ca1b89ebSPatrick Williams ) 1516cef255aSPatrick Williams parser_reviewers.set_defaults(func=subcmd_reviewers) 1526cef255aSPatrick Williams 1536cef255aSPatrick Williams args = parser.parse_args() 1546cef255aSPatrick Williams 1556cef255aSPatrick Williams file = YamlLoader.load(args.path + "/OWNERS") 1566cef255aSPatrick Williams args.func(args, file) 1576cef255aSPatrick Williams 1586cef255aSPatrick Williams 1596cef255aSPatrick Williamsif __name__ == "__main__": 1606cef255aSPatrick Williams main() 161