14507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper#!/usr/bin/python 24507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper# 34507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper#===- git-clang-format - ClangFormat Git Integration ---------*- python -*--===# 44507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper# 54507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper# The LLVM Compiler Infrastructure 64507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper# 74507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper# This file is distributed under the University of Illinois Open Source 84507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper# License. See LICENSE.TXT for details. 94507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper# 104507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper#===------------------------------------------------------------------------===# 114507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 124507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasperr""" 134507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasperclang-format git integration 144507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper============================ 154507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 164507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel JasperThis file provides a clang-format integration for git. Put it somewhere in your 174507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasperpath and ensure that it is executable. Then, "git clang-format" will invoke 184507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasperclang-format on the changes in current files or a specific commit. 194507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 204507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel JasperFor further details, run: 214507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jaspergit clang-format -h 224507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 234507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel JasperRequires Python 2.7 244507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper""" 254507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 264507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasperimport argparse 274507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasperimport collections 284507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasperimport contextlib 294507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasperimport errno 304507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasperimport os 314507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasperimport re 324507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasperimport subprocess 334507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasperimport sys 344507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 354507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasperusage = 'git clang-format [OPTIONS] [<commit>] [--] [<file>...]' 364507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 374507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasperdesc = ''' 384507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel JasperRun clang-format on all lines that differ between the working directory 394507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasperand <commit>, which defaults to HEAD. Changes are only applied to the working 404507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasperdirectory. 414507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 424507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel JasperThe following git-config settings set the default of the corresponding option: 434507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper clangFormat.binary 444507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper clangFormat.commit 454507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper clangFormat.extension 464507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper clangFormat.style 474507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper''' 484507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 494507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper# Name of the temporary index file in which save the output of clang-format. 504507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper# This file is created within the .git directory. 514507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jaspertemp_index_basename = 'clang-format-index' 524507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 534507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 544507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel JasperRange = collections.namedtuple('Range', 'start, count') 554507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 564507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 574507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasperdef main(): 584507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper config = load_git_config() 594507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 604507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper # In order to keep '--' yet allow options after positionals, we need to 614507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper # check for '--' ourselves. (Setting nargs='*' throws away the '--', while 624507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper # nargs=argparse.REMAINDER disallows options after positionals.) 634507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper argv = sys.argv[1:] 644507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper try: 654507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper idx = argv.index('--') 664507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper except ValueError: 674507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper dash_dash = [] 684507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper else: 694507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper dash_dash = argv[idx:] 704507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper argv = argv[:idx] 714507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 724507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper default_extensions = ','.join([ 734507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper # From clang/lib/Frontend/FrontendOptions.cpp, all lower case 744507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 'c', 'h', # C 754507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 'm', # ObjC 764507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 'mm', # ObjC++ 774507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 'cc', 'cp', 'cpp', 'c++', 'cxx', 'hpp', # C++ 786bcf27bb9a4b5c3f79cb44c0e4654a6d7619ad89Stephen Hines # Other languages that clang-format supports 796bcf27bb9a4b5c3f79cb44c0e4654a6d7619ad89Stephen Hines 'proto', 'protodevel', # Protocol Buffers 806bcf27bb9a4b5c3f79cb44c0e4654a6d7619ad89Stephen Hines 'js', # JavaScript 814507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper ]) 824507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 834507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper p = argparse.ArgumentParser( 844507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper usage=usage, formatter_class=argparse.RawDescriptionHelpFormatter, 854507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper description=desc) 864507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper p.add_argument('--binary', 874507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper default=config.get('clangformat.binary', 'clang-format'), 884507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper help='path to clang-format'), 894507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper p.add_argument('--commit', 904507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper default=config.get('clangformat.commit', 'HEAD'), 914507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper help='default commit to use if none is specified'), 924507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper p.add_argument('--diff', action='store_true', 934507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper help='print a diff instead of applying the changes') 944507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper p.add_argument('--extensions', 954507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper default=config.get('clangformat.extensions', 964507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper default_extensions), 974507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper help=('comma-separated list of file extensions to format, ' 984507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 'excluding the period and case-insensitive')), 994507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper p.add_argument('-f', '--force', action='store_true', 1004507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper help='allow changes to unstaged files') 1014507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper p.add_argument('-p', '--patch', action='store_true', 1024507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper help='select hunks interactively') 1034507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper p.add_argument('-q', '--quiet', action='count', default=0, 1044507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper help='print less information') 1054507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper p.add_argument('--style', 1064507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper default=config.get('clangformat.style', None), 1074507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper help='passed to clang-format'), 1084507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper p.add_argument('-v', '--verbose', action='count', default=0, 1094507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper help='print extra information') 1104507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper # We gather all the remaining positional arguments into 'args' since we need 1114507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper # to use some heuristics to determine whether or not <commit> was present. 1124507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper # However, to print pretty messages, we make use of metavar and help. 1134507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper p.add_argument('args', nargs='*', metavar='<commit>', 1144507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper help='revision from which to compute the diff') 1154507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper p.add_argument('ignored', nargs='*', metavar='<file>...', 1164507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper help='if specified, only consider differences in these files') 1174507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper opts = p.parse_args(argv) 1184507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 1194507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper opts.verbose -= opts.quiet 1204507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper del opts.quiet 1214507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 1224507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper commit, files = interpret_args(opts.args, dash_dash, opts.commit) 1234507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper changed_lines = compute_diff_and_extract_lines(commit, files) 1244507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper if opts.verbose >= 1: 1254507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper ignored_files = set(changed_lines) 1264507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper filter_by_extension(changed_lines, opts.extensions.lower().split(',')) 1274507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper if opts.verbose >= 1: 1284507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper ignored_files.difference_update(changed_lines) 1294507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper if ignored_files: 1304507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper print 'Ignoring changes in the following files (wrong extension):' 1314507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper for filename in ignored_files: 1324507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper print ' ', filename 1334507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper if changed_lines: 1344507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper print 'Running clang-format on the following files:' 1354507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper for filename in changed_lines: 1364507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper print ' ', filename 1374507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper if not changed_lines: 1384507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper print 'no modified files to format' 1394507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper return 1404507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper # The computed diff outputs absolute paths, so we must cd before accessing 1414507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper # those files. 1424507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper cd_to_toplevel() 14365e2b74344de606145c0bc5aa6209d375db9e5ebDaniel Jasper old_tree = create_tree_from_workdir(changed_lines) 14465e2b74344de606145c0bc5aa6209d375db9e5ebDaniel Jasper new_tree = run_clang_format_and_save_to_tree(changed_lines, 1454507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper binary=opts.binary, 1464507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper style=opts.style) 1474507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper if opts.verbose >= 1: 1484507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper print 'old tree:', old_tree 1494507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper print 'new tree:', new_tree 1504507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper if old_tree == new_tree: 1514507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper if opts.verbose >= 0: 1524507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper print 'clang-format did not modify any files' 1534507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper elif opts.diff: 1544507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper print_diff(old_tree, new_tree) 1554507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper else: 1564507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper changed_files = apply_changes(old_tree, new_tree, force=opts.force, 1574507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper patch_mode=opts.patch) 1584507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper if (opts.verbose >= 0 and not opts.patch) or opts.verbose >= 1: 1594507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper print 'changed files:' 1604507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper for filename in changed_files: 1614507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper print ' ', filename 1624507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 1634507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 1644507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasperdef load_git_config(non_string_options=None): 1654507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper """Return the git configuration as a dictionary. 1664507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 1674507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper All options are assumed to be strings unless in `non_string_options`, in which 1684507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper is a dictionary mapping option name (in lower case) to either "--bool" or 1694507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper "--int".""" 1704507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper if non_string_options is None: 1714507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper non_string_options = {} 1724507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper out = {} 1734507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper for entry in run('git', 'config', '--list', '--null').split('\0'): 1744507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper if entry: 1754507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper name, value = entry.split('\n', 1) 1764507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper if name in non_string_options: 1774507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper value = run('git', 'config', non_string_options[name], name) 1784507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper out[name] = value 1794507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper return out 1804507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 1814507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 1824507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasperdef interpret_args(args, dash_dash, default_commit): 1834507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper """Interpret `args` as "[commit] [--] [files...]" and return (commit, files). 1844507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 1854507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper It is assumed that "--" and everything that follows has been removed from 1864507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper args and placed in `dash_dash`. 1874507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 1884507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper If "--" is present (i.e., `dash_dash` is non-empty), the argument to its 1894507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper left (if present) is taken as commit. Otherwise, the first argument is 1904507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper checked if it is a commit or a file. If commit is not given, 1914507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper `default_commit` is used.""" 1924507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper if dash_dash: 1934507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper if len(args) == 0: 1944507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper commit = default_commit 1954507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper elif len(args) > 1: 1964507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper die('at most one commit allowed; %d given' % len(args)) 1974507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper else: 1984507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper commit = args[0] 1994507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper object_type = get_object_type(commit) 2004507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper if object_type not in ('commit', 'tag'): 2014507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper if object_type is None: 2024507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper die("'%s' is not a commit" % commit) 2034507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper else: 2044507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper die("'%s' is a %s, but a commit was expected" % (commit, object_type)) 2054507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper files = dash_dash[1:] 2064507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper elif args: 2074507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper if disambiguate_revision(args[0]): 2084507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper commit = args[0] 2094507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper files = args[1:] 2104507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper else: 2114507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper commit = default_commit 2124507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper files = args 2134507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper else: 2144507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper commit = default_commit 2154507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper files = [] 2164507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper return commit, files 2174507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 2184507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 2194507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasperdef disambiguate_revision(value): 2204507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper """Returns True if `value` is a revision, False if it is a file, or dies.""" 2214507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper # If `value` is ambiguous (neither a commit nor a file), the following 2224507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper # command will die with an appropriate error message. 2234507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper run('git', 'rev-parse', value, verbose=False) 2244507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper object_type = get_object_type(value) 2254507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper if object_type is None: 2264507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper return False 2274507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper if object_type in ('commit', 'tag'): 2284507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper return True 2294507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper die('`%s` is a %s, but a commit or filename was expected' % 2304507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper (value, object_type)) 2314507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 2324507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 2334507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasperdef get_object_type(value): 2344507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper """Returns a string description of an object's type, or None if it is not 2354507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper a valid git object.""" 2364507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper cmd = ['git', 'cat-file', '-t', value] 2374507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 2384507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper stdout, stderr = p.communicate() 2394507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper if p.returncode != 0: 2404507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper return None 2414507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper return stdout.strip() 2424507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 2434507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 2444507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasperdef compute_diff_and_extract_lines(commit, files): 2454507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper """Calls compute_diff() followed by extract_lines().""" 2464507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper diff_process = compute_diff(commit, files) 2474507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper changed_lines = extract_lines(diff_process.stdout) 2484507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper diff_process.stdout.close() 2494507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper diff_process.wait() 2504507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper if diff_process.returncode != 0: 2514507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper # Assume error was already printed to stderr. 2524507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper sys.exit(2) 2534507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper return changed_lines 2544507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 2554507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 2564507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasperdef compute_diff(commit, files): 2574507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper """Return a subprocess object producing the diff from `commit`. 2584507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 2594507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper The return value's `stdin` file object will produce a patch with the 2604507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper differences between the working directory and `commit`, filtered on `files` 2614507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper (if non-empty). Zero context lines are used in the patch.""" 2624507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper cmd = ['git', 'diff-index', '-p', '-U0', commit, '--'] 2634507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper cmd.extend(files) 2644507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE) 2654507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper p.stdin.close() 2664507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper return p 2674507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 2684507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 2694507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasperdef extract_lines(patch_file): 2704507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper """Extract the changed lines in `patch_file`. 2714507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 27265e2b74344de606145c0bc5aa6209d375db9e5ebDaniel Jasper The return value is a dictionary mapping filename to a list of (start_line, 27365e2b74344de606145c0bc5aa6209d375db9e5ebDaniel Jasper line_count) pairs. 27465e2b74344de606145c0bc5aa6209d375db9e5ebDaniel Jasper 2754507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper The input must have been produced with ``-U0``, meaning unidiff format with 2764507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper zero lines of context. The return value is a dict mapping filename to a 2774507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper list of line `Range`s.""" 2784507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper matches = {} 2794507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper for line in patch_file: 2804507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper match = re.search(r'^\+\+\+\ [^/]+/(.*)', line) 2814507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper if match: 2824507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper filename = match.group(1).rstrip('\r\n') 2834507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper match = re.search(r'^@@ -[0-9,]+ \+(\d+)(,(\d+))?', line) 2844507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper if match: 2854507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper start_line = int(match.group(1)) 2864507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper line_count = 1 2874507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper if match.group(3): 2884507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper line_count = int(match.group(3)) 2894507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper if line_count > 0: 2904507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper matches.setdefault(filename, []).append(Range(start_line, line_count)) 2914507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper return matches 2924507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 2934507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 2944507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasperdef filter_by_extension(dictionary, allowed_extensions): 2954507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper """Delete every key in `dictionary` that doesn't have an allowed extension. 2964507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 2974507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper `allowed_extensions` must be a collection of lowercase file extensions, 2984507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper excluding the period.""" 2994507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper allowed_extensions = frozenset(allowed_extensions) 3004507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper for filename in dictionary.keys(): 3014507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper base_ext = filename.rsplit('.', 1) 3024507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper if len(base_ext) == 1 or base_ext[1].lower() not in allowed_extensions: 3034507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper del dictionary[filename] 3044507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 3054507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 3064507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasperdef cd_to_toplevel(): 3074507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper """Change to the top level of the git repository.""" 3084507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper toplevel = run('git', 'rev-parse', '--show-toplevel') 3094507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper os.chdir(toplevel) 3104507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 3114507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 3124507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasperdef create_tree_from_workdir(filenames): 3134507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper """Create a new git tree with the given files from the working directory. 3144507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 3154507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper Returns the object ID (SHA-1) of the created tree.""" 3164507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper return create_tree(filenames, '--stdin') 3174507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 3184507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 31965e2b74344de606145c0bc5aa6209d375db9e5ebDaniel Jasperdef run_clang_format_and_save_to_tree(changed_lines, binary='clang-format', 3204507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper style=None): 3214507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper """Run clang-format on each file and save the result to a git tree. 3224507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 3234507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper Returns the object ID (SHA-1) of the created tree.""" 3244507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper def index_info_generator(): 32565e2b74344de606145c0bc5aa6209d375db9e5ebDaniel Jasper for filename, line_ranges in changed_lines.iteritems(): 3264507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper mode = oct(os.stat(filename).st_mode) 32765e2b74344de606145c0bc5aa6209d375db9e5ebDaniel Jasper blob_id = clang_format_to_blob(filename, line_ranges, binary=binary, 3284507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper style=style) 3294507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper yield '%s %s\t%s' % (mode, blob_id, filename) 3304507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper return create_tree(index_info_generator(), '--index-info') 3314507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 3324507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 3334507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasperdef create_tree(input_lines, mode): 3344507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper """Create a tree object from the given input. 3354507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 3364507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper If mode is '--stdin', it must be a list of filenames. If mode is 3374507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper '--index-info' is must be a list of values suitable for "git update-index 3384507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper --index-info", such as "<mode> <SP> <sha1> <TAB> <filename>". Any other mode 3394507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper is invalid.""" 3404507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper assert mode in ('--stdin', '--index-info') 3414507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper cmd = ['git', 'update-index', '--add', '-z', mode] 3424507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper with temporary_index_file(): 3434507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper p = subprocess.Popen(cmd, stdin=subprocess.PIPE) 3444507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper for line in input_lines: 3454507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper p.stdin.write('%s\0' % line) 3464507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper p.stdin.close() 3474507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper if p.wait() != 0: 3484507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper die('`%s` failed' % ' '.join(cmd)) 3494507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper tree_id = run('git', 'write-tree') 3504507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper return tree_id 3514507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 3524507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 35365e2b74344de606145c0bc5aa6209d375db9e5ebDaniel Jasperdef clang_format_to_blob(filename, line_ranges, binary='clang-format', 3544507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper style=None): 3554507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper """Run clang-format on the given file and save the result to a git blob. 3564507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 3574507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper Returns the object ID (SHA-1) of the created blob.""" 3584507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper clang_format_cmd = [binary, filename] 3594507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper if style: 3604507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper clang_format_cmd.extend(['-style='+style]) 36165e2b74344de606145c0bc5aa6209d375db9e5ebDaniel Jasper clang_format_cmd.extend([ 36265e2b74344de606145c0bc5aa6209d375db9e5ebDaniel Jasper '-lines=%s:%s' % (start_line, start_line+line_count-1) 36365e2b74344de606145c0bc5aa6209d375db9e5ebDaniel Jasper for start_line, line_count in line_ranges]) 3644507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper try: 3654507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper clang_format = subprocess.Popen(clang_format_cmd, stdin=subprocess.PIPE, 3664507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper stdout=subprocess.PIPE) 3674507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper except OSError as e: 3684507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper if e.errno == errno.ENOENT: 3694507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper die('cannot find executable "%s"' % binary) 3704507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper else: 3714507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper raise 3724507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper clang_format.stdin.close() 3734507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper hash_object_cmd = ['git', 'hash-object', '-w', '--path='+filename, '--stdin'] 3744507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper hash_object = subprocess.Popen(hash_object_cmd, stdin=clang_format.stdout, 3754507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper stdout=subprocess.PIPE) 3764507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper clang_format.stdout.close() 3774507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper stdout = hash_object.communicate()[0] 3784507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper if hash_object.returncode != 0: 3794507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper die('`%s` failed' % ' '.join(hash_object_cmd)) 3804507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper if clang_format.wait() != 0: 3814507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper die('`%s` failed' % ' '.join(clang_format_cmd)) 3824507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper return stdout.rstrip('\r\n') 3834507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 3844507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 3854507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper@contextlib.contextmanager 3864507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasperdef temporary_index_file(tree=None): 3874507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper """Context manager for setting GIT_INDEX_FILE to a temporary file and deleting 3884507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper the file afterward.""" 3894507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper index_path = create_temporary_index(tree) 3904507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper old_index_path = os.environ.get('GIT_INDEX_FILE') 3914507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper os.environ['GIT_INDEX_FILE'] = index_path 3924507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper try: 3934507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper yield 3944507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper finally: 3954507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper if old_index_path is None: 3964507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper del os.environ['GIT_INDEX_FILE'] 3974507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper else: 3984507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper os.environ['GIT_INDEX_FILE'] = old_index_path 3994507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper os.remove(index_path) 4004507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 4014507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 4024507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasperdef create_temporary_index(tree=None): 4034507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper """Create a temporary index file and return the created file's path. 4044507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 4054507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper If `tree` is not None, use that as the tree to read in. Otherwise, an 4064507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper empty index is created.""" 4074507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper gitdir = run('git', 'rev-parse', '--git-dir') 4084507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper path = os.path.join(gitdir, temp_index_basename) 4094507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper if tree is None: 4104507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper tree = '--empty' 4114507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper run('git', 'read-tree', '--index-output='+path, tree) 4124507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper return path 4134507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 4144507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 4154507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasperdef print_diff(old_tree, new_tree): 4164507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper """Print the diff between the two trees to stdout.""" 4174507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper # We use the porcelain 'diff' and not plumbing 'diff-tree' because the output 4184507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper # is expected to be viewed by the user, and only the former does nice things 4194507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper # like color and pagination. 4204507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper subprocess.check_call(['git', 'diff', old_tree, new_tree, '--']) 4214507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 4224507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 4234507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasperdef apply_changes(old_tree, new_tree, force=False, patch_mode=False): 4244507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper """Apply the changes in `new_tree` to the working directory. 4254507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 4264507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper Bails if there are local changes in those files and not `force`. If 4274507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper `patch_mode`, runs `git checkout --patch` to select hunks interactively.""" 4284507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper changed_files = run('git', 'diff-tree', '-r', '-z', '--name-only', old_tree, 4294507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper new_tree).rstrip('\0').split('\0') 4304507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper if not force: 4314507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper unstaged_files = run('git', 'diff-files', '--name-status', *changed_files) 4324507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper if unstaged_files: 4334507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper print >>sys.stderr, ('The following files would be modified but ' 4344507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 'have unstaged changes:') 4354507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper print >>sys.stderr, unstaged_files 4364507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper print >>sys.stderr, 'Please commit, stage, or stash them first.' 4374507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper sys.exit(2) 4384507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper if patch_mode: 4394507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper # In patch mode, we could just as well create an index from the new tree 4404507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper # and checkout from that, but then the user will be presented with a 4414507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper # message saying "Discard ... from worktree". Instead, we use the old 4424507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper # tree as the index and checkout from new_tree, which gives the slightly 4434507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper # better message, "Apply ... to index and worktree". This is not quite 4444507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper # right, since it won't be applied to the user's index, but oh well. 4454507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper with temporary_index_file(old_tree): 4464507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper subprocess.check_call(['git', 'checkout', '--patch', new_tree]) 4474507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper index_tree = old_tree 4484507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper else: 4494507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper with temporary_index_file(new_tree): 4504507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper run('git', 'checkout-index', '-a', '-f') 4514507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper return changed_files 4524507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 4534507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 4544507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasperdef run(*args, **kwargs): 4554507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper stdin = kwargs.pop('stdin', '') 4564507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper verbose = kwargs.pop('verbose', True) 4574507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper strip = kwargs.pop('strip', True) 4584507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper for name in kwargs: 4594507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper raise TypeError("run() got an unexpected keyword argument '%s'" % name) 4604507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, 4614507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper stdin=subprocess.PIPE) 4624507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper stdout, stderr = p.communicate(input=stdin) 4634507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper if p.returncode == 0: 4644507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper if stderr: 4654507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper if verbose: 4664507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper print >>sys.stderr, '`%s` printed to stderr:' % ' '.join(args) 4674507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper print >>sys.stderr, stderr.rstrip() 4684507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper if strip: 4694507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper stdout = stdout.rstrip('\r\n') 4704507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper return stdout 4714507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper if verbose: 4724507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper print >>sys.stderr, '`%s` returned %s' % (' '.join(args), p.returncode) 4734507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper if stderr: 4744507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper print >>sys.stderr, stderr.rstrip() 4754507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper sys.exit(2) 4764507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 4774507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 4784507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasperdef die(message): 4794507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper print >>sys.stderr, 'error:', message 4804507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper sys.exit(2) 4814507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 4824507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper 4834507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasperif __name__ == '__main__': 4844507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper main() 485