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