Add support for a global status files for OWNERS

This allows for having some global comments such as timezones or
long-term unavailability.

The comments go into build/OWNERS.status (that way, they should be
available in all repos that map in build/ for the gn config files).
The local can be overwritten in codereview.settings.

The format is

email: status

Comments (starting with #) are allowed in that file, but they're ignored.

BUG=694222
R=dpranke@chromium.org

Change-Id: I49f58be87497d1ccaaa74f0a2f3d373403be44e7
Reviewed-on: https://chromium-review.googlesource.com/459542
Commit-Queue: Jochen Eisinger <jochen@chromium.org>
Reviewed-by: Dirk Pranke <dpranke@chromium.org>
diff --git a/owners.py b/owners.py
index 068e35b..2a44a9d 100644
--- a/owners.py
+++ b/owners.py
@@ -68,6 +68,11 @@
 BASIC_EMAIL_REGEXP = r'^[\w\-\+\%\.]+\@[\w\-\+\%\.]+$'
 
 
+# Key for global comments per email address. Should be unlikely to be a
+# pathname.
+GLOBAL_STATUS = '*'
+
+
 def _assert_is_collection(obj):
   assert not isinstance(obj, basestring)
   # Module 'collections' has no 'Iterable' member
@@ -95,14 +100,16 @@
   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):
+  def __init__(self, root, status_file, fopen, os_path):
     """Args:
       root: the path to the root of the Repository
+      status_file: the path relative to root to global status entries or None
       open: function callback to open a text file for reading
       os_path: module/object callback with fields for 'abspath', 'dirname',
           'exists', 'join', and 'relpath'
     """
     self.root = root
+    self.status_file = status_file
     self.fopen = fopen
     self.os_path = os_path
 
@@ -196,6 +203,7 @@
     return dirpath
 
   def load_data_needed_for(self, files):
+    self._read_global_comments()
     for f in files:
       dirpath = self.os_path.dirname(f)
       while not self._owners_for(dirpath):
@@ -267,6 +275,44 @@
       self._add_entry(dirpath, line, owners_path, lineno,
                       ' '.join(comment))
 
+  def _read_global_comments(self):
+    if not self.status_file:
+      return
+
+    owners_status_path = self.os_path.join(self.root, self.status_file)
+    if not self.os_path.exists(owners_status_path):
+      raise IOError('Could not find global status file "%s"' % 
+                    owners_status_path)
+
+    if owners_status_path in self.read_files:
+      return
+
+    self.read_files.add(owners_status_path)
+
+    lineno = 0
+    for line in self.fopen(owners_status_path):
+      lineno += 1
+      line = line.strip()
+      if line.startswith('#'):
+        continue
+      if line == '':
+        continue
+
+      m = re.match('(.+?):(.+)', line)
+      if m:
+        owner = m.group(1).strip()
+        comment = m.group(2).strip()
+        if not self.email_regexp.match(owner):
+          raise SyntaxErrorInOwnersFile(owners_status_path, lineno,
+              'invalid email address: "%s"' % owner)
+
+        self.comments.setdefault(owner, {})
+        self.comments[owner][GLOBAL_STATUS] = comment
+        continue
+
+      raise SyntaxErrorInOwnersFile(owners_status_path, lineno,
+          'cannot parse status entry: "%s"' % line.strip())
+
   def _add_entry(self, owned_paths, directive, owners_path, lineno, comment):
     if directive == 'set noparent':
       self._stop_looking.add(owned_paths)
@@ -281,8 +327,9 @@
         self._owners_to_paths.setdefault(owner, set()).add(owned_paths)
         self._paths_to_owners.setdefault(owned_paths, set()).add(owner)
     elif self.email_regexp.match(directive) or directive == EVERYONE:
-      self.comments.setdefault(directive, {})
-      self.comments[directive][owned_paths] = comment
+      if comment:
+        self.comments.setdefault(directive, {})
+        self.comments[directive][owned_paths] = comment
       self._owners_to_paths.setdefault(directive, set()).add(owned_paths)
       self._paths_to_owners.setdefault(owned_paths, set()).add(directive)
     else: