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