Changes to roll_deps.py
-  Code cleanup
-  Stop assuming that chromium's checkout would be via git-svn.
-  Verbose commit message for the deps revision
-  Shorter branch names.
-  New default: save_branches = yes.
-  New option: --git_hash=GIT_HASH

BUG=skia:1973
BUG=skia:1974
BUG=skia:1993
BUG=skia:1995
R=borenet@google.com

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

git-svn-id: http://skia.googlecode.com/svn/trunk@12974 2bbb7eff-a529-9590-31e7-b0007b416f81
diff --git a/tools/roll_deps.py b/tools/roll_deps.py
index 42b9174..2f98fb5 100755
--- a/tools/roll_deps.py
+++ b/tools/roll_deps.py
@@ -26,7 +26,6 @@
 import re
 import shutil
 import subprocess
-from subprocess import check_call
 import sys
 import tempfile
 
@@ -55,7 +54,8 @@
             options = DepsRollConfig.GetOptionParser()
         # pylint: disable=I0011,E1103
         self.verbose = options.verbose
-        self.save_branches = options.save_branches
+        self.vsp = VerboseSubprocess(self.verbose)
+        self.save_branches = not options.delete_branches
         self.search_depth = options.search_depth
         self.chromium_path = options.chromium_path
         self.git = options.git_path
@@ -110,6 +110,10 @@
         option_parser.add_option(
             '-r', '--revision', type='int', default=None,
             help='The Skia SVN revision number, defaults to top of tree.')
+        option_parser.add_option(
+            '-g', '--git_hash', default=None,
+            help='A partial Skia Git hash.  Do not set this and revision.')
+
         # Anyone using this script on a regular basis should set the
         # SKIA_GIT_CHECKOUT_PATH environment variable.
         option_parser.add_option(
@@ -125,8 +129,8 @@
             '', '--git_path', help='Git executable, defaults to "git".',
             default='git')
         option_parser.add_option(
-            '', '--save_branches', help='Save the temporary branches',
-            action='store_true', dest='save_branches', default=False)
+            '', '--delete_branches', help='Delete the temporary branches',
+            action='store_true', dest='delete_branches', default=False)
         option_parser.add_option(
             '', '--verbose', help='Do not suppress the output from `git cl`.',
             action='store_true', dest='verbose', default=False)
@@ -167,105 +171,352 @@
     pass
 
 
-def strip_output(*args, **kwargs):
-    """Wrap subprocess.check_output and str.strip().
+class VerboseSubprocess(object):
+    """Call subprocess methods, but print out command before executing.
 
