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