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