script: Re-enable clang-format preupload check with our own script git-clang-format with --diff in Debian Testing always return 1, while the useful diff printout is in stdout. Fork out AOSP's clang-format.py so we can run git-clang-format in repo pre-upload. Change-Id: Ief24406d4f60ae46532572729c3c9f6d1c5fff67
diff --git a/preupload-hooks/GLOBAL-PREUPLOAD.cfg b/preupload-hooks/GLOBAL-PREUPLOAD.cfg index ab19809..3e8c2d7 100644 --- a/preupload-hooks/GLOBAL-PREUPLOAD.cfg +++ b/preupload-hooks/GLOBAL-PREUPLOAD.cfg
@@ -7,3 +7,6 @@ [Builtin Hooks] pylint3 = true cpplint = true + +[Hook Scripts] +repo_clang_format = ${REPO_ROOT}/scripts/preupload-hooks/clang-format.py --commit ${PREUPLOAD_COMMIT} --style file --extensions c,h,cc,cpp
diff --git a/preupload-hooks/clang-format.py b/preupload-hooks/clang-format.py new file mode 100755 index 0000000..bedbaf3 --- /dev/null +++ b/preupload-hooks/clang-format.py
@@ -0,0 +1,127 @@ +#!/usr/bin/env python3 +# Copyright 2022 Google LLC +# Copyright 2016 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Wrapper to run git-clang-format and parse its output.""" + +import argparse +import os +import sys + +_path = os.path.realpath(__file__ + '/../../../repohooks/') +if sys.path[0] != _path: + sys.path.insert(0, _path) +del _path +# We have to import our local modules after the sys.path tweak. We can't use +# relative imports because this is an executable program, not a module. +# pylint: disable=wrong-import-position,import-error +import rh.shell +import rh.utils + + +# Since we're asking git-clang-format to print a diff, all modified filenames +# that have formatting errors are printed with this prefix. +DIFF_MARKER_PREFIX = '+++ b/' + + +def get_parser(): + """Return a command line parser.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--clang-format', default='clang-format', + help='The path of the clang-format executable.') + parser.add_argument('--git-clang-format', default='git-clang-format', + help='The path of the git-clang-format executable.') + parser.add_argument('--style', metavar='STYLE', type=str, + help='The style that clang-format will use.') + parser.add_argument('--extensions', metavar='EXTENSIONS', type=str, + help='Comma-separated list of file extensions to ' + 'format.') + parser.add_argument('--fix', action='store_true', + help='Fix any formatting errors automatically.') + + scope = parser.add_mutually_exclusive_group(required=True) + scope.add_argument('--commit', type=str, default='HEAD', + help='Specify the commit to validate.') + scope.add_argument('--working-tree', action='store_true', + help='Validates the files that have changed from ' + 'HEAD in the working directory.') + + parser.add_argument('files', type=str, nargs='*', + help='If specified, only consider differences in ' + 'these files.') + return parser + + +def main(argv): + """The main entry.""" + parser = get_parser() + opts = parser.parse_args(argv) + + cmd = [opts.git_clang_format, '--binary', opts.clang_format, '--diff'] + if opts.style: + cmd.extend(['--style', opts.style]) + if opts.extensions: + cmd.extend(['--extensions', opts.extensions]) + if not opts.working_tree: + cmd.extend([f'{opts.commit}^', opts.commit]) + cmd.extend(['--'] + opts.files) + + # Fail gracefully if clang-format itself aborts/fails. + result = rh.utils.run(cmd, capture_output=True, check=False) + # Newer versions of git-clang-format will exit 1 when it worked. Assume a + # real failure is any exit code above 1, or any time stderr is used, or if + # it exited 1 and produce anything useful to stdout. If it exited 0, + # then assume all is well and we'll attempt to parse its output below. + if (result.returncode > 1 or result.stderr or + (result.stdout and result.returncode)): + print(f'clang-format failed:\ncmd: {result.cmdstr}\n' + f'stdout:\n{result.stdout}\nstderr:\n{result.stderr}', + file=sys.stderr) + return 1 + + stdout = result.stdout + if stdout.rstrip('\n') == 'no modified files to format': + # This is always printed when only files that clang-format does not + # understand were modified. + return 0 + + diff_filenames = [] + for line in stdout.splitlines(): + if line.startswith(DIFF_MARKER_PREFIX): + diff_filenames.append(line[len(DIFF_MARKER_PREFIX):].rstrip()) + + if diff_filenames: + if opts.fix: + result = rh.utils.run(['git', 'apply'], input=stdout, check=False) + if result.returncode: + print('Error: Unable to automatically fix things.\n' + ' Make sure your checkout is clean first.\n' + ' If you have multiple commits, you might have to ' + 'manually rebase your tree first.', + file=sys.stderr) + return result.returncode + else: + print('The following files have formatting errors:') + for filename in diff_filenames: + print(f'\t{filename}') + print('You can try to fix this by running:\n' + f'{sys.argv[0]} --fix {rh.shell.cmd_to_str(argv)}') + return 1 + + return 0 + + +if __name__ == '__main__': + sys.exit(main(sys.argv[1:]))