| Index: owners.py
|
| diff --git a/owners.py b/owners.py
|
| index cc667beea3c575f527470d9bd25269b4e95c6bae..30646ffa24dd203754a08abf5bba6d60f0f938cf 100644
|
| --- a/owners.py
|
| +++ b/owners.py
|
| @@ -78,7 +78,7 @@ class SyntaxErrorInOwnersFile(Exception):
|
| self.msg = msg
|
|
|
| def __str__(self):
|
| - return "%s:%d syntax error: %s" % (self.path, self.lineno, self.msg)
|
| + return '%s:%d syntax error: %s' % (self.path, self.lineno, self.msg)
|
|
|
|
|
| class Database(object):
|
| @@ -111,6 +111,9 @@ class Database(object):
|
| # Mapping of paths to authorized owners.
|
| self.owners_for = {}
|
|
|
| + # Mapping reviewers to the preceding comment per file in the OWNERS files.
|
| + self.comments = {}
|
| +
|
| # Set of paths that stop us from looking above them for owners.
|
| # (This is implicitly true for the root directory).
|
| self.stop_looking = set([''])
|
| @@ -122,7 +125,7 @@ class Database(object):
|
| If author is nonempty, we ensure it is not included in the set returned
|
| in order avoid suggesting the author as a reviewer for their own changes."""
|
| self._check_paths(files)
|
| - self._load_data_needed_for(files)
|
| + self.load_data_needed_for(files)
|
| suggested_owners = self._covering_set_of_owners_for(files, author)
|
| if EVERYONE in suggested_owners:
|
| if len(suggested_owners) > 1:
|
| @@ -140,7 +143,7 @@ class Database(object):
|
| """
|
| self._check_paths(files)
|
| self._check_reviewers(reviewers)
|
| - self._load_data_needed_for(files)
|
| + self.load_data_needed_for(files)
|
|
|
| covered_objs = self._objs_covered_by(reviewers)
|
| uncovered_files = [f for f in files
|
| @@ -182,7 +185,7 @@ class Database(object):
|
| dirpath = self.os_path.dirname(dirpath)
|
| return dirpath
|
|
|
| - def _load_data_needed_for(self, files):
|
| + def load_data_needed_for(self, files):
|
| for f in files:
|
| dirpath = self.os_path.dirname(f)
|
| while not dirpath in self.owners_for:
|
| @@ -195,18 +198,27 @@ class Database(object):
|
| owners_path = self.os_path.join(self.root, dirpath, 'OWNERS')
|
| if not self.os_path.exists(owners_path):
|
| return
|
| -
|
| + comment = []
|
| + in_comment = False
|
| lineno = 0
|
| for line in self.fopen(owners_path):
|
| lineno += 1
|
| line = line.strip()
|
| - if line.startswith('#') or line == '':
|
| + if line.startswith('#'):
|
| + if not in_comment:
|
| + comment = []
|
| + comment.append(line[1:].strip())
|
| + in_comment = True
|
| continue
|
| + if line == '':
|
| + continue
|
| + in_comment = False
|
| +
|
| if line == 'set noparent':
|
| self.stop_looking.add(dirpath)
|
| continue
|
|
|
| - m = re.match("per-file (.+)=(.+)", line)
|
| + m = re.match('per-file (.+)=(.+)', line)
|
| if m:
|
| glob_string = m.group(1).strip()
|
| directive = m.group(2).strip()
|
| @@ -217,20 +229,24 @@ class Database(object):
|
| line)
|
| baselines = self.glob(full_glob_string)
|
| for baseline in (self.os_path.relpath(b, self.root) for b in baselines):
|
| - self._add_entry(baseline, directive, "per-file line",
|
| - owners_path, lineno)
|
| + self._add_entry(baseline, directive, 'per-file line',
|
| + owners_path, lineno, '\n'.join(comment))
|
| continue
|
|
|
| if line.startswith('set '):
|
| raise SyntaxErrorInOwnersFile(owners_path, lineno,
|
| 'unknown option: "%s"' % line[4:].strip())
|
|
|
| - self._add_entry(dirpath, line, "line", owners_path, lineno)
|
| + self._add_entry(dirpath, line, 'line', owners_path, lineno,
|
| + ' '.join(comment))
|
|
|
| - def _add_entry(self, path, directive, line_type, owners_path, lineno):
|
| - if directive == "set noparent":
|
| + def _add_entry(self, path, directive,
|
| + line_type, owners_path, lineno, comment):
|
| + if directive == 'set noparent':
|
| self.stop_looking.add(path)
|
| elif self.email_regexp.match(directive) or directive == EVERYONE:
|
| + self.comments.setdefault(directive, {})
|
| + self.comments[directive][path] = comment
|
| self.owned_by.setdefault(directive, set()).add(path)
|
| self.owners_for.setdefault(path, set()).add(directive)
|
| else:
|
| @@ -240,7 +256,7 @@ class Database(object):
|
|
|
| def _covering_set_of_owners_for(self, files, author):
|
| dirs_remaining = set(self._enclosing_dir_with_owners(f) for f in files)
|
| - all_possible_owners = self._all_possible_owners(dirs_remaining, author)
|
| + all_possible_owners = self.all_possible_owners(dirs_remaining, author)
|
| suggested_owners = set()
|
| while dirs_remaining:
|
| owner = self.lowest_cost_owner(all_possible_owners, dirs_remaining)
|
| @@ -249,7 +265,7 @@ class Database(object):
|
| dirs_remaining -= dirs_to_remove
|
| return suggested_owners
|
|
|
| - def _all_possible_owners(self, dirs, author):
|
| + def all_possible_owners(self, dirs, author):
|
| """Returns a list of (potential owner, distance-from-dir) tuples; a
|
| distance of 1 is the lowest/closest possible distance (which makes the
|
| subsequent math easier)."""
|
| @@ -273,13 +289,13 @@ class Database(object):
|
| return all_possible_owners
|
|
|
| @staticmethod
|
| - def lowest_cost_owner(all_possible_owners, dirs):
|
| + def total_costs_by_owner(all_possible_owners, dirs):
|
| # We want to minimize both the number of reviewers and the distance
|
| # from the files/dirs needing reviews. The "pow(X, 1.75)" below is
|
| # an arbitrarily-selected scaling factor that seems to work well - it
|
| # will select one reviewer in the parent directory over three reviewers
|
| # in subdirs, but not one reviewer over just two.
|
| - total_costs_by_owner = {}
|
| + result = {}
|
| for owner in all_possible_owners:
|
| total_distance = 0
|
| num_directories_owned = 0
|
| @@ -287,13 +303,18 @@ class Database(object):
|
| if dirname in dirs:
|
| total_distance += distance
|
| num_directories_owned += 1
|
| - if num_directories_owned:
|
| - total_costs_by_owner[owner] = (total_distance /
|
| - pow(num_directories_owned, 1.75))
|
| + if num_directories_owned:
|
| + result[owner] = (total_distance /
|
| + pow(num_directories_owned, 1.75))
|
| + return result
|
|
|
| + @staticmethod
|
| + def lowest_cost_owner(all_possible_owners, dirs):
|
| + total_costs_by_owner = Database.total_costs_by_owner(all_possible_owners,
|
| + dirs)
|
| # Return the lowest cost owner. In the case of a tie, pick one randomly.
|
| lowest_cost = min(total_costs_by_owner.itervalues())
|
| lowest_cost_owners = filter(
|
| - lambda owner: total_costs_by_owner[owner] == lowest_cost,
|
| - total_costs_by_owner)
|
| + lambda owner: total_costs_by_owner[owner] == lowest_cost,
|
| + total_costs_by_owner)
|
| return random.Random().choice(lowest_cost_owners)
|
|
|