17330f729Sjoerg#!/usr/bin/env python 27330f729Sjoerg# 37330f729Sjoerg#===- git-clang-format - ClangFormat Git Integration ---------*- python -*--===# 47330f729Sjoerg# 57330f729Sjoerg# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. 67330f729Sjoerg# See https://llvm.org/LICENSE.txt for license information. 77330f729Sjoerg# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception 87330f729Sjoerg# 97330f729Sjoerg#===------------------------------------------------------------------------===# 107330f729Sjoerg 117330f729Sjoergr""" 127330f729Sjoergclang-format git integration 137330f729Sjoerg============================ 147330f729Sjoerg 157330f729SjoergThis file provides a clang-format integration for git. Put it somewhere in your 167330f729Sjoergpath and ensure that it is executable. Then, "git clang-format" will invoke 177330f729Sjoergclang-format on the changes in current files or a specific commit. 187330f729Sjoerg 197330f729SjoergFor further details, run: 207330f729Sjoerggit clang-format -h 217330f729Sjoerg 227330f729SjoergRequires Python 2.7 or Python 3 237330f729Sjoerg""" 247330f729Sjoerg 257330f729Sjoergfrom __future__ import absolute_import, division, print_function 267330f729Sjoergimport argparse 277330f729Sjoergimport collections 287330f729Sjoergimport contextlib 297330f729Sjoergimport errno 307330f729Sjoergimport os 317330f729Sjoergimport re 327330f729Sjoergimport subprocess 337330f729Sjoergimport sys 347330f729Sjoerg 357330f729Sjoergusage = 'git clang-format [OPTIONS] [<commit>] [<commit>] [--] [<file>...]' 367330f729Sjoerg 377330f729Sjoergdesc = ''' 387330f729SjoergIf zero or one commits are given, run clang-format on all lines that differ 397330f729Sjoergbetween the working directory and <commit>, which defaults to HEAD. Changes are 407330f729Sjoergonly applied to the working directory. 417330f729Sjoerg 427330f729SjoergIf two commits are given (requires --diff), run clang-format on all lines in the 437330f729Sjoergsecond <commit> that differ from the first <commit>. 447330f729Sjoerg 457330f729SjoergThe following git-config settings set the default of the corresponding option: 467330f729Sjoerg clangFormat.binary 477330f729Sjoerg clangFormat.commit 48*e038c9c4Sjoerg clangFormat.extensions 497330f729Sjoerg clangFormat.style 507330f729Sjoerg''' 517330f729Sjoerg 527330f729Sjoerg# Name of the temporary index file in which save the output of clang-format. 537330f729Sjoerg# This file is created within the .git directory. 547330f729Sjoergtemp_index_basename = 'clang-format-index' 557330f729Sjoerg 567330f729Sjoerg 577330f729SjoergRange = collections.namedtuple('Range', 'start, count') 587330f729Sjoerg 597330f729Sjoerg 607330f729Sjoergdef main(): 617330f729Sjoerg config = load_git_config() 627330f729Sjoerg 637330f729Sjoerg # In order to keep '--' yet allow options after positionals, we need to 647330f729Sjoerg # check for '--' ourselves. (Setting nargs='*' throws away the '--', while 657330f729Sjoerg # nargs=argparse.REMAINDER disallows options after positionals.) 667330f729Sjoerg argv = sys.argv[1:] 677330f729Sjoerg try: 687330f729Sjoerg idx = argv.index('--') 697330f729Sjoerg except ValueError: 707330f729Sjoerg dash_dash = [] 717330f729Sjoerg else: 727330f729Sjoerg dash_dash = argv[idx:] 737330f729Sjoerg argv = argv[:idx] 747330f729Sjoerg 757330f729Sjoerg default_extensions = ','.join([ 767330f729Sjoerg # From clang/lib/Frontend/FrontendOptions.cpp, all lower case 777330f729Sjoerg 'c', 'h', # C 787330f729Sjoerg 'm', # ObjC 797330f729Sjoerg 'mm', # ObjC++ 807330f729Sjoerg 'cc', 'cp', 'cpp', 'c++', 'cxx', 'hh', 'hpp', 'hxx', # C++ 81*e038c9c4Sjoerg 'cu', 'cuh', # CUDA 827330f729Sjoerg # Other languages that clang-format supports 837330f729Sjoerg 'proto', 'protodevel', # Protocol Buffers 847330f729Sjoerg 'java', # Java 857330f729Sjoerg 'js', # JavaScript 867330f729Sjoerg 'ts', # TypeScript 877330f729Sjoerg 'cs', # C Sharp 887330f729Sjoerg ]) 897330f729Sjoerg 907330f729Sjoerg p = argparse.ArgumentParser( 917330f729Sjoerg usage=usage, formatter_class=argparse.RawDescriptionHelpFormatter, 927330f729Sjoerg description=desc) 937330f729Sjoerg p.add_argument('--binary', 947330f729Sjoerg default=config.get('clangformat.binary', 'clang-format'), 957330f729Sjoerg help='path to clang-format'), 967330f729Sjoerg p.add_argument('--commit', 977330f729Sjoerg default=config.get('clangformat.commit', 'HEAD'), 987330f729Sjoerg help='default commit to use if none is specified'), 997330f729Sjoerg p.add_argument('--diff', action='store_true', 1007330f729Sjoerg help='print a diff instead of applying the changes') 1017330f729Sjoerg p.add_argument('--extensions', 1027330f729Sjoerg default=config.get('clangformat.extensions', 1037330f729Sjoerg default_extensions), 1047330f729Sjoerg help=('comma-separated list of file extensions to format, ' 1057330f729Sjoerg 'excluding the period and case-insensitive')), 1067330f729Sjoerg p.add_argument('-f', '--force', action='store_true', 1077330f729Sjoerg help='allow changes to unstaged files') 1087330f729Sjoerg p.add_argument('-p', '--patch', action='store_true', 1097330f729Sjoerg help='select hunks interactively') 1107330f729Sjoerg p.add_argument('-q', '--quiet', action='count', default=0, 1117330f729Sjoerg help='print less information') 1127330f729Sjoerg p.add_argument('--style', 1137330f729Sjoerg default=config.get('clangformat.style', None), 1147330f729Sjoerg help='passed to clang-format'), 1157330f729Sjoerg p.add_argument('-v', '--verbose', action='count', default=0, 1167330f729Sjoerg help='print extra information') 1177330f729Sjoerg # We gather all the remaining positional arguments into 'args' since we need 1187330f729Sjoerg # to use some heuristics to determine whether or not <commit> was present. 1197330f729Sjoerg # However, to print pretty messages, we make use of metavar and help. 1207330f729Sjoerg p.add_argument('args', nargs='*', metavar='<commit>', 1217330f729Sjoerg help='revision from which to compute the diff') 1227330f729Sjoerg p.add_argument('ignored', nargs='*', metavar='<file>...', 1237330f729Sjoerg help='if specified, only consider differences in these files') 1247330f729Sjoerg opts = p.parse_args(argv) 1257330f729Sjoerg 1267330f729Sjoerg opts.verbose -= opts.quiet 1277330f729Sjoerg del opts.quiet 1287330f729Sjoerg 1297330f729Sjoerg commits, files = interpret_args(opts.args, dash_dash, opts.commit) 1307330f729Sjoerg if len(commits) > 1: 1317330f729Sjoerg if not opts.diff: 1327330f729Sjoerg die('--diff is required when two commits are given') 1337330f729Sjoerg else: 1347330f729Sjoerg if len(commits) > 2: 1357330f729Sjoerg die('at most two commits allowed; %d given' % len(commits)) 1367330f729Sjoerg changed_lines = compute_diff_and_extract_lines(commits, files) 1377330f729Sjoerg if opts.verbose >= 1: 1387330f729Sjoerg ignored_files = set(changed_lines) 1397330f729Sjoerg filter_by_extension(changed_lines, opts.extensions.lower().split(',')) 140*e038c9c4Sjoerg # The computed diff outputs absolute paths, so we must cd before accessing 141*e038c9c4Sjoerg # those files. 142*e038c9c4Sjoerg cd_to_toplevel() 143*e038c9c4Sjoerg filter_symlinks(changed_lines) 1447330f729Sjoerg if opts.verbose >= 1: 1457330f729Sjoerg ignored_files.difference_update(changed_lines) 1467330f729Sjoerg if ignored_files: 147*e038c9c4Sjoerg print( 148*e038c9c4Sjoerg 'Ignoring changes in the following files (wrong extension or symlink):') 1497330f729Sjoerg for filename in ignored_files: 1507330f729Sjoerg print(' %s' % filename) 1517330f729Sjoerg if changed_lines: 1527330f729Sjoerg print('Running clang-format on the following files:') 1537330f729Sjoerg for filename in changed_lines: 1547330f729Sjoerg print(' %s' % filename) 1557330f729Sjoerg if not changed_lines: 156*e038c9c4Sjoerg if opts.verbose >= 0: 1577330f729Sjoerg print('no modified files to format') 1587330f729Sjoerg return 1597330f729Sjoerg if len(commits) > 1: 1607330f729Sjoerg old_tree = commits[1] 1617330f729Sjoerg new_tree = run_clang_format_and_save_to_tree(changed_lines, 1627330f729Sjoerg revision=commits[1], 1637330f729Sjoerg binary=opts.binary, 1647330f729Sjoerg style=opts.style) 1657330f729Sjoerg else: 1667330f729Sjoerg old_tree = create_tree_from_workdir(changed_lines) 1677330f729Sjoerg new_tree = run_clang_format_and_save_to_tree(changed_lines, 1687330f729Sjoerg binary=opts.binary, 1697330f729Sjoerg style=opts.style) 1707330f729Sjoerg if opts.verbose >= 1: 1717330f729Sjoerg print('old tree: %s' % old_tree) 1727330f729Sjoerg print('new tree: %s' % new_tree) 1737330f729Sjoerg if old_tree == new_tree: 1747330f729Sjoerg if opts.verbose >= 0: 1757330f729Sjoerg print('clang-format did not modify any files') 1767330f729Sjoerg elif opts.diff: 1777330f729Sjoerg print_diff(old_tree, new_tree) 1787330f729Sjoerg else: 1797330f729Sjoerg changed_files = apply_changes(old_tree, new_tree, force=opts.force, 1807330f729Sjoerg patch_mode=opts.patch) 1817330f729Sjoerg if (opts.verbose >= 0 and not opts.patch) or opts.verbose >= 1: 1827330f729Sjoerg print('changed files:') 1837330f729Sjoerg for filename in changed_files: 1847330f729Sjoerg print(' %s' % filename) 1857330f729Sjoerg 1867330f729Sjoerg 1877330f729Sjoergdef load_git_config(non_string_options=None): 1887330f729Sjoerg """Return the git configuration as a dictionary. 1897330f729Sjoerg 1907330f729Sjoerg All options are assumed to be strings unless in `non_string_options`, in which 1917330f729Sjoerg is a dictionary mapping option name (in lower case) to either "--bool" or 1927330f729Sjoerg "--int".""" 1937330f729Sjoerg if non_string_options is None: 1947330f729Sjoerg non_string_options = {} 1957330f729Sjoerg out = {} 1967330f729Sjoerg for entry in run('git', 'config', '--list', '--null').split('\0'): 1977330f729Sjoerg if entry: 198*e038c9c4Sjoerg if '\n' in entry: 1997330f729Sjoerg name, value = entry.split('\n', 1) 200*e038c9c4Sjoerg else: 201*e038c9c4Sjoerg # A setting with no '=' ('\n' with --null) is implicitly 'true' 202*e038c9c4Sjoerg name = entry 203*e038c9c4Sjoerg value = 'true' 2047330f729Sjoerg if name in non_string_options: 2057330f729Sjoerg value = run('git', 'config', non_string_options[name], name) 2067330f729Sjoerg out[name] = value 2077330f729Sjoerg return out 2087330f729Sjoerg 2097330f729Sjoerg 2107330f729Sjoergdef interpret_args(args, dash_dash, default_commit): 2117330f729Sjoerg """Interpret `args` as "[commits] [--] [files]" and return (commits, files). 2127330f729Sjoerg 2137330f729Sjoerg It is assumed that "--" and everything that follows has been removed from 2147330f729Sjoerg args and placed in `dash_dash`. 2157330f729Sjoerg 2167330f729Sjoerg If "--" is present (i.e., `dash_dash` is non-empty), the arguments to its 2177330f729Sjoerg left (if present) are taken as commits. Otherwise, the arguments are checked 2187330f729Sjoerg from left to right if they are commits or files. If commits are not given, 2197330f729Sjoerg a list with `default_commit` is used.""" 2207330f729Sjoerg if dash_dash: 2217330f729Sjoerg if len(args) == 0: 2227330f729Sjoerg commits = [default_commit] 2237330f729Sjoerg else: 2247330f729Sjoerg commits = args 2257330f729Sjoerg for commit in commits: 2267330f729Sjoerg object_type = get_object_type(commit) 2277330f729Sjoerg if object_type not in ('commit', 'tag'): 2287330f729Sjoerg if object_type is None: 2297330f729Sjoerg die("'%s' is not a commit" % commit) 2307330f729Sjoerg else: 2317330f729Sjoerg die("'%s' is a %s, but a commit was expected" % (commit, object_type)) 2327330f729Sjoerg files = dash_dash[1:] 2337330f729Sjoerg elif args: 2347330f729Sjoerg commits = [] 2357330f729Sjoerg while args: 2367330f729Sjoerg if not disambiguate_revision(args[0]): 2377330f729Sjoerg break 2387330f729Sjoerg commits.append(args.pop(0)) 2397330f729Sjoerg if not commits: 2407330f729Sjoerg commits = [default_commit] 2417330f729Sjoerg files = args 2427330f729Sjoerg else: 2437330f729Sjoerg commits = [default_commit] 2447330f729Sjoerg files = [] 2457330f729Sjoerg return commits, files 2467330f729Sjoerg 2477330f729Sjoerg 2487330f729Sjoergdef disambiguate_revision(value): 2497330f729Sjoerg """Returns True if `value` is a revision, False if it is a file, or dies.""" 2507330f729Sjoerg # If `value` is ambiguous (neither a commit nor a file), the following 2517330f729Sjoerg # command will die with an appropriate error message. 2527330f729Sjoerg run('git', 'rev-parse', value, verbose=False) 2537330f729Sjoerg object_type = get_object_type(value) 2547330f729Sjoerg if object_type is None: 2557330f729Sjoerg return False 2567330f729Sjoerg if object_type in ('commit', 'tag'): 2577330f729Sjoerg return True 2587330f729Sjoerg die('`%s` is a %s, but a commit or filename was expected' % 2597330f729Sjoerg (value, object_type)) 2607330f729Sjoerg 2617330f729Sjoerg 2627330f729Sjoergdef get_object_type(value): 2637330f729Sjoerg """Returns a string description of an object's type, or None if it is not 2647330f729Sjoerg a valid git object.""" 2657330f729Sjoerg cmd = ['git', 'cat-file', '-t', value] 2667330f729Sjoerg p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 2677330f729Sjoerg stdout, stderr = p.communicate() 2687330f729Sjoerg if p.returncode != 0: 2697330f729Sjoerg return None 2707330f729Sjoerg return convert_string(stdout.strip()) 2717330f729Sjoerg 2727330f729Sjoerg 2737330f729Sjoergdef compute_diff_and_extract_lines(commits, files): 2747330f729Sjoerg """Calls compute_diff() followed by extract_lines().""" 2757330f729Sjoerg diff_process = compute_diff(commits, files) 2767330f729Sjoerg changed_lines = extract_lines(diff_process.stdout) 2777330f729Sjoerg diff_process.stdout.close() 2787330f729Sjoerg diff_process.wait() 2797330f729Sjoerg if diff_process.returncode != 0: 2807330f729Sjoerg # Assume error was already printed to stderr. 2817330f729Sjoerg sys.exit(2) 2827330f729Sjoerg return changed_lines 2837330f729Sjoerg 2847330f729Sjoerg 2857330f729Sjoergdef compute_diff(commits, files): 2867330f729Sjoerg """Return a subprocess object producing the diff from `commits`. 2877330f729Sjoerg 2887330f729Sjoerg The return value's `stdin` file object will produce a patch with the 2897330f729Sjoerg differences between the working directory and the first commit if a single 2907330f729Sjoerg one was specified, or the difference between both specified commits, filtered 2917330f729Sjoerg on `files` (if non-empty). Zero context lines are used in the patch.""" 2927330f729Sjoerg git_tool = 'diff-index' 2937330f729Sjoerg if len(commits) > 1: 2947330f729Sjoerg git_tool = 'diff-tree' 2957330f729Sjoerg cmd = ['git', git_tool, '-p', '-U0'] + commits + ['--'] 2967330f729Sjoerg cmd.extend(files) 2977330f729Sjoerg p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE) 2987330f729Sjoerg p.stdin.close() 2997330f729Sjoerg return p 3007330f729Sjoerg 3017330f729Sjoerg 3027330f729Sjoergdef extract_lines(patch_file): 3037330f729Sjoerg """Extract the changed lines in `patch_file`. 3047330f729Sjoerg 3057330f729Sjoerg The return value is a dictionary mapping filename to a list of (start_line, 3067330f729Sjoerg line_count) pairs. 3077330f729Sjoerg 3087330f729Sjoerg The input must have been produced with ``-U0``, meaning unidiff format with 3097330f729Sjoerg zero lines of context. The return value is a dict mapping filename to a 3107330f729Sjoerg list of line `Range`s.""" 3117330f729Sjoerg matches = {} 3127330f729Sjoerg for line in patch_file: 3137330f729Sjoerg line = convert_string(line) 3147330f729Sjoerg match = re.search(r'^\+\+\+\ [^/]+/(.*)', line) 3157330f729Sjoerg if match: 3167330f729Sjoerg filename = match.group(1).rstrip('\r\n') 3177330f729Sjoerg match = re.search(r'^@@ -[0-9,]+ \+(\d+)(,(\d+))?', line) 3187330f729Sjoerg if match: 3197330f729Sjoerg start_line = int(match.group(1)) 3207330f729Sjoerg line_count = 1 3217330f729Sjoerg if match.group(3): 3227330f729Sjoerg line_count = int(match.group(3)) 3237330f729Sjoerg if line_count > 0: 3247330f729Sjoerg matches.setdefault(filename, []).append(Range(start_line, line_count)) 3257330f729Sjoerg return matches 3267330f729Sjoerg 3277330f729Sjoerg 3287330f729Sjoergdef filter_by_extension(dictionary, allowed_extensions): 3297330f729Sjoerg """Delete every key in `dictionary` that doesn't have an allowed extension. 3307330f729Sjoerg 3317330f729Sjoerg `allowed_extensions` must be a collection of lowercase file extensions, 3327330f729Sjoerg excluding the period.""" 3337330f729Sjoerg allowed_extensions = frozenset(allowed_extensions) 3347330f729Sjoerg for filename in list(dictionary.keys()): 3357330f729Sjoerg base_ext = filename.rsplit('.', 1) 3367330f729Sjoerg if len(base_ext) == 1 and '' in allowed_extensions: 3377330f729Sjoerg continue 3387330f729Sjoerg if len(base_ext) == 1 or base_ext[1].lower() not in allowed_extensions: 3397330f729Sjoerg del dictionary[filename] 3407330f729Sjoerg 3417330f729Sjoerg 342*e038c9c4Sjoergdef filter_symlinks(dictionary): 343*e038c9c4Sjoerg """Delete every key in `dictionary` that is a symlink.""" 344*e038c9c4Sjoerg for filename in list(dictionary.keys()): 345*e038c9c4Sjoerg if os.path.islink(filename): 346*e038c9c4Sjoerg del dictionary[filename] 347*e038c9c4Sjoerg 348*e038c9c4Sjoerg 3497330f729Sjoergdef cd_to_toplevel(): 3507330f729Sjoerg """Change to the top level of the git repository.""" 3517330f729Sjoerg toplevel = run('git', 'rev-parse', '--show-toplevel') 3527330f729Sjoerg os.chdir(toplevel) 3537330f729Sjoerg 3547330f729Sjoerg 3557330f729Sjoergdef create_tree_from_workdir(filenames): 3567330f729Sjoerg """Create a new git tree with the given files from the working directory. 3577330f729Sjoerg 3587330f729Sjoerg Returns the object ID (SHA-1) of the created tree.""" 3597330f729Sjoerg return create_tree(filenames, '--stdin') 3607330f729Sjoerg 3617330f729Sjoerg 3627330f729Sjoergdef run_clang_format_and_save_to_tree(changed_lines, revision=None, 3637330f729Sjoerg binary='clang-format', style=None): 3647330f729Sjoerg """Run clang-format on each file and save the result to a git tree. 3657330f729Sjoerg 3667330f729Sjoerg Returns the object ID (SHA-1) of the created tree.""" 3677330f729Sjoerg def iteritems(container): 3687330f729Sjoerg try: 3697330f729Sjoerg return container.iteritems() # Python 2 3707330f729Sjoerg except AttributeError: 3717330f729Sjoerg return container.items() # Python 3 3727330f729Sjoerg def index_info_generator(): 3737330f729Sjoerg for filename, line_ranges in iteritems(changed_lines): 3747330f729Sjoerg if revision: 3757330f729Sjoerg git_metadata_cmd = ['git', 'ls-tree', 3767330f729Sjoerg '%s:%s' % (revision, os.path.dirname(filename)), 3777330f729Sjoerg os.path.basename(filename)] 3787330f729Sjoerg git_metadata = subprocess.Popen(git_metadata_cmd, stdin=subprocess.PIPE, 3797330f729Sjoerg stdout=subprocess.PIPE) 3807330f729Sjoerg stdout = git_metadata.communicate()[0] 3817330f729Sjoerg mode = oct(int(stdout.split()[0], 8)) 3827330f729Sjoerg else: 3837330f729Sjoerg mode = oct(os.stat(filename).st_mode) 3847330f729Sjoerg # Adjust python3 octal format so that it matches what git expects 3857330f729Sjoerg if mode.startswith('0o'): 3867330f729Sjoerg mode = '0' + mode[2:] 3877330f729Sjoerg blob_id = clang_format_to_blob(filename, line_ranges, 3887330f729Sjoerg revision=revision, 3897330f729Sjoerg binary=binary, 3907330f729Sjoerg style=style) 3917330f729Sjoerg yield '%s %s\t%s' % (mode, blob_id, filename) 3927330f729Sjoerg return create_tree(index_info_generator(), '--index-info') 3937330f729Sjoerg 3947330f729Sjoerg 3957330f729Sjoergdef create_tree(input_lines, mode): 3967330f729Sjoerg """Create a tree object from the given input. 3977330f729Sjoerg 3987330f729Sjoerg If mode is '--stdin', it must be a list of filenames. If mode is 3997330f729Sjoerg '--index-info' is must be a list of values suitable for "git update-index 4007330f729Sjoerg --index-info", such as "<mode> <SP> <sha1> <TAB> <filename>". Any other mode 4017330f729Sjoerg is invalid.""" 4027330f729Sjoerg assert mode in ('--stdin', '--index-info') 4037330f729Sjoerg cmd = ['git', 'update-index', '--add', '-z', mode] 4047330f729Sjoerg with temporary_index_file(): 4057330f729Sjoerg p = subprocess.Popen(cmd, stdin=subprocess.PIPE) 4067330f729Sjoerg for line in input_lines: 4077330f729Sjoerg p.stdin.write(to_bytes('%s\0' % line)) 4087330f729Sjoerg p.stdin.close() 4097330f729Sjoerg if p.wait() != 0: 4107330f729Sjoerg die('`%s` failed' % ' '.join(cmd)) 4117330f729Sjoerg tree_id = run('git', 'write-tree') 4127330f729Sjoerg return tree_id 4137330f729Sjoerg 4147330f729Sjoerg 4157330f729Sjoergdef clang_format_to_blob(filename, line_ranges, revision=None, 4167330f729Sjoerg binary='clang-format', style=None): 4177330f729Sjoerg """Run clang-format on the given file and save the result to a git blob. 4187330f729Sjoerg 4197330f729Sjoerg Runs on the file in `revision` if not None, or on the file in the working 4207330f729Sjoerg directory if `revision` is None. 4217330f729Sjoerg 4227330f729Sjoerg Returns the object ID (SHA-1) of the created blob.""" 4237330f729Sjoerg clang_format_cmd = [binary] 4247330f729Sjoerg if style: 4257330f729Sjoerg clang_format_cmd.extend(['-style='+style]) 4267330f729Sjoerg clang_format_cmd.extend([ 4277330f729Sjoerg '-lines=%s:%s' % (start_line, start_line+line_count-1) 4287330f729Sjoerg for start_line, line_count in line_ranges]) 4297330f729Sjoerg if revision: 4307330f729Sjoerg clang_format_cmd.extend(['-assume-filename='+filename]) 4317330f729Sjoerg git_show_cmd = ['git', 'cat-file', 'blob', '%s:%s' % (revision, filename)] 4327330f729Sjoerg git_show = subprocess.Popen(git_show_cmd, stdin=subprocess.PIPE, 4337330f729Sjoerg stdout=subprocess.PIPE) 4347330f729Sjoerg git_show.stdin.close() 4357330f729Sjoerg clang_format_stdin = git_show.stdout 4367330f729Sjoerg else: 4377330f729Sjoerg clang_format_cmd.extend([filename]) 4387330f729Sjoerg git_show = None 4397330f729Sjoerg clang_format_stdin = subprocess.PIPE 4407330f729Sjoerg try: 4417330f729Sjoerg clang_format = subprocess.Popen(clang_format_cmd, stdin=clang_format_stdin, 4427330f729Sjoerg stdout=subprocess.PIPE) 4437330f729Sjoerg if clang_format_stdin == subprocess.PIPE: 4447330f729Sjoerg clang_format_stdin = clang_format.stdin 4457330f729Sjoerg except OSError as e: 4467330f729Sjoerg if e.errno == errno.ENOENT: 4477330f729Sjoerg die('cannot find executable "%s"' % binary) 4487330f729Sjoerg else: 4497330f729Sjoerg raise 4507330f729Sjoerg clang_format_stdin.close() 4517330f729Sjoerg hash_object_cmd = ['git', 'hash-object', '-w', '--path='+filename, '--stdin'] 4527330f729Sjoerg hash_object = subprocess.Popen(hash_object_cmd, stdin=clang_format.stdout, 4537330f729Sjoerg stdout=subprocess.PIPE) 4547330f729Sjoerg clang_format.stdout.close() 4557330f729Sjoerg stdout = hash_object.communicate()[0] 4567330f729Sjoerg if hash_object.returncode != 0: 4577330f729Sjoerg die('`%s` failed' % ' '.join(hash_object_cmd)) 4587330f729Sjoerg if clang_format.wait() != 0: 4597330f729Sjoerg die('`%s` failed' % ' '.join(clang_format_cmd)) 4607330f729Sjoerg if git_show and git_show.wait() != 0: 4617330f729Sjoerg die('`%s` failed' % ' '.join(git_show_cmd)) 4627330f729Sjoerg return convert_string(stdout).rstrip('\r\n') 4637330f729Sjoerg 4647330f729Sjoerg 4657330f729Sjoerg@contextlib.contextmanager 4667330f729Sjoergdef temporary_index_file(tree=None): 4677330f729Sjoerg """Context manager for setting GIT_INDEX_FILE to a temporary file and deleting 4687330f729Sjoerg the file afterward.""" 4697330f729Sjoerg index_path = create_temporary_index(tree) 4707330f729Sjoerg old_index_path = os.environ.get('GIT_INDEX_FILE') 4717330f729Sjoerg os.environ['GIT_INDEX_FILE'] = index_path 4727330f729Sjoerg try: 4737330f729Sjoerg yield 4747330f729Sjoerg finally: 4757330f729Sjoerg if old_index_path is None: 4767330f729Sjoerg del os.environ['GIT_INDEX_FILE'] 4777330f729Sjoerg else: 4787330f729Sjoerg os.environ['GIT_INDEX_FILE'] = old_index_path 4797330f729Sjoerg os.remove(index_path) 4807330f729Sjoerg 4817330f729Sjoerg 4827330f729Sjoergdef create_temporary_index(tree=None): 4837330f729Sjoerg """Create a temporary index file and return the created file's path. 4847330f729Sjoerg 4857330f729Sjoerg If `tree` is not None, use that as the tree to read in. Otherwise, an 4867330f729Sjoerg empty index is created.""" 4877330f729Sjoerg gitdir = run('git', 'rev-parse', '--git-dir') 4887330f729Sjoerg path = os.path.join(gitdir, temp_index_basename) 4897330f729Sjoerg if tree is None: 4907330f729Sjoerg tree = '--empty' 4917330f729Sjoerg run('git', 'read-tree', '--index-output='+path, tree) 4927330f729Sjoerg return path 4937330f729Sjoerg 4947330f729Sjoerg 4957330f729Sjoergdef print_diff(old_tree, new_tree): 4967330f729Sjoerg """Print the diff between the two trees to stdout.""" 4977330f729Sjoerg # We use the porcelain 'diff' and not plumbing 'diff-tree' because the output 4987330f729Sjoerg # is expected to be viewed by the user, and only the former does nice things 4997330f729Sjoerg # like color and pagination. 5007330f729Sjoerg # 5017330f729Sjoerg # We also only print modified files since `new_tree` only contains the files 5027330f729Sjoerg # that were modified, so unmodified files would show as deleted without the 5037330f729Sjoerg # filter. 5047330f729Sjoerg subprocess.check_call(['git', 'diff', '--diff-filter=M', old_tree, new_tree, 5057330f729Sjoerg '--']) 5067330f729Sjoerg 5077330f729Sjoerg 5087330f729Sjoergdef apply_changes(old_tree, new_tree, force=False, patch_mode=False): 5097330f729Sjoerg """Apply the changes in `new_tree` to the working directory. 5107330f729Sjoerg 5117330f729Sjoerg Bails if there are local changes in those files and not `force`. If 5127330f729Sjoerg `patch_mode`, runs `git checkout --patch` to select hunks interactively.""" 5137330f729Sjoerg changed_files = run('git', 'diff-tree', '--diff-filter=M', '-r', '-z', 5147330f729Sjoerg '--name-only', old_tree, 5157330f729Sjoerg new_tree).rstrip('\0').split('\0') 5167330f729Sjoerg if not force: 5177330f729Sjoerg unstaged_files = run('git', 'diff-files', '--name-status', *changed_files) 5187330f729Sjoerg if unstaged_files: 5197330f729Sjoerg print('The following files would be modified but ' 5207330f729Sjoerg 'have unstaged changes:', file=sys.stderr) 5217330f729Sjoerg print(unstaged_files, file=sys.stderr) 5227330f729Sjoerg print('Please commit, stage, or stash them first.', file=sys.stderr) 5237330f729Sjoerg sys.exit(2) 5247330f729Sjoerg if patch_mode: 5257330f729Sjoerg # In patch mode, we could just as well create an index from the new tree 5267330f729Sjoerg # and checkout from that, but then the user will be presented with a 5277330f729Sjoerg # message saying "Discard ... from worktree". Instead, we use the old 5287330f729Sjoerg # tree as the index and checkout from new_tree, which gives the slightly 5297330f729Sjoerg # better message, "Apply ... to index and worktree". This is not quite 5307330f729Sjoerg # right, since it won't be applied to the user's index, but oh well. 5317330f729Sjoerg with temporary_index_file(old_tree): 5327330f729Sjoerg subprocess.check_call(['git', 'checkout', '--patch', new_tree]) 5337330f729Sjoerg index_tree = old_tree 5347330f729Sjoerg else: 5357330f729Sjoerg with temporary_index_file(new_tree): 5367330f729Sjoerg run('git', 'checkout-index', '-a', '-f') 5377330f729Sjoerg return changed_files 5387330f729Sjoerg 5397330f729Sjoerg 5407330f729Sjoergdef run(*args, **kwargs): 5417330f729Sjoerg stdin = kwargs.pop('stdin', '') 5427330f729Sjoerg verbose = kwargs.pop('verbose', True) 5437330f729Sjoerg strip = kwargs.pop('strip', True) 5447330f729Sjoerg for name in kwargs: 5457330f729Sjoerg raise TypeError("run() got an unexpected keyword argument '%s'" % name) 5467330f729Sjoerg p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, 5477330f729Sjoerg stdin=subprocess.PIPE) 5487330f729Sjoerg stdout, stderr = p.communicate(input=stdin) 5497330f729Sjoerg 5507330f729Sjoerg stdout = convert_string(stdout) 5517330f729Sjoerg stderr = convert_string(stderr) 5527330f729Sjoerg 5537330f729Sjoerg if p.returncode == 0: 5547330f729Sjoerg if stderr: 5557330f729Sjoerg if verbose: 5567330f729Sjoerg print('`%s` printed to stderr:' % ' '.join(args), file=sys.stderr) 5577330f729Sjoerg print(stderr.rstrip(), file=sys.stderr) 5587330f729Sjoerg if strip: 5597330f729Sjoerg stdout = stdout.rstrip('\r\n') 5607330f729Sjoerg return stdout 5617330f729Sjoerg if verbose: 5627330f729Sjoerg print('`%s` returned %s' % (' '.join(args), p.returncode), file=sys.stderr) 5637330f729Sjoerg if stderr: 5647330f729Sjoerg print(stderr.rstrip(), file=sys.stderr) 5657330f729Sjoerg sys.exit(2) 5667330f729Sjoerg 5677330f729Sjoerg 5687330f729Sjoergdef die(message): 5697330f729Sjoerg print('error:', message, file=sys.stderr) 5707330f729Sjoerg sys.exit(2) 5717330f729Sjoerg 5727330f729Sjoerg 5737330f729Sjoergdef to_bytes(str_input): 5747330f729Sjoerg # Encode to UTF-8 to get binary data. 5757330f729Sjoerg if isinstance(str_input, bytes): 5767330f729Sjoerg return str_input 5777330f729Sjoerg return str_input.encode('utf-8') 5787330f729Sjoerg 5797330f729Sjoerg 5807330f729Sjoergdef to_string(bytes_input): 5817330f729Sjoerg if isinstance(bytes_input, str): 5827330f729Sjoerg return bytes_input 5837330f729Sjoerg return bytes_input.encode('utf-8') 5847330f729Sjoerg 5857330f729Sjoerg 5867330f729Sjoergdef convert_string(bytes_input): 5877330f729Sjoerg try: 5887330f729Sjoerg return to_string(bytes_input.decode('utf-8')) 5897330f729Sjoerg except AttributeError: # 'str' object has no attribute 'decode'. 5907330f729Sjoerg return str(bytes_input) 5917330f729Sjoerg except UnicodeError: 5927330f729Sjoerg return str(bytes_input) 5937330f729Sjoerg 5947330f729Sjoergif __name__ == '__main__': 5957330f729Sjoerg main() 596