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