|
@@ -0,0 +1,175 @@
|
|
|
+#!/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 glob_intersect(g1, g2):
|
|
|
+ if not g2:
|
|
|
+ return all(c == '*' for c in g1)
|
|
|
+ if not g1:
|
|
|
+ return all(c == '*' for c in g2)
|
|
|
+ c1, *t1 = g1
|
|
|
+ c2, *t2 = g2
|
|
|
+ if c1 == '*':
|
|
|
+ return glob_intersect(g1, t2) or glob_intersect(t1, g2)
|
|
|
+ if c2 == '*':
|
|
|
+ return glob_intersect(t1, g2) or glob_intersect(g1, t2)
|
|
|
+ return c1 == c2 and glob_intersect(t1, t2)
|
|
|
+
|
|
|
+def add_parent_to_globs(parent, globs):
|
|
|
+ if not parent: return
|
|
|
+ for owners in owners_data:
|
|
|
+ if owners.dir == parent:
|
|
|
+ for directive in owners.directives:
|
|
|
+ for dglob in directive.globs or ['*']:
|
|
|
+ for gglob, glob in globs.items():
|
|
|
+ if glob_intersect(dglob, gglob):
|
|
|
+ if directive.who not in glob:
|
|
|
+ glob.append(directive.who)
|
|
|
+ add_parent_to_globs(owners.parent, globs)
|
|
|
+ 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')
|
|
|
+ while todo:
|
|
|
+ head, *todo = todo
|
|
|
+ if head.parent and not head.parent in done:
|
|
|
+ todo.append(head)
|
|
|
+ continue
|
|
|
+ globs = collections.OrderedDict()
|
|
|
+ for directive in head.directives:
|
|
|
+ for glob in directive.globs or ['*']:
|
|
|
+ if glob not in globs:
|
|
|
+ globs[glob] = []
|
|
|
+ globs[glob].append(directive.who)
|
|
|
+ add_parent_to_globs(head.parent, globs)
|
|
|
+ for glob, owners in globs.items():
|
|
|
+ out.write('%s %s\n' % (
|
|
|
+ os.path.join(head.dir, glob) if head.dir != '.' else glob,
|
|
|
+ ' '.join(owners)))
|
|
|
+ done.add(head.dir)
|