1#!/usr/bin/env python3 2# Copyright 2017 gRPC authors. 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15 16import argparse 17import collections 18import operator 19import os 20import re 21import subprocess 22 23# 24# Find the root of the git tree 25# 26 27git_root = (subprocess.check_output(['git', 'rev-parse', '--show-toplevel']) 28 .decode('utf-8').strip()) 29 30# 31# Parse command line arguments 32# 33 34default_out = os.path.join(git_root, '.github', 'CODEOWNERS') 35 36argp = argparse.ArgumentParser('Generate .github/CODEOWNERS file') 37argp.add_argument( 38 '--out', 39 '-o', 40 type=str, 41 default=default_out, 42 help='Output file (default %s)' % default_out) 43args = argp.parse_args() 44 45# 46# Walk git tree to locate all OWNERS files 47# 48 49owners_files = [ 50 os.path.join(root, 'OWNERS') 51 for root, dirs, files in os.walk(git_root) 52 if 'OWNERS' in files 53] 54 55# 56# Parse owners files 57# 58 59Owners = collections.namedtuple('Owners', 'parent directives dir') 60Directive = collections.namedtuple('Directive', 'who globs') 61 62 63def parse_owners(filename): 64 with open(filename) as f: 65 src = f.read().splitlines() 66 parent = True 67 directives = [] 68 for line in src: 69 line = line.strip() 70 # line := directive | comment 71 if not line: continue 72 if line[0] == '#': continue 73 # it's a directive 74 directive = None 75 if line == 'set noparent': 76 parent = False 77 elif line == '*': 78 directive = Directive(who='*', globs=[]) 79 elif ' ' in line: 80 (who, globs) = line.split(' ', 1) 81 globs_list = [glob for glob in globs.split(' ') if glob] 82 directive = Directive(who=who, globs=globs_list) 83 else: 84 directive = Directive(who=line, globs=[]) 85 if directive: 86 directives.append(directive) 87 return Owners( 88 parent=parent, 89 directives=directives, 90 dir=os.path.relpath(os.path.dirname(filename), git_root)) 91 92 93owners_data = sorted( 94 [parse_owners(filename) for filename in owners_files], 95 key=operator.attrgetter('dir')) 96 97# 98# Modify owners so that parented OWNERS files point to the actual 99# Owners tuple with their parent field 100# 101 102new_owners_data = [] 103for owners in owners_data: 104 if owners.parent == True: 105 best_parent = None 106 best_parent_score = None 107 for possible_parent in owners_data: 108 if possible_parent is owners: continue 109 rel = os.path.relpath(owners.dir, possible_parent.dir) 110 # '..' ==> we had to walk up from possible_parent to get to owners 111 # ==> not a parent 112 if '..' in rel: continue 113 depth = len(rel.split(os.sep)) 114 if not best_parent or depth < best_parent_score: 115 best_parent = possible_parent 116 best_parent_score = depth 117 if best_parent: 118 owners = owners._replace(parent=best_parent.dir) 119 else: 120 owners = owners._replace(parent=None) 121 new_owners_data.append(owners) 122owners_data = new_owners_data 123 124# 125# In bottom to top order, process owners data structures to build up 126# a CODEOWNERS file for GitHub 127# 128 129 130def full_dir(rules_dir, sub_path): 131 return os.path.join(rules_dir, sub_path) if rules_dir != '.' else sub_path 132 133 134# glob using git 135gg_cache = {} 136 137 138def git_glob(glob): 139 global gg_cache 140 if glob in gg_cache: return gg_cache[glob] 141 r = set( 142 subprocess.check_output( 143 ['git', 'ls-files', os.path.join(git_root, glob)]).decode('utf-8') 144 .strip().splitlines()) 145 gg_cache[glob] = r 146 return r 147 148 149def expand_directives(root, directives): 150 globs = collections.OrderedDict() 151 # build a table of glob --> owners 152 for directive in directives: 153 for glob in directive.globs or ['**']: 154 if glob not in globs: 155 globs[glob] = [] 156 if directive.who not in globs[glob]: 157 globs[glob].append(directive.who) 158 # expand owners for intersecting globs 159 sorted_globs = sorted( 160 globs.keys(), 161 key=lambda g: len(git_glob(full_dir(root, g))), 162 reverse=True) 163 out_globs = collections.OrderedDict() 164 for glob_add in sorted_globs: 165 who_add = globs[glob_add] 166 pre_items = [i for i in out_globs.items()] 167 out_globs[glob_add] = who_add.copy() 168 for glob_have, who_have in pre_items: 169 files_add = git_glob(full_dir(root, glob_add)) 170 files_have = git_glob(full_dir(root, glob_have)) 171 intersect = files_have.intersection(files_add) 172 if intersect: 173 for f in sorted(files_add): # sorted to ensure merge stability 174 if f not in intersect: 175 out_globs[os.path.relpath(f, start=root)] = who_add 176 for who in who_have: 177 if who not in out_globs[glob_add]: 178 out_globs[glob_add].append(who) 179 return out_globs 180 181 182def add_parent_to_globs(parent, globs, globs_dir): 183 if not parent: return 184 for owners in owners_data: 185 if owners.dir == parent: 186 owners_globs = expand_directives(owners.dir, owners.directives) 187 for oglob, oglob_who in owners_globs.items(): 188 for gglob, gglob_who in globs.items(): 189 files_parent = git_glob(full_dir(owners.dir, oglob)) 190 files_child = git_glob(full_dir(globs_dir, gglob)) 191 intersect = files_parent.intersection(files_child) 192 gglob_who_orig = gglob_who.copy() 193 if intersect: 194 for f in sorted(files_child 195 ): # sorted to ensure merge stability 196 if f not in intersect: 197 who = gglob_who_orig.copy() 198 globs[os.path.relpath(f, start=globs_dir)] = who 199 for who in oglob_who: 200 if who not in gglob_who: 201 gglob_who.append(who) 202 add_parent_to_globs(owners.parent, globs, globs_dir) 203 return 204 assert (False) 205 206 207todo = owners_data.copy() 208done = set() 209with open(args.out, 'w') as out: 210 out.write('# Auto-generated by the tools/mkowners/mkowners.py tool\n') 211 out.write('# Uses OWNERS files in different modules throughout the\n') 212 out.write('# repository as the source of truth for module ownership.\n') 213 written_globs = [] 214 while todo: 215 head, *todo = todo 216 if head.parent and not head.parent in done: 217 todo.append(head) 218 continue 219 globs = expand_directives(head.dir, head.directives) 220 add_parent_to_globs(head.parent, globs, head.dir) 221 for glob, owners in globs.items(): 222 skip = False 223 for glob1, owners1, dir1 in reversed(written_globs): 224 files = git_glob(full_dir(head.dir, glob)) 225 files1 = git_glob(full_dir(dir1, glob1)) 226 intersect = files.intersection(files1) 227 if files == intersect: 228 if sorted(owners) == sorted(owners1): 229 skip = True # nothing new in this rule 230 break 231 elif intersect: 232 # continuing would cause a semantic change since some files are 233 # affected differently by this rule and CODEOWNERS is order dependent 234 break 235 if not skip: 236 out.write('/%s %s\n' % (full_dir(head.dir, glob), 237 ' '.join(owners))) 238 written_globs.append((glob, owners, head.dir)) 239 done.add(head.dir) 240