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('--out', 38 '-o', 39 type=str, 40 default=default_out, 41 help='Output file (default %s)' % default_out) 42args = argp.parse_args() 43 44# 45# Walk git tree to locate all OWNERS files 46# 47 48owners_files = [ 49 os.path.join(root, 'OWNERS') 50 for root, dirs, files in os.walk(git_root) 51 if 'OWNERS' in files 52] 53 54# 55# Parse owners files 56# 57 58Owners = collections.namedtuple('Owners', 'parent directives dir') 59Directive = collections.namedtuple('Directive', 'who globs') 60 61 62def parse_owners(filename): 63 with open(filename) as f: 64 src = f.read().splitlines() 65 parent = True 66 directives = [] 67 for line in src: 68 line = line.strip() 69 # line := directive | comment 70 if not line: continue 71 if line[0] == '#': continue 72 # it's a directive 73 directive = None 74 if line == 'set noparent': 75 parent = False 76 elif line == '*': 77 directive = Directive(who='*', globs=[]) 78 elif ' ' in line: 79 (who, globs) = line.split(' ', 1) 80 globs_list = [glob for glob in globs.split(' ') if glob] 81 directive = Directive(who=who, globs=globs_list) 82 else: 83 directive = Directive(who=line, globs=[]) 84 if directive: 85 directives.append(directive) 86 return Owners(parent=parent, 87 directives=directives, 88 dir=os.path.relpath(os.path.dirname(filename), git_root)) 89 90 91owners_data = sorted([parse_owners(filename) for filename in owners_files], 92 key=operator.attrgetter('dir')) 93 94# 95# Modify owners so that parented OWNERS files point to the actual 96# Owners tuple with their parent field 97# 98 99new_owners_data = [] 100for owners in owners_data: 101 if owners.parent == True: 102 best_parent = None 103 best_parent_score = None 104 for possible_parent in owners_data: 105 if possible_parent is owners: continue 106 rel = os.path.relpath(owners.dir, possible_parent.dir) 107 # '..' ==> we had to walk up from possible_parent to get to owners 108 # ==> not a parent 109 if '..' in rel: continue 110 depth = len(rel.split(os.sep)) 111 if not best_parent or depth < best_parent_score: 112 best_parent = possible_parent 113 best_parent_score = depth 114 if best_parent: 115 owners = owners._replace(parent=best_parent.dir) 116 else: 117 owners = owners._replace(parent=None) 118 new_owners_data.append(owners) 119owners_data = new_owners_data 120 121# 122# In bottom to top order, process owners data structures to build up 123# a CODEOWNERS file for GitHub 124# 125 126 127def full_dir(rules_dir, sub_path): 128 return os.path.join(rules_dir, sub_path) if rules_dir != '.' else sub_path 129 130 131# glob using git 132gg_cache = {} 133 134 135def git_glob(glob): 136 global gg_cache 137 if glob in gg_cache: return gg_cache[glob] 138 r = set( 139 subprocess.check_output([ 140 'git', 'ls-files', os.path.join(git_root, glob) 141 ]).decode('utf-8').strip().splitlines()) 142 gg_cache[glob] = r 143 return r 144 145 146def expand_directives(root, directives): 147 globs = collections.OrderedDict() 148 # build a table of glob --> owners 149 for directive in directives: 150 for glob in directive.globs or ['**']: 151 if glob not in globs: 152 globs[glob] = [] 153 if directive.who not in globs[glob]: 154 globs[glob].append(directive.who) 155 # expand owners for intersecting globs 156 sorted_globs = sorted(globs.keys(), 157 key=lambda g: len(git_glob(full_dir(root, g))), 158 reverse=True) 159 out_globs = collections.OrderedDict() 160 for glob_add in sorted_globs: 161 who_add = globs[glob_add] 162 pre_items = [i for i in out_globs.items()] 163 out_globs[glob_add] = who_add.copy() 164 for glob_have, who_have in pre_items: 165 files_add = git_glob(full_dir(root, glob_add)) 166 files_have = git_glob(full_dir(root, glob_have)) 167 intersect = files_have.intersection(files_add) 168 if intersect: 169 for f in sorted(files_add): # sorted to ensure merge stability 170 if f not in intersect: 171 out_globs[os.path.relpath(f, start=root)] = who_add 172 for who in who_have: 173 if who not in out_globs[glob_add]: 174 out_globs[glob_add].append(who) 175 return out_globs 176 177 178def add_parent_to_globs(parent, globs, globs_dir): 179 if not parent: return 180 for owners in owners_data: 181 if owners.dir == parent: 182 owners_globs = expand_directives(owners.dir, owners.directives) 183 for oglob, oglob_who in owners_globs.items(): 184 for gglob, gglob_who in globs.items(): 185 files_parent = git_glob(full_dir(owners.dir, oglob)) 186 files_child = git_glob(full_dir(globs_dir, gglob)) 187 intersect = files_parent.intersection(files_child) 188 gglob_who_orig = gglob_who.copy() 189 if intersect: 190 for f in sorted(files_child 191 ): # sorted to ensure merge stability 192 if f not in intersect: 193 who = gglob_who_orig.copy() 194 globs[os.path.relpath(f, start=globs_dir)] = who 195 for who in oglob_who: 196 if who not in gglob_who: 197 gglob_who.append(who) 198 add_parent_to_globs(owners.parent, globs, globs_dir) 199 return 200 assert (False) 201 202 203todo = owners_data.copy() 204done = set() 205with open(args.out, 'w') as out: 206 out.write('# Auto-generated by the tools/mkowners/mkowners.py tool\n') 207 out.write('# Uses OWNERS files in different modules throughout the\n') 208 out.write('# repository as the source of truth for module ownership.\n') 209 written_globs = [] 210 while todo: 211 head, *todo = todo 212 if head.parent and not head.parent in done: 213 todo.append(head) 214 continue 215 globs = expand_directives(head.dir, head.directives) 216 add_parent_to_globs(head.parent, globs, head.dir) 217 for glob, owners in globs.items(): 218 skip = False 219 for glob1, owners1, dir1 in reversed(written_globs): 220 files = git_glob(full_dir(head.dir, glob)) 221 files1 = git_glob(full_dir(dir1, glob1)) 222 intersect = files.intersection(files1) 223 if files == intersect: 224 if sorted(owners) == sorted(owners1): 225 skip = True # nothing new in this rule 226 break 227 elif intersect: 228 # continuing would cause a semantic change since some files are 229 # affected differently by this rule and CODEOWNERS is order dependent 230 break 231 if not skip: 232 out.write('/%s %s\n' % 233 (full_dir(head.dir, glob), ' '.join(owners))) 234 written_globs.append((glob, owners, head.dir)) 235 done.add(head.dir) 236