16cef255aSPatrick Williams#!/bin/env python3 26cef255aSPatrick Williamsimport argparse 36cef255aSPatrick Williamsimport json 4ca1b89ebSPatrick Williamsimport os 5ca1b89ebSPatrick Williamsimport re 66cef255aSPatrick Williamsimport yaml 76cef255aSPatrick Williams 8ca1b89ebSPatrick Williamsfrom sh import git # type: ignore 9ca1b89ebSPatrick Williamsfrom typing import List, Set, TypedDict, Optional 106cef255aSPatrick Williamsfrom yaml.loader import SafeLoader 116cef255aSPatrick Williams 126cef255aSPatrick Williams# A list of Gerrit users (email addresses). 13ca1b89ebSPatrick Williams# Some OWNERS files have empty lists for 'owners' or 'reviewers', which 14ca1b89ebSPatrick Williams# results in a None type for the value. 15ca1b89ebSPatrick 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 22ca1b89ebSPatrick Williamsclass MatchEntry(TypedDict, total=False): 23ca1b89ebSPatrick Williams suffix: str 24ca1b89ebSPatrick Williams regex: str 25ca1b89ebSPatrick Williams partial_regex: str 26ca1b89ebSPatrick Williams exact: str 27ca1b89ebSPatrick Williams owners: UsersList 28ca1b89ebSPatrick Williams reviewers: UsersList 29ca1b89ebSPatrick Williams 30ca1b89ebSPatrick 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 35ca1b89ebSPatrick 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: 61ca1b89ebSPatrick Williams def __init__(self, args: argparse.Namespace, owners: OwnersData): 62ca1b89ebSPatrick Williams files: Set[str] = set( 63ca1b89ebSPatrick Williams git.bake("-C", args.path) 64ca1b89ebSPatrick Williams .show(args.commit, pretty="", name_only=True, _tty_out=False) 65ca1b89ebSPatrick Williams .splitlines() 66ca1b89ebSPatrick Williams ) 676cef255aSPatrick Williams 68ca1b89ebSPatrick Williams self.owners: Set[str] = set(owners.get("owners") or []) 69ca1b89ebSPatrick Williams self.reviewers: Set[str] = set(owners.get("reviewers") or []) 706cef255aSPatrick Williams 71*47b59dc8SPatrick Williams for e in owners.get("matchers", None) or []: 72ca1b89ebSPatrick Williams if "exact" in e: 73ca1b89ebSPatrick Williams self.__exact(files, e) 74ca1b89ebSPatrick Williams elif "partial_regex" in e: 75ca1b89ebSPatrick Williams self.__partial_regex(files, e) 76ca1b89ebSPatrick Williams elif "regex" in e: 77ca1b89ebSPatrick Williams self.__regex(files, e) 78ca1b89ebSPatrick Williams elif "suffix" in e: 79ca1b89ebSPatrick Williams self.__suffix(files, e) 80ca1b89ebSPatrick Williams 81ca1b89ebSPatrick Williams self.reviewers = self.reviewers.difference(self.owners) 82ca1b89ebSPatrick Williams 83ca1b89ebSPatrick Williams def __add_entry(self, entry: MatchEntry) -> None: 84ca1b89ebSPatrick Williams self.owners = self.owners.union(entry.get("owners") or []) 85ca1b89ebSPatrick Williams self.reviewers = self.reviewers.union(entry.get("reviewers") or []) 86ca1b89ebSPatrick Williams 87ca1b89ebSPatrick Williams def __exact(self, files: Set[str], entry: MatchEntry) -> None: 88ca1b89ebSPatrick Williams for f in files: 89ca1b89ebSPatrick Williams if f == entry["exact"]: 90ca1b89ebSPatrick Williams self.__add_entry(entry) 91ca1b89ebSPatrick Williams 92ca1b89ebSPatrick Williams def __partial_regex(self, files: Set[str], entry: MatchEntry) -> None: 93ca1b89ebSPatrick Williams for f in files: 94ca1b89ebSPatrick Williams if re.search(entry["partial_regex"], f): 95ca1b89ebSPatrick Williams self.__add_entry(entry) 96ca1b89ebSPatrick Williams 97ca1b89ebSPatrick Williams def __regex(self, files: Set[str], entry: MatchEntry) -> None: 98ca1b89ebSPatrick Williams for f in files: 99ca1b89ebSPatrick Williams if re.fullmatch(entry["regex"], f): 100ca1b89ebSPatrick Williams self.__add_entry(entry) 101ca1b89ebSPatrick Williams 102ca1b89ebSPatrick Williams def __suffix(self, files: Set[str], entry: MatchEntry) -> None: 103ca1b89ebSPatrick Williams for f in files: 104ca1b89ebSPatrick Williams if os.path.splitext(f)[1] == entry["suffix"]: 105ca1b89ebSPatrick 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: 110ca1b89ebSPatrick 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 = [] 115ca1b89ebSPatrick Williams for o in sorted(matcher.owners): 1166cef255aSPatrick Williams # Gerrit uses 'r' for the required reviewers (owners). 1176cef255aSPatrick Williams result.append(f"r={o}") 118ca1b89ebSPatrick 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: 125ca1b89ebSPatrick Williams for o in sorted(matcher.owners): 1266cef255aSPatrick Williams print(json.dumps({"reviewer": o, "state": "REVIEWER"})) 127ca1b89ebSPatrick 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", 143*47b59dc8SPatrick Williams default=False, 144*47b59dc8SPatrick Williams action='store_true', 1456cef255aSPatrick Williams help="Format as git push options", 1466cef255aSPatrick Williams ) 147ca1b89ebSPatrick Williams parser_reviewers.add_argument( 148ca1b89ebSPatrick Williams "--commit", 149ca1b89ebSPatrick Williams default="HEAD", 150ca1b89ebSPatrick Williams help="Commit(s) to match against", 151ca1b89ebSPatrick Williams ) 1526cef255aSPatrick Williams parser_reviewers.set_defaults(func=subcmd_reviewers) 1536cef255aSPatrick Williams 1546cef255aSPatrick Williams args = parser.parse_args() 1556cef255aSPatrick Williams 1566cef255aSPatrick Williams file = YamlLoader.load(args.path + "/OWNERS") 1576cef255aSPatrick Williams args.func(args, file) 1586cef255aSPatrick Williams 1596cef255aSPatrick Williams 1606cef255aSPatrick Williamsif __name__ == "__main__": 1616cef255aSPatrick Williams main() 162