blob: 3d1ba8a3c1070970a411de0fdd72c015faf23b47 [file] [log] [blame]
Ed Schoutenbf041d92014-09-02 20:59:13 +00001#!/usr/bin/env python
Daniel Jaspere7a50012013-05-23 17:53:42 +00002#
3#===- git-clang-format - ClangFormat Git Integration ---------*- python -*--===#
4#
5# The LLVM Compiler Infrastructure
6#
7# This file is distributed under the University of Illinois Open Source
8# License. See LICENSE.TXT for details.
9#
10#===------------------------------------------------------------------------===#
11
12r"""
13clang-format git integration
14============================
15
16This file provides a clang-format integration for git. Put it somewhere in your
17path and ensure that it is executable. Then, "git clang-format" will invoke
18clang-format on the changes in current files or a specific commit.
19
20For further details, run:
21git clang-format -h
22
23Requires Python 2.7
24"""
25
Eric Fiselier58bef5d2017-04-20 21:05:58 +000026from __future__ import print_function
Daniel Jaspere7a50012013-05-23 17:53:42 +000027import argparse
28import collections
29import contextlib
30import errno
31import os
32import re
33import subprocess
34import sys
35
Stephen Hines90ced942016-09-22 05:52:55 +000036usage = 'git clang-format [OPTIONS] [<commit>] [<commit>] [--] [<file>...]'
Daniel Jaspere7a50012013-05-23 17:53:42 +000037
38desc = '''
Stephen Hines90ced942016-09-22 05:52:55 +000039If zero or one commits are given, run clang-format on all lines that differ
40between the working directory and <commit>, which defaults to HEAD. Changes are
41only applied to the working directory.
42
43If two commits are given (requires --diff), run clang-format on all lines in the
44second <commit> that differ from the first <commit>.
Daniel Jaspere7a50012013-05-23 17:53:42 +000045
46The following git-config settings set the default of the corresponding option:
47 clangFormat.binary
48 clangFormat.commit
49 clangFormat.extension
50 clangFormat.style
51'''
52
53# Name of the temporary index file in which save the output of clang-format.
54# This file is created within the .git directory.
55temp_index_basename = 'clang-format-index'
56
57
58Range = collections.namedtuple('Range', 'start, count')
59
60
61def main():
62 config = load_git_config()
63
64 # In order to keep '--' yet allow options after positionals, we need to
65 # check for '--' ourselves. (Setting nargs='*' throws away the '--', while
66 # nargs=argparse.REMAINDER disallows options after positionals.)
67 argv = sys.argv[1:]
68 try:
69 idx = argv.index('--')
70 except ValueError:
71 dash_dash = []
72 else:
73 dash_dash = argv[idx:]
74 argv = argv[:idx]
75
76 default_extensions = ','.join([
77 # From clang/lib/Frontend/FrontendOptions.cpp, all lower case
78 'c', 'h', # C
79 'm', # ObjC
80 'mm', # ObjC++
81 'cc', 'cp', 'cpp', 'c++', 'cxx', 'hpp', # C++
Daniel Jasperdfacecb2014-04-09 09:22:35 +000082 # Other languages that clang-format supports
83 'proto', 'protodevel', # Protocol Buffers
Stephen Hines815e9bb2016-09-13 05:00:20 +000084 'java', # Java
Daniel Jasperdfacecb2014-04-09 09:22:35 +000085 'js', # JavaScript
Daniel Jasperc105a9a2015-06-19 08:23:10 +000086 'ts', # TypeScript
Daniel Jaspere7a50012013-05-23 17:53:42 +000087 ])
88
89 p = argparse.ArgumentParser(
90 usage=usage, formatter_class=argparse.RawDescriptionHelpFormatter,
91 description=desc)
92 p.add_argument('--binary',
93 default=config.get('clangformat.binary', 'clang-format'),
94 help='path to clang-format'),
95 p.add_argument('--commit',
96 default=config.get('clangformat.commit', 'HEAD'),
97 help='default commit to use if none is specified'),
98 p.add_argument('--diff', action='store_true',
99 help='print a diff instead of applying the changes')
100 p.add_argument('--extensions',
101 default=config.get('clangformat.extensions',
102 default_extensions),
103 help=('comma-separated list of file extensions to format, '
104 'excluding the period and case-insensitive')),
105 p.add_argument('-f', '--force', action='store_true',
106 help='allow changes to unstaged files')
107 p.add_argument('-p', '--patch', action='store_true',
108 help='select hunks interactively')
109 p.add_argument('-q', '--quiet', action='count', default=0,
110 help='print less information')
111 p.add_argument('--style',
112 default=config.get('clangformat.style', None),
113 help='passed to clang-format'),
114 p.add_argument('-v', '--verbose', action='count', default=0,
115 help='print extra information')
116 # We gather all the remaining positional arguments into 'args' since we need
117 # to use some heuristics to determine whether or not <commit> was present.
118 # However, to print pretty messages, we make use of metavar and help.
119 p.add_argument('args', nargs='*', metavar='<commit>',
120 help='revision from which to compute the diff')
121 p.add_argument('ignored', nargs='*', metavar='<file>...',
122 help='if specified, only consider differences in these files')
123 opts = p.parse_args(argv)
124
125 opts.verbose -= opts.quiet
126 del opts.quiet
127
Stephen Hines90ced942016-09-22 05:52:55 +0000128 commits, files = interpret_args(opts.args, dash_dash, opts.commit)
129 if len(commits) > 1:
130 if not opts.diff:
131 die('--diff is required when two commits are given')
132 else:
133 if len(commits) > 2:
134 die('at most two commits allowed; %d given' % len(commits))
135 changed_lines = compute_diff_and_extract_lines(commits, files)
Daniel Jaspere7a50012013-05-23 17:53:42 +0000136 if opts.verbose >= 1:
137 ignored_files = set(changed_lines)
138 filter_by_extension(changed_lines, opts.extensions.lower().split(','))
139 if opts.verbose >= 1:
140 ignored_files.difference_update(changed_lines)
141 if ignored_files:
Eric Fiselier58bef5d2017-04-20 21:05:58 +0000142 print('Ignoring changes in the following files (wrong extension):')
Daniel Jaspere7a50012013-05-23 17:53:42 +0000143 for filename in ignored_files:
Eric Fiselier58bef5d2017-04-20 21:05:58 +0000144 print(' %s' % filename)
Daniel Jaspere7a50012013-05-23 17:53:42 +0000145 if changed_lines:
Eric Fiselier58bef5d2017-04-20 21:05:58 +0000146 print('Running clang-format on the following files:')
Daniel Jaspere7a50012013-05-23 17:53:42 +0000147 for filename in changed_lines:
Eric Fiselier58bef5d2017-04-20 21:05:58 +0000148 print(' %s' % filename)
Daniel Jaspere7a50012013-05-23 17:53:42 +0000149 if not changed_lines:
Eric Fiselier58bef5d2017-04-20 21:05:58 +0000150 print('no modified files to format')
Daniel Jaspere7a50012013-05-23 17:53:42 +0000151 return
152 # The computed diff outputs absolute paths, so we must cd before accessing
153 # those files.
154 cd_to_toplevel()
Stephen Hines90ced942016-09-22 05:52:55 +0000155 if len(commits) > 1:
156 old_tree = commits[1]
157 new_tree = run_clang_format_and_save_to_tree(changed_lines,
158 revision=commits[1],
159 binary=opts.binary,
160 style=opts.style)
161 else:
162 old_tree = create_tree_from_workdir(changed_lines)
163 new_tree = run_clang_format_and_save_to_tree(changed_lines,
164 binary=opts.binary,
165 style=opts.style)
Daniel Jaspere7a50012013-05-23 17:53:42 +0000166 if opts.verbose >= 1:
Eric Fiselier58bef5d2017-04-20 21:05:58 +0000167 print('old tree: %s' % old_tree)
168 print('new tree: %s' % new_tree)
Daniel Jaspere7a50012013-05-23 17:53:42 +0000169 if old_tree == new_tree:
170 if opts.verbose >= 0:
Eric Fiselier58bef5d2017-04-20 21:05:58 +0000171 print('clang-format did not modify any files')
Daniel Jaspere7a50012013-05-23 17:53:42 +0000172 elif opts.diff:
173 print_diff(old_tree, new_tree)
174 else:
175 changed_files = apply_changes(old_tree, new_tree, force=opts.force,
176 patch_mode=opts.patch)
177 if (opts.verbose >= 0 and not opts.patch) or opts.verbose >= 1:
Eric Fiselier58bef5d2017-04-20 21:05:58 +0000178 print('changed files:')
Daniel Jaspere7a50012013-05-23 17:53:42 +0000179 for filename in changed_files:
Eric Fiselier58bef5d2017-04-20 21:05:58 +0000180 print(' %s' % filename)
Daniel Jaspere7a50012013-05-23 17:53:42 +0000181
182
183def load_git_config(non_string_options=None):
184 """Return the git configuration as a dictionary.
185
186 All options are assumed to be strings unless in `non_string_options`, in which
187 is a dictionary mapping option name (in lower case) to either "--bool" or
188 "--int"."""
189 if non_string_options is None:
190 non_string_options = {}
191 out = {}
192 for entry in run('git', 'config', '--list', '--null').split('\0'):
193 if entry:
194 name, value = entry.split('\n', 1)
195 if name in non_string_options:
196 value = run('git', 'config', non_string_options[name], name)
197 out[name] = value
198 return out
199
200
201def interpret_args(args, dash_dash, default_commit):
Stephen Hines90ced942016-09-22 05:52:55 +0000202 """Interpret `args` as "[commits] [--] [files]" and return (commits, files).
Daniel Jaspere7a50012013-05-23 17:53:42 +0000203
204 It is assumed that "--" and everything that follows has been removed from
205 args and placed in `dash_dash`.
206
Stephen Hines90ced942016-09-22 05:52:55 +0000207 If "--" is present (i.e., `dash_dash` is non-empty), the arguments to its
208 left (if present) are taken as commits. Otherwise, the arguments are checked
209 from left to right if they are commits or files. If commits are not given,
210 a list with `default_commit` is used."""
Daniel Jaspere7a50012013-05-23 17:53:42 +0000211 if dash_dash:
212 if len(args) == 0:
Stephen Hines90ced942016-09-22 05:52:55 +0000213 commits = [default_commit]
Daniel Jaspere7a50012013-05-23 17:53:42 +0000214 else:
Stephen Hines90ced942016-09-22 05:52:55 +0000215 commits = args
216 for commit in commits:
217 object_type = get_object_type(commit)
218 if object_type not in ('commit', 'tag'):
219 if object_type is None:
220 die("'%s' is not a commit" % commit)
221 else:
222 die("'%s' is a %s, but a commit was expected" % (commit, object_type))
Daniel Jaspere7a50012013-05-23 17:53:42 +0000223 files = dash_dash[1:]
224 elif args:
Stephen Hines90ced942016-09-22 05:52:55 +0000225 commits = []
226 while args:
227 if not disambiguate_revision(args[0]):
228 break
229 commits.append(args.pop(0))
230 if not commits:
231 commits = [default_commit]
232 files = args
Daniel Jaspere7a50012013-05-23 17:53:42 +0000233 else:
Stephen Hines90ced942016-09-22 05:52:55 +0000234 commits = [default_commit]
Daniel Jaspere7a50012013-05-23 17:53:42 +0000235 files = []
Stephen Hines90ced942016-09-22 05:52:55 +0000236 return commits, files
Daniel Jaspere7a50012013-05-23 17:53:42 +0000237
238
239def disambiguate_revision(value):
240 """Returns True if `value` is a revision, False if it is a file, or dies."""
241 # If `value` is ambiguous (neither a commit nor a file), the following
242 # command will die with an appropriate error message.
243 run('git', 'rev-parse', value, verbose=False)
244 object_type = get_object_type(value)
245 if object_type is None:
246 return False
247 if object_type in ('commit', 'tag'):
248 return True
249 die('`%s` is a %s, but a commit or filename was expected' %
250 (value, object_type))
251
252
253def get_object_type(value):
254 """Returns a string description of an object's type, or None if it is not
255 a valid git object."""
256 cmd = ['git', 'cat-file', '-t', value]
257 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
258 stdout, stderr = p.communicate()
259 if p.returncode != 0:
260 return None
261 return stdout.strip()
262
263
Stephen Hines90ced942016-09-22 05:52:55 +0000264def compute_diff_and_extract_lines(commits, files):
Daniel Jaspere7a50012013-05-23 17:53:42 +0000265 """Calls compute_diff() followed by extract_lines()."""
Stephen Hines90ced942016-09-22 05:52:55 +0000266 diff_process = compute_diff(commits, files)
Daniel Jaspere7a50012013-05-23 17:53:42 +0000267 changed_lines = extract_lines(diff_process.stdout)
268 diff_process.stdout.close()
269 diff_process.wait()
270 if diff_process.returncode != 0:
271 # Assume error was already printed to stderr.
272 sys.exit(2)
273 return changed_lines
274
275
Stephen Hines90ced942016-09-22 05:52:55 +0000276def compute_diff(commits, files):
277 """Return a subprocess object producing the diff from `commits`.
Daniel Jaspere7a50012013-05-23 17:53:42 +0000278
279 The return value's `stdin` file object will produce a patch with the
Stephen Hines90ced942016-09-22 05:52:55 +0000280 differences between the working directory and the first commit if a single
281 one was specified, or the difference between both specified commits, filtered
282 on `files` (if non-empty). Zero context lines are used in the patch."""
283 git_tool = 'diff-index'
284 if len(commits) > 1:
285 git_tool = 'diff-tree'
286 cmd = ['git', git_tool, '-p', '-U0'] + commits + ['--']
Daniel Jaspere7a50012013-05-23 17:53:42 +0000287 cmd.extend(files)
288 p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
289 p.stdin.close()
290 return p
291
292
293def extract_lines(patch_file):
294 """Extract the changed lines in `patch_file`.
295
Daniel Jasper695bad542013-08-01 18:17:13 +0000296 The return value is a dictionary mapping filename to a list of (start_line,
297 line_count) pairs.
298
Daniel Jaspere7a50012013-05-23 17:53:42 +0000299 The input must have been produced with ``-U0``, meaning unidiff format with
300 zero lines of context. The return value is a dict mapping filename to a
301 list of line `Range`s."""
302 matches = {}
303 for line in patch_file:
304 match = re.search(r'^\+\+\+\ [^/]+/(.*)', line)
305 if match:
306 filename = match.group(1).rstrip('\r\n')
307 match = re.search(r'^@@ -[0-9,]+ \+(\d+)(,(\d+))?', line)
308 if match:
309 start_line = int(match.group(1))
310 line_count = 1
311 if match.group(3):
312 line_count = int(match.group(3))
313 if line_count > 0:
314 matches.setdefault(filename, []).append(Range(start_line, line_count))
315 return matches
316
317
318def filter_by_extension(dictionary, allowed_extensions):
319 """Delete every key in `dictionary` that doesn't have an allowed extension.
320
321 `allowed_extensions` must be a collection of lowercase file extensions,
322 excluding the period."""
323 allowed_extensions = frozenset(allowed_extensions)
Eric Fiselier58bef5d2017-04-20 21:05:58 +0000324 for filename in list(dictionary.keys()):
Daniel Jaspere7a50012013-05-23 17:53:42 +0000325 base_ext = filename.rsplit('.', 1)
326 if len(base_ext) == 1 or base_ext[1].lower() not in allowed_extensions:
327 del dictionary[filename]
328
329
330def cd_to_toplevel():
331 """Change to the top level of the git repository."""
332 toplevel = run('git', 'rev-parse', '--show-toplevel')
333 os.chdir(toplevel)
334
335
Daniel Jaspere7a50012013-05-23 17:53:42 +0000336def create_tree_from_workdir(filenames):
337 """Create a new git tree with the given files from the working directory.
338
339 Returns the object ID (SHA-1) of the created tree."""
340 return create_tree(filenames, '--stdin')
341
342
Stephen Hines90ced942016-09-22 05:52:55 +0000343def run_clang_format_and_save_to_tree(changed_lines, revision=None,
344 binary='clang-format', style=None):
Daniel Jaspere7a50012013-05-23 17:53:42 +0000345 """Run clang-format on each file and save the result to a git tree.
346
347 Returns the object ID (SHA-1) of the created tree."""
Eric Fiselier3f5152d2017-04-20 21:23:58 +0000348 def iteritems(container):
349 try:
350 return container.iteritems() # Python 2
351 except AttributeError:
352 return container.items() # Python 3
Daniel Jaspere7a50012013-05-23 17:53:42 +0000353 def index_info_generator():
Eric Fiselier3f5152d2017-04-20 21:23:58 +0000354 for filename, line_ranges in iteritems(changed_lines):
Stephen Hines171244f2016-11-08 05:50:14 +0000355 if revision:
356 git_metadata_cmd = ['git', 'ls-tree',
357 '%s:%s' % (revision, os.path.dirname(filename)),
358 os.path.basename(filename)]
359 git_metadata = subprocess.Popen(git_metadata_cmd, stdin=subprocess.PIPE,
360 stdout=subprocess.PIPE)
361 stdout = git_metadata.communicate()[0]
362 mode = oct(int(stdout.split()[0], 8))
363 else:
364 mode = oct(os.stat(filename).st_mode)
Eric Fiselier58bef5d2017-04-20 21:05:58 +0000365 # Adjust python3 octal format so that it matches what git expects
366 if mode.startswith('0o'):
367 mode = '0' + mode[2:]
Stephen Hines90ced942016-09-22 05:52:55 +0000368 blob_id = clang_format_to_blob(filename, line_ranges,
369 revision=revision,
370 binary=binary,
Daniel Jaspere7a50012013-05-23 17:53:42 +0000371 style=style)
372 yield '%s %s\t%s' % (mode, blob_id, filename)
373 return create_tree(index_info_generator(), '--index-info')
374
375
376def create_tree(input_lines, mode):
377 """Create a tree object from the given input.
378
379 If mode is '--stdin', it must be a list of filenames. If mode is
380 '--index-info' is must be a list of values suitable for "git update-index
381 --index-info", such as "<mode> <SP> <sha1> <TAB> <filename>". Any other mode
382 is invalid."""
383 assert mode in ('--stdin', '--index-info')
384 cmd = ['git', 'update-index', '--add', '-z', mode]
385 with temporary_index_file():
386 p = subprocess.Popen(cmd, stdin=subprocess.PIPE)
387 for line in input_lines:
388 p.stdin.write('%s\0' % line)
389 p.stdin.close()
390 if p.wait() != 0:
391 die('`%s` failed' % ' '.join(cmd))
392 tree_id = run('git', 'write-tree')
393 return tree_id
394
395
Stephen Hines90ced942016-09-22 05:52:55 +0000396def clang_format_to_blob(filename, line_ranges, revision=None,
397 binary='clang-format', style=None):
Daniel Jaspere7a50012013-05-23 17:53:42 +0000398 """Run clang-format on the given file and save the result to a git blob.
399
Stephen Hines90ced942016-09-22 05:52:55 +0000400 Runs on the file in `revision` if not None, or on the file in the working
401 directory if `revision` is None.
402
Daniel Jaspere7a50012013-05-23 17:53:42 +0000403 Returns the object ID (SHA-1) of the created blob."""
Stephen Hines90ced942016-09-22 05:52:55 +0000404 clang_format_cmd = [binary]
Daniel Jaspere7a50012013-05-23 17:53:42 +0000405 if style:
406 clang_format_cmd.extend(['-style='+style])
Daniel Jasper695bad542013-08-01 18:17:13 +0000407 clang_format_cmd.extend([
408 '-lines=%s:%s' % (start_line, start_line+line_count-1)
409 for start_line, line_count in line_ranges])
Stephen Hines90ced942016-09-22 05:52:55 +0000410 if revision:
411 clang_format_cmd.extend(['-assume-filename='+filename])
412 git_show_cmd = ['git', 'cat-file', 'blob', '%s:%s' % (revision, filename)]
413 git_show = subprocess.Popen(git_show_cmd, stdin=subprocess.PIPE,
414 stdout=subprocess.PIPE)
415 git_show.stdin.close()
416 clang_format_stdin = git_show.stdout
417 else:
418 clang_format_cmd.extend([filename])
419 git_show = None
420 clang_format_stdin = subprocess.PIPE
Daniel Jaspere7a50012013-05-23 17:53:42 +0000421 try:
Stephen Hines90ced942016-09-22 05:52:55 +0000422 clang_format = subprocess.Popen(clang_format_cmd, stdin=clang_format_stdin,
Daniel Jaspere7a50012013-05-23 17:53:42 +0000423 stdout=subprocess.PIPE)
Stephen Hines90ced942016-09-22 05:52:55 +0000424 if clang_format_stdin == subprocess.PIPE:
425 clang_format_stdin = clang_format.stdin
Daniel Jaspere7a50012013-05-23 17:53:42 +0000426 except OSError as e:
427 if e.errno == errno.ENOENT:
428 die('cannot find executable "%s"' % binary)
429 else:
430 raise
Stephen Hines90ced942016-09-22 05:52:55 +0000431 clang_format_stdin.close()
Daniel Jaspere7a50012013-05-23 17:53:42 +0000432 hash_object_cmd = ['git', 'hash-object', '-w', '--path='+filename, '--stdin']
433 hash_object = subprocess.Popen(hash_object_cmd, stdin=clang_format.stdout,
434 stdout=subprocess.PIPE)
435 clang_format.stdout.close()
436 stdout = hash_object.communicate()[0]
437 if hash_object.returncode != 0:
438 die('`%s` failed' % ' '.join(hash_object_cmd))
439 if clang_format.wait() != 0:
440 die('`%s` failed' % ' '.join(clang_format_cmd))
Stephen Hines90ced942016-09-22 05:52:55 +0000441 if git_show and git_show.wait() != 0:
442 die('`%s` failed' % ' '.join(git_show_cmd))
Daniel Jaspere7a50012013-05-23 17:53:42 +0000443 return stdout.rstrip('\r\n')
444
445
446@contextlib.contextmanager
447def temporary_index_file(tree=None):
448 """Context manager for setting GIT_INDEX_FILE to a temporary file and deleting
449 the file afterward."""
450 index_path = create_temporary_index(tree)
451 old_index_path = os.environ.get('GIT_INDEX_FILE')
452 os.environ['GIT_INDEX_FILE'] = index_path
453 try:
454 yield
455 finally:
456 if old_index_path is None:
457 del os.environ['GIT_INDEX_FILE']
458 else:
459 os.environ['GIT_INDEX_FILE'] = old_index_path
460 os.remove(index_path)
461
462
463def create_temporary_index(tree=None):
464 """Create a temporary index file and return the created file's path.
465
466 If `tree` is not None, use that as the tree to read in. Otherwise, an
467 empty index is created."""
468 gitdir = run('git', 'rev-parse', '--git-dir')
469 path = os.path.join(gitdir, temp_index_basename)
470 if tree is None:
471 tree = '--empty'
472 run('git', 'read-tree', '--index-output='+path, tree)
473 return path
474
475
476def print_diff(old_tree, new_tree):
477 """Print the diff between the two trees to stdout."""
478 # We use the porcelain 'diff' and not plumbing 'diff-tree' because the output
479 # is expected to be viewed by the user, and only the former does nice things
480 # like color and pagination.
Stephen Hines90ced942016-09-22 05:52:55 +0000481 #
482 # We also only print modified files since `new_tree` only contains the files
483 # that were modified, so unmodified files would show as deleted without the
484 # filter.
485 subprocess.check_call(['git', 'diff', '--diff-filter=M', old_tree, new_tree,
486 '--'])
Daniel Jaspere7a50012013-05-23 17:53:42 +0000487
488
489def apply_changes(old_tree, new_tree, force=False, patch_mode=False):
490 """Apply the changes in `new_tree` to the working directory.
491
492 Bails if there are local changes in those files and not `force`. If
493 `patch_mode`, runs `git checkout --patch` to select hunks interactively."""
Stephen Hines90ced942016-09-22 05:52:55 +0000494 changed_files = run('git', 'diff-tree', '--diff-filter=M', '-r', '-z',
495 '--name-only', old_tree,
Daniel Jaspere7a50012013-05-23 17:53:42 +0000496 new_tree).rstrip('\0').split('\0')
497 if not force:
498 unstaged_files = run('git', 'diff-files', '--name-status', *changed_files)
499 if unstaged_files:
Eric Fiselier58bef5d2017-04-20 21:05:58 +0000500 print('The following files would be modified but '
501 'have unstaged changes:', file=sys.stderr)
502 print(unstaged_files, file=sys.stderr)
503 print('Please commit, stage, or stash them first.', file=sys.stderr)
Daniel Jaspere7a50012013-05-23 17:53:42 +0000504 sys.exit(2)
505 if patch_mode:
506 # In patch mode, we could just as well create an index from the new tree
507 # and checkout from that, but then the user will be presented with a
508 # message saying "Discard ... from worktree". Instead, we use the old
509 # tree as the index and checkout from new_tree, which gives the slightly
510 # better message, "Apply ... to index and worktree". This is not quite
511 # right, since it won't be applied to the user's index, but oh well.
512 with temporary_index_file(old_tree):
513 subprocess.check_call(['git', 'checkout', '--patch', new_tree])
514 index_tree = old_tree
515 else:
516 with temporary_index_file(new_tree):
517 run('git', 'checkout-index', '-a', '-f')
518 return changed_files
519
520
521def run(*args, **kwargs):
522 stdin = kwargs.pop('stdin', '')
523 verbose = kwargs.pop('verbose', True)
524 strip = kwargs.pop('strip', True)
525 for name in kwargs:
526 raise TypeError("run() got an unexpected keyword argument '%s'" % name)
527 p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
528 stdin=subprocess.PIPE)
529 stdout, stderr = p.communicate(input=stdin)
530 if p.returncode == 0:
531 if stderr:
532 if verbose:
Eric Fiselier58bef5d2017-04-20 21:05:58 +0000533 print('`%s` printed to stderr:' % ' '.join(args), file=sys.stderr)
534 print(stderr.rstrip(), file=sys.stderr)
Daniel Jaspere7a50012013-05-23 17:53:42 +0000535 if strip:
536 stdout = stdout.rstrip('\r\n')
537 return stdout
538 if verbose:
Eric Fiselier58bef5d2017-04-20 21:05:58 +0000539 print('`%s` returned %s' % (' '.join(args), p.returncode), file=sys.stderr)
Daniel Jaspere7a50012013-05-23 17:53:42 +0000540 if stderr:
Eric Fiselier58bef5d2017-04-20 21:05:58 +0000541 print(stderr.rstrip(), file=sys.stderr)
Daniel Jaspere7a50012013-05-23 17:53:42 +0000542 sys.exit(2)
543
544
545def die(message):
Eric Fiselier58bef5d2017-04-20 21:05:58 +0000546 print('error:', message, file=sys.stderr)
Daniel Jaspere7a50012013-05-23 17:53:42 +0000547 sys.exit(2)
548
549
550if __name__ == '__main__':
551 main()