-    Pass the given arguments into subprocess.check_output() and return
-    the results, after stripping any excess whitespace.
-
-    Args:
-        *args: to be passed to subprocess.check_output()
-        **kwargs: to be passed to subprocess.check_output()
-
-    Returns:
-        The output of the process as a string without leading or
-        trailing whitespace.
-    Raises:
-        OSError or subprocess.CalledProcessError: raised by check_output.
+    Attributes:
+        verbose: (boolean) should we print out the command or not.  If
+                 not, this is the same as calling the subprocess method
+        quiet: (boolean) suppress stdout on check_call and call.
+        prefix: (string) When verbose, what to print before each command.
     """
-    return str(subprocess.check_output(*args, **kwargs)).strip()
+
+    def __init__(self, verbose):
+        self.verbose = verbose
+        self.quiet = not verbose
+        self.prefix = '~~$ '
+
+    @staticmethod
+    def _fix(string):
+        """Quote and escape a string if necessary."""
+        if ' ' in string or '\n' in string:
+            string = '"%s"' % string.replace('\n', '\\n')
+        return string
+
+    @staticmethod
+    def print_subprocess_args(prefix, *args, **kwargs):
+        """Print out args in a human-readable manner."""
+        if 'cwd' in kwargs:
+            print '%scd %s' % (prefix, kwargs['cwd'])
+        print prefix + ' '.join(VerboseSubprocess._fix(arg) for arg in args[0])
+        if 'cwd' in kwargs:
+            print '%scd -' % prefix
+
+    def check_call(self, *args, **kwargs):
+        """Wrapper for subprocess.check_call().
+
+        Args:
+            *args: to be passed to subprocess.check_call()
+            **kwargs: to be passed to subprocess.check_call()
+        Returns:
+            Whatever subprocess.check_call() returns.
+        Raises:
+            OSError or subprocess.CalledProcessError: raised by check_call.
+        """
+        if self.verbose:
+            self.print_subprocess_args(self.prefix, *args, **kwargs)
+        if self.quiet:
+            with open(os.devnull, 'w') as devnull:
+                return subprocess.check_call(*args, stdout=devnull, **kwargs)
+        else:
+            return subprocess.check_call(*args, **kwargs)
+
+    def call(self, *args, **kwargs):
+        """Wrapper for subprocess.check().
+
+        Args:
+            *args: to be passed to subprocess.check_call()
+            **kwargs: to be passed to subprocess.check_call()
+        Returns:
+            Whatever subprocess.call() returns.
+        Raises:
+            OSError or subprocess.CalledProcessError: raised by call.
+        """
+        if self.verbose:
+            self.print_subprocess_args(self.prefix, *args, **kwargs)
+        if self.quiet:
+            with open(os.devnull, 'w') as devnull:
+                return subprocess.call(*args, stdout=devnull, **kwargs)
+        else:
+            return subprocess.call(*args, **kwargs)
+
+    def check_output(self, *args, **kwargs):
+        """Wrapper for subprocess.check_output().
+
+        Args:
+            *args: to be passed to subprocess.check_output()
+            **kwargs: to be passed to subprocess.check_output()
+        Returns:
+            Whatever subprocess.check_output() returns.
+        Raises:
+            OSError or subprocess.CalledProcessError: raised by check_output.
+        """
+        if self.verbose:
+            self.print_subprocess_args(self.prefix, *args, **kwargs)
+        return subprocess.check_output(*args, **kwargs)
+
+    def strip_output(self, *args, **kwargs):
+        """Wrap subprocess.check_output and str.strip().
+
+        Pass the given arguments into subprocess.check_output() and return
+        the results, after stripping any excess whitespace.
+
+        Args:
+            *args: to be passed to subprocess.check_output()
+            **kwargs: to be passed to subprocess.check_output()
+
+        Returns:
+            The output of the process as a string without leading or
+            trailing whitespace.
+        Raises:
+            OSError or subprocess.CalledProcessError: raised by check_output.
+        """
+        if self.verbose:
+            self.print_subprocess_args(self.prefix, *args, **kwargs)
+        return str(subprocess.check_output(*args, **kwargs)).strip()
+
+    def popen(self, *args, **kwargs):
+        """Wrapper for subprocess.Popen().
+
+        Args:
+            *args: to be passed to subprocess.Popen()
+            **kwargs: to be passed to subprocess.Popen()
+        Returns:
+            The output of subprocess.Popen()
+        Raises:
+            OSError or subprocess.CalledProcessError: raised by Popen.
+        """
+        if self.verbose:
+            self.print_subprocess_args(self.prefix, *args, **kwargs)
+        return subprocess.Popen(*args, **kwargs)
 
 
-def create_temp_skia_clone(config, depth):
-    """Clones Skia in a temp dir.
+class ChangeDir(object):
+    """Use with a with-statement to temporarily change directories."""
+    # pylint: disable=I0011,R0903
 
