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++
786bcf27bb9a4b5c3f79cb44c0e4654a6d7619ad89Stephen Hines      # Other languages that clang-format supports
796bcf27bb9a4b5c3f79cb44c0e4654a6d7619ad89Stephen Hines      'proto', 'protodevel',  # Protocol Buffers
806bcf27bb9a4b5c3f79cb44c0e4654a6d7619ad89Stephen Hines      'js',  # JavaScript
814507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper      ])
824507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper
834507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  p = argparse.ArgumentParser(
844507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    usage=usage, formatter_class=argparse.RawDescriptionHelpFormatter,
854507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    description=desc)
864507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  p.add_argument('--binary',
874507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper                 default=config.get('clangformat.binary', 'clang-format'),
884507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper                 help='path to clang-format'),
894507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  p.add_argument('--commit',
904507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper                 default=config.get('clangformat.commit', 'HEAD'),
914507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper                 help='default commit to use if none is specified'),
924507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  p.add_argument('--diff', action='store_true',
934507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper                 help='print a diff instead of applying the changes')
944507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  p.add_argument('--extensions',
954507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper                 default=config.get('clangformat.extensions',
964507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper                                    default_extensions),
974507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper                 help=('comma-separated list of file extensions to format, '
984507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper                       'excluding the period and case-insensitive')),
994507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  p.add_argument('-f', '--force', action='store_true',
1004507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper                 help='allow changes to unstaged files')
1014507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  p.add_argument('-p', '--patch', action='store_true',
1024507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper                 help='select hunks interactively')
1034507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  p.add_argument('-q', '--quiet', action='count', default=0,
1044507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper                 help='print less information')
1054507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  p.add_argument('--style',
1064507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper                 default=config.get('clangformat.style', None),
1074507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper                 help='passed to clang-format'),
1084507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  p.add_argument('-v', '--verbose', action='count', default=0,
1094507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper                 help='print extra information')
1104507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  # We gather all the remaining positional arguments into 'args' since we need
1114507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  # to use some heuristics to determine whether or not <commit> was present.
1124507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  # However, to print pretty messages, we make use of metavar and help.
1134507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  p.add_argument('args', nargs='*', metavar='<commit>',
1144507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper                 help='revision from which to compute the diff')
1154507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  p.add_argument('ignored', nargs='*', metavar='<file>...',
1164507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper                 help='if specified, only consider differences in these files')
1174507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  opts = p.parse_args(argv)
1184507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper
1194507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  opts.verbose -= opts.quiet
1204507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  del opts.quiet
1214507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper
1224507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  commit, files = interpret_args(opts.args, dash_dash, opts.commit)
1234507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  changed_lines = compute_diff_and_extract_lines(commit, files)
1244507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  if opts.verbose >= 1:
1254507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    ignored_files = set(changed_lines)
1264507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  filter_by_extension(changed_lines, opts.extensions.lower().split(','))
1274507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  if opts.verbose >= 1:
1284507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    ignored_files.difference_update(changed_lines)
1294507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    if ignored_files:
1304507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper      print 'Ignoring changes in the following files (wrong extension):'
1314507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper      for filename in ignored_files:
1324507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper        print '   ', filename
1334507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    if changed_lines:
1344507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper      print 'Running clang-format on the following files:'
1354507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper      for filename in changed_lines:
1364507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper        print '   ', filename
1374507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  if not changed_lines:
1384507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    print 'no modified files to format'
1394507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    return
1404507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  # The computed diff outputs absolute paths, so we must cd before accessing
1414507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  # those files.
1424507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  cd_to_toplevel()
14365e2b74344de606145c0bc5aa6209d375db9e5ebDaniel Jasper  old_tree = create_tree_from_workdir(changed_lines)
14465e2b74344de606145c0bc5aa6209d375db9e5ebDaniel Jasper  new_tree = run_clang_format_and_save_to_tree(changed_lines,
1454507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper                                               binary=opts.binary,
1464507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper                                               style=opts.style)
1474507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  if opts.verbose >= 1:
1484507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    print 'old tree:', old_tree
1494507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    print 'new tree:', new_tree
1504507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  if old_tree == new_tree:
1514507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    if opts.verbose >= 0:
1524507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper      print 'clang-format did not modify any files'
1534507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  elif opts.diff:
1544507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    print_diff(old_tree, new_tree)
1554507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  else:
1564507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    changed_files = apply_changes(old_tree, new_tree, force=opts.force,
1574507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper                                  patch_mode=opts.patch)
1584507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    if (opts.verbose >= 0 and not opts.patch) or opts.verbose >= 1:
1594507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper      print 'changed files:'
1604507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper      for filename in changed_files:
1614507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper        print '   ', filename
1624507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper
1634507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper
1644507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasperdef load_git_config(non_string_options=None):
1654507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  """Return the git configuration as a dictionary.
1664507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper
1674507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  All options are assumed to be strings unless in `non_string_options`, in which
1684507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  is a dictionary mapping option name (in lower case) to either "--bool" or
1694507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  "--int"."""
1704507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  if non_string_options is None:
1714507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    non_string_options = {}
1724507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  out = {}
1734507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  for entry in run('git', 'config', '--list', '--null').split('\0'):
1744507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    if entry:
1754507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper      name, value = entry.split('\n', 1)
1764507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper      if name in non_string_options:
1774507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper        value = run('git', 'config', non_string_options[name], name)
1784507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper      out[name] = value
1794507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  return out
1804507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper
1814507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper
1824507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasperdef interpret_args(args, dash_dash, default_commit):
1834507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  """Interpret `args` as "[commit] [--] [files...]" and return (commit, files).
1844507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper
1854507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  It is assumed that "--" and everything that follows has been removed from
1864507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  args and placed in `dash_dash`.
1874507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper
1884507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  If "--" is present (i.e., `dash_dash` is non-empty), the argument to its
1894507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  left (if present) is taken as commit.  Otherwise, the first argument is
1904507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  checked if it is a commit or a file.  If commit is not given,
1914507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  `default_commit` is used."""
1924507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  if dash_dash:
1934507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    if len(args) == 0:
1944507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper      commit = default_commit
1954507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    elif len(args) > 1:
1964507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper      die('at most one commit allowed; %d given' % len(args))
1974507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    else:
1984507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper      commit = args[0]
1994507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    object_type = get_object_type(commit)
2004507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    if object_type not in ('commit', 'tag'):
2014507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper      if object_type is None:
2024507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper        die("'%s' is not a commit" % commit)
2034507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper      else:
2044507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper        die("'%s' is a %s, but a commit was expected" % (commit, object_type))
2054507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    files = dash_dash[1:]
2064507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  elif args:
2074507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    if disambiguate_revision(args[0]):
2084507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper      commit = args[0]
2094507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper      files = args[1:]
2104507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    else:
2114507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper      commit = default_commit
2124507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper      files = args
2134507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  else:
2144507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    commit = default_commit
2154507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    files = []
2164507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  return commit, files
2174507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper
2184507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper
2194507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasperdef disambiguate_revision(value):
2204507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  """Returns True if `value` is a revision, False if it is a file, or dies."""
2214507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  # If `value` is ambiguous (neither a commit nor a file), the following
2224507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  # command will die with an appropriate error message.
2234507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  run('git', 'rev-parse', value, verbose=False)
2244507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  object_type = get_object_type(value)
2254507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  if object_type is None:
2264507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    return False
2274507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  if object_type in ('commit', 'tag'):
2284507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    return True
2294507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  die('`%s` is a %s, but a commit or filename was expected' %
2304507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper      (value, object_type))
2314507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper
2324507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper
2334507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasperdef get_object_type(value):
2344507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  """Returns a string description of an object's type, or None if it is not
2354507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  a valid git object."""
2364507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  cmd = ['git', 'cat-file', '-t', value]
2374507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
2384507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  stdout, stderr = p.communicate()
2394507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  if p.returncode != 0:
2404507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    return None
2414507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  return stdout.strip()
2424507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper
2434507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper
2444507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasperdef compute_diff_and_extract_lines(commit, files):
2454507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  """Calls compute_diff() followed by extract_lines()."""
2464507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  diff_process = compute_diff(commit, files)
2474507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  changed_lines = extract_lines(diff_process.stdout)
2484507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  diff_process.stdout.close()
2494507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  diff_process.wait()
2504507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  if diff_process.returncode != 0:
2514507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    # Assume error was already printed to stderr.
2524507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    sys.exit(2)
2534507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  return changed_lines
2544507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper
2554507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper
2564507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasperdef compute_diff(commit, files):
2574507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  """Return a subprocess object producing the diff from `commit`.
2584507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper
2594507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  The return value's `stdin` file object will produce a patch with the
2604507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  differences between the working directory and `commit`, filtered on `files`
2614507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  (if non-empty).  Zero context lines are used in the patch."""
2624507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  cmd = ['git', 'diff-index', '-p', '-U0', commit, '--']
2634507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  cmd.extend(files)
2644507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
2654507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  p.stdin.close()
2664507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  return p
2674507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper
2684507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper
2694507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasperdef extract_lines(patch_file):
2704507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  """Extract the changed lines in `patch_file`.
2714507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper
27265e2b74344de606145c0bc5aa6209d375db9e5ebDaniel Jasper  The return value is a dictionary mapping filename to a list of (start_line,
27365e2b74344de606145c0bc5aa6209d375db9e5ebDaniel Jasper  line_count) pairs.
27465e2b74344de606145c0bc5aa6209d375db9e5ebDaniel Jasper
2754507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  The input must have been produced with ``-U0``, meaning unidiff format with
2764507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  zero lines of context.  The return value is a dict mapping filename to a
2774507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  list of line `Range`s."""
2784507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  matches = {}
2794507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  for line in patch_file:
2804507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    match = re.search(r'^\+\+\+\ [^/]+/(.*)', line)
2814507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    if match:
2824507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper      filename = match.group(1).rstrip('\r\n')
2834507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    match = re.search(r'^@@ -[0-9,]+ \+(\d+)(,(\d+))?', line)
2844507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    if match:
2854507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper      start_line = int(match.group(1))
2864507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper      line_count = 1
2874507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper      if match.group(3):
2884507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper        line_count = int(match.group(3))
2894507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper      if line_count > 0:
2904507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper        matches.setdefault(filename, []).append(Range(start_line, line_count))
2914507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  return matches
2924507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper
2934507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper
2944507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasperdef filter_by_extension(dictionary, allowed_extensions):
2954507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  """Delete every key in `dictionary` that doesn't have an allowed extension.
2964507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper
2974507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  `allowed_extensions` must be a collection of lowercase file extensions,
2984507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  excluding the period."""
2994507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  allowed_extensions = frozenset(allowed_extensions)
3004507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  for filename in dictionary.keys():
3014507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    base_ext = filename.rsplit('.', 1)
3024507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    if len(base_ext) == 1 or base_ext[1].lower() not in allowed_extensions:
3034507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper      del dictionary[filename]
3044507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper
3054507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper
3064507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasperdef cd_to_toplevel():
3074507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  """Change to the top level of the git repository."""
3084507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  toplevel = run('git', 'rev-parse', '--show-toplevel')
3094507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  os.chdir(toplevel)
3104507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper
3114507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper
3124507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasperdef create_tree_from_workdir(filenames):
3134507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  """Create a new git tree with the given files from the working directory.
3144507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper
3154507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  Returns the object ID (SHA-1) of the created tree."""
3164507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  return create_tree(filenames, '--stdin')
3174507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper
3184507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper
31965e2b74344de606145c0bc5aa6209d375db9e5ebDaniel Jasperdef run_clang_format_and_save_to_tree(changed_lines, binary='clang-format',
3204507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper                                      style=None):
3214507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  """Run clang-format on each file and save the result to a git tree.
3224507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper
3234507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  Returns the object ID (SHA-1) of the created tree."""
3244507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  def index_info_generator():
32565e2b74344de606145c0bc5aa6209d375db9e5ebDaniel Jasper    for filename, line_ranges in changed_lines.iteritems():
3264507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper      mode = oct(os.stat(filename).st_mode)
32765e2b74344de606145c0bc5aa6209d375db9e5ebDaniel Jasper      blob_id = clang_format_to_blob(filename, line_ranges, binary=binary,
3284507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper                                     style=style)
3294507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper      yield '%s %s\t%s' % (mode, blob_id, filename)
3304507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  return create_tree(index_info_generator(), '--index-info')
3314507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper
3324507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper
3334507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasperdef create_tree(input_lines, mode):
3344507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  """Create a tree object from the given input.
3354507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper
3364507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  If mode is '--stdin', it must be a list of filenames.  If mode is
3374507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  '--index-info' is must be a list of values suitable for "git update-index
3384507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  --index-info", such as "<mode> <SP> <sha1> <TAB> <filename>".  Any other mode
3394507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  is invalid."""
3404507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  assert mode in ('--stdin', '--index-info')
3414507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  cmd = ['git', 'update-index', '--add', '-z', mode]
3424507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  with temporary_index_file():
3434507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    p = subprocess.Popen(cmd, stdin=subprocess.PIPE)
3444507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    for line in input_lines:
3454507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper      p.stdin.write('%s\0' % line)
3464507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    p.stdin.close()
3474507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    if p.wait() != 0:
3484507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper      die('`%s` failed' % ' '.join(cmd))
3494507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    tree_id = run('git', 'write-tree')
3504507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    return tree_id
3514507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper
3524507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper
35365e2b74344de606145c0bc5aa6209d375db9e5ebDaniel Jasperdef clang_format_to_blob(filename, line_ranges, binary='clang-format',
3544507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper                         style=None):
3554507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  """Run clang-format on the given file and save the result to a git blob.
3564507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper
3574507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  Returns the object ID (SHA-1) of the created blob."""
3584507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  clang_format_cmd = [binary, filename]
3594507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  if style:
3604507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    clang_format_cmd.extend(['-style='+style])
36165e2b74344de606145c0bc5aa6209d375db9e5ebDaniel Jasper  clang_format_cmd.extend([
36265e2b74344de606145c0bc5aa6209d375db9e5ebDaniel Jasper      '-lines=%s:%s' % (start_line, start_line+line_count-1)
36365e2b74344de606145c0bc5aa6209d375db9e5ebDaniel Jasper      for start_line, line_count in line_ranges])
3644507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  try:
3654507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    clang_format = subprocess.Popen(clang_format_cmd, stdin=subprocess.PIPE,
3664507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper                                    stdout=subprocess.PIPE)
3674507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  except OSError as e:
3684507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    if e.errno == errno.ENOENT:
3694507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper      die('cannot find executable "%s"' % binary)
3704507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    else:
3714507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper      raise
3724507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  clang_format.stdin.close()
3734507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  hash_object_cmd = ['git', 'hash-object', '-w', '--path='+filename, '--stdin']
3744507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  hash_object = subprocess.Popen(hash_object_cmd, stdin=clang_format.stdout,
3754507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper                                 stdout=subprocess.PIPE)
3764507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  clang_format.stdout.close()
3774507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  stdout = hash_object.communicate()[0]
3784507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  if hash_object.returncode != 0:
3794507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    die('`%s` failed' % ' '.join(hash_object_cmd))
3804507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  if clang_format.wait() != 0:
3814507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    die('`%s` failed' % ' '.join(clang_format_cmd))
3824507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  return stdout.rstrip('\r\n')
3834507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper
3844507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper
3854507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper@contextlib.contextmanager
3864507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasperdef temporary_index_file(tree=None):
3874507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  """Context manager for setting GIT_INDEX_FILE to a temporary file and deleting
3884507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  the file afterward."""
3894507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  index_path = create_temporary_index(tree)
3904507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  old_index_path = os.environ.get('GIT_INDEX_FILE')
3914507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  os.environ['GIT_INDEX_FILE'] = index_path
3924507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  try:
3934507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    yield
3944507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  finally:
3954507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    if old_index_path is None:
3964507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper      del os.environ['GIT_INDEX_FILE']
3974507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    else:
3984507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper      os.environ['GIT_INDEX_FILE'] = old_index_path
3994507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    os.remove(index_path)
4004507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper
4014507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper
4024507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasperdef create_temporary_index(tree=None):
4034507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  """Create a temporary index file and return the created file's path.
4044507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper
4054507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  If `tree` is not None, use that as the tree to read in.  Otherwise, an
4064507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  empty index is created."""
4074507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  gitdir = run('git', 'rev-parse', '--git-dir')
4084507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  path = os.path.join(gitdir, temp_index_basename)
4094507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  if tree is None:
4104507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    tree = '--empty'
4114507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  run('git', 'read-tree', '--index-output='+path, tree)
4124507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  return path
4134507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper
4144507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper
4154507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasperdef print_diff(old_tree, new_tree):
4164507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  """Print the diff between the two trees to stdout."""
4174507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  # We use the porcelain 'diff' and not plumbing 'diff-tree' because the output
4184507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  # is expected to be viewed by the user, and only the former does nice things
4194507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  # like color and pagination.
4204507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  subprocess.check_call(['git', 'diff', old_tree, new_tree, '--'])
4214507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper
4224507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper
4234507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasperdef apply_changes(old_tree, new_tree, force=False, patch_mode=False):
4244507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  """Apply the changes in `new_tree` to the working directory.
4254507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper
4264507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  Bails if there are local changes in those files and not `force`.  If
4274507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  `patch_mode`, runs `git checkout --patch` to select hunks interactively."""
4284507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  changed_files = run('git', 'diff-tree', '-r', '-z', '--name-only', old_tree,
4294507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper                      new_tree).rstrip('\0').split('\0')
4304507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  if not force:
4314507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    unstaged_files = run('git', 'diff-files', '--name-status', *changed_files)
4324507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    if unstaged_files:
4334507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper      print >>sys.stderr, ('The following files would be modified but '
4344507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper                           'have unstaged changes:')
4354507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper      print >>sys.stderr, unstaged_files
4364507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper      print >>sys.stderr, 'Please commit, stage, or stash them first.'
4374507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper      sys.exit(2)
4384507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  if patch_mode:
4394507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    # In patch mode, we could just as well create an index from the new tree
4404507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    # and checkout from that, but then the user will be presented with a
4414507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    # message saying "Discard ... from worktree".  Instead, we use the old
4424507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    # tree as the index and checkout from new_tree, which gives the slightly
4434507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    # better message, "Apply ... to index and worktree".  This is not quite
4444507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    # right, since it won't be applied to the user's index, but oh well.
4454507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    with temporary_index_file(old_tree):
4464507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper      subprocess.check_call(['git', 'checkout', '--patch', new_tree])
4474507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    index_tree = old_tree
4484507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  else:
4494507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    with temporary_index_file(new_tree):
4504507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper      run('git', 'checkout-index', '-a', '-f')
4514507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  return changed_files
4524507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper
4534507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper
4544507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasperdef run(*args, **kwargs):
4554507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  stdin = kwargs.pop('stdin', '')
4564507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  verbose = kwargs.pop('verbose', True)
4574507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  strip = kwargs.pop('strip', True)
4584507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  for name in kwargs:
4594507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    raise TypeError("run() got an unexpected keyword argument '%s'" % name)
4604507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
4614507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper                       stdin=subprocess.PIPE)
4624507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  stdout, stderr = p.communicate(input=stdin)
4634507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  if p.returncode == 0:
4644507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    if stderr:
4654507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper      if verbose:
4664507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper        print >>sys.stderr, '`%s` printed to stderr:' % ' '.join(args)
4674507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper      print >>sys.stderr, stderr.rstrip()
4684507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    if strip:
4694507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper      stdout = stdout.rstrip('\r\n')
4704507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    return stdout
4714507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  if verbose:
4724507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    print >>sys.stderr, '`%s` returned %s' % (' '.join(args), p.returncode)
4734507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  if stderr:
4744507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper    print >>sys.stderr, stderr.rstrip()
4754507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  sys.exit(2)
4764507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper
4774507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper
4784507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasperdef die(message):
4794507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  print >>sys.stderr, 'error:', message
4804507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  sys.exit(2)
4814507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper
4824507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper
4834507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasperif __name__ == '__main__':
4844507a2cc6f65c8891c574cf0d80c1cad2d3cffaaDaniel Jasper  main()
485