Add a clang-format pre-upload hook
This change converts adds the `clang_format_check` pre-upload hook,
modelled after the one in AOSP's repohooks. This is disabled by default,
and all projects must opt in to enable it.
[Hook Overrides]
clang_format_check: true
# Optional if you need to limit clang-format to just a few projects.
# e.g. for platform2/
[Hook Overrides Options]
clang_format_check: project_a/ project_b/
CQ-DEPEND=CL:688744,CL:*464767
BUG=b:65376611
TEST=../repohooks/pre-upload.py # On platform2 with a badly-formatted
# change.
Change-Id: I088a9f8585e74d3d0db3b647febdecdbf79f3d03
Reviewed-on: https://chromium-review.googlesource.com/685283
Commit-Ready: Luis Hector Chavez <lhchavez@chromium.org>
Tested-by: Luis Hector Chavez <lhchavez@chromium.org>
Reviewed-by: Dan Erat <derat@chromium.org>
diff --git a/clang-format.py b/clang-format.py
new file mode 100755
index 0000000..8e884d4
--- /dev/null
+++ b/clang-format.py
@@ -0,0 +1,147 @@
+#!/usr/bin/env python2
+# -*- coding:utf-8 -*-
+# Copyright 2017 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Wrapper to run git-clang-format and parse its output."""
+
+from __future__ import print_function
+
+import hashlib
+import io
+import os
+import sys
+
+_path = os.path.realpath(__file__ + '/../../..')
+if sys.path[0] != _path:
+ sys.path.insert(0, _path)
+del _path
+
+from chromite.lib import commandline
+from chromite.lib import constants
+from chromite.lib import cros_build_lib
+
+
+# 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/'
+
+BUILDTOOLS_PATH = os.path.join(constants.SOURCE_ROOT, 'chromium', 'src',
+ 'buildtools')
+
+
+def _GetSha1Hash(path):
+ """Gets the SHA-1 hash of |path|, or None if the file does not exist."""
+ if not os.path.exists(path):
+ return None
+ with open(path, 'rb') as f:
+ m = hashlib.sha1()
+ while True:
+ buf = f.read(io.DEFAULT_BUFFER_SIZE)
+ if not buf:
+ break
+ m.update(buf)
+ return m.hexdigest()
+
+
+def _GetDefaultClangFormatPath():
+ """Gets the default clang-format binary path.
+
+ This also ensures that the binary itself is up-to-date.
+ """
+
+ clang_format_path = os.path.join(BUILDTOOLS_PATH, 'linux64/clang-format')
+ hash_file_path = os.path.join(BUILDTOOLS_PATH, 'linux64/clang-format.sha1')
+ with open(hash_file_path, 'r') as f:
+ expected_hash = f.read().strip()
+ if expected_hash != _GetSha1Hash(clang_format_path):
+ # See chromium/src/buildtools/clang_format/README.txt for more details.
+ cmd = [os.path.join(constants.DEPOT_TOOLS_DIR,
+ 'download_from_google_storage.py'), '-b',
+ 'chromium-clang-format', '-s', hash_file_path]
+ cros_build_lib.RunCommand(cmd=cmd, print_cmd=False)
+ return clang_format_path
+
+
+def main(argv):
+ """Checks if a project is correctly formatted with clang-format.
+
+ Returns 1 if there are any clang-format-worthy changes in the project (or
+ on a provided list of files/directories in the project), 0 otherwise.
+ """
+
+ parser = commandline.ArgumentParser(description=__doc__)
+ parser.add_argument('--clang-format', default=_GetDefaultClangFormatPath(),
+ help='The path of the clang-format executable.')
+ parser.add_argument('--git-clang-format',
+ default=os.path.join(BUILDTOOLS_PATH, 'clang_format',
+ 'script', 'git-clang-format'),
+ help='The path of the git-clang-format executable.')
+ parser.add_argument('--style', metavar='STYLE', type=str, default='file',
+ 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/directories.')
+
+ 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(['%s^' % opts.commit, opts.commit])
+ cmd.extend(['--'] + opts.files)
+
+ # Fail gracefully if clang-format itself aborts/fails.
+ try:
+ result = cros_build_lib.RunCommand(cmd=cmd,
+ print_cmd=False,
+ stdout_to_pipe=True)
+ except cros_build_lib.RunCommandError as e:
+ print('clang-format failed:\n' + str(e), file=sys.stderr)
+ print('\nPlease report this to the clang team.', file=sys.stderr)
+ return 1
+
+ stdout = result.output
+ 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:
+ cros_build_lib.RunCommand(cmd=['git', 'apply'],
+ print_cmd=False,
+ input=stdout)
+ else:
+ print('The following files have formatting errors:')
+ for filename in diff_filenames:
+ print('\t%s' % filename)
+ print('You can run `%s --fix %s` to fix this' %
+ (sys.argv[0],
+ ' '.join(cros_build_lib.ShellQuote(arg) for arg in argv)))
+ return 1
+
+if __name__ == '__main__':
+ commandline.ScriptWrapperMain(lambda _: main)
diff --git a/pre-upload.py b/pre-upload.py
index 3bd6f66..59b47ff 100755
--- a/pre-upload.py
+++ b/pre-upload.py
@@ -1189,6 +1189,26 @@
# Project-specific hooks
+def _check_clang_format(_project, commit, options=()):
+ """Runs clang-format on the given project"""
+ hooks_dir = _get_hooks_dir()
+ options = list(options)
+ if commit == PRE_SUBMIT:
+ options.append('--commit=HEAD')
+ else:
+ options.extend(['--commit', commit])
+ cmd = ['%s/clang-format.py' % hooks_dir] + options
+ cmd_result = cros_build_lib.RunCommand(cmd=cmd,
+ print_cmd=False,
+ input=_get_patch(commit),
+ stdout_to_pipe=True,
+ combine_stdout_stderr=True,
+ error_code_ok=True)
+ if cmd_result.returncode:
+ return HookFailure('clang-format.py errors/warnings\n\n' +
+ cmd_result.output)
+
+
def _run_checkpatch(_project, commit, options=()):
"""Runs checkpatch.pl on the given project"""
hooks_dir = _get_hooks_dir()
@@ -1487,6 +1507,7 @@
# A dictionary of flags (keys) that can appear in the config file, and the hook
# that the flag controls (value).
_HOOK_FLAGS = {
+ 'clang_format_check': _check_clang_format,
'checkpatch_check': _run_checkpatch,
'stray_whitespace_check': _check_no_stray_whitespace,
'json_check': _run_json_check,