|
|
|
#!/usr/bin/env python3
|
|
|
|
# Copyright 2017 gRPC authors.
|
|
|
|
#
|
|
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
# you may not use this file except in compliance with the License.
|
|
|
|
# You may obtain a copy of the License at
|
|
|
|
#
|
|
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
#
|
|
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
# See the License for the specific language governing permissions and
|
|
|
|
# limitations under the License.
|
|
|
|
|
|
|
|
import argparse
|
|
|
|
import collections
|
|
|
|
import operator
|
|
|
|
import os
|
|
|
|
import re
|
|
|
|
import subprocess
|
|
|
|
|
|
|
|
#
|
|
|
|
# Find the root of the git tree
|
|
|
|
#
|
|
|
|
|
|
|
|
git_root = (
|
|
|
|
subprocess.check_output(["git", "rev-parse", "--show-toplevel"])
|
|
|
|
.decode("utf-8")
|
|
|
|
.strip()
|
|
|
|
)
|
|
|
|
|
|
|
|
#
|
|
|
|
# Parse command line arguments
|
|
|
|
#
|
|
|
|
|
|
|
|
default_out = os.path.join(git_root, ".github", "CODEOWNERS")
|
|
|
|
|
|
|
|
argp = argparse.ArgumentParser("Generate .github/CODEOWNERS file")
|
|
|
|
argp.add_argument(
|
|
|
|
"--out",
|
|
|
|
"-o",
|
|
|
|
type=str,
|
|
|
|
default=default_out,
|
|
|
|
help="Output file (default %s)" % default_out,
|
|
|
|
)
|
|
|
|
args = argp.parse_args()
|
|
|
|
|
|
|
|
#
|
|
|
|
# Walk git tree to locate all OWNERS files
|
|
|
|
#
|
|
|
|
|
|
|
|
owners_files = [
|
|
|
|
os.path.join(root, "OWNERS")
|
|
|
|
for root, dirs, files in os.walk(git_root)
|
|
|
|
if "OWNERS" in files
|
|
|
|
]
|
|
|
|
|
|
|
|
#
|
|
|
|
# Parse owners files
|
|
|
|
#
|
|
|
|
|
|
|
|
Owners = collections.namedtuple("Owners", "parent directives dir")
|
|
|
|
Directive = collections.namedtuple("Directive", "who globs")
|
|
|
|
|
|
|
|
|
|
|
|
def parse_owners(filename):
|
|
|
|
with open(filename) as f:
|
|
|
|
src = f.read().splitlines()
|
|
|
|
parent = True
|
|
|
|
directives = []
|
|
|
|
for line in src:
|
|
|
|
line = line.strip()
|
|
|
|
# line := directive | comment
|
|
|
|
if not line:
|
|
|
|
continue
|
|
|
|
if line[0] == "#":
|
|
|
|
continue
|
|
|
|
# it's a directive
|
|
|
|
directive = None
|
|
|
|
if line == "set noparent":
|
|
|
|
parent = False
|
|
|
|
elif line == "*":
|
|
|
|
directive = Directive(who="*", globs=[])
|
|
|
|
elif " " in line:
|
|
|
|
(who, globs) = line.split(" ", 1)
|
|
|
|
globs_list = [glob for glob in globs.split(" ") if glob]
|
|
|
|
directive = Directive(who=who, globs=globs_list)
|
|
|
|
else:
|
|
|
|
directive = Directive(who=line, globs=[])
|
|
|
|
if directive:
|
|
|
|
directives.append(directive)
|
|
|
|
return Owners(
|
|
|
|
parent=parent,
|
|
|
|
directives=directives,
|
|
|
|
dir=os.path.relpath(os.path.dirname(filename), git_root),
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
owners_data = sorted(
|
|
|
|
[parse_owners(filename) for filename in owners_files],
|
|
|
|
key=operator.attrgetter("dir"),
|
|
|
|
)
|
|
|
|
|
|
|
|
#
|
|
|
|
# Modify owners so that parented OWNERS files point to the actual
|
|
|
|
# Owners tuple with their parent field
|
|
|
|
#
|
|
|
|
|
|
|
|
new_owners_data = []
|
|
|
|
for owners in owners_data:
|
|
|
|
if owners.parent == True:
|
|
|
|
best_parent = None
|
|
|
|
best_parent_score = None
|
|
|
|
for possible_parent in owners_data:
|
|
|
|
if possible_parent is owners:
|
|
|
|
continue
|
|
|
|
rel = os.path.relpath(owners.dir, possible_parent.dir)
|
|
|
|
# '..' ==> we had to walk up from possible_parent to get to owners
|
|
|
|
# ==> not a parent
|
|
|
|
if ".." in rel:
|
|
|
|
continue
|
|
|
|
depth = len(rel.split(os.sep))
|
|
|
|
if not best_parent or depth < best_parent_score:
|
|
|
|
best_parent = possible_parent
|
|
|
|
best_parent_score = depth
|
|
|
|
if best_parent:
|
|
|
|
owners = owners._replace(parent=best_parent.dir)
|
|
|
|
else:
|
|
|
|
owners = owners._replace(parent=None)
|
|
|
|
new_owners_data.append(owners)
|
|
|
|
owners_data = new_owners_data
|
|
|
|
|
|
|
|
#
|
|
|
|
# In bottom to top order, process owners data structures to build up
|
|
|
|
# a CODEOWNERS file for GitHub
|
|
|
|
#
|
|
|
|
|
|
|
|
|
|
|
|
def full_dir(rules_dir, sub_path):
|
|
|
|
return os.path.join(rules_dir, sub_path) if rules_dir != "." else sub_path
|
|
|
|
|
|
|
|
|
|
|
|
# glob using git
|
|
|
|
gg_cache = {}
|
|
|
|
|
|
|
|
|
|
|
|
def git_glob(glob):
|
|
|
|
global gg_cache
|
|
|
|
if glob in gg_cache:
|
|
|
|
return gg_cache[glob]
|
|
|
|
r = set(
|
|
|
|
subprocess.check_output(
|
|
|
|
["git", "ls-files", os.path.join(git_root, glob)]
|
|
|
|
)
|
|
|
|
.decode("utf-8")
|
|
|
|
.strip()
|
|
|
|
.splitlines()
|
|
|
|
)
|
|
|
|
gg_cache[glob] = r
|
|
|
|
return r
|
|
|
|
|
|
|
|
|
|
|
|
def expand_directives(root, directives):
|
|
|
|
globs = collections.OrderedDict()
|
|
|
|
# build a table of glob --> owners
|
|
|
|
for directive in directives:
|
|
|
|
for glob in directive.globs or ["**"]:
|
|
|
|
if glob not in globs:
|
|
|
|
globs[glob] = []
|
|
|
|
if directive.who not in globs[glob]:
|
|
|
|
globs[glob].append(directive.who)
|
|
|
|
# expand owners for intersecting globs
|
|
|
|
sorted_globs = sorted(
|
|
|
|
list(globs.keys()),
|
|
|
|
key=lambda g: len(git_glob(full_dir(root, g))),
|
|
|
|
reverse=True,
|
|
|
|
)
|
|
|
|
out_globs = collections.OrderedDict()
|
|
|
|
for glob_add in sorted_globs:
|
|
|
|
who_add = globs[glob_add]
|
|
|
|
pre_items = [i for i in list(out_globs.items())]
|
|
|
|
out_globs[glob_add] = who_add.copy()
|
|
|
|
for glob_have, who_have in pre_items:
|
|
|
|
files_add = git_glob(full_dir(root, glob_add))
|
|
|
|
files_have = git_glob(full_dir(root, glob_have))
|
|
|
|
intersect = files_have.intersection(files_add)
|
|
|
|
if intersect:
|
|
|
|
for f in sorted(files_add): # sorted to ensure merge stability
|
|
|
|
if f not in intersect:
|
|
|
|
out_globs[os.path.relpath(f, start=root)] = who_add
|
|
|
|
for who in who_have:
|
|
|
|
if who not in out_globs[glob_add]:
|
|
|
|
out_globs[glob_add].append(who)
|
|
|
|
return out_globs
|
|
|
|
|
|
|
|
|
|
|
|
def add_parent_to_globs(parent, globs, globs_dir):
|
|
|
|
if not parent:
|
|
|
|
return
|
|
|
|
for owners in owners_data:
|
|
|
|
if owners.dir == parent:
|
|
|
|
owners_globs = expand_directives(owners.dir, owners.directives)
|
|
|
|
for oglob, oglob_who in list(owners_globs.items()):
|
|
|
|
for gglob, gglob_who in list(globs.items()):
|
|
|
|
files_parent = git_glob(full_dir(owners.dir, oglob))
|
|
|
|
files_child = git_glob(full_dir(globs_dir, gglob))
|
|
|
|
intersect = files_parent.intersection(files_child)
|
|
|
|
gglob_who_orig = gglob_who.copy()
|
|
|
|
if intersect:
|
|
|
|
for f in sorted(
|
|
|
|
files_child
|
|
|
|
): # sorted to ensure merge stability
|
|
|
|
if f not in intersect:
|
|
|
|
who = gglob_who_orig.copy()
|
|
|
|
globs[os.path.relpath(f, start=globs_dir)] = who
|
|
|
|
for who in oglob_who:
|
|
|
|
if who not in gglob_who:
|
|
|
|
gglob_who.append(who)
|
|
|
|
add_parent_to_globs(owners.parent, globs, globs_dir)
|
|
|
|
return
|
|
|
|
assert False
|
|
|
|
|
|
|
|
|
|
|
|
todo = owners_data.copy()
|
|
|
|
done = set()
|
|
|
|
with open(args.out, "w") as out:
|
|
|
|
out.write("# Auto-generated by the tools/mkowners/mkowners.py tool\n")
|
|
|
|
out.write("# Uses OWNERS files in different modules throughout the\n")
|
|
|
|
out.write("# repository as the source of truth for module ownership.\n")
|
|
|
|
written_globs = []
|
|
|
|
while todo:
|
|
|
|
head, *todo = todo
|
|
|
|
if head.parent and not head.parent in done:
|
|
|
|
todo.append(head)
|
|
|
|
continue
|
|
|
|
globs = expand_directives(head.dir, head.directives)
|
|
|
|
add_parent_to_globs(head.parent, globs, head.dir)
|
|
|
|
for glob, owners in list(globs.items()):
|
|
|
|
skip = False
|
|
|
|
for glob1, owners1, dir1 in reversed(written_globs):
|
|
|
|
files = git_glob(full_dir(head.dir, glob))
|
|
|
|
files1 = git_glob(full_dir(dir1, glob1))
|
|
|
|
intersect = files.intersection(files1)
|
|
|
|
if files == intersect:
|
|
|
|
if sorted(owners) == sorted(owners1):
|
|
|
|
skip = True # nothing new in this rule
|
|
|
|
break
|
|
|
|
elif intersect:
|
|
|
|
# continuing would cause a semantic change since some files are
|
|
|
|
# affected differently by this rule and CODEOWNERS is order dependent
|
|
|
|
break
|
|
|
|
if not skip:
|
|
|
|
out.write(
|
|
|
|
"/%s %s\n" % (full_dir(head.dir, glob), " ".join(owners))
|
|
|
|
)
|
|
|
|
written_globs.append((glob, owners, head.dir))
|
|
|
|
done.add(head.dir)
|