add_codereview_message script to append messages to a CL

Add add_codereview_message.py script.  This script takes a message and
a codereview URL as arguments calls `git cl upload` to append the
message to the given codereview issue.

Motivation: We are automating a process of testing Chromium/Skia DEPS
rolls where roll_deps.py is automatically run every night, and then at
a later time, compare_codereview.py is run to generate a report of the
trybot results.  This script can be used to append that report to the
DEPS roll Cl.

This CL also refactors functioanlity common to roll_deps and
add_codereview_message:
*   Add tools/git_utils.py module.
    -   ChangeGitBranch class was factored out of
        roll_deps.GitBranchCLUpload.  The other half of that class
        became roll_deps.git_cl_uploader function.  I make use of
        this new class in both roll_deps and upload_deps_roll.
    -   test_git_executable moved to this new module.
*   Add tools/misc_utils.py - move VerboseSubprocess, ChangeDir,
    and ReSearch classes from roll_deps module.
*   Remove generic functions from roll_deps.
*   Add git_cl_uploader function to roll_deps.  Refactor
    roll_deps.roll_deps function to make use of it

BUG=skia:
R=borenet@google.com, robertphillips@google.com

Author: halcanary@google.com

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

git-svn-id: http://skia.googlecode.com/svn/trunk@13251 2bbb7eff-a529-9590-31e7-b0007b416f81
diff --git a/tools/add_codereview_message.py b/tools/add_codereview_message.py
new file mode 100755
index 0000000..6710390
--- /dev/null
+++ b/tools/add_codereview_message.py
@@ -0,0 +1,148 @@
+#!/usr/bin/python2
+
+# Copyright 2014 Google Inc.
+#
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Add message to codereview issue.
+
+This script takes a codereview URL or a codereview issue number as its
+argument and a (possibly multi-line) message on stdin.  It then calls
+`git cl upload` to append the message to the given codereview issue.
+
+Usage:
+  echo MESSAGE | %prog -c CHECKOUT_PATH CODEREVIEW_ISSUE
+or:
+  cd /path/to/git/checkout
+  %prog CODEREVIEW_ISSUE <<EOF
+  MESSAGE
+  EOF
+or:
+  %prog --help
+"""
+
+import optparse
+import os
+import sys
+
+import git_utils
+import misc_utils
+
+
+DEFAULT_REVIEWERS = ','.join([
+    'rmistry@google.com',
+    'reed@google.com',
+    'bsalomon@google.com',
+    'robertphillips@google.com',
+    ])
+
+
+DEFAULT_CC_LIST = ','.join([
+    'skia-team@google.com',
+    ])
+
+
+def add_codereview_message(codereview_url, message, checkout_path,
+                           skip_cl_upload, verbose, reviewers, cclist):
+    """Add a message to a given codereview.
+
+    Args:
+        codereview_url: (string) we will extract the issue number from
+            this url, or this could simply be the issue number.
+        message: (string) will be passed to `git cl upload -m $MESSAGE`
+        checkout_path: (string) location of the git
+            repository checkout to be used.
+        skip_cl_upload: (boolean) if true, don't actually
+            add the message and keep the temporary branch around.
+        verbose: (boolean) print out details useful for debugging.
+        reviewers: (string) comma-separated list of reviewers
+        cclist: (string) comma-separated list of addresses to be
+            carbon-copied
+    """
+    # pylint: disable=I0011,R0913
+    git = git_utils.git_executable()
+    issue = codereview_url.strip('/').split('/')[-1]
+    vsp = misc_utils.VerboseSubprocess(verbose)
+    if skip_cl_upload:
+        branch_name = 'issue_%s' % issue
+    else:
+        branch_name = None
+    upstream = 'origin/master'
+
+    with misc_utils.ChangeDir(checkout_path, verbose):
+        vsp.check_call([git, 'fetch', '-q', 'origin'])
+
+        with git_utils.ChangeGitBranch(branch_name, upstream, verbose):
+            vsp.check_call([git, 'cl', 'patch', issue])
+
+            git_upload = [
+                git, 'cl', 'upload', '-t', 'bot report', '-m', message]
+            if cclist:
+                git_upload.append('--cc=' + cclist)
+            if reviewers:
+                git_upload.append('--reviewers=' + reviewers)
+
+            if skip_cl_upload:
+                branch_name = git_utils.git_branch_name(verbose)
+                space = '    '
+                print 'You should call:'
+                misc_utils.print_subprocess_args(space, ['cd', os.getcwd()])
+                misc_utils.print_subprocess_args(
+                    space, [git, 'checkout', branch_name])
+                misc_utils.print_subprocess_args(space, git_upload)
+            else:
+                vsp.check_call(git_upload)
+                print vsp.check_output([git, 'cl', 'issue'])
+
+
+def main(argv):
+    """main function; see module-level docstring and GetOptionParser help.
+
+    Args:
+        argv: sys.argv[1:]-type argument list.
+    """
+    option_parser = optparse.OptionParser(usage=__doc__)
+    option_parser.add_option(
+        '-c', '--checkout_path',
+        default=os.curdir,
+        help='Path to the Git repository checkout,'
+        ' defaults to current working directory.')
+    option_parser.add_option(
+        '', '--skip_cl_upload', action='store_true', default=False,
+        help='Skip the cl upload step; useful for testing.')
+    option_parser.add_option(
+        '', '--verbose', action='store_true', dest='verbose', default=False,
+        help='Do not suppress the output from `git cl`.',)
+    option_parser.add_option(
+        '', '--git_path', default='git',
+        help='Git executable, defaults to "git".',)
+    option_parser.add_option(
+        '', '--reviewers', default=DEFAULT_REVIEWERS,
+        help=('Comma-separated list of reviewers.  Default is "%s".'
+              % DEFAULT_REVIEWERS))
+    option_parser.add_option(
+        '', '--cc', default=DEFAULT_CC_LIST,
+        help=('Comma-separated list of addresses to be carbon-copied.'
+              '  Default is "%s".' %  DEFAULT_CC_LIST))
+
+    options, arguments = option_parser.parse_args(argv)
+
+    if not options.checkout_path:
+        option_parser.error('Must specify checkout_path.')
+    if not git_utils.git_executable():
+        option_parser.error('Invalid git executable.')
+    if len(arguments) > 1:
+        option_parser.error('Extra arguments.')
+    if len(arguments) != 1:
+        option_parser.error('Missing Codereview URL.')
+
+    message = sys.stdin.read()
+    add_codereview_message(arguments[0], message, options.checkout_path,
+                           options.skip_cl_upload, options.verbose,
+                           options.reviewers, options.cc)
+
+
+if __name__ == '__main__':
+    main(sys.argv[1:])
+