-    Args:
-        config: (roll_deps.DepsRollConfig) object containing options.
-        depth: (int) how far back to clone the tree.
-    Returns:
-        temporary directory path if succcessful.
-    Raises:
-        OSError, subprocess.CalledProcessError on failure.
+    def __init__(self, directory, verbose=False):
+        self._directory = directory
+        self._verbose = verbose
+
+    def __enter__(self):
+        if self._verbose:
+            print '~~$ cd %s' % self._directory
+        cwd = os.getcwd()
+        os.chdir(self._directory)
+        self._directory = cwd
+
+    def __exit__(self, etype, value, traceback):
+        if self._verbose:
+            print '~~$ cd %s' % self._directory
+        os.chdir(self._directory)
+
+
+class ReSearch(object):
+    """A collection of static methods for regexing things."""
+
+    @staticmethod
+    def search_within_stream(input_stream, pattern, default=None):
+        """Search for regular expression in a file-like object.
+
+        Opens a file for reading and searches line by line for a match to
+        the regex and returns the parenthesized group named return for the
+        first match.  Does not search across newlines.
+
+        For example:
+            pattern = '^root(:[^:]*){4}:(?P<return>[^:]*)'
+            with open('/etc/passwd', 'r') as stream:
+                return search_within_file(stream, pattern)
+        should return root's home directory (/root on my system).
+
+        Args:
+            input_stream: file-like object to be read
+            pattern: (string) to be passed to re.compile
+            default: what to return if no match
+
+        Returns:
+            A string or whatever default is
+        """
+        pattern_object = re.compile(pattern)
+        for line in input_stream:
+            match = pattern_object.search(line)
+            if match:
+                return match.group('return')
+        return default
+
+    @staticmethod
+    def search_within_string(input_string, pattern, default=None):
+        """Search for regular expression in a string.
+
+        Args:
+            input_string: (string) to be searched
+            pattern: (string) to be passed to re.compile
+            default: what to return if no match
+
+        Returns:
+            A string or whatever default is
+        """
+        match = re.search(pattern, input_string)
+        return match.group('return') if match else default
+
+    @staticmethod
+    def search_within_output(verbose, pattern, default, *args, **kwargs):
+        """Search for regular expression in a process output.
+
+        Does not search across newlines.
+
+        Args:
+            verbose: (boolean) shoule we call
+                     VerboseSubprocess.print_subprocess_args?
+            pattern: (string) to be passed to re.compile
+            default: what to return if no match
+            *args: to be passed to subprocess.Popen()
+            **kwargs: to be passed to subprocess.Popen()
+
+        Returns:
+            A string or whatever default is
+        """
+        if verbose:
+            VerboseSubprocess.print_subprocess_args(
+                '~~$ ', *args, **kwargs)
+        proc = subprocess.Popen(*args, stdout=subprocess.PIPE, **kwargs)
+        return ReSearch.search_within_stream(proc.stdout, pattern, default)
+
+
+def get_svn_revision(config, commit):
+    """Works in both git and git-svn. returns a string."""
+    svn_format = (
+        '(git-svn-id: [^@ ]+@|SVN changes up to revision |'
+        'LKGR w/ DEPS up to revision )(?P<return>[0-9]+)')
+    svn_revision = ReSearch.search_within_output(
+        config.verbose, svn_format, None,
+        [config.git, 'log', '-n', '1', '--format=format:%B', commit])
+    if not svn_revision:
+        raise DepsRollError(
+            'Revision number missing from Chromium origin/master.')
+    return int(svn_revision)
+
+
+class SkiaGitCheckout(object):
+    """Class to create a temporary skia git checkout, if necessary.
     """
