From 8c5d2c1e7500e72f1e76edf608bf54e2d60c40b5 Mon Sep 17 00:00:00 2001 From: "halcanary@google.com" Date: Wed, 8 Jan 2014 21:29:34 +0000 Subject: [PATCH] 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 --- tools/roll_deps.py | 562 ++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 410 insertions(+), 152 deletions(-) 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 os import re import shutil import subprocess -from subprocess import check_call import sys import tempfile @@ -55,7 +54,8 @@ class DepsRollConfig(object): 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 @@ class DepsRollConfig(object): 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 @@ class DepsRollConfig(object): '', '--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 @@ class DepsRollError(Exception): 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. + + 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. + """ + + 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) + + +class ChangeDir(object): + """Use with a with-statement to temporarily change directories.""" + # pylint: disable=I0011,R0903 + + 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[^:]*)' + 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[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. + """ + # 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) - Pass the given arguments into subprocess.check_output() and return - the results, after stripping any excess whitespace. + +def revision_and_hash(config): + """Finds revision number and git hash of origin/master in the Skia tree. Args: - *args: to be passed to subprocess.check_output() - **kwargs: to be passed to subprocess.check_output() + config: (roll_deps.DepsRollConfig) object containing options. Returns: - The output of the process as a string without leading or - trailing whitespace. + A tuple (revision, hash) + revision: (int) SVN revision number. + git_hash: (string) full Git commit hash. + Raises: - OSError or subprocess.CalledProcessError: raised by check_output. + roll_deps.DepsRollError: if the revision can't be found. + OSError: failed to execute git or git-cl. + subprocess.CalledProcessError: git returned unexpected status. """ - return str(subprocess.check_output(*args, **kwargs)).strip() + 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 -def create_temp_skia_clone(config, depth): - """Clones Skia in a temp dir. +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. - depth: (int) how far back to clone the tree. + revision: (int) SVN revision number. + Returns: - temporary directory path if succcessful. + A tuple (revision, hash) + revision: (int) SVN revision number. + git_hash: (string) full Git commit hash. + Raises: - OSError, subprocess.CalledProcessError on failure. + 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 - 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 + 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 find_revision_and_hash(config, revision): - """Finds revision number and git hash of origin/master in the Skia tree. +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. - revision: (int or None) SVN revision number. If None, use - tip-of-tree. + partial_hash: (string) Partial git commit hash. 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: - raise DepsRollError('Git hash can not be found.') - return revision, git_hash - finally: - if use_temp: - shutil.rmtree(skia_dir) + 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 @@ class GitBranchCLUpload(object): 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 @@ class GitBranchCLUpload(object): 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]) - - check_call( - [git, 'checkout', '-q', '-b', - self._branch_name, 'origin/master']) + vsp.check_call([git, 'checkout', '-q', 'master']) + vsp.check_call([git, 'branch', '-q', '-D', self._branch_name]) - 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 @@ def change_skia_deps(revision, git_hash, depspath): 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 @@ def roll_deps(config, revision, git_hash): 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[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 @@ def roll_deps(config, revision, git_hash): 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, '(?Phttps?://[^) ]+)', '?') + + 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 @@ def roll_deps(config, revision, git_hash): 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 @@ def find_hash_and_roll_deps(config, revision): 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 @@ def main(args): 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__': -- 2.7.4