blob: e0ad998bdc7a5c4579f98b5cad488419c9cac19a [file] [log] [blame]
Craig Tillere476f7d2017-07-12 10:40:56 -07001#!/usr/bin/env python3
Craig Tiller80a87ea2017-07-13 09:11:06 -07002# Copyright 2017 gRPC authors.
Craig Tillere476f7d2017-07-12 10:40:56 -07003#
Craig Tiller80a87ea2017-07-13 09:11:06 -07004# 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
Craig Tillere476f7d2017-07-12 10:40:56 -07007#
Craig Tiller80a87ea2017-07-13 09:11:06 -07008# http://www.apache.org/licenses/LICENSE-2.0
Craig Tillere476f7d2017-07-12 10:40:56 -07009#
Craig Tiller80a87ea2017-07-13 09:11:06 -070010# 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.
Craig Tillere476f7d2017-07-12 10:40:56 -070015
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
28 .check_output(['git', 'rev-parse', '--show-toplevel'])
29 .decode('utf-8')
30 .strip())
31
32#
33# Parse command line arguments
34#
35
36default_out = os.path.join(git_root, '.github', 'CODEOWNERS')
37
38argp = argparse.ArgumentParser('Generate .github/CODEOWNERS file')
39argp.add_argument('--out', '-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 = [os.path.join(root, 'OWNERS')
50 for root, dirs, files in os.walk(git_root)
51 if 'OWNERS' in files]
52
53#
54# Parse owners files
55#
56
57Owners = collections.namedtuple('Owners', 'parent directives dir')
58Directive = collections.namedtuple('Directive', 'who globs')
59
60def parse_owners(filename):
61 with open(filename) as f:
62 src = f.read().splitlines()
63 parent = True
64 directives = []
65 for line in src:
66 line = line.strip()
67 # line := directive | comment
68 if not line: continue
69 if line[0] == '#': continue
70 # it's a directive
71 directive = None
72 if line == 'set noparent':
73 parent = False
74 elif line == '*':
75 directive = Directive(who='*', globs=[])
76 elif ' ' in line:
77 (who, globs) = line.split(' ', 1)
78 globs_list = [glob
79 for glob in globs.split(' ')
80 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
90owners_data = sorted([parse_owners(filename)
91 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
Nicolas Nobleeb020ce2017-07-13 13:20:13 -0700126def full_dir(rules_dir, sub_path):
127 return os.path.join(rules_dir, sub_path) if rules_dir != '.' else sub_path
128
Craig Tiller7976bdd2017-07-14 11:30:14 -0700129# glob using git
130gg_cache = {}
131def git_glob(glob):
132 global gg_cache
133 if glob in gg_cache: return gg_cache[glob]
134 r = set(subprocess
Craig Tiller98240d02017-07-14 14:16:01 -0700135 .check_output(['git', 'ls-files', os.path.join(git_root, glob)])
Craig Tiller7976bdd2017-07-14 11:30:14 -0700136 .decode('utf-8')
137 .strip()
138 .splitlines())
139 gg_cache[glob] = r
140 return r
141
142def expand_directives(root, directives):
143 globs = collections.OrderedDict()
144 # build a table of glob --> owners
145 for directive in directives:
146 for glob in directive.globs or ['**']:
147 if glob not in globs:
148 globs[glob] = []
149 if directive.who not in globs[glob]:
150 globs[glob].append(directive.who)
151 # expand owners for intersecting globs
152 sorted_globs = sorted(globs.keys(),
Craig Tiller98240d02017-07-14 14:16:01 -0700153 key=lambda g: len(git_glob(full_dir(root, g))),
Craig Tiller7976bdd2017-07-14 11:30:14 -0700154 reverse=True)
Craig Tiller7976bdd2017-07-14 11:30:14 -0700155 out_globs = collections.OrderedDict()
156 for glob_add in sorted_globs:
157 who_add = globs[glob_add]
Craig Tiller7976bdd2017-07-14 11:30:14 -0700158 pre_items = [i for i in out_globs.items()]
159 out_globs[glob_add] = who_add.copy()
160 for glob_have, who_have in pre_items:
161 files_add = git_glob(full_dir(root, glob_add))
162 files_have = git_glob(full_dir(root, glob_have))
163 intersect = files_have.intersection(files_add)
164 if intersect:
Craig Tiller98240d02017-07-14 14:16:01 -0700165 for f in sorted(files_add): # sorted to ensure merge stability
Craig Tiller7976bdd2017-07-14 11:30:14 -0700166 if f not in intersect:
Craig Tiller92a85552017-07-14 11:36:09 -0700167 out_globs[os.path.relpath(f, start=root)] = who_add
Craig Tiller7976bdd2017-07-14 11:30:14 -0700168 for who in who_have:
169 if who not in out_globs[glob_add]:
170 out_globs[glob_add].append(who)
171 return out_globs
Craig Tillere476f7d2017-07-12 10:40:56 -0700172
Nicolas Nobleeb020ce2017-07-13 13:20:13 -0700173def add_parent_to_globs(parent, globs, globs_dir):
Craig Tillere476f7d2017-07-12 10:40:56 -0700174 if not parent: return
175 for owners in owners_data:
176 if owners.dir == parent:
Craig Tiller7976bdd2017-07-14 11:30:14 -0700177 owners_globs = expand_directives(owners.dir, owners.directives)
178 for oglob, oglob_who in owners_globs.items():
179 for gglob, gglob_who in globs.items():
180 files_parent = git_glob(full_dir(owners.dir, oglob))
181 files_child = git_glob(full_dir(globs_dir, gglob))
182 intersect = files_parent.intersection(files_child)
183 gglob_who_orig = gglob_who.copy()
184 if intersect:
Craig Tiller98240d02017-07-14 14:16:01 -0700185 for f in sorted(files_child): # sorted to ensure merge stability
Craig Tiller7976bdd2017-07-14 11:30:14 -0700186 if f not in intersect:
Craig Tiller92a85552017-07-14 11:36:09 -0700187 who = gglob_who_orig.copy()
188 globs[os.path.relpath(f, start=globs_dir)] = who
Craig Tiller7976bdd2017-07-14 11:30:14 -0700189 for who in oglob_who:
190 if who not in gglob_who:
191 gglob_who.append(who)
Nicolas Nobleeb020ce2017-07-13 13:20:13 -0700192 add_parent_to_globs(owners.parent, globs, globs_dir)
Craig Tillere476f7d2017-07-12 10:40:56 -0700193 return
194 assert(False)
195
196todo = owners_data.copy()
197done = set()
198with open(args.out, 'w') as out:
199 out.write('# Auto-generated by the tools/mkowners/mkowners.py tool\n')
200 out.write('# Uses OWNERS files in different modules throughout the\n')
201 out.write('# repository as the source of truth for module ownership.\n')
Craig Tiller98240d02017-07-14 14:16:01 -0700202 written_globs = []
Craig Tillere476f7d2017-07-12 10:40:56 -0700203 while todo:
204 head, *todo = todo
205 if head.parent and not head.parent in done:
206 todo.append(head)
207 continue
Craig Tiller7976bdd2017-07-14 11:30:14 -0700208 globs = expand_directives(head.dir, head.directives)
Nicolas Nobleeb020ce2017-07-13 13:20:13 -0700209 add_parent_to_globs(head.parent, globs, head.dir)
Craig Tillere476f7d2017-07-12 10:40:56 -0700210 for glob, owners in globs.items():
Craig Tiller98240d02017-07-14 14:16:01 -0700211 skip = False
212 for glob1, owners1, dir1 in reversed(written_globs):
213 files = git_glob(full_dir(head.dir, glob))
214 files1 = git_glob(full_dir(dir1, glob1))
215 intersect = files.intersection(files1)
216 if files == intersect:
217 if sorted(owners) == sorted(owners1):
218 skip = True # nothing new in this rule
219 break
220 elif intersect:
221 # continuing would cause a semantic change since some files are
222 # affected differently by this rule and CODEOWNERS is order dependent
223 break
224 if not skip:
225 out.write('/%s %s\n' % (
226 full_dir(head.dir, glob), ' '.join(owners)))
227 written_globs.append((glob, owners, head.dir))
Craig Tillere476f7d2017-07-12 10:40:56 -0700228 done.add(head.dir)