Add ability to modify CL reviewers' list

This adds the ability to specify reviewers for a gerrit CL. The new
option ('reviewers') is uncomfortably similar to an existing one
('review') and is not quite following the pattern of others, as it
allows variable number of arguments (list of emails to be
added/removed). The necessary command line error processing
adjustments have been made.

The new option is supposed to be invoked as follows:

$ gerrit reviewers CL [email [email]..]

Email addresses prepended with '~' will be removed from the reviewer
list. Using '-' would be better, but parser considers it another
command line option, and requiring to use '--' for certain cases seems
worse than using ~ instead of -.

An email validation regex string is added to constants.

BUG=none
TEST=manual
 . ran the following set of commands

   $ gerrit reviewers *38487  ~sjg@google.com dianders@google.com
   $ gerrit reviewers *38487  sjg@google.com ~dianders@google.com
   $ gerrit reviewers *38487  sjg@google.com dianders@google.com

  through the web interface observed the list of reviewers changing
  as expected.

Change-Id: If433e13dbe32d6de66ef4cecf4522e61b40b565e
Signed-off-by: Vadim Bendebury <vbendeb@chromium.org>
Reviewed-on: https://gerrit.chromium.org/gerrit/56422
Reviewed-by: Mike Frysinger <vapier@chromium.org>
diff --git a/scripts/gerrit.py b/scripts/gerrit.py
index 89fa29a..3da6a21 100644
--- a/scripts/gerrit.py
+++ b/scripts/gerrit.py
@@ -13,7 +13,7 @@
 
 import inspect
 import os
-import sys
+import re
 
 from chromite.buildbot import constants
 from chromite.lib import commandline
@@ -238,6 +238,29 @@
   ReviewCommand(opts, idx, ['--submit'])
 
 
+def UserActReviewers(opts, idx, *emails):
+  """Add/remove reviewers' emails for CL <n> (prepend with '~' to remove)"""
+
+  # Allow for optional leading '~'.
+  email_validator = re.compile(r'^[~]?%s$' % constants.EMAIL_REGEX)
+  add_list, remove_list, invalid_list = [], [], []
+
+  for x in emails:
+    if not email_validator.match(x):
+      invalid_list.append(x)
+    elif x[0] == '~':
+      remove_list.append(x[1:])
+    else:
+      add_list.append(x)
+
+  if invalid_list:
+    cros_build_lib.Die(
+        'Invalid email address(es): %s' % ', '.join(invalid_list))
+
+  if add_list or remove_list:
+    opts.gerrit.SetReviewers(idx, add=add_list, remove=remove_list)
+
+
 def main(argv):
   # Locate actions that are exposed to the user.  All functions that start
   # with "UserAct" are fair game.
@@ -300,13 +323,16 @@
   functor = globals().get(act_pfx + cmd.capitalize())
   if functor:
     argspec = inspect.getargspec(functor)
-    if len(argspec.args) - 1 != len(args):
+    if argspec.varargs:
+      if len(args) < len(argspec.args):
+        parser.error('incorrect number of args: %s expects at least %s' %
+                     (cmd, len(argspec.args)))
+    elif len(argspec.args) - 1 != len(args):
       parser.error('incorrect number of args: %s expects %s' %
                    (cmd, len(argspec.args) - 1))
     try:
       functor(opts, *args)
-    except cros_build_lib.RunCommandError:
-      # An error message has been issued on stderr by now.
-      sys.exit(1)
+    except (cros_build_lib.RunCommandError, gerrit.GerritException) as e:
+      cros_build_lib.Die(e.message)
   else:
     parser.error('unknown action: %s' % (cmd,))