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