• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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