xref: /netbsd-src/external/apache2/llvm/dist/clang/tools/clang-format/git-clang-format (revision e038c9c4676b0f19b1b7dd08a940c6ed64a6d5ae)
17330f729Sjoerg#!/usr/bin/env python
27330f729Sjoerg#
37330f729Sjoerg#===- git-clang-format - ClangFormat Git Integration ---------*- python -*--===#
47330f729Sjoerg#
57330f729Sjoerg# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
67330f729Sjoerg# See https://llvm.org/LICENSE.txt for license information.
77330f729Sjoerg# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
87330f729Sjoerg#
97330f729Sjoerg#===------------------------------------------------------------------------===#
107330f729Sjoerg
117330f729Sjoergr"""
127330f729Sjoergclang-format git integration
137330f729Sjoerg============================
147330f729Sjoerg
157330f729SjoergThis file provides a clang-format integration for git. Put it somewhere in your
167330f729Sjoergpath and ensure that it is executable. Then, "git clang-format" will invoke
177330f729Sjoergclang-format on the changes in current files or a specific commit.
187330f729Sjoerg
197330f729SjoergFor further details, run:
207330f729Sjoerggit clang-format -h
217330f729Sjoerg
227330f729SjoergRequires Python 2.7 or Python 3
237330f729Sjoerg"""
247330f729Sjoerg
257330f729Sjoergfrom __future__ import absolute_import, division, print_function
267330f729Sjoergimport argparse
277330f729Sjoergimport collections
287330f729Sjoergimport contextlib
297330f729Sjoergimport errno
307330f729Sjoergimport os
317330f729Sjoergimport re
327330f729Sjoergimport subprocess
337330f729Sjoergimport sys
347330f729Sjoerg
357330f729Sjoergusage = 'git clang-format [OPTIONS] [<commit>] [<commit>] [--] [<file>...]'
367330f729Sjoerg
377330f729Sjoergdesc = '''
387330f729SjoergIf zero or one commits are given, run clang-format on all lines that differ
397330f729Sjoergbetween the working directory and <commit>, which defaults to HEAD.  Changes are
407330f729Sjoergonly applied to the working directory.
417330f729Sjoerg
427330f729SjoergIf two commits are given (requires --diff), run clang-format on all lines in the
437330f729Sjoergsecond <commit> that differ from the first <commit>.
447330f729Sjoerg
457330f729SjoergThe following git-config settings set the default of the corresponding option:
467330f729Sjoerg  clangFormat.binary
477330f729Sjoerg  clangFormat.commit
48*e038c9c4Sjoerg  clangFormat.extensions
497330f729Sjoerg  clangFormat.style
507330f729Sjoerg'''
517330f729Sjoerg
527330f729Sjoerg# Name of the temporary index file in which save the output of clang-format.
537330f729Sjoerg# This file is created within the .git directory.
547330f729Sjoergtemp_index_basename = 'clang-format-index'
557330f729Sjoerg
567330f729Sjoerg
577330f729SjoergRange = collections.namedtuple('Range', 'start, count')
587330f729Sjoerg
597330f729Sjoerg
607330f729Sjoergdef main():
617330f729Sjoerg  config = load_git_config()
627330f729Sjoerg
637330f729Sjoerg  # In order to keep '--' yet allow options after positionals, we need to
647330f729Sjoerg  # check for '--' ourselves.  (Setting nargs='*' throws away the '--', while
657330f729Sjoerg  # nargs=argparse.REMAINDER disallows options after positionals.)
667330f729Sjoerg  argv = sys.argv[1:]
677330f729Sjoerg  try:
687330f729Sjoerg    idx = argv.index('--')
697330f729Sjoerg  except ValueError:
707330f729Sjoerg    dash_dash = []
717330f729Sjoerg  else:
727330f729Sjoerg    dash_dash = argv[idx:]
737330f729Sjoerg    argv = argv[:idx]
747330f729Sjoerg
757330f729Sjoerg  default_extensions = ','.join([
767330f729Sjoerg      # From clang/lib/Frontend/FrontendOptions.cpp, all lower case
777330f729Sjoerg      'c', 'h',  # C
787330f729Sjoerg      'm',  # ObjC
797330f729Sjoerg      'mm',  # ObjC++
807330f729Sjoerg      'cc', 'cp', 'cpp', 'c++', 'cxx', 'hh', 'hpp', 'hxx',  # C++
81*e038c9c4Sjoerg      'cu', 'cuh',  # CUDA
827330f729Sjoerg      # Other languages that clang-format supports
837330f729Sjoerg      'proto', 'protodevel',  # Protocol Buffers
847330f729Sjoerg      'java',  # Java
857330f729Sjoerg      'js',  # JavaScript
867330f729Sjoerg      'ts',  # TypeScript
877330f729Sjoerg      'cs',  # C Sharp
887330f729Sjoerg      ])
897330f729Sjoerg
907330f729Sjoerg  p = argparse.ArgumentParser(
917330f729Sjoerg    usage=usage, formatter_class=argparse.RawDescriptionHelpFormatter,
927330f729Sjoerg    description=desc)
937330f729Sjoerg  p.add_argument('--binary',
947330f729Sjoerg                 default=config.get('clangformat.binary', 'clang-format'),
957330f729Sjoerg                 help='path to clang-format'),
967330f729Sjoerg  p.add_argument('--commit',
977330f729Sjoerg                 default=config.get('clangformat.commit', 'HEAD'),
987330f729Sjoerg                 help='default commit to use if none is specified'),
997330f729Sjoerg  p.add_argument('--diff', action='store_true',
1007330f729Sjoerg                 help='print a diff instead of applying the changes')
1017330f729Sjoerg  p.add_argument('--extensions',
1027330f729Sjoerg                 default=config.get('clangformat.extensions',
1037330f729Sjoerg                                    default_extensions),
1047330f729Sjoerg                 help=('comma-separated list of file extensions to format, '
1057330f729Sjoerg                       'excluding the period and case-insensitive')),
1067330f729Sjoerg  p.add_argument('-f', '--force', action='store_true',
1077330f729Sjoerg                 help='allow changes to unstaged files')
1087330f729Sjoerg  p.add_argument('-p', '--patch', action='store_true',
1097330f729Sjoerg                 help='select hunks interactively')
1107330f729Sjoerg  p.add_argument('-q', '--quiet', action='count', default=0,
1117330f729Sjoerg                 help='print less information')
1127330f729Sjoerg  p.add_argument('--style',
1137330f729Sjoerg                 default=config.get('clangformat.style', None),
1147330f729Sjoerg                 help='passed to clang-format'),
1157330f729Sjoerg  p.add_argument('-v', '--verbose', action='count', default=0,
1167330f729Sjoerg                 help='print extra information')
1177330f729Sjoerg  # We gather all the remaining positional arguments into 'args' since we need
1187330f729Sjoerg  # to use some heuristics to determine whether or not <commit> was present.
1197330f729Sjoerg  # However, to print pretty messages, we make use of metavar and help.
1207330f729Sjoerg  p.add_argument('args', nargs='*', metavar='<commit>',
1217330f729Sjoerg                 help='revision from which to compute the diff')
1227330f729Sjoerg  p.add_argument('ignored', nargs='*', metavar='<file>...',
1237330f729Sjoerg                 help='if specified, only consider differences in these files')
1247330f729Sjoerg  opts = p.parse_args(argv)
1257330f729Sjoerg
1267330f729Sjoerg  opts.verbose -= opts.quiet
1277330f729Sjoerg  del opts.quiet
1287330f729Sjoerg
1297330f729Sjoerg  commits, files = interpret_args(opts.args, dash_dash, opts.commit)
1307330f729Sjoerg  if len(commits) > 1:
1317330f729Sjoerg    if not opts.diff:
1327330f729Sjoerg      die('--diff is required when two commits are given')
1337330f729Sjoerg  else:
1347330f729Sjoerg    if len(commits) > 2:
1357330f729Sjoerg      die('at most two commits allowed; %d given' % len(commits))
1367330f729Sjoerg  changed_lines = compute_diff_and_extract_lines(commits, files)
1377330f729Sjoerg  if opts.verbose >= 1:
1387330f729Sjoerg    ignored_files = set(changed_lines)
1397330f729Sjoerg  filter_by_extension(changed_lines, opts.extensions.lower().split(','))
140*e038c9c4Sjoerg  # The computed diff outputs absolute paths, so we must cd before accessing
141*e038c9c4Sjoerg  # those files.
142*e038c9c4Sjoerg  cd_to_toplevel()
143*e038c9c4Sjoerg  filter_symlinks(changed_lines)
1447330f729Sjoerg  if opts.verbose >= 1:
1457330f729Sjoerg    ignored_files.difference_update(changed_lines)
1467330f729Sjoerg    if ignored_files:
147*e038c9c4Sjoerg      print(
148*e038c9c4Sjoerg        'Ignoring changes in the following files (wrong extension or symlink):')
1497330f729Sjoerg      for filename in ignored_files:
1507330f729Sjoerg        print('    %s' % filename)
1517330f729Sjoerg    if changed_lines:
1527330f729Sjoerg      print('Running clang-format on the following files:')
1537330f729Sjoerg      for filename in changed_lines:
1547330f729Sjoerg        print('    %s' % filename)
1557330f729Sjoerg  if not changed_lines:
156*e038c9c4Sjoerg    if opts.verbose >= 0:
1577330f729Sjoerg      print('no modified files to format')
1587330f729Sjoerg    return
1597330f729Sjoerg  if len(commits) > 1:
1607330f729Sjoerg    old_tree = commits[1]
1617330f729Sjoerg    new_tree = run_clang_format_and_save_to_tree(changed_lines,
1627330f729Sjoerg                                                 revision=commits[1],
1637330f729Sjoerg                                                 binary=opts.binary,
1647330f729Sjoerg                                                 style=opts.style)
1657330f729Sjoerg  else:
1667330f729Sjoerg    old_tree = create_tree_from_workdir(changed_lines)
1677330f729Sjoerg    new_tree = run_clang_format_and_save_to_tree(changed_lines,
1687330f729Sjoerg                                                 binary=opts.binary,
1697330f729Sjoerg                                                 style=opts.style)
1707330f729Sjoerg  if opts.verbose >= 1:
1717330f729Sjoerg    print('old tree: %s' % old_tree)
1727330f729Sjoerg    print('new tree: %s' % new_tree)
1737330f729Sjoerg  if old_tree == new_tree:
1747330f729Sjoerg    if opts.verbose >= 0:
1757330f729Sjoerg      print('clang-format did not modify any files')
1767330f729Sjoerg  elif opts.diff:
1777330f729Sjoerg    print_diff(old_tree, new_tree)
1787330f729Sjoerg  else:
1797330f729Sjoerg    changed_files = apply_changes(old_tree, new_tree, force=opts.force,
1807330f729Sjoerg                                  patch_mode=opts.patch)
1817330f729Sjoerg    if (opts.verbose >= 0 and not opts.patch) or opts.verbose >= 1:
1827330f729Sjoerg      print('changed files:')
1837330f729Sjoerg      for filename in changed_files:
1847330f729Sjoerg        print('    %s' % filename)
1857330f729Sjoerg
1867330f729Sjoerg
1877330f729Sjoergdef load_git_config(non_string_options=None):
1887330f729Sjoerg  """Return the git configuration as a dictionary.
1897330f729Sjoerg
1907330f729Sjoerg  All options are assumed to be strings unless in `non_string_options`, in which
1917330f729Sjoerg  is a dictionary mapping option name (in lower case) to either "--bool" or
1927330f729Sjoerg  "--int"."""
1937330f729Sjoerg  if non_string_options is None:
1947330f729Sjoerg    non_string_options = {}
1957330f729Sjoerg  out = {}
1967330f729Sjoerg  for entry in run('git', 'config', '--list', '--null').split('\0'):
1977330f729Sjoerg    if entry:
198*e038c9c4Sjoerg      if '\n' in entry:
1997330f729Sjoerg        name, value = entry.split('\n', 1)
200*e038c9c4Sjoerg      else:
201*e038c9c4Sjoerg        # A setting with no '=' ('\n' with --null) is implicitly 'true'
202*e038c9c4Sjoerg        name = entry
203*e038c9c4Sjoerg        value = 'true'
2047330f729Sjoerg      if name in non_string_options:
2057330f729Sjoerg        value = run('git', 'config', non_string_options[name], name)
2067330f729Sjoerg      out[name] = value
2077330f729Sjoerg  return out
2087330f729Sjoerg
2097330f729Sjoerg
2107330f729Sjoergdef interpret_args(args, dash_dash, default_commit):
2117330f729Sjoerg  """Interpret `args` as "[commits] [--] [files]" and return (commits, files).
2127330f729Sjoerg
2137330f729Sjoerg  It is assumed that "--" and everything that follows has been removed from
2147330f729Sjoerg  args and placed in `dash_dash`.
2157330f729Sjoerg
2167330f729Sjoerg  If "--" is present (i.e., `dash_dash` is non-empty), the arguments to its
2177330f729Sjoerg  left (if present) are taken as commits.  Otherwise, the arguments are checked
2187330f729Sjoerg  from left to right if they are commits or files.  If commits are not given,
2197330f729Sjoerg  a list with `default_commit` is used."""
2207330f729Sjoerg  if dash_dash:
2217330f729Sjoerg    if len(args) == 0:
2227330f729Sjoerg      commits = [default_commit]
2237330f729Sjoerg    else:
2247330f729Sjoerg      commits = args
2257330f729Sjoerg    for commit in commits:
2267330f729Sjoerg      object_type = get_object_type(commit)
2277330f729Sjoerg      if object_type not in ('commit', 'tag'):
2287330f729Sjoerg        if object_type is None:
2297330f729Sjoerg          die("'%s' is not a commit" % commit)
2307330f729Sjoerg        else:
2317330f729Sjoerg          die("'%s' is a %s, but a commit was expected" % (commit, object_type))
2327330f729Sjoerg    files = dash_dash[1:]
2337330f729Sjoerg  elif args:
2347330f729Sjoerg    commits = []
2357330f729Sjoerg    while args:
2367330f729Sjoerg      if not disambiguate_revision(args[0]):
2377330f729Sjoerg        break
2387330f729Sjoerg      commits.append(args.pop(0))
2397330f729Sjoerg    if not commits:
2407330f729Sjoerg      commits = [default_commit]
2417330f729Sjoerg    files = args
2427330f729Sjoerg  else:
2437330f729Sjoerg    commits = [default_commit]
2447330f729Sjoerg    files = []
2457330f729Sjoerg  return commits, files
2467330f729Sjoerg
2477330f729Sjoerg
2487330f729Sjoergdef disambiguate_revision(value):
2497330f729Sjoerg  """Returns True if `value` is a revision, False if it is a file, or dies."""
2507330f729Sjoerg  # If `value` is ambiguous (neither a commit nor a file), the following
2517330f729Sjoerg  # command will die with an appropriate error message.
2527330f729Sjoerg  run('git', 'rev-parse', value, verbose=False)
2537330f729Sjoerg  object_type = get_object_type(value)
2547330f729Sjoerg  if object_type is None:
2557330f729Sjoerg    return False
2567330f729Sjoerg  if object_type in ('commit', 'tag'):
2577330f729Sjoerg    return True
2587330f729Sjoerg  die('`%s` is a %s, but a commit or filename was expected' %
2597330f729Sjoerg      (value, object_type))
2607330f729Sjoerg
2617330f729Sjoerg
2627330f729Sjoergdef get_object_type(value):
2637330f729Sjoerg  """Returns a string description of an object's type, or None if it is not
2647330f729Sjoerg  a valid git object."""
2657330f729Sjoerg  cmd = ['git', 'cat-file', '-t', value]
2667330f729Sjoerg  p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
2677330f729Sjoerg  stdout, stderr = p.communicate()
2687330f729Sjoerg  if p.returncode != 0:
2697330f729Sjoerg    return None
2707330f729Sjoerg  return convert_string(stdout.strip())
2717330f729Sjoerg
2727330f729Sjoerg
2737330f729Sjoergdef compute_diff_and_extract_lines(commits, files):
2747330f729Sjoerg  """Calls compute_diff() followed by extract_lines()."""
2757330f729Sjoerg  diff_process = compute_diff(commits, files)
2767330f729Sjoerg  changed_lines = extract_lines(diff_process.stdout)
2777330f729Sjoerg  diff_process.stdout.close()
2787330f729Sjoerg  diff_process.wait()
2797330f729Sjoerg  if diff_process.returncode != 0:
2807330f729Sjoerg    # Assume error was already printed to stderr.
2817330f729Sjoerg    sys.exit(2)
2827330f729Sjoerg  return changed_lines
2837330f729Sjoerg
2847330f729Sjoerg
2857330f729Sjoergdef compute_diff(commits, files):
2867330f729Sjoerg  """Return a subprocess object producing the diff from `commits`.
2877330f729Sjoerg
2887330f729Sjoerg  The return value's `stdin` file object will produce a patch with the
2897330f729Sjoerg  differences between the working directory and the first commit if a single
2907330f729Sjoerg  one was specified, or the difference between both specified commits, filtered
2917330f729Sjoerg  on `files` (if non-empty).  Zero context lines are used in the patch."""
2927330f729Sjoerg  git_tool = 'diff-index'
2937330f729Sjoerg  if len(commits) > 1:
2947330f729Sjoerg    git_tool = 'diff-tree'
2957330f729Sjoerg  cmd = ['git', git_tool, '-p', '-U0'] + commits + ['--']
2967330f729Sjoerg  cmd.extend(files)
2977330f729Sjoerg  p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
2987330f729Sjoerg  p.stdin.close()
2997330f729Sjoerg  return p
3007330f729Sjoerg
3017330f729Sjoerg
3027330f729Sjoergdef extract_lines(patch_file):
3037330f729Sjoerg  """Extract the changed lines in `patch_file`.
3047330f729Sjoerg
3057330f729Sjoerg  The return value is a dictionary mapping filename to a list of (start_line,
3067330f729Sjoerg  line_count) pairs.
3077330f729Sjoerg
3087330f729Sjoerg  The input must have been produced with ``-U0``, meaning unidiff format with
3097330f729Sjoerg  zero lines of context.  The return value is a dict mapping filename to a
3107330f729Sjoerg  list of line `Range`s."""
3117330f729Sjoerg  matches = {}
3127330f729Sjoerg  for line in patch_file:
3137330f729Sjoerg    line = convert_string(line)
3147330f729Sjoerg    match = re.search(r'^\+\+\+\ [^/]+/(.*)', line)
3157330f729Sjoerg    if match:
3167330f729Sjoerg      filename = match.group(1).rstrip('\r\n')
3177330f729Sjoerg    match = re.search(r'^@@ -[0-9,]+ \+(\d+)(,(\d+))?', line)
3187330f729Sjoerg    if match:
3197330f729Sjoerg      start_line = int(match.group(1))
3207330f729Sjoerg      line_count = 1
3217330f729Sjoerg      if match.group(3):
3227330f729Sjoerg        line_count = int(match.group(3))
3237330f729Sjoerg      if line_count > 0:
3247330f729Sjoerg        matches.setdefault(filename, []).append(Range(start_line, line_count))
3257330f729Sjoerg  return matches
3267330f729Sjoerg
3277330f729Sjoerg
3287330f729Sjoergdef filter_by_extension(dictionary, allowed_extensions):
3297330f729Sjoerg  """Delete every key in `dictionary` that doesn't have an allowed extension.
3307330f729Sjoerg
3317330f729Sjoerg  `allowed_extensions` must be a collection of lowercase file extensions,
3327330f729Sjoerg  excluding the period."""
3337330f729Sjoerg  allowed_extensions = frozenset(allowed_extensions)
3347330f729Sjoerg  for filename in list(dictionary.keys()):
3357330f729Sjoerg    base_ext = filename.rsplit('.', 1)
3367330f729Sjoerg    if len(base_ext) == 1 and '' in allowed_extensions:
3377330f729Sjoerg        continue
3387330f729Sjoerg    if len(base_ext) == 1 or base_ext[1].lower() not in allowed_extensions:
3397330f729Sjoerg      del dictionary[filename]
3407330f729Sjoerg
3417330f729Sjoerg
342*e038c9c4Sjoergdef filter_symlinks(dictionary):
343*e038c9c4Sjoerg  """Delete every key in `dictionary` that is a symlink."""
344*e038c9c4Sjoerg  for filename in list(dictionary.keys()):
345*e038c9c4Sjoerg    if os.path.islink(filename):
346*e038c9c4Sjoerg      del dictionary[filename]
347*e038c9c4Sjoerg
348*e038c9c4Sjoerg
3497330f729Sjoergdef cd_to_toplevel():
3507330f729Sjoerg  """Change to the top level of the git repository."""
3517330f729Sjoerg  toplevel = run('git', 'rev-parse', '--show-toplevel')
3527330f729Sjoerg  os.chdir(toplevel)
3537330f729Sjoerg
3547330f729Sjoerg
3557330f729Sjoergdef create_tree_from_workdir(filenames):
3567330f729Sjoerg  """Create a new git tree with the given files from the working directory.
3577330f729Sjoerg
3587330f729Sjoerg  Returns the object ID (SHA-1) of the created tree."""
3597330f729Sjoerg  return create_tree(filenames, '--stdin')
3607330f729Sjoerg
3617330f729Sjoerg
3627330f729Sjoergdef run_clang_format_and_save_to_tree(changed_lines, revision=None,
3637330f729Sjoerg                                      binary='clang-format', style=None):
3647330f729Sjoerg  """Run clang-format on each file and save the result to a git tree.
3657330f729Sjoerg
3667330f729Sjoerg  Returns the object ID (SHA-1) of the created tree."""
3677330f729Sjoerg  def iteritems(container):
3687330f729Sjoerg      try:
3697330f729Sjoerg          return container.iteritems() # Python 2
3707330f729Sjoerg      except AttributeError:
3717330f729Sjoerg          return container.items() # Python 3
3727330f729Sjoerg  def index_info_generator():
3737330f729Sjoerg    for filename, line_ranges in iteritems(changed_lines):
3747330f729Sjoerg      if revision:
3757330f729Sjoerg        git_metadata_cmd = ['git', 'ls-tree',
3767330f729Sjoerg                            '%s:%s' % (revision, os.path.dirname(filename)),
3777330f729Sjoerg                            os.path.basename(filename)]
3787330f729Sjoerg        git_metadata = subprocess.Popen(git_metadata_cmd, stdin=subprocess.PIPE,
3797330f729Sjoerg                                        stdout=subprocess.PIPE)
3807330f729Sjoerg        stdout = git_metadata.communicate()[0]
3817330f729Sjoerg        mode = oct(int(stdout.split()[0], 8))
3827330f729Sjoerg      else:
3837330f729Sjoerg        mode = oct(os.stat(filename).st_mode)
3847330f729Sjoerg      # Adjust python3 octal format so that it matches what git expects
3857330f729Sjoerg      if mode.startswith('0o'):
3867330f729Sjoerg          mode = '0' + mode[2:]
3877330f729Sjoerg      blob_id = clang_format_to_blob(filename, line_ranges,
3887330f729Sjoerg                                     revision=revision,
3897330f729Sjoerg                                     binary=binary,
3907330f729Sjoerg                                     style=style)
3917330f729Sjoerg      yield '%s %s\t%s' % (mode, blob_id, filename)
3927330f729Sjoerg  return create_tree(index_info_generator(), '--index-info')
3937330f729Sjoerg
3947330f729Sjoerg
3957330f729Sjoergdef create_tree(input_lines, mode):
3967330f729Sjoerg  """Create a tree object from the given input.
3977330f729Sjoerg
3987330f729Sjoerg  If mode is '--stdin', it must be a list of filenames.  If mode is
3997330f729Sjoerg  '--index-info' is must be a list of values suitable for "git update-index
4007330f729Sjoerg  --index-info", such as "<mode> <SP> <sha1> <TAB> <filename>".  Any other mode
4017330f729Sjoerg  is invalid."""
4027330f729Sjoerg  assert mode in ('--stdin', '--index-info')
4037330f729Sjoerg  cmd = ['git', 'update-index', '--add', '-z', mode]
4047330f729Sjoerg  with temporary_index_file():
4057330f729Sjoerg    p = subprocess.Popen(cmd, stdin=subprocess.PIPE)
4067330f729Sjoerg    for line in input_lines:
4077330f729Sjoerg      p.stdin.write(to_bytes('%s\0' % line))
4087330f729Sjoerg    p.stdin.close()
4097330f729Sjoerg    if p.wait() != 0:
4107330f729Sjoerg      die('`%s` failed' % ' '.join(cmd))
4117330f729Sjoerg    tree_id = run('git', 'write-tree')
4127330f729Sjoerg    return tree_id
4137330f729Sjoerg
4147330f729Sjoerg
4157330f729Sjoergdef clang_format_to_blob(filename, line_ranges, revision=None,
4167330f729Sjoerg                         binary='clang-format', style=None):
4177330f729Sjoerg  """Run clang-format on the given file and save the result to a git blob.
4187330f729Sjoerg
4197330f729Sjoerg  Runs on the file in `revision` if not None, or on the file in the working
4207330f729Sjoerg  directory if `revision` is None.
4217330f729Sjoerg
4227330f729Sjoerg  Returns the object ID (SHA-1) of the created blob."""
4237330f729Sjoerg  clang_format_cmd = [binary]
4247330f729Sjoerg  if style:
4257330f729Sjoerg    clang_format_cmd.extend(['-style='+style])
4267330f729Sjoerg  clang_format_cmd.extend([
4277330f729Sjoerg      '-lines=%s:%s' % (start_line, start_line+line_count-1)
4287330f729Sjoerg      for start_line, line_count in line_ranges])
4297330f729Sjoerg  if revision:
4307330f729Sjoerg    clang_format_cmd.extend(['-assume-filename='+filename])
4317330f729Sjoerg    git_show_cmd = ['git', 'cat-file', 'blob', '%s:%s' % (revision, filename)]
4327330f729Sjoerg    git_show = subprocess.Popen(git_show_cmd, stdin=subprocess.PIPE,
4337330f729Sjoerg                                stdout=subprocess.PIPE)
4347330f729Sjoerg    git_show.stdin.close()
4357330f729Sjoerg    clang_format_stdin = git_show.stdout
4367330f729Sjoerg  else:
4377330f729Sjoerg    clang_format_cmd.extend([filename])
4387330f729Sjoerg    git_show = None
4397330f729Sjoerg    clang_format_stdin = subprocess.PIPE
4407330f729Sjoerg  try:
4417330f729Sjoerg    clang_format = subprocess.Popen(clang_format_cmd, stdin=clang_format_stdin,
4427330f729Sjoerg                                    stdout=subprocess.PIPE)
4437330f729Sjoerg    if clang_format_stdin == subprocess.PIPE:
4447330f729Sjoerg      clang_format_stdin = clang_format.stdin
4457330f729Sjoerg  except OSError as e:
4467330f729Sjoerg    if e.errno == errno.ENOENT:
4477330f729Sjoerg      die('cannot find executable "%s"' % binary)
4487330f729Sjoerg    else:
4497330f729Sjoerg      raise
4507330f729Sjoerg  clang_format_stdin.close()
4517330f729Sjoerg  hash_object_cmd = ['git', 'hash-object', '-w', '--path='+filename, '--stdin']
4527330f729Sjoerg  hash_object = subprocess.Popen(hash_object_cmd, stdin=clang_format.stdout,
4537330f729Sjoerg                                 stdout=subprocess.PIPE)
4547330f729Sjoerg  clang_format.stdout.close()
4557330f729Sjoerg  stdout = hash_object.communicate()[0]
4567330f729Sjoerg  if hash_object.returncode != 0:
4577330f729Sjoerg    die('`%s` failed' % ' '.join(hash_object_cmd))
4587330f729Sjoerg  if clang_format.wait() != 0:
4597330f729Sjoerg    die('`%s` failed' % ' '.join(clang_format_cmd))
4607330f729Sjoerg  if git_show and git_show.wait() != 0:
4617330f729Sjoerg    die('`%s` failed' % ' '.join(git_show_cmd))
4627330f729Sjoerg  return convert_string(stdout).rstrip('\r\n')
4637330f729Sjoerg
4647330f729Sjoerg
4657330f729Sjoerg@contextlib.contextmanager
4667330f729Sjoergdef temporary_index_file(tree=None):
4677330f729Sjoerg  """Context manager for setting GIT_INDEX_FILE to a temporary file and deleting
4687330f729Sjoerg  the file afterward."""
4697330f729Sjoerg  index_path = create_temporary_index(tree)
4707330f729Sjoerg  old_index_path = os.environ.get('GIT_INDEX_FILE')
4717330f729Sjoerg  os.environ['GIT_INDEX_FILE'] = index_path
4727330f729Sjoerg  try:
4737330f729Sjoerg    yield
4747330f729Sjoerg  finally:
4757330f729Sjoerg    if old_index_path is None:
4767330f729Sjoerg      del os.environ['GIT_INDEX_FILE']
4777330f729Sjoerg    else:
4787330f729Sjoerg      os.environ['GIT_INDEX_FILE'] = old_index_path
4797330f729Sjoerg    os.remove(index_path)
4807330f729Sjoerg
4817330f729Sjoerg
4827330f729Sjoergdef create_temporary_index(tree=None):
4837330f729Sjoerg  """Create a temporary index file and return the created file's path.
4847330f729Sjoerg
4857330f729Sjoerg  If `tree` is not None, use that as the tree to read in.  Otherwise, an
4867330f729Sjoerg  empty index is created."""
4877330f729Sjoerg  gitdir = run('git', 'rev-parse', '--git-dir')
4887330f729Sjoerg  path = os.path.join(gitdir, temp_index_basename)
4897330f729Sjoerg  if tree is None:
4907330f729Sjoerg    tree = '--empty'
4917330f729Sjoerg  run('git', 'read-tree', '--index-output='+path, tree)
4927330f729Sjoerg  return path
4937330f729Sjoerg
4947330f729Sjoerg
4957330f729Sjoergdef print_diff(old_tree, new_tree):
4967330f729Sjoerg  """Print the diff between the two trees to stdout."""
4977330f729Sjoerg  # We use the porcelain 'diff' and not plumbing 'diff-tree' because the output
4987330f729Sjoerg  # is expected to be viewed by the user, and only the former does nice things
4997330f729Sjoerg  # like color and pagination.
5007330f729Sjoerg  #
5017330f729Sjoerg  # We also only print modified files since `new_tree` only contains the files
5027330f729Sjoerg  # that were modified, so unmodified files would show as deleted without the
5037330f729Sjoerg  # filter.
5047330f729Sjoerg  subprocess.check_call(['git', 'diff', '--diff-filter=M', old_tree, new_tree,
5057330f729Sjoerg                         '--'])
5067330f729Sjoerg
5077330f729Sjoerg
5087330f729Sjoergdef apply_changes(old_tree, new_tree, force=False, patch_mode=False):
5097330f729Sjoerg  """Apply the changes in `new_tree` to the working directory.
5107330f729Sjoerg
5117330f729Sjoerg  Bails if there are local changes in those files and not `force`.  If
5127330f729Sjoerg  `patch_mode`, runs `git checkout --patch` to select hunks interactively."""
5137330f729Sjoerg  changed_files = run('git', 'diff-tree', '--diff-filter=M', '-r', '-z',
5147330f729Sjoerg                      '--name-only', old_tree,
5157330f729Sjoerg                      new_tree).rstrip('\0').split('\0')
5167330f729Sjoerg  if not force:
5177330f729Sjoerg    unstaged_files = run('git', 'diff-files', '--name-status', *changed_files)
5187330f729Sjoerg    if unstaged_files:
5197330f729Sjoerg      print('The following files would be modified but '
5207330f729Sjoerg                'have unstaged changes:', file=sys.stderr)
5217330f729Sjoerg      print(unstaged_files, file=sys.stderr)
5227330f729Sjoerg      print('Please commit, stage, or stash them first.', file=sys.stderr)
5237330f729Sjoerg      sys.exit(2)
5247330f729Sjoerg  if patch_mode:
5257330f729Sjoerg    # In patch mode, we could just as well create an index from the new tree
5267330f729Sjoerg    # and checkout from that, but then the user will be presented with a
5277330f729Sjoerg    # message saying "Discard ... from worktree".  Instead, we use the old
5287330f729Sjoerg    # tree as the index and checkout from new_tree, which gives the slightly
5297330f729Sjoerg    # better message, "Apply ... to index and worktree".  This is not quite
5307330f729Sjoerg    # right, since it won't be applied to the user's index, but oh well.
5317330f729Sjoerg    with temporary_index_file(old_tree):
5327330f729Sjoerg      subprocess.check_call(['git', 'checkout', '--patch', new_tree])
5337330f729Sjoerg    index_tree = old_tree
5347330f729Sjoerg  else:
5357330f729Sjoerg    with temporary_index_file(new_tree):
5367330f729Sjoerg      run('git', 'checkout-index', '-a', '-f')
5377330f729Sjoerg  return changed_files
5387330f729Sjoerg
5397330f729Sjoerg
5407330f729Sjoergdef run(*args, **kwargs):
5417330f729Sjoerg  stdin = kwargs.pop('stdin', '')
5427330f729Sjoerg  verbose = kwargs.pop('verbose', True)
5437330f729Sjoerg  strip = kwargs.pop('strip', True)
5447330f729Sjoerg  for name in kwargs:
5457330f729Sjoerg    raise TypeError("run() got an unexpected keyword argument '%s'" % name)
5467330f729Sjoerg  p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
5477330f729Sjoerg                       stdin=subprocess.PIPE)
5487330f729Sjoerg  stdout, stderr = p.communicate(input=stdin)
5497330f729Sjoerg
5507330f729Sjoerg  stdout = convert_string(stdout)
5517330f729Sjoerg  stderr = convert_string(stderr)
5527330f729Sjoerg
5537330f729Sjoerg  if p.returncode == 0:
5547330f729Sjoerg    if stderr:
5557330f729Sjoerg      if verbose:
5567330f729Sjoerg        print('`%s` printed to stderr:' % ' '.join(args), file=sys.stderr)
5577330f729Sjoerg      print(stderr.rstrip(), file=sys.stderr)
5587330f729Sjoerg    if strip:
5597330f729Sjoerg      stdout = stdout.rstrip('\r\n')
5607330f729Sjoerg    return stdout
5617330f729Sjoerg  if verbose:
5627330f729Sjoerg    print('`%s` returned %s' % (' '.join(args), p.returncode), file=sys.stderr)
5637330f729Sjoerg  if stderr:
5647330f729Sjoerg    print(stderr.rstrip(), file=sys.stderr)
5657330f729Sjoerg  sys.exit(2)
5667330f729Sjoerg
5677330f729Sjoerg
5687330f729Sjoergdef die(message):
5697330f729Sjoerg  print('error:', message, file=sys.stderr)
5707330f729Sjoerg  sys.exit(2)
5717330f729Sjoerg
5727330f729Sjoerg
5737330f729Sjoergdef to_bytes(str_input):
5747330f729Sjoerg    # Encode to UTF-8 to get binary data.
5757330f729Sjoerg    if isinstance(str_input, bytes):
5767330f729Sjoerg        return str_input
5777330f729Sjoerg    return str_input.encode('utf-8')
5787330f729Sjoerg
5797330f729Sjoerg
5807330f729Sjoergdef to_string(bytes_input):
5817330f729Sjoerg    if isinstance(bytes_input, str):
5827330f729Sjoerg        return bytes_input
5837330f729Sjoerg    return bytes_input.encode('utf-8')
5847330f729Sjoerg
5857330f729Sjoerg
5867330f729Sjoergdef convert_string(bytes_input):
5877330f729Sjoerg    try:
5887330f729Sjoerg        return to_string(bytes_input.decode('utf-8'))
5897330f729Sjoerg    except AttributeError: # 'str' object has no attribute 'decode'.
5907330f729Sjoerg        return str(bytes_input)
5917330f729Sjoerg    except UnicodeError:
5927330f729Sjoerg        return str(bytes_input)
5937330f729Sjoerg
5947330f729Sjoergif __name__ == '__main__':
5957330f729Sjoerg  main()
596