1*6cef255aSPatrick Williams#!/bin/env python3 2*6cef255aSPatrick Williamsimport argparse 3*6cef255aSPatrick Williamsimport json 4*6cef255aSPatrick Williamsimport yaml 5*6cef255aSPatrick Williams 6*6cef255aSPatrick Williamsfrom typing import List, TypedDict 7*6cef255aSPatrick Williamsfrom yaml.loader import SafeLoader 8*6cef255aSPatrick Williams 9*6cef255aSPatrick Williams# A list of Gerrit users (email addresses). 10*6cef255aSPatrick WilliamsUsersList = List[str] 11*6cef255aSPatrick Williams 12*6cef255aSPatrick Williams# A YAML node with an extra line number. 13*6cef255aSPatrick Williamsclass NumberedNode(TypedDict): 14*6cef255aSPatrick Williams line_number: int 15*6cef255aSPatrick Williams 16*6cef255aSPatrick Williams 17*6cef255aSPatrick Williams# The root YAML node of an OWNERS file 18*6cef255aSPatrick Williamsclass OwnersData(NumberedNode, TypedDict, total=False): 19*6cef255aSPatrick Williams owners: UsersList 20*6cef255aSPatrick Williams reviewers: UsersList 21*6cef255aSPatrick Williams 22*6cef255aSPatrick Williams 23*6cef255aSPatrick Williams# A YAML loader that adds the start line number onto each node (for 24*6cef255aSPatrick Williams# later linting support) 25*6cef255aSPatrick Williamsclass YamlLoader(SafeLoader): 26*6cef255aSPatrick Williams def construct_mapping( 27*6cef255aSPatrick Williams self, node: yaml.nodes.Node, deep: bool = False 28*6cef255aSPatrick Williams ) -> NumberedNode: 29*6cef255aSPatrick Williams mapping: NumberedNode = super(YamlLoader, self).construct_mapping( 30*6cef255aSPatrick Williams node, deep=deep 31*6cef255aSPatrick Williams ) # type: ignore 32*6cef255aSPatrick Williams mapping["line_number"] = node.start_mark.line + 1 33*6cef255aSPatrick Williams return mapping 34*6cef255aSPatrick Williams 35*6cef255aSPatrick Williams # Load a file and return the OwnersData. 36*6cef255aSPatrick Williams @staticmethod 37*6cef255aSPatrick Williams def load(file: str) -> OwnersData: 38*6cef255aSPatrick Williams data: OwnersData 39*6cef255aSPatrick Williams with open(file, "r") as f: 40*6cef255aSPatrick Williams data = yaml.load(f, Loader=YamlLoader) 41*6cef255aSPatrick Williams return data 42*6cef255aSPatrick Williams 43*6cef255aSPatrick Williams 44*6cef255aSPatrick Williams# Class to match commit information with OWNERS files. 45*6cef255aSPatrick Williams# TODO: git commit piece not yet supported. 46*6cef255aSPatrick Williamsclass CommitMatch: 47*6cef255aSPatrick Williams def __init__(self, owners: OwnersData): 48*6cef255aSPatrick Williams self.data = owners 49*6cef255aSPatrick Williams 50*6cef255aSPatrick Williams def owners(self) -> UsersList: 51*6cef255aSPatrick Williams return self.data["owners"] if "owners" in self.data else [] 52*6cef255aSPatrick Williams 53*6cef255aSPatrick Williams def reviewers(self) -> UsersList: 54*6cef255aSPatrick Williams return self.data["reviewers"] if "reviewers" in self.data else [] 55*6cef255aSPatrick Williams 56*6cef255aSPatrick Williams 57*6cef255aSPatrick Williams# The subcommand to get the reviewers. 58*6cef255aSPatrick Williamsdef subcmd_reviewers(args: argparse.Namespace, data: OwnersData) -> None: 59*6cef255aSPatrick Williams matcher = CommitMatch(data) 60*6cef255aSPatrick Williams 61*6cef255aSPatrick Williams # Print in `git push refs/for/branch%<reviewers>` format. 62*6cef255aSPatrick Williams if args.push_args: 63*6cef255aSPatrick Williams result = [] 64*6cef255aSPatrick Williams for o in matcher.owners(): 65*6cef255aSPatrick Williams # Gerrit uses 'r' for the required reviewers (owners). 66*6cef255aSPatrick Williams result.append(f"r={o}") 67*6cef255aSPatrick Williams for r in matcher.reviewers(): 68*6cef255aSPatrick Williams # Gerrit uses 'cc' for the optional reviewers. 69*6cef255aSPatrick Williams result.append(f"cc={r}") 70*6cef255aSPatrick Williams print(",".join(result)) 71*6cef255aSPatrick Williams # Print as Gerrit Add Reviewers POST format. 72*6cef255aSPatrick Williams # https://gerrit.openbmc.org/Documentation/rest-api-changes.html#add-reviewer 73*6cef255aSPatrick Williams else: 74*6cef255aSPatrick Williams for o in matcher.owners(): 75*6cef255aSPatrick Williams print(json.dumps({"reviewer": o, "state": "REVIEWER"})) 76*6cef255aSPatrick Williams for r in matcher.reviewers(): 77*6cef255aSPatrick Williams print(json.dumps({"reviewer": r, "state": "CC"})) 78*6cef255aSPatrick Williams 79*6cef255aSPatrick Williams 80*6cef255aSPatrick Williamsdef main() -> None: 81*6cef255aSPatrick Williams parser = argparse.ArgumentParser() 82*6cef255aSPatrick Williams parser.add_argument( 83*6cef255aSPatrick Williams "-p", "--path", default=".", help="Root path to analyse" 84*6cef255aSPatrick Williams ) 85*6cef255aSPatrick Williams subparsers = parser.add_subparsers() 86*6cef255aSPatrick Williams 87*6cef255aSPatrick Williams parser_reviewers = subparsers.add_parser( 88*6cef255aSPatrick Williams "reviewers", help="Generate List of Reviewers" 89*6cef255aSPatrick Williams ) 90*6cef255aSPatrick Williams parser_reviewers.add_argument( 91*6cef255aSPatrick Williams "--push-args", 92*6cef255aSPatrick Williams action=argparse.BooleanOptionalAction, 93*6cef255aSPatrick Williams help="Format as git push options", 94*6cef255aSPatrick Williams ) 95*6cef255aSPatrick Williams parser_reviewers.set_defaults(func=subcmd_reviewers) 96*6cef255aSPatrick Williams 97*6cef255aSPatrick Williams args = parser.parse_args() 98*6cef255aSPatrick Williams 99*6cef255aSPatrick Williams file = YamlLoader.load(args.path + "/OWNERS") 100*6cef255aSPatrick Williams args.func(args, file) 101*6cef255aSPatrick Williams 102*6cef255aSPatrick Williams 103*6cef255aSPatrick Williamsif __name__ == "__main__": 104*6cef255aSPatrick Williams main() 105