12c593315Sopenharmony_ci#!/usr/bin/env python 22c593315Sopenharmony_ci# 32c593315Sopenharmony_ci#===- git-clang-format - ClangFormat Git Integration ---------*- python -*--===# 42c593315Sopenharmony_ci# 52c593315Sopenharmony_ci# The LLVM Compiler Infrastructure 62c593315Sopenharmony_ci# 72c593315Sopenharmony_ci# This file is distributed under the University of Illinois Open Source 82c593315Sopenharmony_ci# License. See LICENSE.TXT for details. 92c593315Sopenharmony_ci# 102c593315Sopenharmony_ci#===------------------------------------------------------------------------===# 112c593315Sopenharmony_ci 122c593315Sopenharmony_cir""" 132c593315Sopenharmony_ciclang-format git integration 142c593315Sopenharmony_ci============================ 152c593315Sopenharmony_ci 162c593315Sopenharmony_ciThis file provides a clang-format integration for git. Put it somewhere in your 172c593315Sopenharmony_cipath and ensure that it is executable. Then, "git clang-format" will invoke 182c593315Sopenharmony_ciclang-format on the changes in current files or a specific commit. 192c593315Sopenharmony_ci 202c593315Sopenharmony_ciFor further details, run: 212c593315Sopenharmony_cigit clang-format -h 222c593315Sopenharmony_ci 232c593315Sopenharmony_ciRequires Python 2.7 242c593315Sopenharmony_ci""" 252c593315Sopenharmony_ci 262c593315Sopenharmony_ciimport argparse 272c593315Sopenharmony_ciimport collections 282c593315Sopenharmony_ciimport contextlib 292c593315Sopenharmony_ciimport errno 302c593315Sopenharmony_ciimport os 312c593315Sopenharmony_ciimport re 322c593315Sopenharmony_ciimport subprocess 332c593315Sopenharmony_ciimport sys 342c593315Sopenharmony_ci 352c593315Sopenharmony_ciusage = 'git clang-format [OPTIONS] [<commit>] [--] [<file>...]' 362c593315Sopenharmony_ci 372c593315Sopenharmony_cidesc = ''' 382c593315Sopenharmony_ciRun clang-format on all lines that differ between the working directory 392c593315Sopenharmony_ciand <commit>, which defaults to HEAD. Changes are only applied to the working 402c593315Sopenharmony_cidirectory. 412c593315Sopenharmony_ci 422c593315Sopenharmony_ciThe following git-config settings set the default of the corresponding option: 432c593315Sopenharmony_ci clangFormat.binary 442c593315Sopenharmony_ci clangFormat.commit 452c593315Sopenharmony_ci clangFormat.extension 462c593315Sopenharmony_ci clangFormat.style 472c593315Sopenharmony_ci''' 482c593315Sopenharmony_ci 492c593315Sopenharmony_ci# Name of the temporary index file in which save the output of clang-format. 502c593315Sopenharmony_ci# This file is created within the .git directory. 512c593315Sopenharmony_citemp_index_basename = 'clang-format-index' 522c593315Sopenharmony_ci 532c593315Sopenharmony_ci 542c593315Sopenharmony_ciRange = collections.namedtuple('Range', 'start, count') 552c593315Sopenharmony_ci 562c593315Sopenharmony_ci 572c593315Sopenharmony_cidef main(): 582c593315Sopenharmony_ci config = load_git_config() 592c593315Sopenharmony_ci 602c593315Sopenharmony_ci # In order to keep '--' yet allow options after positionals, we need to 612c593315Sopenharmony_ci # check for '--' ourselves. (Setting nargs='*' throws away the '--', while 622c593315Sopenharmony_ci # nargs=argparse.REMAINDER disallows options after positionals.) 632c593315Sopenharmony_ci argv = sys.argv[1:] 642c593315Sopenharmony_ci try: 652c593315Sopenharmony_ci idx = argv.index('--') 662c593315Sopenharmony_ci except ValueError: 672c593315Sopenharmony_ci dash_dash = [] 682c593315Sopenharmony_ci else: 692c593315Sopenharmony_ci dash_dash = argv[idx:] 702c593315Sopenharmony_ci argv = argv[:idx] 712c593315Sopenharmony_ci 722c593315Sopenharmony_ci default_extensions = ','.join([ 732c593315Sopenharmony_ci # From clang/lib/Frontend/FrontendOptions.cpp, all lower case 742c593315Sopenharmony_ci 'c', 'h', # C 752c593315Sopenharmony_ci 'm', # ObjC 762c593315Sopenharmony_ci 'mm', # ObjC++ 772c593315Sopenharmony_ci 'cc', 'cp', 'cpp', 'c++', 'cxx', 'hpp', # C++ 782c593315Sopenharmony_ci # Other languages that clang-format supports 792c593315Sopenharmony_ci 'proto', 'protodevel', # Protocol Buffers 802c593315Sopenharmony_ci 'js', # JavaScript 812c593315Sopenharmony_ci ]) 822c593315Sopenharmony_ci 832c593315Sopenharmony_ci p = argparse.ArgumentParser( 842c593315Sopenharmony_ci usage=usage, formatter_class=argparse.RawDescriptionHelpFormatter, 852c593315Sopenharmony_ci description=desc) 862c593315Sopenharmony_ci p.add_argument('--binary', 872c593315Sopenharmony_ci default=config.get('clangformat.binary', 'clang-format'), 882c593315Sopenharmony_ci help='path to clang-format'), 892c593315Sopenharmony_ci p.add_argument('--commit', 902c593315Sopenharmony_ci default=config.get('clangformat.commit', 'HEAD'), 912c593315Sopenharmony_ci help='default commit to use if none is specified'), 922c593315Sopenharmony_ci p.add_argument('--diff', action='store_true', 932c593315Sopenharmony_ci help='print a diff instead of applying the changes') 942c593315Sopenharmony_ci p.add_argument('--extensions', 952c593315Sopenharmony_ci default=config.get('clangformat.extensions', 962c593315Sopenharmony_ci default_extensions), 972c593315Sopenharmony_ci help=('comma-separated list of file extensions to format, ' 982c593315Sopenharmony_ci 'excluding the period and case-insensitive')), 992c593315Sopenharmony_ci p.add_argument('-f', '--force', action='store_true', 1002c593315Sopenharmony_ci help='allow changes to unstaged files') 1012c593315Sopenharmony_ci p.add_argument('-p', '--patch', action='store_true', 1022c593315Sopenharmony_ci help='select hunks interactively') 1032c593315Sopenharmony_ci p.add_argument('-q', '--quiet', action='count', default=0, 1042c593315Sopenharmony_ci help='print less information') 1052c593315Sopenharmony_ci p.add_argument('--style', 1062c593315Sopenharmony_ci default=config.get('clangformat.style', None), 1072c593315Sopenharmony_ci help='passed to clang-format'), 1082c593315Sopenharmony_ci p.add_argument('-v', '--verbose', action='count', default=0, 1092c593315Sopenharmony_ci help='print extra information') 1102c593315Sopenharmony_ci # We gather all the remaining positional arguments into 'args' since we need 1112c593315Sopenharmony_ci # to use some heuristics to determine whether or not <commit> was present. 1122c593315Sopenharmony_ci # However, to print pretty messages, we make use of metavar and help. 1132c593315Sopenharmony_ci p.add_argument('args', nargs='*', metavar='<commit>', 1142c593315Sopenharmony_ci help='revision from which to compute the diff') 1152c593315Sopenharmony_ci p.add_argument('ignored', nargs='*', metavar='<file>...', 1162c593315Sopenharmony_ci help='if specified, only consider differences in these files') 1172c593315Sopenharmony_ci opts = p.parse_args(argv) 1182c593315Sopenharmony_ci 1192c593315Sopenharmony_ci opts.verbose -= opts.quiet 1202c593315Sopenharmony_ci del opts.quiet 1212c593315Sopenharmony_ci 1222c593315Sopenharmony_ci commit, files = interpret_args(opts.args, dash_dash, opts.commit) 1232c593315Sopenharmony_ci changed_lines = compute_diff_and_extract_lines(commit, files) 1242c593315Sopenharmony_ci if opts.verbose >= 1: 1252c593315Sopenharmony_ci ignored_files = set(changed_lines) 1262c593315Sopenharmony_ci filter_by_extension(changed_lines, opts.extensions.lower().split(',')) 1272c593315Sopenharmony_ci if opts.verbose >= 1: 1282c593315Sopenharmony_ci ignored_files.difference_update(changed_lines) 1292c593315Sopenharmony_ci if ignored_files: 1302c593315Sopenharmony_ci print 'Ignoring changes in the following files (wrong extension):' 1312c593315Sopenharmony_ci for filename in ignored_files: 1322c593315Sopenharmony_ci print ' ', filename 1332c593315Sopenharmony_ci if changed_lines: 1342c593315Sopenharmony_ci print 'Running clang-format on the following files:' 1352c593315Sopenharmony_ci for filename in changed_lines: 1362c593315Sopenharmony_ci print ' ', filename 1372c593315Sopenharmony_ci if not changed_lines: 1382c593315Sopenharmony_ci print 'no modified files to format' 1392c593315Sopenharmony_ci return 1402c593315Sopenharmony_ci # The computed diff outputs absolute paths, so we must cd before accessing 1412c593315Sopenharmony_ci # those files. 1422c593315Sopenharmony_ci cd_to_toplevel() 1432c593315Sopenharmony_ci old_tree = create_tree_from_workdir(changed_lines) 1442c593315Sopenharmony_ci new_tree = run_clang_format_and_save_to_tree(changed_lines, 1452c593315Sopenharmony_ci binary=opts.binary, 1462c593315Sopenharmony_ci style=opts.style) 1472c593315Sopenharmony_ci if opts.verbose >= 1: 1482c593315Sopenharmony_ci print 'old tree:', old_tree 1492c593315Sopenharmony_ci print 'new tree:', new_tree 1502c593315Sopenharmony_ci if old_tree == new_tree: 1512c593315Sopenharmony_ci if opts.verbose >= 0: 1522c593315Sopenharmony_ci print 'clang-format did not modify any files' 1532c593315Sopenharmony_ci elif opts.diff: 1542c593315Sopenharmony_ci print_diff(old_tree, new_tree) 1552c593315Sopenharmony_ci else: 1562c593315Sopenharmony_ci changed_files = apply_changes(old_tree, new_tree, force=opts.force, 1572c593315Sopenharmony_ci patch_mode=opts.patch) 1582c593315Sopenharmony_ci if (opts.verbose >= 0 and not opts.patch) or opts.verbose >= 1: 1592c593315Sopenharmony_ci print 'changed files:' 1602c593315Sopenharmony_ci for filename in changed_files: 1612c593315Sopenharmony_ci print ' ', filename 1622c593315Sopenharmony_ci 1632c593315Sopenharmony_ci 1642c593315Sopenharmony_cidef load_git_config(non_string_options=None): 1652c593315Sopenharmony_ci """Return the git configuration as a dictionary. 1662c593315Sopenharmony_ci 1672c593315Sopenharmony_ci All options are assumed to be strings unless in `non_string_options`, in which 1682c593315Sopenharmony_ci is a dictionary mapping option name (in lower case) to either "--bool" or 1692c593315Sopenharmony_ci "--int".""" 1702c593315Sopenharmony_ci if non_string_options is None: 1712c593315Sopenharmony_ci non_string_options = {} 1722c593315Sopenharmony_ci out = {} 1732c593315Sopenharmony_ci for entry in run('git', 'config', '--list', '--null').split('\0'): 1742c593315Sopenharmony_ci if entry: 1752c593315Sopenharmony_ci name, value = entry.split('\n', 1) 1762c593315Sopenharmony_ci if name in non_string_options: 1772c593315Sopenharmony_ci value = run('git', 'config', non_string_options[name], name) 1782c593315Sopenharmony_ci out[name] = value 1792c593315Sopenharmony_ci return out 1802c593315Sopenharmony_ci 1812c593315Sopenharmony_ci 1822c593315Sopenharmony_cidef interpret_args(args, dash_dash, default_commit): 1832c593315Sopenharmony_ci """Interpret `args` as "[commit] [--] [files...]" and return (commit, files). 1842c593315Sopenharmony_ci 1852c593315Sopenharmony_ci It is assumed that "--" and everything that follows has been removed from 1862c593315Sopenharmony_ci args and placed in `dash_dash`. 1872c593315Sopenharmony_ci 1882c593315Sopenharmony_ci If "--" is present (i.e., `dash_dash` is non-empty), the argument to its 1892c593315Sopenharmony_ci left (if present) is taken as commit. Otherwise, the first argument is 1902c593315Sopenharmony_ci checked if it is a commit or a file. If commit is not given, 1912c593315Sopenharmony_ci `default_commit` is used.""" 1922c593315Sopenharmony_ci if dash_dash: 1932c593315Sopenharmony_ci if len(args) == 0: 1942c593315Sopenharmony_ci commit = default_commit 1952c593315Sopenharmony_ci elif len(args) > 1: 1962c593315Sopenharmony_ci die('at most one commit allowed; %d given' % len(args)) 1972c593315Sopenharmony_ci else: 1982c593315Sopenharmony_ci commit = args[0] 1992c593315Sopenharmony_ci object_type = get_object_type(commit) 2002c593315Sopenharmony_ci if object_type not in ('commit', 'tag'): 2012c593315Sopenharmony_ci if object_type is None: 2022c593315Sopenharmony_ci die("'%s' is not a commit" % commit) 2032c593315Sopenharmony_ci else: 2042c593315Sopenharmony_ci die("'%s' is a %s, but a commit was expected" % (commit, object_type)) 2052c593315Sopenharmony_ci files = dash_dash[1:] 2062c593315Sopenharmony_ci elif args: 2072c593315Sopenharmony_ci if disambiguate_revision(args[0]): 2082c593315Sopenharmony_ci commit = args[0] 2092c593315Sopenharmony_ci files = args[1:] 2102c593315Sopenharmony_ci else: 2112c593315Sopenharmony_ci commit = default_commit 2122c593315Sopenharmony_ci files = args 2132c593315Sopenharmony_ci else: 2142c593315Sopenharmony_ci commit = default_commit 2152c593315Sopenharmony_ci files = [] 2162c593315Sopenharmony_ci return commit, files 2172c593315Sopenharmony_ci 2182c593315Sopenharmony_ci 2192c593315Sopenharmony_cidef disambiguate_revision(value): 2202c593315Sopenharmony_ci """Returns True if `value` is a revision, False if it is a file, or dies.""" 2212c593315Sopenharmony_ci # If `value` is ambiguous (neither a commit nor a file), the following 2222c593315Sopenharmony_ci # command will die with an appropriate error message. 2232c593315Sopenharmony_ci run('git', 'rev-parse', value, verbose=False) 2242c593315Sopenharmony_ci object_type = get_object_type(value) 2252c593315Sopenharmony_ci if object_type is None: 2262c593315Sopenharmony_ci return False 2272c593315Sopenharmony_ci if object_type in ('commit', 'tag'): 2282c593315Sopenharmony_ci return True 2292c593315Sopenharmony_ci die('`%s` is a %s, but a commit or filename was expected' % 2302c593315Sopenharmony_ci (value, object_type)) 2312c593315Sopenharmony_ci 2322c593315Sopenharmony_ci 2332c593315Sopenharmony_cidef get_object_type(value): 2342c593315Sopenharmony_ci """Returns a string description of an object's type, or None if it is not 2352c593315Sopenharmony_ci a valid git object.""" 2362c593315Sopenharmony_ci cmd = ['git', 'cat-file', '-t', value] 2372c593315Sopenharmony_ci p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 2382c593315Sopenharmony_ci stdout, stderr = p.communicate() 2392c593315Sopenharmony_ci if p.returncode != 0: 2402c593315Sopenharmony_ci return None 2412c593315Sopenharmony_ci return stdout.strip() 2422c593315Sopenharmony_ci 2432c593315Sopenharmony_ci 2442c593315Sopenharmony_cidef compute_diff_and_extract_lines(commit, files): 2452c593315Sopenharmony_ci """Calls compute_diff() followed by extract_lines().""" 2462c593315Sopenharmony_ci diff_process = compute_diff(commit, files) 2472c593315Sopenharmony_ci changed_lines = extract_lines(diff_process.stdout) 2482c593315Sopenharmony_ci diff_process.stdout.close() 2492c593315Sopenharmony_ci diff_process.wait() 2502c593315Sopenharmony_ci if diff_process.returncode != 0: 2512c593315Sopenharmony_ci # Assume error was already printed to stderr. 2522c593315Sopenharmony_ci sys.exit(2) 2532c593315Sopenharmony_ci return changed_lines 2542c593315Sopenharmony_ci 2552c593315Sopenharmony_ci 2562c593315Sopenharmony_cidef compute_diff(commit, files): 2572c593315Sopenharmony_ci """Return a subprocess object producing the diff from `commit`. 2582c593315Sopenharmony_ci 2592c593315Sopenharmony_ci The return value's `stdin` file object will produce a patch with the 2602c593315Sopenharmony_ci differences between the working directory and `commit`, filtered on `files` 2612c593315Sopenharmony_ci (if non-empty). Zero context lines are used in the patch.""" 2622c593315Sopenharmony_ci cmd = ['git', 'diff-index', '-p', '-U0', commit, '--'] 2632c593315Sopenharmony_ci cmd.extend(files) 2642c593315Sopenharmony_ci p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE) 2652c593315Sopenharmony_ci p.stdin.close() 2662c593315Sopenharmony_ci return p 2672c593315Sopenharmony_ci 2682c593315Sopenharmony_ci 2692c593315Sopenharmony_cidef extract_lines(patch_file): 2702c593315Sopenharmony_ci """Extract the changed lines in `patch_file`. 2712c593315Sopenharmony_ci 2722c593315Sopenharmony_ci The return value is a dictionary mapping filename to a list of (start_line, 2732c593315Sopenharmony_ci line_count) pairs. 2742c593315Sopenharmony_ci 2752c593315Sopenharmony_ci The input must have been produced with ``-U0``, meaning unidiff format with 2762c593315Sopenharmony_ci zero lines of context. The return value is a dict mapping filename to a 2772c593315Sopenharmony_ci list of line `Range`s.""" 2782c593315Sopenharmony_ci matches = {} 2792c593315Sopenharmony_ci for line in patch_file: 2802c593315Sopenharmony_ci match = re.search(r'^\+\+\+\ [^/]+/(.*)', line) 2812c593315Sopenharmony_ci if match: 2822c593315Sopenharmony_ci filename = match.group(1).rstrip('\r\n') 2832c593315Sopenharmony_ci match = re.search(r'^@@ -[0-9,]+ \+(\d+)(,(\d+))?', line) 2842c593315Sopenharmony_ci if match: 2852c593315Sopenharmony_ci start_line = int(match.group(1)) 2862c593315Sopenharmony_ci line_count = 1 2872c593315Sopenharmony_ci if match.group(3): 2882c593315Sopenharmony_ci line_count = int(match.group(3)) 2892c593315Sopenharmony_ci if line_count > 0: 2902c593315Sopenharmony_ci matches.setdefault(filename, []).append(Range(start_line, line_count)) 2912c593315Sopenharmony_ci return matches 2922c593315Sopenharmony_ci 2932c593315Sopenharmony_ci 2942c593315Sopenharmony_cidef filter_by_extension(dictionary, allowed_extensions): 2952c593315Sopenharmony_ci """Delete every key in `dictionary` that doesn't have an allowed extension. 2962c593315Sopenharmony_ci 2972c593315Sopenharmony_ci `allowed_extensions` must be a collection of lowercase file extensions, 2982c593315Sopenharmony_ci excluding the period.""" 2992c593315Sopenharmony_ci allowed_extensions = frozenset(allowed_extensions) 3002c593315Sopenharmony_ci for filename in dictionary.keys(): 3012c593315Sopenharmony_ci base_ext = filename.rsplit('.', 1) 3022c593315Sopenharmony_ci if len(base_ext) == 1 or base_ext[1].lower() not in allowed_extensions: 3032c593315Sopenharmony_ci del dictionary[filename] 3042c593315Sopenharmony_ci 3052c593315Sopenharmony_ci 3062c593315Sopenharmony_cidef cd_to_toplevel(): 3072c593315Sopenharmony_ci """Change to the top level of the git repository.""" 3082c593315Sopenharmony_ci toplevel = run('git', 'rev-parse', '--show-toplevel') 3092c593315Sopenharmony_ci os.chdir(toplevel) 3102c593315Sopenharmony_ci 3112c593315Sopenharmony_ci 3122c593315Sopenharmony_cidef create_tree_from_workdir(filenames): 3132c593315Sopenharmony_ci """Create a new git tree with the given files from the working directory. 3142c593315Sopenharmony_ci 3152c593315Sopenharmony_ci Returns the object ID (SHA-1) of the created tree.""" 3162c593315Sopenharmony_ci return create_tree(filenames, '--stdin') 3172c593315Sopenharmony_ci 3182c593315Sopenharmony_ci 3192c593315Sopenharmony_cidef run_clang_format_and_save_to_tree(changed_lines, binary='clang-format', 3202c593315Sopenharmony_ci style=None): 3212c593315Sopenharmony_ci """Run clang-format on each file and save the result to a git tree. 3222c593315Sopenharmony_ci 3232c593315Sopenharmony_ci Returns the object ID (SHA-1) of the created tree.""" 3242c593315Sopenharmony_ci def index_info_generator(): 3252c593315Sopenharmony_ci for filename, line_ranges in changed_lines.iteritems(): 3262c593315Sopenharmony_ci mode = oct(os.stat(filename).st_mode) 3272c593315Sopenharmony_ci blob_id = clang_format_to_blob(filename, line_ranges, binary=binary, 3282c593315Sopenharmony_ci style=style) 3292c593315Sopenharmony_ci yield '%s %s\t%s' % (mode, blob_id, filename) 3302c593315Sopenharmony_ci return create_tree(index_info_generator(), '--index-info') 3312c593315Sopenharmony_ci 3322c593315Sopenharmony_ci 3332c593315Sopenharmony_cidef create_tree(input_lines, mode): 3342c593315Sopenharmony_ci """Create a tree object from the given input. 3352c593315Sopenharmony_ci 3362c593315Sopenharmony_ci If mode is '--stdin', it must be a list of filenames. If mode is 3372c593315Sopenharmony_ci '--index-info' is must be a list of values suitable for "git update-index 3382c593315Sopenharmony_ci --index-info", such as "<mode> <SP> <sha1> <TAB> <filename>". Any other mode 3392c593315Sopenharmony_ci is invalid.""" 3402c593315Sopenharmony_ci assert mode in ('--stdin', '--index-info') 3412c593315Sopenharmony_ci cmd = ['git', 'update-index', '--add', '-z', mode] 3422c593315Sopenharmony_ci with temporary_index_file(): 3432c593315Sopenharmony_ci p = subprocess.Popen(cmd, stdin=subprocess.PIPE) 3442c593315Sopenharmony_ci for line in input_lines: 3452c593315Sopenharmony_ci p.stdin.write('%s\0' % line) 3462c593315Sopenharmony_ci p.stdin.close() 3472c593315Sopenharmony_ci if p.wait() != 0: 3482c593315Sopenharmony_ci die('`%s` failed' % ' '.join(cmd)) 3492c593315Sopenharmony_ci tree_id = run('git', 'write-tree') 3502c593315Sopenharmony_ci return tree_id 3512c593315Sopenharmony_ci 3522c593315Sopenharmony_ci 3532c593315Sopenharmony_cidef clang_format_to_blob(filename, line_ranges, binary='clang-format', 3542c593315Sopenharmony_ci style=None): 3552c593315Sopenharmony_ci """Run clang-format on the given file and save the result to a git blob. 3562c593315Sopenharmony_ci 3572c593315Sopenharmony_ci Returns the object ID (SHA-1) of the created blob.""" 3582c593315Sopenharmony_ci clang_format_cmd = [binary, filename] 3592c593315Sopenharmony_ci if style: 3602c593315Sopenharmony_ci clang_format_cmd.extend(['-style='+style]) 3612c593315Sopenharmony_ci clang_format_cmd.extend([ 3622c593315Sopenharmony_ci '-lines=%s:%s' % (start_line, start_line+line_count-1) 3632c593315Sopenharmony_ci for start_line, line_count in line_ranges]) 3642c593315Sopenharmony_ci try: 3652c593315Sopenharmony_ci clang_format = subprocess.Popen(clang_format_cmd, stdin=subprocess.PIPE, 3662c593315Sopenharmony_ci stdout=subprocess.PIPE) 3672c593315Sopenharmony_ci except OSError as e: 3682c593315Sopenharmony_ci if e.errno == errno.ENOENT: 3692c593315Sopenharmony_ci die('cannot find executable "%s"' % binary) 3702c593315Sopenharmony_ci else: 3712c593315Sopenharmony_ci raise 3722c593315Sopenharmony_ci clang_format.stdin.close() 3732c593315Sopenharmony_ci hash_object_cmd = ['git', 'hash-object', '-w', '--path='+filename, '--stdin'] 3742c593315Sopenharmony_ci hash_object = subprocess.Popen(hash_object_cmd, stdin=clang_format.stdout, 3752c593315Sopenharmony_ci stdout=subprocess.PIPE) 3762c593315Sopenharmony_ci clang_format.stdout.close() 3772c593315Sopenharmony_ci stdout = hash_object.communicate()[0] 3782c593315Sopenharmony_ci if hash_object.returncode != 0: 3792c593315Sopenharmony_ci die('`%s` failed' % ' '.join(hash_object_cmd)) 3802c593315Sopenharmony_ci if clang_format.wait() != 0: 3812c593315Sopenharmony_ci die('`%s` failed' % ' '.join(clang_format_cmd)) 3822c593315Sopenharmony_ci return stdout.rstrip('\r\n') 3832c593315Sopenharmony_ci 3842c593315Sopenharmony_ci 3852c593315Sopenharmony_ci@contextlib.contextmanager 3862c593315Sopenharmony_cidef temporary_index_file(tree=None): 3872c593315Sopenharmony_ci """Context manager for setting GIT_INDEX_FILE to a temporary file and deleting 3882c593315Sopenharmony_ci the file afterward.""" 3892c593315Sopenharmony_ci index_path = create_temporary_index(tree) 3902c593315Sopenharmony_ci old_index_path = os.environ.get('GIT_INDEX_FILE') 3912c593315Sopenharmony_ci os.environ['GIT_INDEX_FILE'] = index_path 3922c593315Sopenharmony_ci try: 3932c593315Sopenharmony_ci yield 3942c593315Sopenharmony_ci finally: 3952c593315Sopenharmony_ci if old_index_path is None: 3962c593315Sopenharmony_ci del os.environ['GIT_INDEX_FILE'] 3972c593315Sopenharmony_ci else: 3982c593315Sopenharmony_ci os.environ['GIT_INDEX_FILE'] = old_index_path 3992c593315Sopenharmony_ci os.remove(index_path) 4002c593315Sopenharmony_ci 4012c593315Sopenharmony_ci 4022c593315Sopenharmony_cidef create_temporary_index(tree=None): 4032c593315Sopenharmony_ci """Create a temporary index file and return the created file's path. 4042c593315Sopenharmony_ci 4052c593315Sopenharmony_ci If `tree` is not None, use that as the tree to read in. Otherwise, an 4062c593315Sopenharmony_ci empty index is created.""" 4072c593315Sopenharmony_ci gitdir = run('git', 'rev-parse', '--git-dir') 4082c593315Sopenharmony_ci path = os.path.join(gitdir, temp_index_basename) 4092c593315Sopenharmony_ci if tree is None: 4102c593315Sopenharmony_ci tree = '--empty' 4112c593315Sopenharmony_ci run('git', 'read-tree', '--index-output='+path, tree) 4122c593315Sopenharmony_ci return path 4132c593315Sopenharmony_ci 4142c593315Sopenharmony_ci 4152c593315Sopenharmony_cidef print_diff(old_tree, new_tree): 4162c593315Sopenharmony_ci """Print the diff between the two trees to stdout.""" 4172c593315Sopenharmony_ci # We use the porcelain 'diff' and not plumbing 'diff-tree' because the output 4182c593315Sopenharmony_ci # is expected to be viewed by the user, and only the former does nice things 4192c593315Sopenharmony_ci # like color and pagination. 4202c593315Sopenharmony_ci subprocess.check_call(['git', 'diff', old_tree, new_tree, '--']) 4212c593315Sopenharmony_ci 4222c593315Sopenharmony_ci 4232c593315Sopenharmony_cidef apply_changes(old_tree, new_tree, force=False, patch_mode=False): 4242c593315Sopenharmony_ci """Apply the changes in `new_tree` to the working directory. 4252c593315Sopenharmony_ci 4262c593315Sopenharmony_ci Bails if there are local changes in those files and not `force`. If 4272c593315Sopenharmony_ci `patch_mode`, runs `git checkout --patch` to select hunks interactively.""" 4282c593315Sopenharmony_ci changed_files = run('git', 'diff-tree', '-r', '-z', '--name-only', old_tree, 4292c593315Sopenharmony_ci new_tree).rstrip('\0').split('\0') 4302c593315Sopenharmony_ci if not force: 4312c593315Sopenharmony_ci unstaged_files = run('git', 'diff-files', '--name-status', *changed_files) 4322c593315Sopenharmony_ci if unstaged_files: 4332c593315Sopenharmony_ci print >>sys.stderr, ('The following files would be modified but ' 4342c593315Sopenharmony_ci 'have unstaged changes:') 4352c593315Sopenharmony_ci print >>sys.stderr, unstaged_files 4362c593315Sopenharmony_ci print >>sys.stderr, 'Please commit, stage, or stash them first.' 4372c593315Sopenharmony_ci sys.exit(2) 4382c593315Sopenharmony_ci if patch_mode: 4392c593315Sopenharmony_ci # In patch mode, we could just as well create an index from the new tree 4402c593315Sopenharmony_ci # and checkout from that, but then the user will be presented with a 4412c593315Sopenharmony_ci # message saying "Discard ... from worktree". Instead, we use the old 4422c593315Sopenharmony_ci # tree as the index and checkout from new_tree, which gives the slightly 4432c593315Sopenharmony_ci # better message, "Apply ... to index and worktree". This is not quite 4442c593315Sopenharmony_ci # right, since it won't be applied to the user's index, but oh well. 4452c593315Sopenharmony_ci with temporary_index_file(old_tree): 4462c593315Sopenharmony_ci subprocess.check_call(['git', 'checkout', '--patch', new_tree]) 4472c593315Sopenharmony_ci index_tree = old_tree 4482c593315Sopenharmony_ci else: 4492c593315Sopenharmony_ci with temporary_index_file(new_tree): 4502c593315Sopenharmony_ci run('git', 'checkout-index', '-a', '-f') 4512c593315Sopenharmony_ci return changed_files 4522c593315Sopenharmony_ci 4532c593315Sopenharmony_ci 4542c593315Sopenharmony_cidef run(*args, **kwargs): 4552c593315Sopenharmony_ci stdin = kwargs.pop('stdin', '') 4562c593315Sopenharmony_ci verbose = kwargs.pop('verbose', True) 4572c593315Sopenharmony_ci strip = kwargs.pop('strip', True) 4582c593315Sopenharmony_ci for name in kwargs: 4592c593315Sopenharmony_ci raise TypeError("run() got an unexpected keyword argument '%s'" % name) 4602c593315Sopenharmony_ci p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, 4612c593315Sopenharmony_ci stdin=subprocess.PIPE) 4622c593315Sopenharmony_ci stdout, stderr = p.communicate(input=stdin) 4632c593315Sopenharmony_ci if p.returncode == 0: 4642c593315Sopenharmony_ci if stderr: 4652c593315Sopenharmony_ci if verbose: 4662c593315Sopenharmony_ci print >>sys.stderr, '`%s` printed to stderr:' % ' '.join(args) 4672c593315Sopenharmony_ci print >>sys.stderr, stderr.rstrip() 4682c593315Sopenharmony_ci if strip: 4692c593315Sopenharmony_ci stdout = stdout.rstrip('\r\n') 4702c593315Sopenharmony_ci return stdout 4712c593315Sopenharmony_ci if verbose: 4722c593315Sopenharmony_ci print >>sys.stderr, '`%s` returned %s' % (' '.join(args), p.returncode) 4732c593315Sopenharmony_ci if stderr: 4742c593315Sopenharmony_ci print >>sys.stderr, stderr.rstrip() 4752c593315Sopenharmony_ci sys.exit(2) 4762c593315Sopenharmony_ci 4772c593315Sopenharmony_ci 4782c593315Sopenharmony_cidef die(message): 4792c593315Sopenharmony_ci print >>sys.stderr, 'error:', message 4802c593315Sopenharmony_ci sys.exit(2) 4812c593315Sopenharmony_ci 4822c593315Sopenharmony_ci 4832c593315Sopenharmony_ciif __name__ == '__main__': 4842c593315Sopenharmony_ci main() 485