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