tools: owners: support recursive OWNERS
Search for any OWNERS files anywhere in the repository, rather than
just at the root, and incorporate their information into the
review-set.
Tested: Modified meta-aspeed/recipes-bsp/u-boot/u-boot-aspeed.inc
and observed additional reviewers as expected.
```
$ tools/owners --path $(wd path lfopenbmc) reviewers
{"reviewer": "bradleyb@fuzziesquirrel.com", "state": "REVIEWER", "notify": "NONE", "notify_details": {"TO": {"accounts": ["bradleyb@fuzziesquirrel.com"]}}}
{"reviewer": "edtanous@google.com", "state": "REVIEWER", "notify": "NONE", "notify_details": {"TO": {"accounts": ["edtanous@google.com"]}}}
{"reviewer": "geissonator@yahoo.com", "state": "REVIEWER", "notify": "NONE", "notify_details": {"TO": {"accounts": ["geissonator@yahoo.com"]}}}
{"reviewer": "joel@jms.id.au", "state": "REVIEWER", "notify": "NONE", "notify_details": {"TO": {"accounts": ["joel@jms.id.au"]}}}
{"reviewer": "patrick@stwcx.xyz", "state": "REVIEWER", "notify": "NONE", "notify_details": {"TO": {"accounts": ["patrick@stwcx.xyz"]}}}
{"reviewer": "chiawei_wang@aspeedtech.com", "state": "CC", "notify": "NONE", "notify_details": {"TO": {"accounts": ["chiawei_wang@aspeedtech.com"]}}}
{"reviewer": "zweiss@equinix.com", "state": "CC", "notify": "NONE", "notify_details": {"TO": {"accounts": ["zweiss@equinix.com"]}}}
```
Signed-off-by: Patrick Williams <patrick@stwcx.xyz>
Change-Id: I08775e3975897066d79c22ba38f0fdef364ed708
diff --git a/tools/owners b/tools/owners
index c5a9736..249ea5a 100755
--- a/tools/owners
+++ b/tools/owners
@@ -6,7 +6,7 @@
import yaml
from sh import git # type: ignore
-from typing import List, Set, TypedDict, Optional
+from typing import Dict, List, Set, TypedDict, Optional
from yaml.loader import SafeLoader
# A list of Gerrit users (email addresses).
@@ -58,25 +58,54 @@
# Class to match commit information with OWNERS files.
class CommitMatch:
- def __init__(self, args: argparse.Namespace, owners: OwnersData):
+ def __init__(
+ self, args: argparse.Namespace, owners: Dict[str, OwnersData]
+ ):
files: Set[str] = set(
git.bake("-C", args.path)
.show(args.commit, pretty="", name_only=True, _tty_out=False)
.splitlines()
)
- self.owners: Set[str] = set(owners.get("owners") or [])
- self.reviewers: Set[str] = set(owners.get("reviewers") or [])
+ root_owners = owners[""]
- for e in owners.get("matchers", None) or []:
- if "exact" in e:
- self.__exact(files, e)
- elif "partial_regex" in e:
- self.__partial_regex(files, e)
- elif "regex" in e:
- self.__regex(files, e)
- elif "suffix" in e:
- self.__suffix(files, e)
+ self.owners: Set[str] = set()
+ self.reviewers: Set[str] = set()
+
+ for f in files:
+ path = f
+
+ while True:
+ path = os.path.dirname(path)
+
+ if path not in owners:
+ if not path:
+ break
+ continue
+
+ local_owners = owners[path]
+
+ self.owners = self.owners.union(
+ local_owners.get("owners") or []
+ )
+ self.reviewers = self.reviewers.union(
+ local_owners.get("reviewers") or []
+ )
+
+ rel_file = os.path.relpath(f, path)
+
+ for e in local_owners.get("matchers", None) or []:
+ if "exact" in e:
+ self.__exact(rel_file, e)
+ elif "partial_regex" in e:
+ self.__partial_regex(rel_file, e)
+ elif "regex" in e:
+ self.__regex(rel_file, e)
+ elif "suffix" in e:
+ self.__suffix(rel_file, e)
+
+ if not path:
+ break
self.reviewers = self.reviewers.difference(self.owners)
@@ -84,29 +113,27 @@
self.owners = self.owners.union(entry.get("owners") or [])
self.reviewers = self.reviewers.union(entry.get("reviewers") or [])
- def __exact(self, files: Set[str], entry: MatchEntry) -> None:
- for f in files:
- if f == entry["exact"]:
- self.__add_entry(entry)
+ def __exact(self, file: str, entry: MatchEntry) -> None:
+ if file == entry["exact"]:
+ self.__add_entry(entry)
- def __partial_regex(self, files: Set[str], entry: MatchEntry) -> None:
- for f in files:
- if re.search(entry["partial_regex"], f):
- self.__add_entry(entry)
+ def __partial_regex(self, file: str, entry: MatchEntry) -> None:
+ if re.search(entry["partial_regex"], file):
+ self.__add_entry(entry)
- def __regex(self, files: Set[str], entry: MatchEntry) -> None:
- for f in files:
- if re.fullmatch(entry["regex"], f):
- self.__add_entry(entry)
+ def __regex(self, file: str, entry: MatchEntry) -> None:
+ if re.fullmatch(entry["regex"], file):
+ self.__add_entry(entry)
- def __suffix(self, files: Set[str], entry: MatchEntry) -> None:
- for f in files:
- if os.path.splitext(f)[1] == entry["suffix"]:
- self.__add_entry(entry)
+ def __suffix(self, file: str, entry: MatchEntry) -> None:
+ if os.path.splitext(file)[1] == entry["suffix"]:
+ self.__add_entry(entry)
# The subcommand to get the reviewers.
-def subcmd_reviewers(args: argparse.Namespace, data: OwnersData) -> None:
+def subcmd_reviewers(
+ args: argparse.Namespace, data: Dict[str, OwnersData]
+) -> None:
matcher = CommitMatch(args, data)
# Print in `git push refs/for/branch%<reviewers>` format.
@@ -164,8 +191,17 @@
args = parser.parse_args()
- file = YamlLoader.load(args.path + "/OWNERS")
- args.func(args, file)
+ owners_files = git.bake("-C", args.path)(
+ "ls-files", "OWNERS", "**/OWNERS"
+ ).splitlines()
+
+ files = {}
+ for f in owners_files:
+ file = YamlLoader.load(os.path.join(args.path, f))
+ dirpath = os.path.dirname(f)
+ files[dirpath] = file
+
+ args.func(args, files)
if __name__ == "__main__":