| #!/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) |