|
|
|
# Copyright (c) 2010 The Chromium Authors. All rights reserved.
|
|
|
|
# Use of this source code is governed by a BSD-style license that can be
|
|
|
|
# found in the LICENSE file.
|
|
|
|
|
|
|
|
"""A database of OWNERS files."""
|
|
|
|
|
|
|
|
class Assertion(AssertionError):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
class SyntaxErrorInOwnersFile(Exception):
|
|
|
|
def __init__(self, path, line, msg):
|
|
|
|
super(SyntaxErrorInOwnersFile, self).__init__((path, line, msg))
|
|
|
|
self.path = path
|
|
|
|
self.line = line
|
|
|
|
self.msg = msg
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
if self.msg:
|
|
|
|
return "%s:%d syntax error: %s" % (self.path, self.line, self.msg)
|
|
|
|
else:
|
|
|
|
return "%s:%d syntax error" % (self.path, self.line)
|
|
|
|
|
|
|
|
|
|
|
|
# Wildcard email-address in the OWNERS file.
|
|
|
|
ANYONE = '*'
|
|
|
|
|
|
|
|
|
|
|
|
class Database(object):
|
|
|
|
"""A database of OWNERS files for a repository.
|
|
|
|
|
|
|
|
This class allows you to find a suggested set of reviewers for a list
|
|
|
|
of changed files, and see if a list of changed files is covered by a
|
|
|
|
list of reviewers."""
|
|
|
|
|
|
|
|
def __init__(self, root, fopen, os_path):
|
|
|
|
"""Args:
|
|
|
|
root: the path to the root of the Repository
|
|
|
|
all_owners: the list of every owner in the system
|
|
|
|
open: function callback to open a text file for reading
|
|
|
|
os_path: module/object callback with fields for 'exists',
|
|
|
|
'dirname', and 'join'
|
|
|
|
"""
|
|
|
|
self.root = root
|
|
|
|
self.fopen = fopen
|
|
|
|
self.os_path = os_path
|
|
|
|
|
|
|
|
# Mapping of files to authorized owners.
|
|
|
|
self.files_owned_by = {}
|
|
|
|
|
|
|
|
# Mapping of owners to the files they own.
|
|
|
|
self.owners_for = {}
|
|
|
|
|
|
|
|
# In-memory cached map of files to their OWNERS files.
|
|
|
|
self.owners_file_for = {}
|
|
|
|
|
|
|
|
# In-memory cache of OWNERS files and their contents
|
|
|
|
self.owners_files = {}
|
|
|
|
|
|
|
|
def ReviewersFor(self, files):
|
|
|
|
"""Returns a suggested set of reviewers that will cover the set of files.
|
|
|
|
|
|
|
|
The set of files are paths relative to (and under) self.root."""
|
|
|
|
self._LoadDataNeededFor(files)
|
|
|
|
return self._CoveringSetOfOwnersFor(files)
|
|
|
|
|
|
|
|
def FilesAreCoveredBy(self, files, reviewers):
|
|
|
|
return not self.FilesNotCoveredBy(files, reviewers)
|
|
|
|
|
|
|
|
def FilesNotCoveredBy(self, files, reviewers):
|
|
|
|
covered_files = set()
|
|
|
|
for reviewer in reviewers:
|
|
|
|
covered_files = covered_files.union(self.files_owned_by[reviewer])
|
|
|
|
return files.difference(covered_files)
|
|
|
|
|
|
|
|
def _LoadDataNeededFor(self, files):
|
|
|
|
for f in files:
|
|
|
|
self._LoadOwnersFor(f)
|
|
|
|
|
|
|
|
def _LoadOwnersFor(self, f):
|
|
|
|
if f not in self.owners_for:
|
|
|
|
owner_file = self._FindOwnersFileFor(f)
|
|
|
|
self.owners_file_for[f] = owner_file
|
|
|
|
self._ReadOwnersFile(owner_file, f)
|
|
|
|
|
|
|
|
def _FindOwnersFileFor(self, f):
|
|
|
|
# This is really a "do ... until dirname = ''"
|
|
|
|
dirname = self.os_path.dirname(f)
|
|
|
|
while dirname:
|
|
|
|
owner_path = self.os_path.join(dirname, 'OWNERS')
|
|
|
|
if self.os_path.exists(owner_path):
|
|
|
|
return owner_path
|
|
|
|
dirname = self.os_path.dirname(dirname)
|
|
|
|
owner_path = self.os_path.join(dirname, 'OWNERS')
|
|
|
|
if self.os_path.exists(owner_path):
|
|
|
|
return owner_path
|
|
|
|
raise Assertion('No OWNERS file found for %s' % f)
|
|
|
|
|
|
|
|
def _ReadOwnersFile(self, owner_file, affected_file):
|
|
|
|
owners_for = self.owners_for.setdefault(affected_file, set())
|
|
|
|
for owner in self.fopen(owner_file):
|
|
|
|
owner = owner.strip()
|
|
|
|
self.files_owned_by.setdefault(owner, set()).add(affected_file)
|
|
|
|
owners_for.add(owner)
|
|
|
|
|
|
|
|
def _CoveringSetOfOwnersFor(self, files):
|
|
|
|
# TODO(dpranke): implement the greedy algorithm for covering sets, and
|
|
|
|
# consider returning multiple options in case there are several equally
|
|
|
|
# short combinations of owners.
|
|
|
|
every_owner = set()
|
|
|
|
for f in files:
|
|
|
|
every_owner = every_owner.union(self.owners_for[f])
|
|
|
|
return every_owner
|