Index: tools/clang-format/git-clang-format =================================================================== --- tools/clang-format/git-clang-format +++ tools/clang-format/git-clang-format @@ -35,9 +35,9 @@ usage = 'git clang-format [OPTIONS] [] [--] [...]' desc = ''' -Run clang-format on all lines that differ between the working directory -and , which defaults to HEAD. Changes are only applied to the working -directory. +Run clang-format on all lines that differ between the working directory and + (which defaults to HEAD), or all lines that changed in a specific +commit. Changes are only applied to the working directory. The following git-config settings set the default of the corresponding option: clangFormat.binary @@ -121,8 +121,14 @@ opts.verbose -= opts.quiet del opts.quiet - commit, files = interpret_args(opts.args, dash_dash, opts.commit) - changed_lines = compute_diff_and_extract_lines(commit, files) + commits, files = interpret_args(opts.args, dash_dash, opts.commit) + if not opts.diff: + if len(commits) > 1: + die('at most one commit allowed; %d given' % len(commits)) + else: + if len(commits) > 2: + die('at most two commits allowed; %d given' % len(commits)) + changed_lines = compute_diff_and_extract_lines(commits, files) if opts.verbose >= 1: ignored_files = set(changed_lines) filter_by_extension(changed_lines, opts.extensions.lower().split(',')) @@ -142,10 +148,17 @@ # The computed diff outputs absolute paths, so we must cd before accessing # those files. cd_to_toplevel() - old_tree = create_tree_from_workdir(changed_lines) - new_tree = run_clang_format_and_save_to_tree(changed_lines, - binary=opts.binary, - style=opts.style) + if len(commits) > 1: + old_tree = get_tree_from_commit(commits[1]) + new_tree = run_clang_format_and_save_to_tree(changed_lines, + revision=commits[1], + binary=opts.binary, + style=opts.style) + else: + old_tree = create_tree_from_workdir(changed_lines) + new_tree = run_clang_format_and_save_to_tree(changed_lines, + binary=opts.binary, + style=opts.style) if opts.verbose >= 1: print 'old tree:', old_tree print 'new tree:', new_tree @@ -182,40 +195,42 @@ def interpret_args(args, dash_dash, default_commit): - """Interpret `args` as "[commit] [--] [files...]" and return (commit, files). + """Interpret `args` as "[commits...] [--] [files...]" and return (commits, + files). It is assumed that "--" and everything that follows has been removed from args and placed in `dash_dash`. - If "--" is present (i.e., `dash_dash` is non-empty), the argument to its - left (if present) is taken as commit. Otherwise, the first argument is - checked if it is a commit or a file. If commit is not given, - `default_commit` is used.""" + If "--" is present (i.e., `dash_dash` is non-empty), the arguments to its + left (if present) are taken as commits. Otherwise, the arguments are checked + from left to right if they are commits or files. If commits are not given, + a list with `default_commit` is used.""" if dash_dash: if len(args) == 0: - commit = default_commit - elif len(args) > 1: - die('at most one commit allowed; %d given' % len(args)) + commits = [default_commit] else: - commit = args[0] - object_type = get_object_type(commit) - if object_type not in ('commit', 'tag'): - if object_type is None: - die("'%s' is not a commit" % commit) - else: - die("'%s' is a %s, but a commit was expected" % (commit, object_type)) + commits = args + for commit in commits: + object_type = get_object_type(commit) + if object_type not in ('commit', 'tag'): + if object_type is None: + die("'%s' is not a commit" % commit) + else: + die("'%s' is a %s, but a commit was expected" % (commit, object_type)) files = dash_dash[1:] elif args: - if disambiguate_revision(args[0]): - commit = args[0] - files = args[1:] - else: - commit = default_commit - files = args + commits = [] + while args: + if not disambiguate_revision(args[0]): + break + commits.append(args.pop(0)) + if not commits: + commits = [default_commit] + files = args else: - commit = default_commit + commits = [default_commit] files = [] - return commit, files + return commits, files def disambiguate_revision(value): @@ -243,9 +258,9 @@ return stdout.strip() -def compute_diff_and_extract_lines(commit, files): +def compute_diff_and_extract_lines(commits, files): """Calls compute_diff() followed by extract_lines().""" - diff_process = compute_diff(commit, files) + diff_process = compute_diff(commits, files) changed_lines = extract_lines(diff_process.stdout) diff_process.stdout.close() diff_process.wait() @@ -255,13 +270,17 @@ return changed_lines -def compute_diff(commit, files): - """Return a subprocess object producing the diff from `commit`. +def compute_diff(commits, files): + """Return a subprocess object producing the diff from `commits`. The return value's `stdin` file object will produce a patch with the - differences between the working directory and `commit`, filtered on `files` - (if non-empty). Zero context lines are used in the patch.""" - cmd = ['git', 'diff-index', '-p', '-U0', commit, '--'] + differences between the working directory and the first commit if a single + one was specified, or the difference between both specified commits, filtered + on `files` (if non-empty). Zero context lines are used in the patch.""" + git_tool = 'diff-index' + if len(commits) > 1: + git_tool = 'diff-tree' + cmd = ['git', git_tool, '-p', '-U0'] + commits + ['--'] cmd.extend(files) p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE) p.stdin.close() @@ -311,6 +330,11 @@ os.chdir(toplevel) +def get_tree_from_commit(commit): + """Returns the object ID (SHA-1) of the tree associated with `commit`""" + return run('git', 'show', '-s', '--format=%T', commit) + + def create_tree_from_workdir(filenames): """Create a new git tree with the given files from the working directory. @@ -318,15 +342,17 @@ return create_tree(filenames, '--stdin') -def run_clang_format_and_save_to_tree(changed_lines, binary='clang-format', - style=None): +def run_clang_format_and_save_to_tree(changed_lines, revision=None, + binary='clang-format', style=None): """Run clang-format on each file and save the result to a git tree. Returns the object ID (SHA-1) of the created tree.""" def index_info_generator(): for filename, line_ranges in changed_lines.iteritems(): mode = oct(os.stat(filename).st_mode) - blob_id = clang_format_to_blob(filename, line_ranges, binary=binary, + blob_id = clang_format_to_blob(filename, line_ranges, + revision=revision, + binary=binary, style=style) yield '%s %s\t%s' % (mode, blob_id, filename) return create_tree(index_info_generator(), '--index-info') @@ -352,26 +378,42 @@ return tree_id -def clang_format_to_blob(filename, line_ranges, binary='clang-format', - style=None): +def clang_format_to_blob(filename, line_ranges, revision=None, + binary='clang-format', style=None): """Run clang-format on the given file and save the result to a git blob. + If `revision` is empty, clang-format will be run on the file in the working + directory. + Returns the object ID (SHA-1) of the created blob.""" - clang_format_cmd = [binary, filename] + clang_format_cmd = [binary] if style: clang_format_cmd.extend(['-style='+style]) clang_format_cmd.extend([ '-lines=%s:%s' % (start_line, start_line+line_count-1) for start_line, line_count in line_ranges]) + if revision: + clang_format_cmd.extend(['-assume-filename='+filename]) + git_show_cmd = ['git', 'show', '%s:%s' % (revision, filename)] + git_show = subprocess.Popen(git_show_cmd, stdin=subprocess.PIPE, + stdout=subprocess.PIPE) + git_show.stdin.close() + clang_format_stdin = git_show.stdout + else: + clang_format_cmd.extend([filename]) + git_show = None + clang_format_stdin = subprocess.PIPE try: - clang_format = subprocess.Popen(clang_format_cmd, stdin=subprocess.PIPE, + clang_format = subprocess.Popen(clang_format_cmd, stdin=clang_format_stdin, stdout=subprocess.PIPE) + if clang_format_stdin == subprocess.PIPE: + clang_format_stdin = clang_format.stdin except OSError as e: if e.errno == errno.ENOENT: die('cannot find executable "%s"' % binary) else: raise - clang_format.stdin.close() + clang_format_stdin.close() hash_object_cmd = ['git', 'hash-object', '-w', '--path='+filename, '--stdin'] hash_object = subprocess.Popen(hash_object_cmd, stdin=clang_format.stdout, stdout=subprocess.PIPE) @@ -381,6 +423,8 @@ die('`%s` failed' % ' '.join(hash_object_cmd)) if clang_format.wait() != 0: die('`%s` failed' % ' '.join(clang_format_cmd)) + if git_show and git_show.wait() != 0: + die('`%s` failed' % ' '.join(git_show_cmd)) return stdout.rstrip('\r\n')