-    git = config.git
-    skia_dir = tempfile.mkdtemp(prefix='git_skia_tmp_')
-    try:
-        check_call(
-            [git, 'clone', '-q', '--depth=%d' % depth,
-             '--single-branch', config.skia_url, skia_dir])
-        return skia_dir
-    except (OSError, subprocess.CalledProcessError) as error:
-        shutil.rmtree(skia_dir)
-        raise error
+    # pylint: disable=I0011,R0903
+
+    def __init__(self, config, depth):
+        self._config = config
+        self._depth = depth
+        self._use_temp = None
+        self._original_cwd = None
+
+    def __enter__(self):
+        config = self._config
+        git = config.git
+        skia_dir = None
+        self._original_cwd = os.getcwd()
+        if config.skia_git_checkout_path:
+            skia_dir = config.skia_git_checkout_path
+            ## Update origin/master if needed.
+            if self._config.verbose:
+                print '~~$', 'cd', skia_dir
+            os.chdir(skia_dir)
+            config.vsp.check_call([git, 'fetch', '-q', 'origin'])
+            self._use_temp = None
+        else:
+            skia_dir = tempfile.mkdtemp(prefix='git_skia_tmp_')
+            self._use_temp = skia_dir
+            try:
+                os.chdir(skia_dir)
+                config.vsp.check_call(
+                    [git, 'clone', '-q', '--depth=%d' % self._depth,
+                     '--single-branch', config.skia_url, '.'])
+            except (OSError, subprocess.CalledProcessError) as error:
+                shutil.rmtree(skia_dir)
+                raise error
+
+    def __exit__(self, etype, value, traceback):
+        if self._config.verbose:
+            print '~~$', 'cd', self._original_cwd
+        os.chdir(self._original_cwd)
+        if self._use_temp:
+            shutil.rmtree(self._use_temp)
 
 
-def find_revision_and_hash(config, revision):
+def revision_and_hash(config):
     """Finds revision number and git hash of origin/master in the Skia tree.
 
     Args:
         config: (roll_deps.DepsRollConfig) object containing options.
-        revision: (int or None) SVN revision number.  If None, use
-            tip-of-tree.
 
     Returns:
         A tuple (revision, hash)
             revision: (int) SVN revision number.
-            hash: (string) full Git commit hash.
+            git_hash: (string) full Git commit hash.
 
     Raises:
         roll_deps.DepsRollError: if the revision can't be found.
         OSError: failed to execute git or git-cl.
         subprocess.CalledProcessError: git returned unexpected status.
     """
-    git = config.git
-    use_temp = False
-    skia_dir = None
-    depth = 1 if (revision is None) else config.search_depth
-    try:
-        if config.skia_git_checkout_path:
-            skia_dir = config.skia_git_checkout_path
-            ## Update origin/master if needed.
-            check_call([git, 'fetch', '-q', 'origin'], cwd=skia_dir)
-        else:
-            skia_dir = create_temp_skia_clone(config, depth)
-            assert skia_dir
-            use_temp = True
-
-        if revision is None:
-            message = subprocess.check_output(
-                [git, 'log', '-n', '1', '--format=format:%B',
-                 'origin/master'], cwd=skia_dir)
-            svn_format = (
-                'git-svn-id: http://skia.googlecode.com/svn/trunk@([0-9]+) ')
-            search = re.search(svn_format, message)
-            if not search:
-                raise DepsRollError(
-                    'Revision number missing from origin/master.')
-            revision = int(search.group(1))
-            git_hash = strip_output(
-                [git, 'show-ref', 'origin/master', '--hash'], cwd=skia_dir)
-        else:
-            revision_regex = config.revision_format % revision
-            git_hash = strip_output(
-                [git, 'log', '--grep', revision_regex, '--format=format:%H',
-                 'origin/master'], cwd=skia_dir)
-
-        if revision < 0  or not git_hash:
+    with SkiaGitCheckout(config, 1):
+        revision = get_svn_revision(config, 'origin/master')
+        git_hash = config.vsp.strip_output(
+            [config.git, 'show-ref', 'origin/master', '--hash'])
+        if not git_hash:
             raise DepsRollError('Git hash can not be found.')
