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