Patrick Williams | 6cef255 | 2022-05-29 15:35:17 -0500 | [diff] [blame] | 1 | #!/bin/env python3 |
| 2 | import argparse |
| 3 | import json |
Patrick Williams | ca1b89e | 2022-06-19 20:53:43 -0500 | [diff] [blame] | 4 | import os |
| 5 | import re |
Patrick Williams | 6cef255 | 2022-05-29 15:35:17 -0500 | [diff] [blame] | 6 | import yaml |
| 7 | |
Patrick Williams | ca1b89e | 2022-06-19 20:53:43 -0500 | [diff] [blame] | 8 | from sh import git # type: ignore |
| 9 | from typing import List, Set, TypedDict, Optional |
Patrick Williams | 6cef255 | 2022-05-29 15:35:17 -0500 | [diff] [blame] | 10 | from yaml.loader import SafeLoader |
| 11 | |
| 12 | # A list of Gerrit users (email addresses). |
Patrick Williams | ca1b89e | 2022-06-19 20:53:43 -0500 | [diff] [blame] | 13 | # Some OWNERS files have empty lists for 'owners' or 'reviewers', which |
| 14 | # results in a None type for the value. |
| 15 | UsersList = Optional[List[str]] |
Patrick Williams | 6cef255 | 2022-05-29 15:35:17 -0500 | [diff] [blame] | 16 | |
| 17 | # A YAML node with an extra line number. |
| 18 | class NumberedNode(TypedDict): |
| 19 | line_number: int |
| 20 | |
| 21 | |
Patrick Williams | ca1b89e | 2022-06-19 20:53:43 -0500 | [diff] [blame] | 22 | class MatchEntry(TypedDict, total=False): |
| 23 | suffix: str |
| 24 | regex: str |
| 25 | partial_regex: str |
| 26 | exact: str |
| 27 | owners: UsersList |
| 28 | reviewers: UsersList |
| 29 | |
| 30 | |
Patrick Williams | 6cef255 | 2022-05-29 15:35:17 -0500 | [diff] [blame] | 31 | # The root YAML node of an OWNERS file |
| 32 | class OwnersData(NumberedNode, TypedDict, total=False): |
| 33 | owners: UsersList |
| 34 | reviewers: UsersList |
Patrick Williams | ca1b89e | 2022-06-19 20:53:43 -0500 | [diff] [blame] | 35 | matchers: List[MatchEntry] |
Patrick Williams | 6cef255 | 2022-05-29 15:35:17 -0500 | [diff] [blame] | 36 | |
| 37 | |
| 38 | # A YAML loader that adds the start line number onto each node (for |
| 39 | # later linting support) |
| 40 | class YamlLoader(SafeLoader): |
| 41 | def construct_mapping( |
| 42 | self, node: yaml.nodes.Node, deep: bool = False |
| 43 | ) -> NumberedNode: |
| 44 | mapping: NumberedNode = super(YamlLoader, self).construct_mapping( |
| 45 | node, deep=deep |
| 46 | ) # type: ignore |
| 47 | mapping["line_number"] = node.start_mark.line + 1 |
| 48 | return mapping |
| 49 | |
| 50 | # Load a file and return the OwnersData. |
| 51 | @staticmethod |
| 52 | def load(file: str) -> OwnersData: |
| 53 | data: OwnersData |
| 54 | with open(file, "r") as f: |
| 55 | data = yaml.load(f, Loader=YamlLoader) |
| 56 | return data |
| 57 | |
| 58 | |
| 59 | # Class to match commit information with OWNERS files. |
Patrick Williams | 6cef255 | 2022-05-29 15:35:17 -0500 | [diff] [blame] | 60 | class CommitMatch: |
Patrick Williams | ca1b89e | 2022-06-19 20:53:43 -0500 | [diff] [blame] | 61 | def __init__(self, args: argparse.Namespace, owners: OwnersData): |
| 62 | files: Set[str] = set( |
| 63 | git.bake("-C", args.path) |
| 64 | .show(args.commit, pretty="", name_only=True, _tty_out=False) |
| 65 | .splitlines() |
| 66 | ) |
Patrick Williams | 6cef255 | 2022-05-29 15:35:17 -0500 | [diff] [blame] | 67 | |
Patrick Williams | ca1b89e | 2022-06-19 20:53:43 -0500 | [diff] [blame] | 68 | self.owners: Set[str] = set(owners.get("owners") or []) |
| 69 | self.reviewers: Set[str] = set(owners.get("reviewers") or []) |
Patrick Williams | 6cef255 | 2022-05-29 15:35:17 -0500 | [diff] [blame] | 70 | |
Patrick Williams | 47b59dc | 2022-07-18 16:58:42 -0500 | [diff] [blame] | 71 | for e in owners.get("matchers", None) or []: |
Patrick Williams | ca1b89e | 2022-06-19 20:53:43 -0500 | [diff] [blame] | 72 | if "exact" in e: |
| 73 | self.__exact(files, e) |
| 74 | elif "partial_regex" in e: |
| 75 | self.__partial_regex(files, e) |
| 76 | elif "regex" in e: |
| 77 | self.__regex(files, e) |
| 78 | elif "suffix" in e: |
| 79 | self.__suffix(files, e) |
| 80 | |
| 81 | self.reviewers = self.reviewers.difference(self.owners) |
| 82 | |
| 83 | def __add_entry(self, entry: MatchEntry) -> None: |
| 84 | self.owners = self.owners.union(entry.get("owners") or []) |
| 85 | self.reviewers = self.reviewers.union(entry.get("reviewers") or []) |
| 86 | |
| 87 | def __exact(self, files: Set[str], entry: MatchEntry) -> None: |
| 88 | for f in files: |
| 89 | if f == entry["exact"]: |
| 90 | self.__add_entry(entry) |
| 91 | |
| 92 | def __partial_regex(self, files: Set[str], entry: MatchEntry) -> None: |
| 93 | for f in files: |
| 94 | if re.search(entry["partial_regex"], f): |
| 95 | self.__add_entry(entry) |
| 96 | |
| 97 | def __regex(self, files: Set[str], entry: MatchEntry) -> None: |
| 98 | for f in files: |
| 99 | if re.fullmatch(entry["regex"], f): |
| 100 | self.__add_entry(entry) |
| 101 | |
| 102 | def __suffix(self, files: Set[str], entry: MatchEntry) -> None: |
| 103 | for f in files: |
| 104 | if os.path.splitext(f)[1] == entry["suffix"]: |
| 105 | self.__add_entry(entry) |
Patrick Williams | 6cef255 | 2022-05-29 15:35:17 -0500 | [diff] [blame] | 106 | |
| 107 | |
| 108 | # The subcommand to get the reviewers. |
| 109 | def subcmd_reviewers(args: argparse.Namespace, data: OwnersData) -> None: |
Patrick Williams | ca1b89e | 2022-06-19 20:53:43 -0500 | [diff] [blame] | 110 | matcher = CommitMatch(args, data) |
Patrick Williams | 6cef255 | 2022-05-29 15:35:17 -0500 | [diff] [blame] | 111 | |
| 112 | # Print in `git push refs/for/branch%<reviewers>` format. |
| 113 | if args.push_args: |
| 114 | result = [] |
Patrick Williams | ca1b89e | 2022-06-19 20:53:43 -0500 | [diff] [blame] | 115 | for o in sorted(matcher.owners): |
Patrick Williams | 6cef255 | 2022-05-29 15:35:17 -0500 | [diff] [blame] | 116 | # Gerrit uses 'r' for the required reviewers (owners). |
| 117 | result.append(f"r={o}") |
Patrick Williams | ca1b89e | 2022-06-19 20:53:43 -0500 | [diff] [blame] | 118 | for r in sorted(matcher.reviewers): |
Patrick Williams | 6cef255 | 2022-05-29 15:35:17 -0500 | [diff] [blame] | 119 | # Gerrit uses 'cc' for the optional reviewers. |
| 120 | result.append(f"cc={r}") |
| 121 | print(",".join(result)) |
| 122 | # Print as Gerrit Add Reviewers POST format. |
| 123 | # https://gerrit.openbmc.org/Documentation/rest-api-changes.html#add-reviewer |
| 124 | else: |
Patrick Williams | 29986f1 | 2022-07-19 10:46:07 -0500 | [diff] [blame^] | 125 | |
| 126 | def review_js(reviewer: str, state: str) -> str: |
| 127 | return json.dumps( |
| 128 | { |
| 129 | "reviewer": reviewer, |
| 130 | "state": state, |
| 131 | "notify": "NONE", |
| 132 | "notify_details": {"TO": {"accounts": [reviewer]}}, |
| 133 | } |
| 134 | ) |
| 135 | |
Patrick Williams | ca1b89e | 2022-06-19 20:53:43 -0500 | [diff] [blame] | 136 | for o in sorted(matcher.owners): |
Patrick Williams | 29986f1 | 2022-07-19 10:46:07 -0500 | [diff] [blame^] | 137 | print(review_js(o, "REVIEWER")) |
Patrick Williams | ca1b89e | 2022-06-19 20:53:43 -0500 | [diff] [blame] | 138 | for r in sorted(matcher.reviewers): |
Patrick Williams | 29986f1 | 2022-07-19 10:46:07 -0500 | [diff] [blame^] | 139 | print(review_js(r, "CC")) |
Patrick Williams | 6cef255 | 2022-05-29 15:35:17 -0500 | [diff] [blame] | 140 | |
| 141 | |
| 142 | def main() -> None: |
| 143 | parser = argparse.ArgumentParser() |
| 144 | parser.add_argument( |
| 145 | "-p", "--path", default=".", help="Root path to analyse" |
| 146 | ) |
| 147 | subparsers = parser.add_subparsers() |
| 148 | |
| 149 | parser_reviewers = subparsers.add_parser( |
| 150 | "reviewers", help="Generate List of Reviewers" |
| 151 | ) |
| 152 | parser_reviewers.add_argument( |
| 153 | "--push-args", |
Patrick Williams | 47b59dc | 2022-07-18 16:58:42 -0500 | [diff] [blame] | 154 | default=False, |
Patrick Williams | 29986f1 | 2022-07-19 10:46:07 -0500 | [diff] [blame^] | 155 | action="store_true", |
Patrick Williams | 6cef255 | 2022-05-29 15:35:17 -0500 | [diff] [blame] | 156 | help="Format as git push options", |
| 157 | ) |
Patrick Williams | ca1b89e | 2022-06-19 20:53:43 -0500 | [diff] [blame] | 158 | parser_reviewers.add_argument( |
| 159 | "--commit", |
| 160 | default="HEAD", |
| 161 | help="Commit(s) to match against", |
| 162 | ) |
Patrick Williams | 6cef255 | 2022-05-29 15:35:17 -0500 | [diff] [blame] | 163 | parser_reviewers.set_defaults(func=subcmd_reviewers) |
| 164 | |
| 165 | args = parser.parse_args() |
| 166 | |
| 167 | file = YamlLoader.load(args.path + "/OWNERS") |
| 168 | args.func(args, file) |
| 169 | |
| 170 | |
| 171 | if __name__ == "__main__": |
| 172 | main() |