Merge pull request #747 from Kriskras99/master

clang-format update and addition
This commit is contained in:
Ben Vanik 2017-07-12 18:38:14 -07:00 committed by GitHub
commit e11ba02e06
6 changed files with 605 additions and 99 deletions

63
third_party/clang-format/LICENSE.TXT vendored Normal file
View File

@ -0,0 +1,63 @@
==============================================================================
LLVM Release License
==============================================================================
University of Illinois/NCSA
Open Source License
Copyright (c) 2007-2016 University of Illinois at Urbana-Champaign.
All rights reserved.
Developed by:
LLVM Team
University of Illinois at Urbana-Champaign
http://llvm.org
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal with
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimers.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimers in the
documentation and/or other materials provided with the distribution.
* Neither the names of the LLVM Team, University of Illinois at
Urbana-Champaign, nor the names of its contributors may be used to
endorse or promote products derived from this Software without specific
prior written permission.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE
SOFTWARE.
==============================================================================
The LLVM software contains code written by third parties. Such software will
have its own individual LICENSE.TXT file in the directory in which it appears.
This file will describe the copyrights, license, and restrictions which apply
to that code.
The disclaimer of warranty in the University of Illinois Open Source License
applies to all code in the LLVM Distribution, and nothing in any of the
other licenses gives permission to use the names of the LLVM Team or the
University of Illinois to endorse or promote products derived from this
Software.
The following pieces of software have additional or alternate copyrights,
licenses, and/or restrictions:
Program Directory
------- ---------
<none yet>

View File

@ -0,0 +1,27 @@
-- In this file, change "/path/to/" to the path where you installed clang-format
-- and save it to ~/Library/Application Support/BBEdit/Scripts. You can then
-- select the script from the Script menu and clang-format will format the
-- selection. Note that you can rename the menu item by renaming the script, and
-- can assign the menu item a keyboard shortcut in the BBEdit preferences, under
-- Menus & Shortcuts.
on urlToPOSIXPath(theURL)
return do shell script "python -c \"import urllib, urlparse, sys; print urllib.unquote(urlparse.urlparse(sys.argv[1])[2])\" " & quoted form of theURL
end urlToPOSIXPath
tell application "BBEdit"
set selectionOffset to characterOffset of selection
set selectionLength to length of selection
set fileURL to URL of text document 1
end tell
set filePath to urlToPOSIXPath(fileURL)
set newContents to do shell script "/path/to/clang-format -offset=" & selectionOffset & " -length=" & selectionLength & " " & quoted form of filePath
tell application "BBEdit"
-- "set contents of text document 1 to newContents" scrolls to the bottom while
-- replacing a selection flashes a bit but doesn't affect the scroll position.
set currentLength to length of contents of text document 1
select characters 1 thru currentLength of text document 1
set text of selection to newContents
select characters selectionOffset thru (selectionOffset + selectionLength - 1) of text document 1
end tell

View File

@ -17,7 +17,7 @@ This script reads input from a unified diff and reformats all the changed
lines. This is useful to reformat all the lines touched by a specific patch. lines. This is useful to reformat all the lines touched by a specific patch.
Example usage for git/svn users: Example usage for git/svn users:
git diff -U0 HEAD^ | clang-format-diff.py -p1 -i git diff -U0 --no-color HEAD^ | clang-format-diff.py -p1 -i
svn diff --diff-cmd=diff -x-U0 | clang-format-diff.py -i svn diff --diff-cmd=diff -x-U0 | clang-format-diff.py -i
""" """
@ -31,10 +31,6 @@ import StringIO
import sys import sys
# Change this to the full path if clang-format is not on the path.
binary = 'clang-format'
def main(): def main():
parser = argparse.ArgumentParser(description= parser = argparse.ArgumentParser(description=
'Reformat changed lines in diff. Without -i ' 'Reformat changed lines in diff. Without -i '
@ -48,16 +44,19 @@ def main():
help='custom pattern selecting file paths to reformat ' help='custom pattern selecting file paths to reformat '
'(case sensitive, overrides -iregex)') '(case sensitive, overrides -iregex)')
parser.add_argument('-iregex', metavar='PATTERN', default= parser.add_argument('-iregex', metavar='PATTERN', default=
r'.*\.(cpp|cc|c\+\+|cxx|c|cl|h|hpp|m|mm|inc|js|proto' r'.*\.(cpp|cc|c\+\+|cxx|c|cl|h|hpp|m|mm|inc|js|ts|proto'
r'|protodevel|java)', r'|protodevel|java)',
help='custom pattern selecting file paths to reformat ' help='custom pattern selecting file paths to reformat '
'(case insensitive, overridden by -regex)') '(case insensitive, overridden by -regex)')
parser.add_argument('-sort-includes', action='store_true', default=False,
help='let clang-format sort include blocks')
parser.add_argument('-v', '--verbose', action='store_true', parser.add_argument('-v', '--verbose', action='store_true',
help='be more verbose, ineffective without -i') help='be more verbose, ineffective without -i')
parser.add_argument( parser.add_argument('-style',
'-style', help='formatting style to apply (LLVM, Google, Chromium, '
help= 'Mozilla, WebKit)')
'formatting style to apply (LLVM, Google, Chromium, Mozilla, WebKit)') parser.add_argument('-binary', default='clang-format',
help='location of binary to use for clang-format')
args = parser.parse_args() args = parser.parse_args()
# Extract changed lines for each file. # Extract changed lines for each file.
@ -93,9 +92,11 @@ def main():
for filename, lines in lines_by_file.iteritems(): for filename, lines in lines_by_file.iteritems():
if args.i and args.verbose: if args.i and args.verbose:
print 'Formatting', filename print 'Formatting', filename
command = [binary, filename] command = [args.binary, filename]
if args.i: if args.i:
command.append('-i') command.append('-i')
if args.sort_includes:
command.append('-sort-includes')
command.extend(lines) command.extend(lines)
if args.style: if args.style:
command.extend(['-style', args.style]) command.extend(['-style', args.style])

