1e5dd7070Spatrick#!/usr/bin/env python 2e5dd7070Spatrick# 3e5dd7070Spatrick#===- git-clang-format - ClangFormat Git Integration ---------*- python -*--===# 4e5dd7070Spatrick# 5e5dd7070Spatrick# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. 6e5dd7070Spatrick# See https://llvm.org/LICENSE.txt for license information. 7e5dd7070Spatrick# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception 8e5dd7070Spatrick# 9e5dd7070Spatrick#===------------------------------------------------------------------------===# 10e5dd7070Spatrick 11e5dd7070Spatrickr""" 12e5dd7070Spatrickclang-format git integration 13e5dd7070Spatrick============================ 14e5dd7070Spatrick 15e5dd7070SpatrickThis file provides a clang-format integration for git. Put it somewhere in your 16e5dd7070Spatrickpath and ensure that it is executable. Then, "git clang-format" will invoke 17e5dd7070Spatrickclang-format on the changes in current files or a specific commit. 18e5dd7070Spatrick 19e5dd7070SpatrickFor further details, run: 20e5dd7070Spatrickgit clang-format -h 21e5dd7070Spatrick 22e5dd7070SpatrickRequires Python 2.7 or Python 3 23e5dd7070Spatrick""" 24e5dd7070Spatrick 25e5dd7070Spatrickfrom __future__ import absolute_import, division, print_function 26e5dd7070Spatrickimport argparse 27e5dd7070Spatrickimport collections 28e5dd7070Spatrickimport contextlib 29e5dd7070Spatrickimport errno 30e5dd7070Spatrickimport os 31e5dd7070Spatrickimport re 32e5dd7070Spatrickimport subprocess 33e5dd7070Spatrickimport sys 34e5dd7070Spatrick 35e5dd7070Spatrickusage = 'git clang-format [OPTIONS] [<commit>] [<commit>] [--] [<file>...]' 36e5dd7070Spatrick 37e5dd7070Spatrickdesc = ''' 38e5dd7070SpatrickIf zero or one commits are given, run clang-format on all lines that differ 39e5dd7070Spatrickbetween the working directory and <commit>, which defaults to HEAD. Changes are 40e5dd7070Spatrickonly applied to the working directory. 41e5dd7070Spatrick 42e5dd7070SpatrickIf two commits are given (requires --diff), run clang-format on all lines in the 43e5dd7070Spatricksecond <commit> that differ from the first <commit>. 44e5dd7070Spatrick 45e5dd7070SpatrickThe following git-config settings set the default of the corresponding option: 46e5dd7070Spatrick clangFormat.binary 47e5dd7070Spatrick clangFormat.commit 48ec727ea7Spatrick clangFormat.extensions 49e5dd7070Spatrick clangFormat.style 50e5dd7070Spatrick''' 51e5dd7070Spatrick 52e5dd7070Spatrick# Name of the temporary index file in which save the output of clang-format. 53e5dd7070Spatrick# This file is created within the .git directory. 54e5dd7070Spatricktemp_index_basename = 'clang-format-index' 55e5dd7070Spatrick 56e5dd7070Spatrick 57e5dd7070SpatrickRange = collections.namedtuple('Range', 'start, count') 58e5dd7070Spatrick 59e5dd7070Spatrick 60e5dd7070Spatrickdef main(): 61e5dd7070Spatrick config = load_git_config() 62e5dd7070Spatrick 63e5dd7070Spatrick # In order to keep '--' yet allow options after positionals, we need to 64e5dd7070Spatrick # check for '--' ourselves. (Setting nargs='*' throws away the '--', while 65e5dd7070Spatrick # nargs=argparse.REMAINDER disallows options after positionals.) 66e5dd7070Spatrick argv = sys.argv[1:] 67e5dd7070Spatrick try: 68e5dd7070Spatrick idx = argv.index('--') 69e5dd7070Spatrick except ValueError: 70e5dd7070Spatrick dash_dash = [] 71e5dd7070Spatrick else: 72e5dd7070Spatrick dash_dash = argv[idx:] 73e5dd7070Spatrick argv = argv[:idx] 74e5dd7070Spatrick 75e5dd7070Spatrick default_extensions = ','.join([ 76e5dd7070Spatrick # From clang/lib/Frontend/FrontendOptions.cpp, all lower case 77e5dd7070Spatrick 'c', 'h', # C 78e5dd7070Spatrick 'm', # ObjC 79e5dd7070Spatrick 'mm', # ObjC++ 80e5dd7070Spatrick 'cc', 'cp', 'cpp', 'c++', 'cxx', 'hh', 'hpp', 'hxx', # C++ 81*a9ac8606Spatrick 'cu', 'cuh', # CUDA 82e5dd7070Spatrick # Other languages that clang-format supports 83e5dd7070Spatrick 'proto', 'protodevel', # Protocol Buffers 84e5dd7070Spatrick 'java', # Java 85e5dd7070Spatrick 'js', # JavaScript 86e5dd7070Spatrick 'ts', # TypeScript 87e5dd7070Spatrick 'cs', # C Sharp 88*a9ac8606Spatrick 'json', # Json 89e5dd7070Spatrick ]) 90e5dd7070Spatrick 91e5dd7070Spatrick p = argparse.ArgumentParser( 92e5dd7070Spatrick usage=usage, formatter_class=argparse.RawDescriptionHelpFormatter, 93e5dd7070Spatrick description=desc) 94e5dd7070Spatrick p.add_argument('--binary', 95e5dd7070Spatrick default=config.get('clangformat.binary', 'clang-format'), 96e5dd7070Spatrick help='path to clang-format'), 97e5dd7070Spatrick p.add_argument('--commit', 98e5dd7070Spatrick default=config.get('clangformat.commit', 'HEAD'), 99e5dd7070Spatrick help='default commit to use if none is specified'), 100e5dd7070Spatrick p.add_argument('--diff', action='store_true', 101e5dd7070Spatrick help='print a diff instead of applying the changes') 102e5dd7070Spatrick p.add_argument('--extensions', 103e5dd7070Spatrick default=config.get('clangformat.extensions', 104e5dd7070Spatrick default_extensions), 105e5dd7070Spatrick help=('comma-separated list of file extensions to format, ' 106e5dd7070Spatrick 'excluding the period and case-insensitive')), 107e5dd7070Spatrick p.add_argument('-f', '--force', action='store_true', 108e5dd7070Spatrick help='allow changes to unstaged files') 109e5dd7070Spatrick p.add_argument('-p', '--patch', action='store_true', 110e5dd7070Spatrick help='select hunks interactively') 111e5dd7070Spatrick p.add_argument('-q', '--quiet', action='count', default=0, 112e5dd7070Spatrick help='print less information') 113e5dd7070Spatrick p.add_argument('--style', 114e5dd7070Spatrick default=config.get('clangformat.style', None), 115e5dd7070Spatrick help='passed to clang-format'), 116e5dd7070Spatrick p.add_argument('-v', '--verbose', action='count', default=0, 117e5dd7070Spatrick help='print extra information') 118e5dd7070Spatrick # We gather all the remaining positional arguments into 'args' since we need 119e5dd7070Spatrick # to use some heuristics to determine whether or not <commit> was present. 120e5dd7070Spatrick # However, to print pretty messages, we make use of metavar and help. 121e5dd7070Spatrick p.add_argument('args', nargs='*', metavar='<commit>', 122e5dd7070Spatrick help='revision from which to compute the diff') 123e5dd7070Spatrick p.add_argument('ignored', nargs='*', metavar='<file>...', 124e5dd7070Spatrick help='if specified, only consider differences in these files') 125e5dd7070Spatrick opts = p.parse_args(argv) 126e5dd7070Spatrick 127e5dd7070Spatrick opts.verbose -= opts.quiet 128e5dd7070Spatrick del opts.quiet 129e5dd7070Spatrick 130e5dd7070Spatrick commits, files = interpret_args(opts.args, dash_dash, opts.commit) 131e5dd7070Spatrick if len(commits) > 1: 132e5dd7070Spatrick if not opts.diff: 133e5dd7070Spatrick die('--diff is required when two commits are given') 134e5dd7070Spatrick else: 135e5dd7070Spatrick if len(commits) > 2: 136e5dd7070Spatrick die('at most two commits allowed; %d given' % len(commits)) 137e5dd7070Spatrick changed_lines = compute_diff_and_extract_lines(commits, files) 138e5dd7070Spatrick if opts.verbose >= 1: 139e5dd7070Spatrick ignored_files = set(changed_lines) 140e5dd7070Spatrick filter_by_extension(changed_lines, opts.extensions.lower().split(',')) 141*a9ac8606Spatrick # The computed diff outputs absolute paths, so we must cd before accessing 142*a9ac8606Spatrick # those files. 143*a9ac8606Spatrick cd_to_toplevel() 144*a9ac8606Spatrick filter_symlinks(changed_lines) 145e5dd7070Spatrick if opts.verbose >= 1: 146e5dd7070Spatrick ignored_files.difference_update(changed_lines) 147e5dd7070Spatrick if ignored_files: 148*a9ac8606Spatrick print( 149*a9ac8606Spatrick 'Ignoring changes in the following files (wrong extension or symlink):') 150e5dd7070Spatrick for filename in ignored_files: 151e5dd7070Spatrick print(' %s' % filename) 152e5dd7070Spatrick if changed_lines: 153e5dd7070Spatrick print('Running clang-format on the following files:') 154e5dd7070Spatrick for filename in changed_lines: 155e5dd7070Spatrick print(' %s' % filename) 156e5dd7070Spatrick if not changed_lines: 157*a9ac8606Spatrick if opts.verbose >= 0: 158e5dd7070Spatrick print('no modified files to format') 159e5dd7070Spatrick return 160e5dd7070Spatrick if len(commits) > 1: 161e5dd7070Spatrick old_tree = commits[1] 162e5dd7070Spatrick new_tree = run_clang_format_and_save_to_tree(changed_lines, 163e5dd7070Spatrick revision=commits[1], 164e5dd7070Spatrick binary=opts.binary, 165e5dd7070Spatrick style=opts.style) 166e5dd7070Spatrick else: 167e5dd7070Spatrick old_tree = create_tree_from_workdir(changed_lines) 168e5dd7070Spatrick new_tree = run_clang_format_and_save_to_tree(changed_lines, 169e5dd7070Spatrick binary=opts.binary, 170e5dd7070Spatrick style=opts.style) 171e5dd7070Spatrick if opts.verbose >= 1: 172e5dd7070Spatrick print('old tree: %s' % old_tree) 173e5dd7070Spatrick print('new tree: %s' % new_tree) 174e5dd7070Spatrick if old_tree == new_tree: 175e5dd7070Spatrick if opts.verbose >= 0: 176e5dd7070Spatrick print('clang-format did not modify any files') 177e5dd7070Spatrick elif opts.diff: 178e5dd7070Spatrick print_diff(old_tree, new_tree) 179e5dd7070Spatrick else: 180e5dd7070Spatrick changed_files = apply_changes(old_tree, new_tree, force=opts.force, 181e5dd7070Spatrick patch_mode=opts.patch) 182e5dd7070Spatrick if (opts.verbose >= 0 and not opts.patch) or opts.verbose >= 1: 183e5dd7070Spatrick print('changed files:') 184e5dd7070Spatrick for filename in changed_files: 185e5dd7070Spatrick print(' %s' % filename) 186e5dd7070Spatrick 187e5dd7070Spatrick 188e5dd7070Spatrickdef load_git_config(non_string_options=None): 189e5dd7070Spatrick """Return the git configuration as a dictionary. 190e5dd7070Spatrick 191e5dd7070Spatrick All options are assumed to be strings unless in `non_string_options`, in which 192e5dd7070Spatrick is a dictionary mapping option name (in lower case) to either "--bool" or 193e5dd7070Spatrick "--int".""" 194e5dd7070Spatrick if non_string_options is None: 195e5dd7070Spatrick non_string_options = {} 196e5dd7070Spatrick out = {} 197e5dd7070Spatrick for entry in run('git', 'config', '--list', '--null').split('\0'): 198e5dd7070Spatrick if entry: 199ec727ea7Spatrick if '\n' in entry: 200e5dd7070Spatrick name, value = entry.split('\n', 1) 201ec727ea7Spatrick else: 202ec727ea7Spatrick # A setting with no '=' ('\n' with --null) is implicitly 'true' 203ec727ea7Spatrick name = entry 204ec727ea7Spatrick value = 'true' 205e5dd7070Spatrick if name in non_string_options: 206e5dd7070Spatrick value = run('git', 'config', non_string_options[name], name) 207e5dd7070Spatrick out[name] = value 208e5dd7070Spatrick return out 209e5dd7070Spatrick 210e5dd7070Spatrick 211e5dd7070Spatrickdef interpret_args(args, dash_dash, default_commit): 212e5dd7070Spatrick """Interpret `args` as "[commits] [--] [files]" and return (commits, files). 213e5dd7070Spatrick 214e5dd7070Spatrick It is assumed that "--" and everything that follows has been removed from 215e5dd7070Spatrick args and placed in `dash_dash`. 216e5dd7070Spatrick 217e5dd7070Spatrick If "--" is present (i.e., `dash_dash` is non-empty), the arguments to its 218e5dd7070Spatrick left (if present) are taken as commits. Otherwise, the arguments are checked 219e5dd7070Spatrick from left to right if they are commits or files. If commits are not given, 220e5dd7070Spatrick a list with `default_commit` is used.""" 221e5dd7070Spatrick if dash_dash: 222e5dd7070Spatrick if len(args) == 0: 223e5dd7070Spatrick commits = [default_commit] 224e5dd7070Spatrick else: 225e5dd7070Spatrick commits = args 226e5dd7070Spatrick for commit in commits: 227e5dd7070Spatrick object_type = get_object_type(commit) 228e5dd7070Spatrick if object_type not in ('commit', 'tag'): 229e5dd7070Spatrick if object_type is None: 230e5dd7070Spatrick die("'%s' is not a commit" % commit) 231e5dd7070Spatrick else: 232e5dd7070Spatrick die("'%s' is a %s, but a commit was expected" % (commit, object_type)) 233e5dd7070Spatrick files = dash_dash[1:] 234e5dd7070Spatrick elif args: 235e5dd7070Spatrick commits = [] 236e5dd7070Spatrick while args: 237e5dd7070Spatrick if not disambiguate_revision(args[0]): 238e5dd7070Spatrick break 239e5dd7070Spatrick commits.append(args.pop(0)) 240e5dd7070Spatrick if not commits: 241e5dd7070Spatrick commits = [default_commit] 242e5dd7070Spatrick files = args 243e5dd7070Spatrick else: 244e5dd7070Spatrick commits = [default_commit] 245e5dd7070Spatrick files = [] 246e5dd7070Spatrick return commits, files 247e5dd7070Spatrick 248e5dd7070Spatrick 249e5dd7070Spatrickdef disambiguate_revision(value): 250e5dd7070Spatrick """Returns True if `value` is a revision, False if it is a file, or dies.""" 251e5dd7070Spatrick # If `value` is ambiguous (neither a commit nor a file), the following 252e5dd7070Spatrick # command will die with an appropriate error message. 253e5dd7070Spatrick run('git', 'rev-parse', value, verbose=False) 254e5dd7070Spatrick object_type = get_object_type(value) 255e5dd7070Spatrick if object_type is None: 256e5dd7070Spatrick return False 257e5dd7070Spatrick if object_type in ('commit', 'tag'): 258e5dd7070Spatrick return True 259e5dd7070Spatrick die('`%s` is a %s, but a commit or filename was expected' % 260e5dd7070Spatrick (value, object_type)) 261e5dd7070Spatrick 262e5dd7070Spatrick 263e5dd7070Spatrickdef get_object_type(value): 264e5dd7070Spatrick """Returns a string description of an object's type, or None if it is not 265e5dd7070Spatrick a valid git object.""" 266e5dd7070Spatrick cmd = ['git', 'cat-file', '-t', value] 267e5dd7070Spatrick p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 268e5dd7070Spatrick stdout, stderr = p.communicate() 269e5dd7070Spatrick if p.returncode != 0: 270e5dd7070Spatrick return None 271e5dd7070Spatrick return convert_string(stdout.strip()) 272e5dd7070Spatrick 273e5dd7070Spatrick 274e5dd7070Spatrickdef compute_diff_and_extract_lines(commits, files): 275e5dd7070Spatrick """Calls compute_diff() followed by extract_lines().""" 276e5dd7070Spatrick diff_process = compute_diff(commits, files) 277e5dd7070Spatrick changed_lines = extract_lines(diff_process.stdout) 278e5dd7070Spatrick diff_process.stdout.close() 279e5dd7070Spatrick diff_process.wait() 280e5dd7070Spatrick if diff_process.returncode != 0: 281e5dd7070Spatrick # Assume error was already printed to stderr. 282e5dd7070Spatrick sys.exit(2) 283e5dd7070Spatrick return changed_lines 284e5dd7070Spatrick 285e5dd7070Spatrick 286e5dd7070Spatrickdef compute_diff(commits, files): 287e5dd7070Spatrick """Return a subprocess object producing the diff from `commits`. 288e5dd7070Spatrick 289e5dd7070Spatrick The return value's `stdin` file object will produce a patch with the 290e5dd7070Spatrick differences between the working directory and the first commit if a single 291e5dd7070Spatrick one was specified, or the difference between both specified commits, filtered 292e5dd7070Spatrick on `files` (if non-empty). Zero context lines are used in the patch.""" 293e5dd7070Spatrick git_tool = 'diff-index' 294e5dd7070Spatrick if len(commits) > 1: 295e5dd7070Spatrick git_tool = 'diff-tree' 296e5dd7070Spatrick cmd = ['git', git_tool, '-p', '-U0'] + commits + ['--'] 297e5dd7070Spatrick cmd.extend(files) 298e5dd7070Spatrick p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE) 299e5dd7070Spatrick p.stdin.close() 300e5dd7070Spatrick return p 301e5dd7070Spatrick 302e5dd7070Spatrick 303e5dd7070Spatrickdef extract_lines(patch_file): 304e5dd7070Spatrick """Extract the changed lines in `patch_file`. 305e5dd7070Spatrick 306e5dd7070Spatrick The return value is a dictionary mapping filename to a list of (start_line, 307e5dd7070Spatrick line_count) pairs. 308e5dd7070Spatrick 309e5dd7070Spatrick The input must have been produced with ``-U0``, meaning unidiff format with 310e5dd7070Spatrick zero lines of context. The return value is a dict mapping filename to a 311e5dd7070Spatrick list of line `Range`s.""" 312e5dd7070Spatrick matches = {} 313e5dd7070Spatrick for line in patch_file: 314e5dd7070Spatrick line = convert_string(line) 315e5dd7070Spatrick match = re.search(r'^\+\+\+\ [^/]+/(.*)', line) 316e5dd7070Spatrick if match: 317e5dd7070Spatrick filename = match.group(1).rstrip('\r\n') 318e5dd7070Spatrick match = re.search(r'^@@ -[0-9,]+ \+(\d+)(,(\d+))?', line) 319e5dd7070Spatrick if match: 320e5dd7070Spatrick start_line = int(match.group(1)) 321e5dd7070Spatrick line_count = 1 322e5dd7070Spatrick if match.group(3): 323e5dd7070Spatrick line_count = int(match.group(3)) 324e5dd7070Spatrick if line_count > 0: 325e5dd7070Spatrick matches.setdefault(filename, []).append(Range(start_line, line_count)) 326e5dd7070Spatrick return matches 327e5dd7070Spatrick 328e5dd7070Spatrick 329e5dd7070Spatrickdef filter_by_extension(dictionary, allowed_extensions): 330e5dd7070Spatrick """Delete every key in `dictionary` that doesn't have an allowed extension. 331e5dd7070Spatrick 332e5dd7070Spatrick `allowed_extensions` must be a collection of lowercase file extensions, 333e5dd7070Spatrick excluding the period.""" 334e5dd7070Spatrick allowed_extensions = frozenset(allowed_extensions) 335e5dd7070Spatrick for filename in list(dictionary.keys()): 336e5dd7070Spatrick base_ext = filename.rsplit('.', 1) 337e5dd7070Spatrick if len(base_ext) == 1 and '' in allowed_extensions: 338e5dd7070Spatrick continue 339e5dd7070Spatrick if len(base_ext) == 1 or base_ext[1].lower() not in allowed_extensions: 340e5dd7070Spatrick del dictionary[filename] 341e5dd7070Spatrick 342e5dd7070Spatrick 343*a9ac8606Spatrickdef filter_symlinks(dictionary): 344*a9ac8606Spatrick """Delete every key in `dictionary` that is a symlink.""" 345*a9ac8606Spatrick for filename in list(dictionary.keys()): 346*a9ac8606Spatrick if os.path.islink(filename): 347*a9ac8606Spatrick del dictionary[filename] 348*a9ac8606Spatrick 349*a9ac8606Spatrick 350e5dd7070Spatrickdef cd_to_toplevel(): 351e5dd7070Spatrick """Change to the top level of the git repository.""" 352e5dd7070Spatrick toplevel = run('git', 'rev-parse', '--show-toplevel') 353e5dd7070Spatrick os.chdir(toplevel) 354e5dd7070Spatrick 355e5dd7070Spatrick 356e5dd7070Spatrickdef create_tree_from_workdir(filenames): 357e5dd7070Spatrick """Create a new git tree with the given files from the working directory. 358e5dd7070Spatrick 359e5dd7070Spatrick Returns the object ID (SHA-1) of the created tree.""" 360e5dd7070Spatrick return create_tree(filenames, '--stdin') 361e5dd7070Spatrick 362e5dd7070Spatrick 363e5dd7070Spatrickdef run_clang_format_and_save_to_tree(changed_lines, revision=None, 364e5dd7070Spatrick binary='clang-format', style=None): 365e5dd7070Spatrick """Run clang-format on each file and save the result to a git tree. 366e5dd7070Spatrick 367e5dd7070Spatrick Returns the object ID (SHA-1) of the created tree.""" 368e5dd7070Spatrick def iteritems(container): 369e5dd7070Spatrick try: 370e5dd7070Spatrick return container.iteritems() # Python 2 371e5dd7070Spatrick except AttributeError: 372e5dd7070Spatrick return container.items() # Python 3 373e5dd7070Spatrick def index_info_generator(): 374e5dd7070Spatrick for filename, line_ranges in iteritems(changed_lines): 375e5dd7070Spatrick if revision: 376e5dd7070Spatrick git_metadata_cmd = ['git', 'ls-tree', 377e5dd7070Spatrick '%s:%s' % (revision, os.path.dirname(filename)), 378e5dd7070Spatrick os.path.basename(filename)] 379e5dd7070Spatrick git_metadata = subprocess.Popen(git_metadata_cmd, stdin=subprocess.PIPE, 380e5dd7070Spatrick stdout=subprocess.PIPE) 381e5dd7070Spatrick stdout = git_metadata.communicate()[0] 382e5dd7070Spatrick mode = oct(int(stdout.split()[0], 8)) 383e5dd7070Spatrick else: 384e5dd7070Spatrick mode = oct(os.stat(filename).st_mode) 385e5dd7070Spatrick # Adjust python3 octal format so that it matches what git expects 386e5dd7070Spatrick if mode.startswith('0o'): 387e5dd7070Spatrick mode = '0' + mode[2:] 388e5dd7070Spatrick blob_id = clang_format_to_blob(filename, line_ranges, 389e5dd7070Spatrick revision=revision, 390e5dd7070Spatrick binary=binary, 391e5dd7070Spatrick style=style) 392e5dd7070Spatrick yield '%s %s\t%s' % (mode, blob_id, filename) 393e5dd7070Spatrick return create_tree(index_info_generator(), '--index-info') 394e5dd7070Spatrick 395e5dd7070Spatrick 396e5dd7070Spatrickdef create_tree(input_lines, mode): 397e5dd7070Spatrick """Create a tree object from the given input. 398e5dd7070Spatrick 399e5dd7070Spatrick If mode is '--stdin', it must be a list of filenames. If mode is 400e5dd7070Spatrick '--index-info' is must be a list of values suitable for "git update-index 401e5dd7070Spatrick --index-info", such as "<mode> <SP> <sha1> <TAB> <filename>". Any other mode 402e5dd7070Spatrick is invalid.""" 403e5dd7070Spatrick assert mode in ('--stdin', '--index-info') 404e5dd7070Spatrick cmd = ['git', 'update-index', '--add', '-z', mode] 405e5dd7070Spatrick with temporary_index_file(): 406e5dd7070Spatrick p = subprocess.Popen(cmd, stdin=subprocess.PIPE) 407e5dd7070Spatrick for line in input_lines: 408e5dd7070Spatrick p.stdin.write(to_bytes('%s\0' % line)) 409e5dd7070Spatrick p.stdin.close() 410e5dd7070Spatrick if p.wait() != 0: 411e5dd7070Spatrick die('`%s` failed' % ' '.join(cmd)) 412e5dd7070Spatrick tree_id = run('git', 'write-tree') 413e5dd7070Spatrick return tree_id 414e5dd7070Spatrick 415e5dd7070Spatrick 416e5dd7070Spatrickdef clang_format_to_blob(filename, line_ranges, revision=None, 417e5dd7070Spatrick binary='clang-format', style=None): 418e5dd7070Spatrick """Run clang-format on the given file and save the result to a git blob. 419e5dd7070Spatrick 420e5dd7070Spatrick Runs on the file in `revision` if not None, or on the file in the working 421e5dd7070Spatrick directory if `revision` is None. 422e5dd7070Spatrick 423e5dd7070Spatrick Returns the object ID (SHA-1) of the created blob.""" 424e5dd7070Spatrick clang_format_cmd = [binary] 425e5dd7070Spatrick if style: 426e5dd7070Spatrick clang_format_cmd.extend(['-style='+style]) 427e5dd7070Spatrick clang_format_cmd.extend([ 428e5dd7070Spatrick '-lines=%s:%s' % (start_line, start_line+line_count-1) 429e5dd7070Spatrick for start_line, line_count in line_ranges]) 430e5dd7070Spatrick if revision: 431e5dd7070Spatrick clang_format_cmd.extend(['-assume-filename='+filename]) 432e5dd7070Spatrick git_show_cmd = ['git', 'cat-file', 'blob', '%s:%s' % (revision, filename)] 433e5dd7070Spatrick git_show = subprocess.Popen(git_show_cmd, stdin=subprocess.PIPE, 434e5dd7070Spatrick stdout=subprocess.PIPE) 435e5dd7070Spatrick git_show.stdin.close() 436e5dd7070Spatrick clang_format_stdin = git_show.stdout 437e5dd7070Spatrick else: 438e5dd7070Spatrick clang_format_cmd.extend([filename]) 439e5dd7070Spatrick git_show = None 440e5dd7070Spatrick clang_format_stdin = subprocess.PIPE 441e5dd7070Spatrick try: 442e5dd7070Spatrick clang_format = subprocess.Popen(clang_format_cmd, stdin=clang_format_stdin, 443e5dd7070Spatrick stdout=subprocess.PIPE) 444e5dd7070Spatrick if clang_format_stdin == subprocess.PIPE: 445e5dd7070Spatrick clang_format_stdin = clang_format.stdin 446e5dd7070Spatrick except OSError as e: 447e5dd7070Spatrick if e.errno == errno.ENOENT: 448e5dd7070Spatrick die('cannot find executable "%s"' % binary) 449e5dd7070Spatrick else: 450e5dd7070Spatrick raise 451e5dd7070Spatrick clang_format_stdin.close() 452e5dd7070Spatrick hash_object_cmd = ['git', 'hash-object', '-w', '--path='+filename, '--stdin'] 453e5dd7070Spatrick hash_object = subprocess.Popen(hash_object_cmd, stdin=clang_format.stdout, 454e5dd7070Spatrick stdout=subprocess.PIPE) 455e5dd7070Spatrick clang_format.stdout.close() 456e5dd7070Spatrick stdout = hash_object.communicate()[0] 457e5dd7070Spatrick if hash_object.returncode != 0: 458e5dd7070Spatrick die('`%s` failed' % ' '.join(hash_object_cmd)) 459e5dd7070Spatrick if clang_format.wait() != 0: 460e5dd7070Spatrick die('`%s` failed' % ' '.join(clang_format_cmd)) 461e5dd7070Spatrick if git_show and git_show.wait() != 0: 462e5dd7070Spatrick die('`%s` failed' % ' '.join(git_show_cmd)) 463e5dd7070Spatrick return convert_string(stdout).rstrip('\r\n') 464e5dd7070Spatrick 465e5dd7070Spatrick 466e5dd7070Spatrick@contextlib.contextmanager 467e5dd7070Spatrickdef temporary_index_file(tree=None): 468e5dd7070Spatrick """Context manager for setting GIT_INDEX_FILE to a temporary file and deleting 469e5dd7070Spatrick the file afterward.""" 470e5dd7070Spatrick index_path = create_temporary_index(tree) 471e5dd7070Spatrick old_index_path = os.environ.get('GIT_INDEX_FILE') 472e5dd7070Spatrick os.environ['GIT_INDEX_FILE'] = index_path 473e5dd7070Spatrick try: 474e5dd7070Spatrick yield 475e5dd7070Spatrick finally: 476e5dd7070Spatrick if old_index_path is None: 477e5dd7070Spatrick del os.environ['GIT_INDEX_FILE'] 478e5dd7070Spatrick else: 479e5dd7070Spatrick os.environ['GIT_INDEX_FILE'] = old_index_path 480e5dd7070Spatrick os.remove(index_path) 481e5dd7070Spatrick 482e5dd7070Spatrick 483e5dd7070Spatrickdef create_temporary_index(tree=None): 484e5dd7070Spatrick """Create a temporary index file and return the created file's path. 485e5dd7070Spatrick 486e5dd7070Spatrick If `tree` is not None, use that as the tree to read in. Otherwise, an 487e5dd7070Spatrick empty index is created.""" 488e5dd7070Spatrick gitdir = run('git', 'rev-parse', '--git-dir') 489e5dd7070Spatrick path = os.path.join(gitdir, temp_index_basename) 490e5dd7070Spatrick if tree is None: 491e5dd7070Spatrick tree = '--empty' 492e5dd7070Spatrick run('git', 'read-tree', '--index-output='+path, tree) 493e5dd7070Spatrick return path 494e5dd7070Spatrick 495e5dd7070Spatrick 496e5dd7070Spatrickdef print_diff(old_tree, new_tree): 497e5dd7070Spatrick """Print the diff between the two trees to stdout.""" 498e5dd7070Spatrick # We use the porcelain 'diff' and not plumbing 'diff-tree' because the output 499e5dd7070Spatrick # is expected to be viewed by the user, and only the former does nice things 500e5dd7070Spatrick # like color and pagination. 501e5dd7070Spatrick # 502e5dd7070Spatrick # We also only print modified files since `new_tree` only contains the files 503e5dd7070Spatrick # that were modified, so unmodified files would show as deleted without the 504e5dd7070Spatrick # filter. 505e5dd7070Spatrick subprocess.check_call(['git', 'diff', '--diff-filter=M', old_tree, new_tree, 506e5dd7070Spatrick '--']) 507e5dd7070Spatrick 508e5dd7070Spatrick 509e5dd7070Spatrickdef apply_changes(old_tree, new_tree, force=False, patch_mode=False): 510e5dd7070Spatrick """Apply the changes in `new_tree` to the working directory. 511e5dd7070Spatrick 512e5dd7070Spatrick Bails if there are local changes in those files and not `force`. If 513e5dd7070Spatrick `patch_mode`, runs `git checkout --patch` to select hunks interactively.""" 514e5dd7070Spatrick changed_files = run('git', 'diff-tree', '--diff-filter=M', '-r', '-z', 515e5dd7070Spatrick '--name-only', old_tree, 516e5dd7070Spatrick new_tree).rstrip('\0').split('\0') 517e5dd7070Spatrick if not force: 518e5dd7070Spatrick unstaged_files = run('git', 'diff-files', '--name-status', *changed_files) 519e5dd7070Spatrick if unstaged_files: 520e5dd7070Spatrick print('The following files would be modified but ' 521e5dd7070Spatrick 'have unstaged changes:', file=sys.stderr) 522e5dd7070Spatrick print(unstaged_files, file=sys.stderr) 523e5dd7070Spatrick print('Please commit, stage, or stash them first.', file=sys.stderr) 524e5dd7070Spatrick sys.exit(2) 525e5dd7070Spatrick if patch_mode: 526e5dd7070Spatrick # In patch mode, we could just as well create an index from the new tree 527e5dd7070Spatrick # and checkout from that, but then the user will be presented with a 528e5dd7070Spatrick # message saying "Discard ... from worktree". Instead, we use the old 529e5dd7070Spatrick # tree as the index and checkout from new_tree, which gives the slightly 530e5dd7070Spatrick # better message, "Apply ... to index and worktree". This is not quite 531e5dd7070Spatrick # right, since it won't be applied to the user's index, but oh well. 532e5dd7070Spatrick with temporary_index_file(old_tree): 533e5dd7070Spatrick subprocess.check_call(['git', 'checkout', '--patch', new_tree]) 534e5dd7070Spatrick index_tree = old_tree 535e5dd7070Spatrick else: 536e5dd7070Spatrick with temporary_index_file(new_tree): 537e5dd7070Spatrick run('git', 'checkout-index', '-a', '-f') 538e5dd7070Spatrick return changed_files 539e5dd7070Spatrick 540e5dd7070Spatrick 541e5dd7070Spatrickdef run(*args, **kwargs): 542e5dd7070Spatrick stdin = kwargs.pop('stdin', '') 543e5dd7070Spatrick verbose = kwargs.pop('verbose', True) 544e5dd7070Spatrick strip = kwargs.pop('strip', True) 545e5dd7070Spatrick for name in kwargs: 546e5dd7070Spatrick raise TypeError("run() got an unexpected keyword argument '%s'" % name) 547e5dd7070Spatrick p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, 548e5dd7070Spatrick stdin=subprocess.PIPE) 549e5dd7070Spatrick stdout, stderr = p.communicate(input=stdin) 550e5dd7070Spatrick 551e5dd7070Spatrick stdout = convert_string(stdout) 552e5dd7070Spatrick stderr = convert_string(stderr) 553e5dd7070Spatrick 554e5dd7070Spatrick if p.returncode == 0: 555e5dd7070Spatrick if stderr: 556e5dd7070Spatrick if verbose: 557e5dd7070Spatrick print('`%s` printed to stderr:' % ' '.join(args), file=sys.stderr) 558e5dd7070Spatrick print(stderr.rstrip(), file=sys.stderr) 559e5dd7070Spatrick if strip: 560e5dd7070Spatrick stdout = stdout.rstrip('\r\n') 561e5dd7070Spatrick return stdout 562e5dd7070Spatrick if verbose: 563e5dd7070Spatrick print('`%s` returned %s' % (' '.join(args), p.returncode), file=sys.stderr) 564e5dd7070Spatrick if stderr: 565e5dd7070Spatrick print(stderr.rstrip(), file=sys.stderr) 566e5dd7070Spatrick sys.exit(2) 567e5dd7070Spatrick 568e5dd7070Spatrick 569e5dd7070Spatrickdef die(message): 570e5dd7070Spatrick print('error:', message, file=sys.stderr) 571e5dd7070Spatrick sys.exit(2) 572e5dd7070Spatrick 573e5dd7070Spatrick 574e5dd7070Spatrickdef to_bytes(str_input): 575e5dd7070Spatrick # Encode to UTF-8 to get binary data. 576e5dd7070Spatrick if isinstance(str_input, bytes): 577e5dd7070Spatrick return str_input 578e5dd7070Spatrick return str_input.encode('utf-8') 579e5dd7070Spatrick 580e5dd7070Spatrick 581e5dd7070Spatrickdef to_string(bytes_input): 582e5dd7070Spatrick if isinstance(bytes_input, str): 583e5dd7070Spatrick return bytes_input 584e5dd7070Spatrick return bytes_input.encode('utf-8') 585e5dd7070Spatrick 586e5dd7070Spatrick 587e5dd7070Spatrickdef convert_string(bytes_input): 588e5dd7070Spatrick try: 589e5dd7070Spatrick return to_string(bytes_input.decode('utf-8')) 590e5dd7070Spatrick except AttributeError: # 'str' object has no attribute 'decode'. 591e5dd7070Spatrick return str(bytes_input) 592e5dd7070Spatrick except UnicodeError: 593e5dd7070Spatrick return str(bytes_input) 594e5dd7070Spatrick 595e5dd7070Spatrickif __name__ == '__main__': 596e5dd7070Spatrick main() 597