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