193
third_party/clang-format/clang-format.el vendored Normal file
View File

@ -0,0 +1,193 @@
;;; clang-format.el --- Format code using clang-format -*- lexical-binding: t; -*-
;; Keywords: tools, c
;; Package-Requires: ((cl-lib "0.3"))
;;; Commentary:
;; This package allows to filter code through clang-format to fix its formatting.
;; clang-format is a tool that formats C/C++/Obj-C code according to a set of
;; style options, see <http://clang.llvm.org/docs/ClangFormatStyleOptions.html>.
;; Note that clang-format 3.4 or newer is required.
;; clang-format.el is available via MELPA and can be installed via
;;
;; M-x package-install clang-format
;;
;; when ("melpa" . "http://melpa.org/packages/") is included in
;; `package-archives'. Alternatively, ensure the directory of this
;; file is in your `load-path' and add
;;
;; (require 'clang-format)
;;
;; to your .emacs configuration.
;; You may also want to bind `clang-format-region' to a key:
;;
;; (global-set-key [C-M-tab] 'clang-format-region)
;;; Code:
(require 'cl-lib)
(require 'xml)
(defgroup clang-format nil
"Format code using clang-format."
:group 'tools)
(defcustom clang-format-executable
(or (executable-find "clang-format")
"clang-format")
"Location of the clang-format executable.
A string containing the name or the full path of the executable."
:group 'clang-format
:type '(file :must-match t)
:risky t)
(defcustom clang-format-style "file"
"Style argument to pass to clang-format.
By default clang-format will load the style configuration from
a file named .clang-format located in one of the parent directories
of the buffer."
:group 'clang-format
:type 'string
:safe #'stringp)
(make-variable-buffer-local 'clang-format-style)
(defun clang-format--extract (xml-node)
"Extract replacements and cursor information from XML-NODE."
(unless (and (listp xml-node) (eq (xml-node-name xml-node) 'replacements))
(error "Expected <replacements> node"))
(let ((nodes (xml-node-children xml-node))
(incomplete-format (xml-get-attribute xml-node 'incomplete_format))
replacements
cursor)
(dolist (node nodes)
(when (listp node)
(let* ((children (xml-node-children node))
(text (car children)))
(cl-case (xml-node-name node)
('replacement
(let* ((offset (xml-get-attribute-or-nil node 'offset))
(length (xml-get-attribute-or-nil node 'length)))
(when (or (null offset) (null length))
(error "<replacement> node does not have offset and length attributes"))
(when (cdr children)
(error "More than one child node in <replacement> node"))
(setq offset (string-to-number offset))
(setq length (string-to-number length))
(push (list offset length text) replacements)))
('cursor
(setq cursor (string-to-number text)))))))
;; Sort by decreasing offset, length.
(setq replacements (sort (delq nil replacements)
(lambda (a b)
(or (> (car a) (car b))
(and (= (car a) (car b))
(> (cadr a) (cadr b)))))))
(list replacements cursor (string= incomplete-format "true"))))
(defun clang-format--replace (offset length &optional text)
"Replace the region defined by OFFSET and LENGTH with TEXT.
OFFSET and LENGTH are measured in bytes, not characters. OFFSET
is a zero-based file offset, assuming utf-8-unix coding."
(let ((start (clang-format--filepos-to-bufferpos offset 'exact 'utf-8-unix))
(end (clang-format--filepos-to-bufferpos (+ offset length) 'exact
'utf-8-unix)))
(goto-char start)
(delete-region start end)
(when text
(insert text))))
;; bufferpos-to-filepos and filepos-to-bufferpos are new in Emacs 25.1.
;; Provide fallbacks for older versions.
(defalias 'clang-format--bufferpos-to-filepos
(if (fboundp 'bufferpos-to-filepos)
'bufferpos-to-filepos
(lambda (position &optional _quality _coding-system)
(1- (position-bytes position)))))
(defalias 'clang-format--filepos-to-bufferpos
(if (fboundp 'filepos-to-bufferpos)
'filepos-to-bufferpos
(lambda (byte &optional _quality _coding-system)
(byte-to-position (1+ byte)))))
;;;###autoload
(defun clang-format-region (start end &optional style)
"Use clang-format to format the code between START and END according to STYLE.
If called interactively uses the region or the current statement if there
is no active region. If no style is given uses `clang-format-style'."
(interactive
(if (use-region-p)
(list (region-beginning) (region-end))
(list (point) (point))))
(unless style
(setq style clang-format-style))
(let ((file-start (clang-format--bufferpos-to-filepos start 'approximate
'utf-8-unix))
(file-end (clang-format--bufferpos-to-filepos end 'approximate
'utf-8-unix))
(cursor (clang-format--bufferpos-to-filepos (point) 'exact 'utf-8-unix))
(temp-buffer (generate-new-buffer " *clang-format-temp*"))
(temp-file (make-temp-file "clang-format"))
;; Output is XML, which is always UTF-8. Input encoding should match
;; the encoding used to convert between buffer and file positions,
;; otherwise the offsets calculated above are off. For simplicity, we
;; always use utf-8-unix and ignore the buffer coding system.
(default-process-coding-system '(utf-8-unix . utf-8-unix)))
(unwind-protect
(let ((status (call-process-region
nil nil clang-format-executable
nil `(,temp-buffer ,temp-file) nil
"-output-replacements-xml"
"-assume-filename" (or (buffer-file-name) "")
"-style" style
"-offset" (number-to-string file-start)
"-length" (number-to-string (- file-end file-start))
"-cursor" (number-to-string cursor)))
(stderr (with-temp-buffer
(unless (zerop (cadr (insert-file-contents temp-file)))
(insert ": "))
(buffer-substring-no-properties
(point-min) (line-end-position)))))
(cond
((stringp status)
(error "(clang-format killed by signal %s%s)" status stderr))
((not (zerop status))
(error "(clang-format failed with code %d%s)" status stderr)))
(cl-destructuring-bind (replacements cursor incomplete-format)
(with-current-buffer temp-buffer
(clang-format--extract (car (xml-parse-region))))
(save-excursion
(dolist (rpl replacements)
(apply #'clang-format--replace rpl)))
(when cursor
(goto-char (clang-format--filepos-to-bufferpos cursor 'exact
'utf-8-unix)))
(if incomplete-format
(message "(clang-format: incomplete (syntax errors)%s)" stderr)
(message "(clang-format: success%s)" stderr))))
(delete-file temp-file)
(when (buffer-name temp-buffer) (kill-buffer temp-buffer)))))
;;;###autoload
(defun clang-format-buffer (&optional style)
"Use clang-format to format the current buffer according to STYLE."
(interactive)
(clang-format-region (point-min) (point-max) style))
;;;###autoload
(defalias 'clang-format 'clang-format-region)
(provide 'clang-format)
;;; clang-format.el ends here

127
third_party/clang-format/clang-format.py vendored Normal file
View File

@ -0,0 +1,127 @@
# This file is a minimal clang-format vim-integration. To install:
# - Change 'binary' if clang-format is not on the path (see below).
# - Add to your .vimrc:
#
# map <C-I> :pyf <path-to-this-file>/clang-format.py<cr>
# imap <C-I> <c-o>:pyf <path-to-this-file>/clang-format.py<cr>
#
# The first line enables clang-format for NORMAL and VISUAL mode, the second
# line adds support for INSERT mode. Change "C-I" to another binding if you
# need clang-format on a different key (C-I stands for Ctrl+i).
#
# With this integration you can press the bound key and clang-format will
# format the current line in NORMAL and INSERT mode or the selected region in
# VISUAL mode. The line or region is extended to the next bigger syntactic
# entity.
#
# You can also pass in the variable "l:lines" to choose the range for
# formatting. This variable can either contain "<start line>:<end line>" or
# "all" to format the full file. So, to format the full file, write a function
# like:
# :function FormatFile()
# : let l:lines="all"
# : pyf <path-to-this-file>/clang-format.py
# :endfunction
#
# It operates on the current, potentially unsaved buffer and does not create
# or save any files. To revert a formatting, just undo.
from __future__ import print_function
import difflib
import json
import platform
import subprocess
import sys
import vim
# set g:clang_format_path to the path to clang-format if it is not on the path
# Change this to the full path if clang-format is not on the path.
binary = 'clang-format'
if vim.eval('exists("g:clang_format_path")') == "1":
binary = vim.eval('g:clang_format_path')
# Change this to format according to other formatting styles. See the output of
# 'clang-format --help' for a list of supported styles. The default looks for
# a '.clang-format' or '_clang-format' file to indicate the style that should be
# used.
style = 'file'
fallback_style = None
if vim.eval('exists("g:clang_format_fallback_style")') == "1":
fallback_style = vim.eval('g:clang_format_fallback_style')
def get_buffer(encoding):
if platform.python_version_tuple()[0] == '3':
return vim.current.buffer
return [ line.decode(encoding) for line in vim.current.buffer ]
def main():
# Get the current text.
encoding = vim.eval("&encoding")
buf = get_buffer(encoding)
text = '\n'.join(buf)
# Determine range to format.
if vim.eval('exists("l:lines")') == '1':
lines = vim.eval('l:lines')
elif vim.eval('exists("l:formatdiff")') == '1':
with open(vim.current.buffer.name, 'r') as f:
ondisk = f.read().splitlines();
sequence = difflib.SequenceMatcher(None, ondisk, vim.current.buffer)
lines = []
for op in reversed(sequence.get_opcodes()):
if op[0] not in ['equal', 'delete']:
lines += ['-lines', '%s:%s' % (op[3] + 1, op[4])]
if lines == []:
return
else:
lines = ['-lines', '%s:%s' % (vim.current.range.start + 1,
vim.current.range.end + 1)]
# Determine the cursor position.
cursor = int(vim.eval('line2byte(line("."))+col(".")')) - 2
if cursor < 0:
print('Couldn\'t determine cursor position. Is your file empty?')
return
# Avoid flashing an ugly, ugly cmd prompt on Windows when invoking clang-format.
startupinfo = None
if sys.platform.startswith('win32'):
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
startupinfo.wShowWindow = subprocess.SW_HIDE
# Call formatter.
command = [binary, '-style', style, '-cursor', str(cursor)]
if lines != 'all':
command += lines
if fallback_style:
command.extend(['-fallback-style', fallback_style])
if vim.current.buffer.name:
command.extend(['-assume-filename', vim.current.buffer.name])
p = subprocess.Popen(command,
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
stdin=subprocess.PIPE, startupinfo=startupinfo)
stdout, stderr = p.communicate(input=text.encode(encoding))
# If successful, replace buffer contents.
if stderr:
print(stderr)
if not stdout:
print(
'No output from clang-format (crashed?).\n'
'Please report to bugs.llvm.org.'
)
else:
lines = stdout.decode(encoding).split('\n')
output = json.loads(lines[0])
lines = lines[1:]
sequence = difflib.SequenceMatcher(None, buf, lines)
for op in reversed(sequence.get_opcodes()):
if op[0] is not 'equal':
vim.current.buffer[op[1]:op[2]] = lines[op[3]:op[4]]
if output.get('IncompleteFormat'):
print('clang-format: incomplete (syntax errors)')
vim.command('goto %d' % (output['Cursor'] + 1))
main()

View File

@ -20,9 +20,10 @@ clang-format on the changes in current files or a specific commit.
For further details, run: For further details, run:
git clang-format -h git clang-format -h
Requires Python 2.7 Requires Python 2.7 or Python 3
""" """
from __future__ import print_function
import argparse import argparse
import collections import collections
import contextlib import contextlib
@ -32,12 +33,15 @@ import re
import subprocess import subprocess
import sys import sys
usage = 'git clang-format [OPTIONS] [<commit>] [--] [<file>...]' usage = 'git clang-format [OPTIONS] [<commit>] [<commit>] [--] [<file>...]'
desc = ''' desc = '''
Run clang-format on all lines that differ between the working directory If zero or one commits are given, run clang-format on all lines that differ
and <commit>, which defaults to HEAD. Changes are only applied to the working between the working directory and <commit>, which defaults to HEAD. Changes are
directory. only applied to the working directory.
If two commits are given (requires --diff), run clang-format on all lines in the
second <commit> that differ from the first <commit>.
The following git-config settings set the default of the corresponding option: The following git-config settings set the default of the corresponding option:
clangFormat.binary clangFormat.binary
@ -77,7 +81,9 @@ def main():
'cc', 'cp', 'cpp', 'c++', 'cxx', 'hpp', # C++ 'cc', 'cp', 'cpp', 'c++', 'cxx', 'hpp', # C++
# Other languages that clang-format supports # Other languages that clang-format supports
'proto', 'protodevel', # Protocol Buffers 'proto', 'protodevel', # Protocol Buffers
'java', # Java
'js', # JavaScript 'js', # JavaScript
'ts', # TypeScript
]) ])
p = argparse.ArgumentParser( p = argparse.ArgumentParser(
@ -119,46 +125,59 @@ def main():
opts.verbose -= opts.quiet opts.verbose -= opts.quiet
del opts.quiet del opts.quiet
commit, files = interpret_args(opts.args, dash_dash, opts.commit) commits, files = interpret_args(opts.args, dash_dash, opts.commit)
changed_lines = compute_diff_and_extract_lines(commit, files) if len(commits) > 1:
if not opts.diff:
die('--diff is required when two commits are given')
else:
if len(commits) > 2:
die('at most two commits allowed; %d given' % len(commits))
changed_lines = compute_diff_and_extract_lines(commits, files)
if opts.verbose >= 1: if opts.verbose >= 1:
ignored_files = set(changed_lines) ignored_files = set(changed_lines)
filter_by_extension(changed_lines, opts.extensions.lower().split(',')) filter_by_extension(changed_lines, opts.extensions.lower().split(','))
if opts.verbose >= 1: if opts.verbose >= 1:
ignored_files.difference_update(changed_lines) ignored_files.difference_update(changed_lines)
if ignored_files: if ignored_files:
print 'Ignoring changes in the following files (wrong extension):' print('Ignoring changes in the following files (wrong extension):')
for filename in ignored_files: for filename in ignored_files:
print ' ', filename print(' %s' % filename)
if changed_lines: if changed_lines:
print 'Running clang-format on the following files:' print('Running clang-format on the following files:')
for filename in changed_lines: for filename in changed_lines:
print ' ', filename print(' %s' % filename)
if not changed_lines: if not changed_lines:
print 'no modified files to format' print('no modified files to format')
return return
# The computed diff outputs absolute paths, so we must cd before accessing # The computed diff outputs absolute paths, so we must cd before accessing
# those files. # those files.
cd_to_toplevel() cd_to_toplevel()
if len(commits) > 1:
old_tree = commits[1]
new_tree = run_clang_format_and_save_to_tree(changed_lines,
revision=commits[1],
binary=opts.binary,
style=opts.style)
else:
old_tree = create_tree_from_workdir(changed_lines) old_tree = create_tree_from_workdir(changed_lines)
new_tree = run_clang_format_and_save_to_tree(changed_lines, new_tree = run_clang_format_and_save_to_tree(changed_lines,
binary=opts.binary, binary=opts.binary,
style=opts.style) style=opts.style)
if opts.verbose >= 1: if opts.verbose >= 1:
print 'old tree:', old_tree print('old tree: %s' % old_tree)
print 'new tree:', new_tree print('new tree: %s' % new_tree)
if old_tree == new_tree: if old_tree == new_tree:
if opts.verbose >= 0: if opts.verbose >= 0:
print 'clang-format did not modify any files' print('clang-format did not modify any files')
elif opts.diff: elif opts.diff:
print_diff(old_tree, new_tree) print_diff(old_tree, new_tree)
else: else:
changed_files = apply_changes(old_tree, new_tree, force=opts.force, changed_files = apply_changes(old_tree, new_tree, force=opts.force,
patch_mode=opts.patch) patch_mode=opts.patch)
if (opts.verbose >= 0 and not opts.patch) or opts.verbose >= 1: if (opts.verbose >= 0 and not opts.patch) or opts.verbose >= 1:
print 'changed files:' print('changed files:')
for filename in changed_files: for filename in changed_files:
print ' ', filename print(' %s' % filename)
def load_git_config(non_string_options=None): def load_git_config(non_string_options=None):
@ -180,22 +199,21 @@ def load_git_config(non_string_options=None):
def interpret_args(args, dash_dash, default_commit): def interpret_args(args, dash_dash, default_commit):
"""Interpret `args` as "[commit] [--] [files...]" and return (commit, files). """Interpret `args` as "[commits] [--] [files]" and return (commits, files).
It is assumed that "--" and everything that follows has been removed from It is assumed that "--" and everything that follows has been removed from
args and placed in `dash_dash`. args and placed in `dash_dash`.
If "--" is present (i.e., `dash_dash` is non-empty), the argument to its If "--" is present (i.e., `dash_dash` is non-empty), the arguments to its
left (if present) is taken as commit. Otherwise, the first argument is left (if present) are taken as commits. Otherwise, the arguments are checked
checked if it is a commit or a file. If commit is not given, from left to right if they are commits or files. If commits are not given,
`default_commit` is used.""" a list with `default_commit` is used."""
if dash_dash: if dash_dash:
if len(args) == 0: if len(args) == 0:
commit = default_commit commits = [default_commit]
elif len(args) > 1:
die('at most one commit allowed; %d given' % len(args))
else: else:
commit = args[0] commits = args
for commit in commits:
object_type = get_object_type(commit) object_type = get_object_type(commit)
if object_type not in ('commit', 'tag'): if object_type not in ('commit', 'tag'):
if object_type is None: if object_type is None:
@ -204,16 +222,18 @@ def interpret_args(args, dash_dash, default_commit):
die("'%s' is a %s, but a commit was expected" % (commit, object_type)) die("'%s' is a %s, but a commit was expected" % (commit, object_type))
files = dash_dash[1:] files = dash_dash[1:]
elif args: elif args:
if disambiguate_revision(args[0]): commits = []
commit = args[0] while args:
files = args[1:] if not disambiguate_revision(args[0]):
else: break
commit = default_commit commits.append(args.pop(0))
if not commits:
commits = [default_commit]
files = args files = args
else: else:
commit = default_commit commits = [default_commit]
files = [] files = []
return commit, files return commits, files
def disambiguate_revision(value): def disambiguate_revision(value):
@ -238,12 +258,12 @@ def get_object_type(value):
stdout, stderr = p.communicate() stdout, stderr = p.communicate()
if p.returncode != 0: if p.returncode != 0:
return None return None
return stdout.strip() return convert_string(stdout.strip())
def compute_diff_and_extract_lines(commit, files): def compute_diff_and_extract_lines(commits, files):
"""Calls compute_diff() followed by extract_lines().""" """Calls compute_diff() followed by extract_lines()."""
diff_process = compute_diff(commit, files) diff_process = compute_diff(commits, files)
changed_lines = extract_lines(diff_process.stdout) changed_lines = extract_lines(diff_process.stdout)
diff_process.stdout.close() diff_process.stdout.close()
diff_process.wait() diff_process.wait()
@ -253,13 +273,17 @@ def compute_diff_and_extract_lines(commit, files):
return changed_lines return changed_lines
def compute_diff(commit, files): def compute_diff(commits, files):
"""Return a subprocess object producing the diff from `commit`. """Return a subprocess object producing the diff from `commits`.
The return value's `stdin` file object will produce a patch with the The return value's `stdin` file object will produce a patch with the
differences between the working directory and `commit`, filtered on `files` differences between the working directory and the first commit if a single
(if non-empty). Zero context lines are used in the patch.""" one was specified, or the difference between both specified commits, filtered
cmd = ['git', 'diff-index', '-p', '-U0', commit, '--'] on `files` (if non-empty). Zero context lines are used in the patch."""
git_tool = 'diff-index'
if len(commits) > 1:
git_tool = 'diff-tree'
cmd = ['git', git_tool, '-p', '-U0'] + commits + ['--']
cmd.extend(files) cmd.extend(files)
p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE) p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
p.stdin.close() p.stdin.close()
@ -277,6 +301,7 @@ def extract_lines(patch_file):
list of line `Range`s.""" list of line `Range`s."""
matches = {} matches = {}
for line in patch_file: for line in patch_file:
line = convert_string(line)
match = re.search(r'^\+\+\+\ [^/]+/(.*)', line) match = re.search(r'^\+\+\+\ [^/]+/(.*)', line)
if match: if match:
filename = match.group(1).rstrip('\r\n') filename = match.group(1).rstrip('\r\n')
@ -297,8 +322,10 @@ def filter_by_extension(dictionary, allowed_extensions):
`allowed_extensions` must be a collection of lowercase file extensions, `allowed_extensions` must be a collection of lowercase file extensions,
excluding the period.""" excluding the period."""
allowed_extensions = frozenset(allowed_extensions) allowed_extensions = frozenset(allowed_extensions)
for filename in dictionary.keys(): for filename in list(dictionary.keys()):
base_ext = filename.rsplit('.', 1) base_ext = filename.rsplit('.', 1)
if len(base_ext) == 1 and '' in allowed_extensions:
continue
if len(base_ext) == 1 or base_ext[1].lower() not in allowed_extensions: if len(base_ext) == 1 or base_ext[1].lower() not in allowed_extensions:
del dictionary[filename] del dictionary[filename]
@ -316,15 +343,34 @@ def create_tree_from_workdir(filenames):
return create_tree(filenames, '--stdin') return create_tree(filenames, '--stdin')
def run_clang_format_and_save_to_tree(changed_lines, binary='clang-format', def run_clang_format_and_save_to_tree(changed_lines, revision=None,
style=None): binary='clang-format', style=None):
"""Run clang-format on each file and save the result to a git tree. """Run clang-format on each file and save the result to a git tree.
Returns the object ID (SHA-1) of the created tree.""" Returns the object ID (SHA-1) of the created tree."""
def iteritems(container):
try:
return container.iteritems() # Python 2
except AttributeError:
return container.items() # Python 3
def index_info_generator(): def index_info_generator():
for filename, line_ranges in changed_lines.iteritems(): for filename, line_ranges in iteritems(changed_lines):
if revision:
git_metadata_cmd = ['git', 'ls-tree',
'%s:%s' % (revision, os.path.dirname(filename)),
os.path.basename(filename)]
git_metadata = subprocess.Popen(git_metadata_cmd, stdin=subprocess.PIPE,
stdout=subprocess.PIPE)
stdout = git_metadata.communicate()[0]
mode = oct(int(stdout.split()[0], 8))
else:
mode = oct(os.stat(filename).st_mode) mode = oct(os.stat(filename).st_mode)
blob_id = clang_format_to_blob(filename, line_ranges, binary=binary, # Adjust python3 octal format so that it matches what git expects
if mode.startswith('0o'):
mode = '0' + mode[2:]
blob_id = clang_format_to_blob(filename, line_ranges,
revision=revision,
binary=binary,
style=style) style=style)
yield '%s %s\t%s' % (mode, blob_id, filename) yield '%s %s\t%s' % (mode, blob_id, filename)
return create_tree(index_info_generator(), '--index-info') return create_tree(index_info_generator(), '--index-info')
@ -342,7 +388,7 @@ def create_tree(input_lines, mode):
with temporary_index_file(): with temporary_index_file():
p = subprocess.Popen(cmd, stdin=subprocess.PIPE) p = subprocess.Popen(cmd, stdin=subprocess.PIPE)
for line in input_lines: for line in input_lines:
p.stdin.write('%s\0' % line) p.stdin.write(to_bytes('%s\0' % line))
p.stdin.close() p.stdin.close()
if p.wait() != 0: if p.wait() != 0:
die('`%s` failed' % ' '.join(cmd)) die('`%s` failed' % ' '.join(cmd))
@ -350,26 +396,42 @@ def create_tree(input_lines, mode):
return tree_id return tree_id
def clang_format_to_blob(filename, line_ranges, binary='clang-format', def clang_format_to_blob(filename, line_ranges, revision=None,
style=None): binary='clang-format', style=None):
"""Run clang-format on the given file and save the result to a git blob. """Run clang-format on the given file and save the result to a git blob.
Runs on the file in `revision` if not None, or on the file in the working
directory if `revision` is None.
Returns the object ID (SHA-1) of the created blob.""" Returns the object ID (SHA-1) of the created blob."""
clang_format_cmd = [binary, filename] clang_format_cmd = [binary]
if style: if style:
clang_format_cmd.extend(['-style='+style]) clang_format_cmd.extend(['-style='+style])
clang_format_cmd.extend([ clang_format_cmd.extend([
'-lines=%s:%s' % (start_line, start_line+line_count-1) '-lines=%s:%s' % (start_line, start_line+line_count-1)
for start_line, line_count in line_ranges]) for start_line, line_count in line_ranges])
try: if revision:
clang_format = subprocess.Popen(clang_format_cmd, stdin=subprocess.PIPE, clang_format_cmd.extend(['-assume-filename='+filename])
git_show_cmd = ['git', 'cat-file', 'blob', '%s:%s' % (revision, filename)]
git_show = subprocess.Popen(git_show_cmd, stdin=subprocess.PIPE,
stdout=subprocess.PIPE) stdout=subprocess.PIPE)
git_show.stdin.close()
clang_format_stdin = git_show.stdout
else:
clang_format_cmd.extend([filename])
git_show = None
clang_format_stdin = subprocess.PIPE
try:
clang_format = subprocess.Popen(clang_format_cmd, stdin=clang_format_stdin,
stdout=subprocess.PIPE)
if clang_format_stdin == subprocess.PIPE:
clang_format_stdin = clang_format.stdin
except OSError as e: except OSError as e:
if e.errno == errno.ENOENT: if e.errno == errno.ENOENT:
die('cannot find executable "%s"' % binary) die('cannot find executable "%s"' % binary)
else: else:
raise raise
clang_format.stdin.close() clang_format_stdin.close()
hash_object_cmd = ['git', 'hash-object', '-w', '--path='+filename, '--stdin'] hash_object_cmd = ['git', 'hash-object', '-w', '--path='+filename, '--stdin']
hash_object = subprocess.Popen(hash_object_cmd, stdin=clang_format.stdout, hash_object = subprocess.Popen(hash_object_cmd, stdin=clang_format.stdout,
stdout=subprocess.PIPE) stdout=subprocess.PIPE)
@ -379,7 +441,9 @@ def clang_format_to_blob(filename, line_ranges, binary='clang-format',
die('`%s` failed' % ' '.join(hash_object_cmd)) die('`%s` failed' % ' '.join(hash_object_cmd))
if clang_format.wait() != 0: if clang_format.wait() != 0:
die('`%s` failed' % ' '.join(clang_format_cmd)) die('`%s` failed' % ' '.join(clang_format_cmd))
return stdout.rstrip('\r\n') if git_show and git_show.wait() != 0:
die('`%s` failed' % ' '.join(git_show_cmd))
return convert_string(stdout).rstrip('\r\n')
@contextlib.contextmanager @contextlib.contextmanager
@ -417,7 +481,12 @@ def print_diff(old_tree, new_tree):
# We use the porcelain 'diff' and not plumbing 'diff-tree' because the output # We use the porcelain 'diff' and not plumbing 'diff-tree' because the output
# is expected to be viewed by the user, and only the former does nice things # is expected to be viewed by the user, and only the former does nice things
# like color and pagination. # like color and pagination.
subprocess.check_call(['git', 'diff', old_tree, new_tree, '--']) #
# We also only print modified files since `new_tree` only contains the files
# that were modified, so unmodified files would show as deleted without the
# filter.
subprocess.check_call(['git', 'diff', '--diff-filter=M', old_tree, new_tree,
'--'])
def apply_changes(old_tree, new_tree, force=False, patch_mode=False): def apply_changes(old_tree, new_tree, force=False, patch_mode=False):
@ -425,15 +494,16 @@ def apply_changes(old_tree, new_tree, force=False, patch_mode=False):
Bails if there are local changes in those files and not `force`. If Bails if there are local changes in those files and not `force`. If
`patch_mode`, runs `git checkout --patch` to select hunks interactively.""" `patch_mode`, runs `git checkout --patch` to select hunks interactively."""
changed_files = run('git', 'diff-tree', '-r', '-z', '--name-only', old_tree, changed_files = run('git', 'diff-tree', '--diff-filter=M', '-r', '-z',
'--name-only', old_tree,
new_tree).rstrip('\0').split('\0') new_tree).rstrip('\0').split('\0')
if not force: if not force:
unstaged_files = run('git', 'diff-files', '--name-status', *changed_files) unstaged_files = run('git', 'diff-files', '--name-status', *changed_files)
if unstaged_files: if unstaged_files:
print >>sys.stderr, ('The following files would be modified but ' print('The following files would be modified but '
'have unstaged changes:') 'have unstaged changes:', file=sys.stderr)
print >>sys.stderr, unstaged_files print(unstaged_files, file=sys.stderr)
print >>sys.stderr, 'Please commit, stage, or stash them first.' print('Please commit, stage, or stash them first.', file=sys.stderr)
sys.exit(2) sys.exit(2)
if patch_mode: if patch_mode:
# In patch mode, we could just as well create an index from the new tree # In patch mode, we could just as well create an index from the new tree
@ -460,25 +530,50 @@ def run(*args, **kwargs):
p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
stdin=subprocess.PIPE) stdin=subprocess.PIPE)
stdout, stderr = p.communicate(input=stdin) stdout, stderr = p.communicate(input=stdin)
stdout = convert_string(stdout)
stderr = convert_string(stderr)
if p.returncode == 0: if p.returncode == 0:
if stderr: if stderr:
if verbose: if verbose:
print >>sys.stderr, '`%s` printed to stderr:' % ' '.join(args) print('`%s` printed to stderr:' % ' '.join(args), file=sys.stderr)
print >>sys.stderr, stderr.rstrip() print(stderr.rstrip(), file=sys.stderr)
if strip: if strip:
stdout = stdout.rstrip('\r\n') stdout = stdout.rstrip('\r\n')
return stdout return stdout
if verbose: if verbose:
print >>sys.stderr, '`%s` returned %s' % (' '.join(args), p.returncode) print('`%s` returned %s' % (' '.join(args), p.returncode), file=sys.stderr)
if stderr: if stderr:
print >>sys.stderr, stderr.rstrip() print(stderr.rstrip(), file=sys.stderr)
sys.exit(2) sys.exit(2)
def die(message): def die(message):
print >>sys.stderr, 'error:', message print('error:', message, file=sys.stderr)
sys.exit(2) sys.exit(2)
def to_bytes(str_input):
# Encode to UTF-8 to get binary data.
if isinstance(str_input, bytes):
return str_input
return str_input.encode('utf-8')
def to_string(bytes_input):
if isinstance(bytes_input, str):
return bytes_input
return bytes_input.encode('utf-8')
def convert_string(bytes_input):
try:
return to_string(bytes_input.decode('utf-8'))
except AttributeError: # 'str' object has no attribute 'decode'.
return str(bytes_input)
except UnicodeError:
return str(bytes_input)
if __name__ == '__main__': if __name__ == '__main__':
main() main()