Implement support for file: includes in OWNERS files.

This CL implements support for file: include lines in OWNERS files,
both as top-level directives and as per-file directives. The
paths can be either relative or absolute.

Examples of lines in OWNERS files:

  file:test/OWNERS  (relative, top-level)
  file://content/OWNERS  (absolute, top-level)
  per-file mock_impl.h=file:test/OWNERS  (relative, per-file)
  per-file mock_impl.h=file://content/OWNERS  (absolute, per-file)

A whole series of tests to cover this feature have been added
to owners_unittest.py as well.

BUG=119396, 147633

Review URL: https://codereview.chromium.org/1085993004

git-svn-id: svn://svn.chromium.org/chrome/trunk/tools/depot_tools@294854 0039d316-1c4b-4281-b951-d872f2087c98
diff --git a/owners.py b/owners.py
index 30646ff..563675a 100644
--- a/owners.py
+++ b/owners.py
@@ -18,6 +18,7 @@
           | comment
 
 directive := "set noparent"
+          |  "file:" glob
           |  email_address
           |  "*"
 
@@ -45,6 +46,11 @@
 for the glob, and only the "per-file" owners are used for files matching that
 glob.
 
+If the "file:" directive is used, the referred to OWNERS file will be parsed and
+considered when determining the valid set of OWNERS. If the filename starts with
+"//" it is relative to the root of the repository, otherwise it is relative to
+the current file
+
 Examples for all of these combinations can be found in tests/owners_unittest.py.
 """
 
@@ -118,6 +124,9 @@
     # (This is implicitly true for the root directory).
     self.stop_looking = set([''])
 
+    # Set of files which have already been read.
+    self.read_files = set()
+
   def reviewers_for(self, files, author):
     """Returns a suggested set of reviewers that will cover the files.
 
@@ -189,16 +198,23 @@
     for f in files:
       dirpath = self.os_path.dirname(f)
       while not dirpath in self.owners_for:
-        self._read_owners_in_dir(dirpath)
+        self._read_owners(self.os_path.join(dirpath, 'OWNERS'))
         if self._stop_looking(dirpath):
           break
         dirpath = self.os_path.dirname(dirpath)
 
-  def _read_owners_in_dir(self, dirpath):
-    owners_path = self.os_path.join(self.root, dirpath, 'OWNERS')
+  def _read_owners(self, path):
+    owners_path = self.os_path.join(self.root, path)
     if not self.os_path.exists(owners_path):
       return
+
+    if owners_path in self.read_files:
+      return
+
+    self.read_files.add(owners_path)
+
     comment = []
+    dirpath = self.os_path.dirname(path)
     in_comment = False
     lineno = 0
     for line in self.fopen(owners_path):
@@ -244,6 +260,23 @@
                  line_type, owners_path, lineno, comment):
     if directive == 'set noparent':
       self.stop_looking.add(path)
+    elif directive.startswith('file:'):
+      owners_file = self._resolve_include(directive[5:], owners_path)
+      if not owners_file:
+        raise SyntaxErrorInOwnersFile(owners_path, lineno,
+            ('%s does not refer to an existing file.' % directive[5:]))
+
+      self._read_owners(owners_file)
+
+      dirpath = self.os_path.dirname(owners_file)
+      for key in self.owned_by:
+        if not dirpath in self.owned_by[key]:
+          continue
+        self.owned_by[key].add(path)
+
+      if dirpath in self.owners_for:
+        self.owners_for.setdefault(path, set()).update(self.owners_for[dirpath])
+
     elif self.email_regexp.match(directive) or directive == EVERYONE:
       self.comments.setdefault(directive, {})
       self.comments[directive][path] = comment
@@ -251,9 +284,23 @@
       self.owners_for.setdefault(path, set()).add(directive)
     else:
       raise SyntaxErrorInOwnersFile(owners_path, lineno,
-          ('%s is not a "set" directive, "*", '
+          ('%s is not a "set" directive, file include, "*", '
            'or an email address: "%s"' % (line_type, directive)))
 
+  def _resolve_include(self, path, start):
+    if path.startswith('//'):
+      include_path = path[2:]
+    else:
+      assert start.startswith(self.root)
+      start = self.os_path.dirname(start[len(self.root):])
+      include_path = self.os_path.join(start, path)
+
+    owners_path = self.os_path.join(self.root, include_path)
+    if not self.os_path.exists(owners_path):
+      return None
+
+    return include_path
+
   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)