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