mirror of https://github.com/grpc/grpc.git
The C based gRPC (C++, Python, Ruby, Objective-C, PHP, C#)
https://grpc.io/
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
238 lines
8.0 KiB
238 lines
8.0 KiB
#!/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( |
|
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 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 owners_globs.items(): |
|
for gglob, gglob_who in 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 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)
|
|
|