-        return revision, git_hash
-    finally:
-        if use_temp:
-            shutil.rmtree(skia_dir)
+    return revision, git_hash
+
+
+def revision_and_hash_from_revision(config, revision):
+    """Finds revision number and git hash of a commit in the Skia tree.
+
+    Args:
+        config: (roll_deps.DepsRollConfig) object containing options.
+        revision: (int) SVN revision number.
+
+    Returns:
+        A tuple (revision, hash)
+            revision: (int) SVN revision number.
+            git_hash: (string) full Git commit hash.
+
+    Raises:
+        roll_deps.DepsRollError: if the revision can't be found.
+        OSError: failed to execute git or git-cl.
+        subprocess.CalledProcessError: git returned unexpected status.
+    """
+    with SkiaGitCheckout(config, config.search_depth):
+        revision_regex = config.revision_format % revision
+        git_hash = config.vsp.strip_output(
+            [config.git, 'log', '--grep', revision_regex,
+             '--format=format:%H', 'origin/master'])
+        if not git_hash:
+            raise DepsRollError('Git hash can not be found.')
+    return revision, git_hash
+
+
+def revision_and_hash_from_partial(config, partial_hash):
+    """Returns the SVN revision number and full git hash.
+
+    Args:
+        config: (roll_deps.DepsRollConfig) object containing options.
+        partial_hash: (string) Partial git commit hash.
+
+    Returns:
+        A tuple (revision, hash)
+            revision: (int) SVN revision number.
+            git_hash: (string) full Git commit hash.
+
+    Raises:
+        roll_deps.DepsRollError: if the revision can't be found.
+        OSError: failed to execute git or git-cl.
+        subprocess.CalledProcessError: git returned unexpected status.
+    """
+    with SkiaGitCheckout(config, config.search_depth):
+        git_hash = config.vsp.strip_output(
+            ['git', 'log', '-n', '1', '--format=format:%H', partial_hash])
+        if not git_hash:
+            raise DepsRollError('Partial Git hash can not be found.')
+        revision = get_svn_revision(config, git_hash)
+    return revision, git_hash
 
 
 class GitBranchCLUpload(object):
@@ -302,7 +553,6 @@
         self._stash = None
         self._original_branch = None
         self._config = config
-        self._svn_info = None
         self.issue = None
 
     def stage_for_commit(self, *paths):
@@ -315,88 +565,76 @@
 
     def __enter__(self):
         git = self._config.git
+        vsp = self._config.vsp
         def branch_exists(branch):
             """Return true iff branch exists."""
-            return 0 == subprocess.call(
-                [git, 'show-ref', '--quiet', branch])
+            return 0 == vsp.call([git, 'show-ref', '--quiet', branch])
         def has_diff():
             """Return true iff repository has uncommited changes."""
-            return bool(subprocess.call([git, 'diff', '--quiet', 'HEAD']))
+            return bool(vsp.call([git, 'diff', '--quiet', 'HEAD']))
+
         self._stash = has_diff()
         if self._stash:
-            check_call([git, 'stash', 'save'])
+            vsp.check_call([git, 'stash', 'save'])
         try:
