Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(610)

Side by Side Diff: owners.py

Issue 11114005: implement per-file OWNERS support (Closed) Base URL: https://git.chromium.org/chromium/tools/depot_tools.git@master
Patch Set: change "owner" to "directive" in add_entry Created 8 years, 2 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « no previous file | presubmit_support.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 # Copyright (c) 2012 The Chromium Authors. All rights reserved. 1 # Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be 2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file. 3 # found in the LICENSE file.
4 4
5 """A database of OWNERS files.""" 5 """A database of OWNERS files."""
M-A Ruel 2012/10/12 15:19:04 One thing I'd really like is a formal description
Dirk Pranke 2012/10/16 22:42:41 Okay, I'll write up something and see what you thi
6 6
7 import collections 7 import collections
8 import re 8 import re
9 9
10 10
11 # If this is present by itself on a line, this means that everyone can review. 11 # If this is present by itself on a line, this means that everyone can review.
12 EVERYONE = '*' 12 EVERYONE = '*'
13 13
14 14
15 # Recognizes 'X@Y' email addresses. Very simplistic. 15 # Recognizes 'X@Y' email addresses. Very simplistic.
(...skipping 13 matching lines...) Expand all
29 def __init__(self, path, lineno, msg): 29 def __init__(self, path, lineno, msg):
30 super(SyntaxErrorInOwnersFile, self).__init__((path, lineno, msg)) 30 super(SyntaxErrorInOwnersFile, self).__init__((path, lineno, msg))
31 self.path = path 31 self.path = path
32 self.lineno = lineno 32 self.lineno = lineno
33 self.msg = msg 33 self.msg = msg
34 34
35 def __str__(self): 35 def __str__(self):
36 return "%s:%d syntax error: %s" % (self.path, self.lineno, self.msg) 36 return "%s:%d syntax error: %s" % (self.path, self.lineno, self.msg)
37 37
38 38
39 class Database(object): 39 class Database(object):
M-A Ruel 2012/10/12 15:19:04 I think the code would be easier to read if it was
Dirk Pranke 2012/10/16 22:42:41 I'll think about this, but definitely not do it in
40 """A database of OWNERS files for a repository. 40 """A database of OWNERS files for a repository.
41 41
42 This class allows you to find a suggested set of reviewers for a list 42 This class allows you to find a suggested set of reviewers for a list
43 of changed files, and see if a list of changed files is covered by a 43 of changed files, and see if a list of changed files is covered by a
44 list of reviewers.""" 44 list of reviewers."""
45 45
46 def __init__(self, root, fopen, os_path): 46 def __init__(self, root, fopen, os_path, glob):
47 """Args: 47 """Args:
48 root: the path to the root of the Repository 48 root: the path to the root of the Repository
49 open: function callback to open a text file for reading 49 open: function callback to open a text file for reading
50 os_path: module/object callback with fields for 'abspath', 'dirname', 50 os_path: module/object callback with fields for 'abspath', 'dirname',
51 'exists', and 'join' 51 'exists', and 'join'
52 glob: function callback to list entries in a directory match a glob (i.e., glob.glob)
M-A Ruel 2012/10/12 15:19:04 80 cols
Dirk Pranke 2012/10/16 22:42:41 Done.
52 """ 53 """
53 self.root = root 54 self.root = root
54 self.fopen = fopen 55 self.fopen = fopen
55 self.os_path = os_path 56 self.os_path = os_path
57 self.glob = glob
56 58
57 # Pick a default email regexp to use; callers can override as desired. 59 # Pick a default email regexp to use; callers can override as desired.
58 self.email_regexp = re.compile(BASIC_EMAIL_REGEXP) 60 self.email_regexp = re.compile(BASIC_EMAIL_REGEXP)
59 61
60 # Mapping of owners to the paths they own. 62 # Mapping of owners to the paths they own.
61 self.owned_by = {EVERYONE: set()} 63 self.owned_by = {EVERYONE: set()}
62 64
63 # Mapping of paths to authorized owners. 65 # Mapping of paths to authorized owners.
64 self.owners_for = {} 66 self.owners_for = {}
65 67
66 # Set of paths that stop us from looking above them for owners. 68 # Set of paths that stop us from looking above them for owners.
67 # (This is implicitly true for the root directory). 69 # (This is implicitly true for the root directory).
68 self.stop_looking = set(['']) 70 self.stop_looking = set([''])
69 71
70 def reviewers_for(self, files): 72 def reviewers_for(self, files):
71 """Returns a suggested set of reviewers that will cover the files. 73 """Returns a suggested set of reviewers that will cover the files.
72 74
73 files is a sequence of paths relative to (and under) self.root.""" 75 files is a sequence of paths relative to (and under) self.root."""
74 self._check_paths(files) 76 self._check_paths(files)
75 self._load_data_needed_for(files) 77 self._load_data_needed_for(files)
76 return self._covering_set_of_owners_for(files) 78 return self._covering_set_of_owners_for(files)
77 79
80 # TODO(dpranke): rename to objects_not_covered_by
78 def directories_not_covered_by(self, files, reviewers): 81 def directories_not_covered_by(self, files, reviewers):
79 """Returns the set of directories that are not owned by a reviewer. 82 """Returns the set of directories that are not owned by a reviewer.
80 83
81 Determines which of the given files are not owned by at least one of the 84 Determines which of the given files are not owned by at least one of the
82 reviewers, then returns a set containing the applicable enclosing 85 reviewers, then returns a set containing the applicable enclosing
83 directories, i.e. the ones upward from the files that have OWNERS files. 86 directories, i.e. the ones upward from the files that have OWNERS files.
84 87
85 Args: 88 Args:
86 files is a sequence of paths relative to (and under) self.root. 89 files is a sequence of paths relative to (and under) self.root.
87 reviewers is a sequence of strings matching self.email_regexp. 90 reviewers is a sequence of strings matching self.email_regexp.
88 """ 91 """
89 self._check_paths(files) 92 self._check_paths(files)
90 self._check_reviewers(reviewers) 93 self._check_reviewers(reviewers)
91 self._load_data_needed_for(files) 94 self._load_data_needed_for(files)
92 95
93 dirs = set([self.os_path.dirname(f) for f in files]) 96 objs = set()
94 covered_dirs = self._dirs_covered_by(reviewers) 97 for f in files:
95 uncovered_dirs = [self._enclosing_dir_with_owners(d) for d in dirs 98 if f in self.owners_for:
96 if not self._is_dir_covered_by(d, covered_dirs)] 99 objs.add(f)
100 else:
101 objs.add(self.os_path.dirname(f))
97 102
98 return set(uncovered_dirs) 103 covered_objs = self._objs_covered_by(reviewers)
104 uncovered_objs = [self._enclosing_obj_with_owners(o) for o in objs
105 if not self._is_obj_covered_by(o, covered_objs)]
106
107 return set(uncovered_objs)
108
109 objects_not_covered_by = directories_not_covered_by
M-A Ruel 2012/10/12 15:19:04 Why not rename right away? I don't see any advanta
Dirk Pranke 2012/10/16 22:42:41 I thought it would make the patch easier to review
99 110
100 def _check_paths(self, files): 111 def _check_paths(self, files):
101 def _is_under(f, pfx): 112 def _is_under(f, pfx):
102 return self.os_path.abspath(self.os_path.join(pfx, f)).startswith(pfx) 113 return self.os_path.abspath(self.os_path.join(pfx, f)).startswith(pfx)
103 _assert_is_collection(files) 114 _assert_is_collection(files)
104 assert all(_is_under(f, self.os_path.abspath(self.root)) for f in files) 115 assert all(_is_under(f, self.os_path.abspath(self.root)) for f in files)
105 116
106 def _check_reviewers(self, reviewers): 117 def _check_reviewers(self, reviewers):
107 _assert_is_collection(reviewers) 118 _assert_is_collection(reviewers)
108 assert all(self.email_regexp.match(r) for r in reviewers) 119 assert all(self.email_regexp.match(r) for r in reviewers)
109 120
121 # TODO(dpranke): Rename to _objs_covered_by and update_callers
110 def _dirs_covered_by(self, reviewers): 122 def _dirs_covered_by(self, reviewers):
111 dirs = self.owned_by[EVERYONE] 123 dirs = self.owned_by[EVERYONE]
112 for r in reviewers: 124 for r in reviewers:
113 dirs = dirs | self.owned_by.get(r, set()) 125 dirs = dirs | self.owned_by.get(r, set())
114 return dirs 126 return dirs
115 127
128 _objs_covered_by = _dirs_covered_by
129
116 def _stop_looking(self, dirname): 130 def _stop_looking(self, dirname):
117 return dirname in self.stop_looking 131 return dirname in self.stop_looking
118 132
133 # TODO(dpranke): Rename to _is_dir_covered_by and update callers.
119 def _is_dir_covered_by(self, dirname, covered_dirs): 134 def _is_dir_covered_by(self, dirname, covered_dirs):
120 while not dirname in covered_dirs and not self._stop_looking(dirname): 135 while not dirname in covered_dirs and not self._stop_looking(dirname):
121 dirname = self.os_path.dirname(dirname) 136 dirname = self.os_path.dirname(dirname)
122 return dirname in covered_dirs 137 return dirname in covered_dirs
123 138
139 _is_obj_covered_by = _is_dir_covered_by
140
141 # TODO(dpranke): Rename to _enclosing_obj_with_owners and update callers.
124 def _enclosing_dir_with_owners(self, directory): 142 def _enclosing_dir_with_owners(self, directory):
125 """Returns the innermost enclosing directory that has an OWNERS file.""" 143 """Returns the innermost enclosing directory that has an OWNERS file."""
126 dirpath = directory 144 dirpath = directory
127 while not dirpath in self.owners_for: 145 while not dirpath in self.owners_for:
128 if self._stop_looking(dirpath): 146 if self._stop_looking(dirpath):
129 break 147 break
130 dirpath = self.os_path.dirname(dirpath) 148 dirpath = self.os_path.dirname(dirpath)
131 return dirpath 149 return dirpath
132 150
151 _enclosing_obj_with_owners = _enclosing_dir_with_owners
152
133 def _load_data_needed_for(self, files): 153 def _load_data_needed_for(self, files):
134 for f in files: 154 for f in files:
135 dirpath = self.os_path.dirname(f) 155 dirpath = self.os_path.dirname(f)
136 while not dirpath in self.owners_for: 156 while not dirpath in self.owners_for:
137 self._read_owners_in_dir(dirpath) 157 self._read_owners_in_dir(dirpath)
138 if self._stop_looking(dirpath): 158 if self._stop_looking(dirpath):
139 break 159 break
140 dirpath = self.os_path.dirname(dirpath) 160 dirpath = self.os_path.dirname(dirpath)
141 161
142 def _read_owners_in_dir(self, dirpath): 162 def _read_owners_in_dir(self, dirpath):
143 owners_path = self.os_path.join(self.root, dirpath, 'OWNERS') 163 owners_path = self.os_path.join(self.root, dirpath, 'OWNERS')
144 if not self.os_path.exists(owners_path): 164 if not self.os_path.exists(owners_path):
145 return 165 return
146 166
147 lineno = 0 167 lineno = 0
148 for line in self.fopen(owners_path): 168 for line in self.fopen(owners_path):
149 lineno += 1 169 lineno += 1
150 line = line.strip() 170 line = line.strip()
151 if line.startswith('#') or line == '': 171 if line.startswith('#') or line == '':
152 continue 172 continue
153 if line == 'set noparent': 173 if line == 'set noparent':
154 self.stop_looking.add(dirpath) 174 self.stop_looking.add(dirpath)
155 continue 175 continue
176
177 m = re.match("per-file (.+)=(.+)", line)
178 if m:
179 glob_string = m.group(1)
180 directive = m.group(2)
181 full_glob_string = self.os_path.join(self.root, dirpath, glob_string)
M-A Ruel 2012/10/12 15:19:04 You should assert not self.os_path.isabs(glob_stri
Dirk Pranke 2012/10/16 22:42:41 Good point. Actually it's probably better to ensur
182 baselines = self.glob(full_glob_string)
183 for baseline in [self.os_path.relpath(self.root, b) for b in baselines]:
M-A Ruel 2012/10/12 15:19:04 for baseline in (self.os_path.relpath(self.root, b
Dirk Pranke 2012/10/16 22:42:41 I tend to prefer the comprehension notation to the
184 self._add_entry(baseline, directive, "per-file line",
185 owners_path, lineno)
186 continue
187
156 if line.startswith('set '): 188 if line.startswith('set '):
157 raise SyntaxErrorInOwnersFile(owners_path, lineno, 189 raise SyntaxErrorInOwnersFile(owners_path, lineno,
158 'unknown option: "%s"' % line[4:].strip()) 190 'unknown option: "%s"' % line[4:].strip())
159 if self.email_regexp.match(line) or line == EVERYONE: 191
160 self.owned_by.setdefault(line, set()).add(dirpath) 192 self._add_entry(dirpath, line, "line", owners_path, lineno)
161 self.owners_for.setdefault(dirpath, set()).add(line) 193
162 continue 194 def _add_entry(self, path, directive, line_type, owners_path, lineno):
195 if directive == "set noparent":
196 self.stop_looking.add(path)
197 elif self.email_regexp.match(directive) or directive == EVERYONE:
198 self.owned_by.setdefault(directive, set()).add(path)
199 self.owners_for.setdefault(path, set()).add(directive)
200 else:
163 raise SyntaxErrorInOwnersFile(owners_path, lineno, 201 raise SyntaxErrorInOwnersFile(owners_path, lineno,
164 ('line is not a comment, a "set" directive, ' 202 ('%s is not a "set" directive, '
M-A Ruel 2012/10/12 15:19:04 Technically, you'd also want to say it's not an EV
Dirk Pranke 2012/10/16 22:42:41 Done.
165 'or an email address: "%s"' % line)) 203 'or an email address: "%s"' % (line_type, directive)))
204
166 205
167 def _covering_set_of_owners_for(self, files): 206 def _covering_set_of_owners_for(self, files):
168 # Get the set of directories from the files. 207 # Get the set of directories from the files.
169 dirs = set() 208 dirs = set()
170 for f in files: 209 for f in files:
171 dirs.add(self.os_path.dirname(f)) 210 dirs.add(self.os_path.dirname(f))
172 211
173 owned_dirs = {} 212 owned_dirs = {}
174 dir_owners = {} 213 dir_owners = {}
175 214
(...skipping 36 matching lines...) Expand 10 before | Expand all | Expand 10 after
212 break 251 break
213 252
214 final_owners.add(max_owner) 253 final_owners.add(max_owner)
215 254
216 # Remove all directories owned by the current owner from the remaining 255 # Remove all directories owned by the current owner from the remaining
217 # list. 256 # list.
218 for dirname in owned_dirs[max_owner]: 257 for dirname in owned_dirs[max_owner]:
219 dirs.discard(dirname) 258 dirs.discard(dirname)
220 259
221 return final_owners 260 return final_owners
OLDNEW
« no previous file with comments | « no previous file | presubmit_support.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698