1#!/usr/bin/python 2# -*- coding:utf-8 -*- 3# Copyright 2016 The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16 17"""Wrapper to run git-clang-format and parse its output.""" 18 19from __future__ import print_function 20 21import argparse 22import os 23import sys 24 25_path = os.path.realpath(__file__ + '/../..') 26if sys.path[0] != _path: 27 sys.path.insert(0, _path) 28del _path 29 30# We have to import our local modules after the sys.path tweak. We can't use 31# relative imports because this is an executable program, not a module. 32# pylint: disable=wrong-import-position 33import rh.shell 34import rh.utils 35 36 37# Since we're asking git-clang-format to print a diff, all modified filenames 38# that have formatting errors are printed with this prefix. 39DIFF_MARKER_PREFIX = '+++ b/' 40 41 42def get_parser(): 43 """Return a command line parser.""" 44 parser = argparse.ArgumentParser(description=__doc__) 45 parser.add_argument('--clang-format', default='clang-format', 46 help='The path of the clang-format executable.') 47 parser.add_argument('--git-clang-format', default='git-clang-format', 48 help='The path of the git-clang-format executable.') 49 parser.add_argument('--style', metavar='STYLE', type=str, 50 help='The style that clang-format will use.') 51 parser.add_argument('--extensions', metavar='EXTENSIONS', type=str, 52 help='Comma-separated list of file extensions to ' 53 'format.') 54 parser.add_argument('--fix', action='store_true', 55 help='Fix any formatting errors automatically.') 56 57 scope = parser.add_mutually_exclusive_group(required=True) 58 scope.add_argument('--commit', type=str, default='HEAD', 59 help='Specify the commit to validate.') 60 scope.add_argument('--working-tree', action='store_true', 61 help='Validates the files that have changed from ' 62 'HEAD in the working directory.') 63 64 parser.add_argument('files', type=str, nargs='*', 65 help='If specified, only consider differences in ' 66 'these files.') 67 return parser 68 69 70def main(argv): 71 """The main entry.""" 72 parser = get_parser() 73 opts = parser.parse_args(argv) 74 75 cmd = [opts.git_clang_format, '--binary', opts.clang_format, '--diff'] 76 if opts.style: 77 cmd.extend(['--style', opts.style]) 78 if opts.extensions: 79 cmd.extend(['--extensions', opts.extensions]) 80 if not opts.working_tree: 81 cmd.extend(['%s^' % opts.commit, opts.commit]) 82 cmd.extend(['--'] + opts.files) 83 84 # Fail gracefully if clang-format itself aborts/fails. 85 try: 86 result = rh.utils.run(cmd, capture_output=True) 87 except rh.utils.CalledProcessError as e: 88 print('clang-format failed:\n%s' % (e,), file=sys.stderr) 89 print('\nPlease report this to the clang team.', file=sys.stderr) 90 return 1 91 92 stdout = result.stdout 93 if stdout.rstrip('\n') == 'no modified files to format': 94 # This is always printed when only files that clang-format does not 95 # understand were modified. 96 return 0 97 98 diff_filenames = [] 99 for line in stdout.splitlines(): 100 if line.startswith(DIFF_MARKER_PREFIX): 101 diff_filenames.append(line[len(DIFF_MARKER_PREFIX):].rstrip()) 102 103 if diff_filenames: 104 if opts.fix: 105 result = rh.utils.run(['git', 'apply'], input=stdout, check=False) 106 if result.returncode: 107 print('Error: Unable to automatically fix things.\n' 108 ' Make sure your checkout is clean first.\n' 109 ' If you have multiple commits, you might have to ' 110 'manually rebase your tree first.', 111 file=sys.stderr) 112 return result.returncode 113 else: 114 print('The following files have formatting errors:') 115 for filename in diff_filenames: 116 print('\t%s' % filename) 117 print('You can try to fix this by running:\n%s --fix %s' % 118 (sys.argv[0], rh.shell.cmd_to_str(argv))) 119 return 1 120 121 return 0 122 123 124if __name__ == '__main__': 125 sys.exit(main(sys.argv[1:])) 126