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