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/git_utils.py b/tools/git_utils.py
new file mode 100644
index 0000000..a35c85e
--- /dev/null
+++ b/tools/git_utils.py
@@ -0,0 +1,168 @@
+# Copyright 2014 Google Inc.
+#
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Module to host the ChangeGitBranch class and test_git_executable function.
+"""
+
+import os
+import subprocess
+
+import misc_utils
+
+
+class ChangeGitBranch(object):
+    """Class to manage git branches.
+
+    This class allows one to create a new branch in a repository based
+    off of a given commit, and restore the original tree state.
+
+    Assumes current working directory is a git repository.
+
+    Example:
+        with ChangeGitBranch():
+            edit_files(files)
+            git_add(files)
+            git_commit()
+            git_format_patch('HEAD~')
+        # At this point, the repository is returned to its original
+        # state.
+
+    Constructor Args:
+        branch_name: (string) if not None, the name of the branch to
+            use.  If None, then use a temporary branch that will be
+            deleted.  If the branch already exists, then a different
+            branch name will be created.  Use git_branch_name() to
+            find the actual branch name used.
+        upstream_branch: (string) if not None, the name of the branch or
+            commit to branch from.  If None, then use origin/master
+        verbose: (boolean) if true, makes debugging easier.
+
+    Raises:
+        OSError: the git executable disappeared.
+        subprocess.CalledProcessError: git returned unexpected status.
+        Exception: if the given branch name exists, or if the repository
+            isn't clean on exit, or git can't be found.
+    """
+    # pylint: disable=I0011,R0903,R0902
+
+    def __init__(self,
+                 branch_name=None,
+                 upstream_branch=None,
+                 verbose=False):
+        # pylint: disable=I0011,R0913
+        if branch_name:
+            self._branch_name = branch_name
+            self._delete_branch = False
+        else:
+            self._branch_name = 'ChangeGitBranchTempBranch'
+            self._delete_branch = True
+
+        if upstream_branch:
+            self._upstream_branch = upstream_branch
+        else:
+            self._upstream_branch = 'origin/master'
+
+        self._git = git_executable()
+        if not self._git:
+            raise Exception('Git can\'t be found.')
+
+        self._stash = None
+        self._original_branch = None
+        self._vsp = misc_utils.VerboseSubprocess(verbose)
+
+    def _has_git_diff(self):
+        """Return true iff repository has uncommited changes."""
+        return bool(self._vsp.call([self._git, 'diff', '--quiet', 'HEAD']))
+
+    def _branch_exists(self, branch):
+        """Return true iff branch exists."""
+        return 0 == self._vsp.call([self._git, 'show-ref', '--quiet', branch])
+
+    def __enter__(self):
+        git, vsp = self._git, self._vsp
+
+        if self._branch_exists(self._branch_name):
+            i, branch_name = 0, self._branch_name
+            while self._branch_exists(branch_name):
+                i += 1
+                branch_name = '%s_%03d' % (self._branch_name, i)
+            self._branch_name = branch_name
+
+        self._stash = self._has_git_diff()
+        if self._stash:
+            vsp.check_call([git, 'stash', 'save'])
+        self._original_branch = git_branch_name(vsp.verbose)
+        vsp.check_call(
+            [git, 'checkout', '-q', '-b',
+             self._branch_name, self._upstream_branch])
+
+    def __exit__(self, etype, value, traceback):
+        git, vsp = self._git, self._vsp
+
+        if self._has_git_diff():
+            status = vsp.check_output([git, 'status', '-s'])
+            raise Exception('git checkout not clean:\n%s' % status)
+        vsp.check_call([git, 'checkout', '-q', self._original_branch])
+        if self._stash:
+            vsp.check_call([git, 'stash', 'pop'])
+        if self._delete_branch:
+            assert self._original_branch != self._branch_name
+            vsp.check_call([git, 'branch', '-D', self._branch_name])
+
+
+def git_branch_name(verbose=False):
+    """Return a description of the current branch.
+
+    Args:
+        verbose: (boolean) makes debugging easier
+
+    Returns:
+        A string suitable for passing to `git checkout` later.
+    """
+    git = git_executable()
+    vsp = misc_utils.VerboseSubprocess(verbose)
+    try:
+        full_branch = vsp.strip_output([git, 'symbolic-ref', 'HEAD'])
+        return full_branch.split('/')[-1]
+    except (subprocess.CalledProcessError,):
+        # "fatal: ref HEAD is not a symbolic ref"
+        return vsp.strip_output([git, 'rev-parse', 'HEAD'])
+
+
+def test_git_executable(git):
+    """Test the git executable.
+
+    Args:
+        git: git executable path.
+    Returns:
+        True if test is successful.
+    """
+    with open(os.devnull, 'w') as devnull:
+        try:
+            subprocess.call([git, '--version'], stdout=devnull)
+        except (OSError,):
+            return False
+    return True
+
+
+def git_executable():
+    """Find the git executable.
+
+    If the GIT_EXECUTABLE environment variable is set, that will
+    override whatever is found in the PATH.
+
+    If no suitable executable is found, return None
+
+    Returns:
+        A string suiable for passing to subprocess functions, or None.
+    """
+    env_git = os.environ.get('GIT_EXECUTABLE')
+    if env_git and test_git_executable(env_git):
+        return env_git
+    for git in ('git', 'git.exe', 'git.bat'):
+        if test_git_executable(git):
+            return git
+    return None
+