From 3d4f707062824339cab091ab160cb8fe017fa173 Mon Sep 17 00:00:00 2001 From: Christiaan Biesterbosch Date: Fri, 7 Jul 2017 00:29:38 +0200 Subject: [PATCH] Updated clang-format and added the provided editor integrations and LICENSE.txt --- third_party/clang-format/LICENSE.TXT | 63 ++++ .../clang-format-bbedit.applescript | 27 ++ third_party/clang-format/clang-format-diff.py | 23 +- third_party/clang-format/clang-format.el | 193 +++++++++++++ third_party/clang-format/clang-format.py | 127 ++++++++ third_party/clang-format/git-clang-format | 271 ++++++++++++------ 6 files changed, 605 insertions(+), 99 deletions(-) create mode 100644 third_party/clang-format/LICENSE.TXT create mode 100644 third_party/clang-format/clang-format-bbedit.applescript create mode 100644 third_party/clang-format/clang-format.el create mode 100644 third_party/clang-format/clang-format.py diff --git a/third_party/clang-format/LICENSE.TXT b/third_party/clang-format/LICENSE.TXT new file mode 100644 index 000000000..b452ca2ef --- /dev/null +++ b/third_party/clang-format/LICENSE.TXT @@ -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 +------- --------- + + diff --git a/third_party/clang-format/clang-format-bbedit.applescript b/third_party/clang-format/clang-format-bbedit.applescript new file mode 100644 index 000000000..fa88fe900 --- /dev/null +++ b/third_party/clang-format/clang-format-bbedit.applescript @@ -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 diff --git a/third_party/clang-format/clang-format-diff.py b/third_party/clang-format/clang-format-diff.py index 23adb077c..ffa30e70d 100644 --- a/third_party/clang-format/clang-format-diff.py +++ b/third_party/clang-format/clang-format-diff.py @@ -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. 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 """ @@ -31,10 +31,6 @@ import StringIO import sys -# Change this to the full path if clang-format is not on the path. -binary = 'clang-format' - - def main(): parser = argparse.ArgumentParser(description= 'Reformat changed lines in diff. Without -i ' @@ -48,16 +44,19 @@ def main(): help='custom pattern selecting file paths to reformat ' '(case sensitive, overrides -iregex)') 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)', help='custom pattern selecting file paths to reformat ' '(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', help='be more verbose, ineffective without -i') - parser.add_argument( - '-style', - help= - 'formatting style to apply (LLVM, Google, Chromium, Mozilla, WebKit)') + parser.add_argument('-style', + help='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() # Extract changed lines for each file. @@ -93,9 +92,11 @@ def main(): for filename, lines in lines_by_file.iteritems(): if args.i and args.verbose: print 'Formatting', filename - command = [binary, filename] + command = [args.binary, filename] if args.i: command.append('-i') + if args.sort_includes: + command.append('-sort-includes') command.extend(lines) if args.style: command.extend(['-style', args.style]) diff --git a/third_party/clang-format/clang-format.el b/third_party/clang-format/clang-format.el new file mode 100644 index 000000000..aa9c3ff4c --- /dev/null +++ b/third_party/clang-format/clang-format.el @@ -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 . +;; 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 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 " node does not have offset and length attributes")) + (when (cdr children) + (error "More than one child node in 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 diff --git a/third_party/clang-format/clang-format.py b/third_party/clang-format/clang-format.py new file mode 100644 index 000000000..241256634 --- /dev/null +++ b/third_party/clang-format/clang-format.py @@ -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 :pyf /clang-format.py +# imap :pyf /clang-format.py +# +# 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 ":" or +# "all" to format the full file. So, to format the full file, write a function +# like: +# :function FormatFile() +# : let l:lines="all" +# : pyf /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() diff --git a/third_party/clang-format/git-clang-format b/third_party/clang-format/git-clang-format index 78cf7307c..60cd4fb25 100644 --- a/third_party/clang-format/git-clang-format +++ b/third_party/clang-format/git-clang-format @@ -9,20 +9,21 @@ # #===------------------------------------------------------------------------===# -r""" -clang-format git integration -============================ - -This file provides a clang-format integration for git. Put it somewhere in your -path and ensure that it is executable. Then, "git clang-format" will invoke -clang-format on the changes in current files or a specific commit. - -For further details, run: -git clang-format -h - -Requires Python 2.7 -""" +r""" +clang-format git integration +============================ + +This file provides a clang-format integration for git. Put it somewhere in your +path and ensure that it is executable. Then, "git clang-format" will invoke +clang-format on the changes in current files or a specific commit. + +For further details, run: +git clang-format -h + +Requires Python 2.7 or Python 3 +""" +from __future__ import print_function import argparse import collections import contextlib @@ -32,12 +33,15 @@ import re import subprocess import sys -usage = 'git clang-format [OPTIONS] [] [--] [...]' +usage = 'git clang-format [OPTIONS] [] [] [--] [...]' desc = ''' -Run clang-format on all lines that differ between the working directory -and , which defaults to HEAD. Changes are only applied to the working -directory. +If zero or one commits are given, run clang-format on all lines that differ +between the working directory and , which defaults to HEAD. Changes are +only applied to the working directory. + +If two commits are given (requires --diff), run clang-format on all lines in the +second that differ from the first . The following git-config settings set the default of the corresponding option: clangFormat.binary @@ -77,7 +81,9 @@ def main(): 'cc', 'cp', 'cpp', 'c++', 'cxx', 'hpp', # C++ # Other languages that clang-format supports 'proto', 'protodevel', # Protocol Buffers + 'java', # Java 'js', # JavaScript + 'ts', # TypeScript ]) p = argparse.ArgumentParser( @@ -119,46 +125,59 @@ def main(): opts.verbose -= opts.quiet del opts.quiet - commit, files = interpret_args(opts.args, dash_dash, opts.commit) - changed_lines = compute_diff_and_extract_lines(commit, files) + commits, files = interpret_args(opts.args, dash_dash, opts.commit) + 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: ignored_files = set(changed_lines) filter_by_extension(changed_lines, opts.extensions.lower().split(',')) if opts.verbose >= 1: ignored_files.difference_update(changed_lines) 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: - print ' ', filename + print(' %s' % filename) if changed_lines: - print 'Running clang-format on the following files:' + print('Running clang-format on the following files:') for filename in changed_lines: - print ' ', filename + print(' %s' % filename) if not changed_lines: - print 'no modified files to format' + print('no modified files to format') return # The computed diff outputs absolute paths, so we must cd before accessing # those files. cd_to_toplevel() - old_tree = create_tree_from_workdir(changed_lines) - new_tree = run_clang_format_and_save_to_tree(changed_lines, - binary=opts.binary, - style=opts.style) + 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) + new_tree = run_clang_format_and_save_to_tree(changed_lines, + binary=opts.binary, + style=opts.style) if opts.verbose >= 1: - print 'old tree:', old_tree - print 'new tree:', new_tree + print('old tree: %s' % old_tree) + print('new tree: %s' % new_tree) if old_tree == new_tree: if opts.verbose >= 0: - print 'clang-format did not modify any files' + print('clang-format did not modify any files') elif opts.diff: print_diff(old_tree, new_tree) else: changed_files = apply_changes(old_tree, new_tree, force=opts.force, patch_mode=opts.patch) if (opts.verbose >= 0 and not opts.patch) or opts.verbose >= 1: - print 'changed files:' + print('changed files:') for filename in changed_files: - print ' ', filename + print(' %s' % filename) def load_git_config(non_string_options=None): @@ -180,40 +199,41 @@ def load_git_config(non_string_options=None): 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 args and placed in `dash_dash`. - If "--" is present (i.e., `dash_dash` is non-empty), the argument to its - left (if present) is taken as commit. Otherwise, the first argument is - checked if it is a commit or a file. If commit is not given, - `default_commit` is used.""" + If "--" is present (i.e., `dash_dash` is non-empty), the arguments to its + left (if present) are taken as commits. Otherwise, the arguments are checked + from left to right if they are commits or files. If commits are not given, + a list with `default_commit` is used.""" if dash_dash: if len(args) == 0: - commit = default_commit - elif len(args) > 1: - die('at most one commit allowed; %d given' % len(args)) + commits = [default_commit] else: - commit = args[0] - object_type = get_object_type(commit) - if object_type not in ('commit', 'tag'): - if object_type is None: - die("'%s' is not a commit" % commit) - else: - die("'%s' is a %s, but a commit was expected" % (commit, object_type)) + commits = args + for commit in commits: + object_type = get_object_type(commit) + if object_type not in ('commit', 'tag'): + if object_type is None: + die("'%s' is not a commit" % commit) + else: + die("'%s' is a %s, but a commit was expected" % (commit, object_type)) files = dash_dash[1:] elif args: - if disambiguate_revision(args[0]): - commit = args[0] - files = args[1:] - else: - commit = default_commit - files = args + commits = [] + while args: + if not disambiguate_revision(args[0]): + break + commits.append(args.pop(0)) + if not commits: + commits = [default_commit] + files = args else: - commit = default_commit + commits = [default_commit] files = [] - return commit, files + return commits, files def disambiguate_revision(value): @@ -238,12 +258,12 @@ def get_object_type(value): stdout, stderr = p.communicate() if p.returncode != 0: 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().""" - diff_process = compute_diff(commit, files) + diff_process = compute_diff(commits, files) changed_lines = extract_lines(diff_process.stdout) diff_process.stdout.close() diff_process.wait() @@ -253,13 +273,17 @@ def compute_diff_and_extract_lines(commit, files): return changed_lines -def compute_diff(commit, files): - """Return a subprocess object producing the diff from `commit`. +def compute_diff(commits, files): + """Return a subprocess object producing the diff from `commits`. The return value's `stdin` file object will produce a patch with the - differences between the working directory and `commit`, filtered on `files` - (if non-empty). Zero context lines are used in the patch.""" - cmd = ['git', 'diff-index', '-p', '-U0', commit, '--'] + differences between the working directory and the first commit if a single + one was specified, or the difference between both specified commits, filtered + 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) p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE) p.stdin.close() @@ -277,6 +301,7 @@ def extract_lines(patch_file): list of line `Range`s.""" matches = {} for line in patch_file: + line = convert_string(line) match = re.search(r'^\+\+\+\ [^/]+/(.*)', line) if match: 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, excluding the period.""" allowed_extensions = frozenset(allowed_extensions) - for filename in dictionary.keys(): + for filename in list(dictionary.keys()): 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: del dictionary[filename] @@ -316,15 +343,34 @@ def create_tree_from_workdir(filenames): return create_tree(filenames, '--stdin') -def run_clang_format_and_save_to_tree(changed_lines, binary='clang-format', - style=None): +def run_clang_format_and_save_to_tree(changed_lines, revision=None, + binary='clang-format', style=None): """Run clang-format on each file and save the result to a git 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(): - for filename, line_ranges in changed_lines.iteritems(): - mode = oct(os.stat(filename).st_mode) - blob_id = clang_format_to_blob(filename, line_ranges, binary=binary, + 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) + # 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) yield '%s %s\t%s' % (mode, blob_id, filename) return create_tree(index_info_generator(), '--index-info') @@ -342,7 +388,7 @@ def create_tree(input_lines, mode): with temporary_index_file(): p = subprocess.Popen(cmd, stdin=subprocess.PIPE) for line in input_lines: - p.stdin.write('%s\0' % line) + p.stdin.write(to_bytes('%s\0' % line)) p.stdin.close() if p.wait() != 0: die('`%s` failed' % ' '.join(cmd)) @@ -350,26 +396,42 @@ def create_tree(input_lines, mode): return tree_id -def clang_format_to_blob(filename, line_ranges, binary='clang-format', - style=None): +def clang_format_to_blob(filename, line_ranges, revision=None, + binary='clang-format', style=None): """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.""" - clang_format_cmd = [binary, filename] + clang_format_cmd = [binary] if style: clang_format_cmd.extend(['-style='+style]) clang_format_cmd.extend([ '-lines=%s:%s' % (start_line, start_line+line_count-1) for start_line, line_count in line_ranges]) + if revision: + 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) + 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=subprocess.PIPE, + 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: if e.errno == errno.ENOENT: die('cannot find executable "%s"' % binary) else: raise - clang_format.stdin.close() + clang_format_stdin.close() hash_object_cmd = ['git', 'hash-object', '-w', '--path='+filename, '--stdin'] hash_object = subprocess.Popen(hash_object_cmd, stdin=clang_format.stdout, 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)) if clang_format.wait() != 0: 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 @@ -417,7 +481,12 @@ def print_diff(old_tree, new_tree): # 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 # 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): @@ -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 `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') if not force: unstaged_files = run('git', 'diff-files', '--name-status', *changed_files) if unstaged_files: - print >>sys.stderr, ('The following files would be modified but ' - 'have unstaged changes:') - print >>sys.stderr, unstaged_files - print >>sys.stderr, 'Please commit, stage, or stash them first.' + print('The following files would be modified but ' + 'have unstaged changes:', file=sys.stderr) + print(unstaged_files, file=sys.stderr) + print('Please commit, stage, or stash them first.', file=sys.stderr) sys.exit(2) if patch_mode: # 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, stdin=subprocess.PIPE) stdout, stderr = p.communicate(input=stdin) + + stdout = convert_string(stdout) + stderr = convert_string(stderr) + if p.returncode == 0: if stderr: if verbose: - print >>sys.stderr, '`%s` printed to stderr:' % ' '.join(args) - print >>sys.stderr, stderr.rstrip() + print('`%s` printed to stderr:' % ' '.join(args), file=sys.stderr) + print(stderr.rstrip(), file=sys.stderr) if strip: stdout = stdout.rstrip('\r\n') return stdout 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: - print >>sys.stderr, stderr.rstrip() + print(stderr.rstrip(), file=sys.stderr) sys.exit(2) def die(message): - print >>sys.stderr, 'error:', message + print('error:', message, file=sys.stderr) 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__': main()