-            self._original_branch = strip_output(
+            self._original_branch = vsp.strip_output(
                 [git, 'symbolic-ref', '--short', 'HEAD'])
         except (subprocess.CalledProcessError,):
-            self._original_branch = strip_output(
+            self._original_branch = vsp.strip_output(
                 [git, 'rev-parse', 'HEAD'])
 
         if not self._branch_name:
             self._branch_name = self._config.default_branch_name
 
         if branch_exists(self._branch_name):
-            check_call([git, 'checkout', '-q', 'master'])
-            check_call([git, 'branch', '-q', '-D', self._branch_name])
+            vsp.check_call([git, 'checkout', '-q', 'master'])
+            vsp.check_call([git, 'branch', '-q', '-D', self._branch_name])
 
-        check_call(
-            [git, 'checkout', '-q', '-b',
-             self._branch_name, 'origin/master'])
-
-        svn_info = subprocess.check_output(['git', 'svn', 'info'])
-        svn_info_search = re.search(r'Last Changed Rev: ([0-9]+)\W', svn_info)
-        assert svn_info_search
-        self._svn_info = svn_info_search.group(1)
+        vsp.check_call(
+            [git, 'checkout', '-q', '-b', self._branch_name, 'origin/master'])
 
     def __exit__(self, etype, value, traceback):
         # pylint: disable=I0011,R0912
         git = self._config.git
-        def quiet_check_call(*args, **kwargs):
-            """Call check_call, but pipe output to devnull."""
-            with open(os.devnull, 'w') as devnull:
-                check_call(*args, stdout=devnull, **kwargs)
+        vsp = self._config.vsp
+        svn_info = str(get_svn_revision(self._config, 'HEAD'))
 
         for filename in self._file_list:
             assert os.path.exists(filename)
-            check_call([git, 'add', filename])
-        check_call([git, 'commit', '-q', '-m', self._message])
+            vsp.check_call([git, 'add', filename])
+        vsp.check_call([git, 'commit', '-q', '-m', self._message])
 
         git_cl = [git, 'cl', 'upload', '-f', '--cc=skia-team@google.com',
                   '--bypass-hooks', '--bypass-watchlists']
-        git_try = [git, 'cl', 'try', '--revision', self._svn_info]
+        git_try = [git, 'cl', 'try', '--revision', svn_info]
         git_try.extend([arg for bot in self._config.cl_bot_list
                         for arg in ('-b', bot)])
 
         if self._config.skip_cl_upload:
-            print ' '.join(git_cl)
-            print
+            print 'You should call:'
+            print '    cd %s' % os.getcwd()
+            VerboseSubprocess.print_subprocess_args(
+                '    ', [git, 'checkout', self._branch_name])
+            VerboseSubprocess.print_subprocess_args('    ', git_cl)
             if self._config.cl_bot_list:
-                print ' '.join(git_try)
-                print
+                VerboseSubprocess.print_subprocess_args('    ', git_try)
+            print
             self.issue = ''
         else:
-            if self._config.verbose:
-                check_call(git_cl)
-                print
-            else:
-                quiet_check_call(git_cl)
-            self.issue = strip_output([git, 'cl', 'issue'])
+            vsp.check_call(git_cl)
+            self.issue = vsp.strip_output([git, 'cl', 'issue'])
             if self._config.cl_bot_list:
-                if self._config.verbose:
-                    check_call(git_try)
-                    print
-                else:
-                    quiet_check_call(git_try)
+                vsp.check_call(git_try)
 
         # deal with the aftermath of failed executions of this script.
         if self._config.default_branch_name == self._original_branch:
             self._original_branch = 'master'
-        check_call([git, 'checkout', '-q', self._original_branch])
+        vsp.check_call([git, 'checkout', '-q', self._original_branch])
 
         if self._config.default_branch_name == self._branch_name:
-            check_call([git, 'branch', '-q', '-D', self._branch_name])
+            vsp.check_call([git, 'branch', '-q', '-D', self._branch_name])
         if self._stash:
-            check_call([git, 'stash', 'pop'])
+            vsp.check_call([git, 'stash', 'pop'])
 
 
 def change_skia_deps(revision, git_hash, depspath):
@@ -428,18 +666,6 @@
     shutil.move(temp_file.name, depspath)
 
 
-def branch_name(message):
-    """Return the first line of a commit message to be used as a branch name.
-
-    Args:
-        message: (string)
-
-    Returns:
-        A string derived from message suitable for a branch name.
-    """
-    return message.lstrip().split('\n')[0].rstrip().replace(' ', '_')
-
-
 def roll_deps(config, revision, git_hash):
     """Upload changed DEPS and a whitespace change.
 
@@ -457,18 +683,32 @@
         OSError: failed to execute git or git-cl.
         subprocess.CalledProcessError: git returned unexpected status.
     """
+
     git = config.git
-    cwd = os.getcwd()
-    os.chdir(config.chromium_path)
-    try:
-        check_call([git, 'fetch', '-q', 'origin'])
-        master_hash = strip_output(
+    with ChangeDir(config.chromium_path, config.verbose):
+        config.vsp.check_call([git, 'fetch', '-q', 'origin'])
+
+        old_revision = ReSearch.search_within_output(
+            config.verbose, '"skia_revision": "(?P<return>[0-9]+)",', None,
+            [git, 'show', 'origin/master:DEPS'])
+        assert old_revision
+        if revision == int(old_revision):
+            print 'DEPS is up to date!'
+            return None
+
+        master_hash = config.vsp.strip_output(
             [git, 'show-ref', 'origin/master', '--hash'])
+        master_revision = get_svn_revision(config, 'origin/master')
+
+        branch = None
 
         # master_hash[8] gives each whitespace CL a unique name.
-        message = ('whitespace change %s\n\nThis CL was created by'
-                   ' Skia\'s roll_deps.py script.\n') % master_hash[:8]
-        branch = branch_name(message) if config.save_branches else None
+        message = ('whitespace change %s\n\n'
+                   'Chromium base revision: %d / %s\n\n'
+                   'This CL was created by Skia\'s roll_deps.py script.\n'
+                  ) % (master_hash[:8], master_revision, master_hash[:8])
+        if config.save_branches:
+            branch = 'control_%s' % master_hash[:8]
 
         codereview = GitBranchCLUpload(config, message, branch)
         with codereview:
@@ -478,15 +718,21 @@
         whitespace_cl = codereview.issue
         if branch:
             whitespace_cl = '%s\n    branch: %s' % (whitespace_cl, branch)
-        control_url_match = re.search('https?://[^) ]+', codereview.issue)
-        if control_url_match:
-            message = ('roll skia DEPS to %d\n\nThis CL was created by'
-                       ' Skia\'s roll_deps.py script.\n\ncontrol: %s'
-                       % (revision, control_url_match.group(0)))
-        else:
-            message = ('roll skia DEPS to %d\n\nThis CL was created by'
-                       ' Skia\'s roll_deps.py script.') % revision
-        branch = branch_name(message) if config.save_branches else None
+
+        control_url = ReSearch.search_within_string(
+            codereview.issue, '(?P<return>https?://[^) ]+)', '?')
+
+        if config.save_branches:
+            branch = 'roll_%d_%s' % (revision, master_hash[:8])
+        message = (
+            'roll skia DEPS to %d\n\n'
+            'Chromium base revision: %d / %s\n'
+            'Old Skia revision: %s\n'
+            'New Skia revision: %d\n'
+            'Control CL: %s\n\n'
+            'This CL was created by Skia\'s roll_deps.py script.\n'
+            % (revision, master_revision, master_hash[:8],
+               old_revision, revision, control_url))
         codereview = GitBranchCLUpload(config, message, branch)
         with codereview:
             change_skia_deps(revision, git_hash, 'DEPS')
@@ -496,11 +742,9 @@
             deps_cl = '%s\n    branch: %s' % (deps_cl, branch)
 
         return deps_cl, whitespace_cl
-    finally:
-        os.chdir(cwd)
 
 
-def find_hash_and_roll_deps(config, revision):
+def find_hash_and_roll_deps(config, revision=None, partial_hash=None):
     """Call find_hash_from_revision() and roll_deps().
 
     The calls to git will be verbose on standard output.  After a
@@ -511,20 +755,34 @@
         config: (roll_deps.DepsRollConfig) object containing options.
         revision: (int or None) the Skia SVN revision number or None
             to use the tip of the tree.
+        partial_hash: (string or None) a partial pure-git Skia commit
+            hash.  Don't pass both partial_hash and revision.
 
     Raises:
         roll_deps.DepsRollError: if the revision can't be found.
         OSError: failed to execute git or git-cl.
         subprocess.CalledProcessError: git returned unexpected status.
     """
-    revision, git_hash = find_revision_and_hash(config, revision)
+
+    if revision and partial_hash:
+        raise DepsRollError('Pass revision or partial_hash, not both.')
+
+    if partial_hash:
+        revision, git_hash = revision_and_hash_from_partial(
+            config, partial_hash)
+    elif revision:
+        revision, git_hash = revision_and_hash_from_revision(config, revision)
+    else:
+        revision, git_hash = revision_and_hash(config)
 
     print 'revision=%r\nhash=%r\n' % (revision, git_hash)
 
-    deps_issue, whitespace_issue = roll_deps(config, revision, git_hash)
+    roll = roll_deps(config, revision, git_hash)
 
-    print 'DEPS roll:\n    %s\n' % deps_issue
-    print 'Whitespace change:\n    %s\n' % whitespace_issue
+    if roll:
+        deps_issue, whitespace_issue = roll
+        print 'DEPS roll:\n    %s\n' % deps_issue
+        print 'Whitespace change:\n    %s\n' % whitespace_issue
 
 
 def main(args):
@@ -544,7 +802,7 @@
         option_parser.error('Invalid git executable.')
 
     config = DepsRollConfig(options)
-    find_hash_and_roll_deps(config, options.revision)
+    find_hash_and_roll_deps(config, options.revision, options.git_hash)
 
 
 if __name__ == '__main__':