From: Kienan Stewart Date: Tue, 10 Dec 2024 17:11:37 +0000 (-0500) Subject: misc: Add pre-commit hook for common checks X-Git-Url: https://git.lttng.org./?a=commitdiff_plain;h=d60fe7c6eb8ae7d6026bac9d763238e3a50dd0aa;p=lttng-tools.git misc: Add pre-commit hook for common checks The initial version of the pre-commit hook, if installed, will check the changed with clang-format, clang-tidy, and python-black. Furthermore, C/C++ source files will be parsed using python-clang and the comment style (starting with `/*` and ending with `*/`) will be validated. This pre-commit hook will check only the staged files. To install the pre-commit hook: ln -s ../../extras/pre-commit.py .git/hooks/pre-commit Example: ``` $ git hook run pre-commit 2>&1 ERROR:root:Failed rule 'cpp-comment-style' Wrong comment style at , end > // hi Wrong comment style at , end > /** hey **/ Wrong comment style at , end > /* sup **/ Wrong comment style at , end > // Wrong comment style at , end > /**/ ERROR:root:Failed rule 'clang-format' src/bin/lttng-sessiond/main.cpp:1516:63: error: code should be clang-formatted [-Wclang-format-violations] /* Queue of rotation jobs populated by the sessiond-timer. */ ^ src/bin/lttng-sessiond/main.cpp:1518:47: error: code should be clang-formatted [-Wclang-format-violations] struct lttng_thread *client_thread = nullptr; ^ ERROR: root:Failed rule 'python-black' would reformat asdf.py Oh no! 💥 💔 💥 1 file would be reformatted. Change-Id: I91f287a41f242d70aa4feb2b8ca8a6fd46ef708e --- asdf.py 2024-12-10 19:56:34.558499+00:00 +++ asdf.py 2024-12-10 20:48:24.892474+00:00 @@ -1,4 +1,4 @@ #!/usr/bin/python3 -if __name__ == '__main__': +if __name__ == "__main__": pass WARNING:root:Passed: 1 ERROR:root:Failed: 3 ERROR:root:Failed rule: cpp-comment-style ERROR:root:Failed rule: clang-format ERROR:root:Failed rule: python-black ``` Known drawbacks =============== This operates on the whole files instead of the diffs. The following supplementary tools are required for the pre-commit hook to run: * clang-format * clang-tidy * python-blacken * python-clang (shipped with some clang releases) Signed-off-by: Kienan Stewart Change-Id: Icc5e7c9324ebc937cf295619f53557649797d914 Signed-off-by: Jérémie Galarneau --- diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3eaec7489..b5b38e3f1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -38,6 +38,19 @@ configure your local check out to use: git config commit.template .commit_template +A pre-commit hook may also be installed that will use various tools to lint +stylistic errors before the commit is complete. This hook requires the following +development tools: + + * clang-tidy + * clang-format + * python-black + * python-clang + +The pre-commit hook may be installed with the following command: + + ln -s ../../extras/pre-commit.py .git/hooks/pre-commit + Once your changes have been committed to your local branch, you may use the [git-review](https://opendev.org/opendev/git-review) plugin to submit them directly to [Gerrit](https://review.lttng.org) using the following command: diff --git a/extras/pre-commit.py b/extras/pre-commit.py new file mode 100755 index 000000000..788ea7205 --- /dev/null +++ b/extras/pre-commit.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +# +# SPDX-License-Identifier: GPL-2.0-only +# SPDX-FileCopyrightText: 2024 Kienan Stewart +# + +import logging +import os +import re +import subprocess +import sys + +import clang.cindex + + +def clang_format(files): + source_files_re = re.compile(r"^.*(\.cpp|\.hpp|\.c|\.h)$") + files = [f for f in files if source_files_re.match(f)] + if not files: + return "No source files for clang-format check", 0 + + logging.debug("Files for clang-format: {}".format(files)) + format_process = subprocess.Popen( + ["clang-format", "--dry-run", "-Werror"] + files, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + stdout, _ = format_process.communicate() + return stdout.decode("utf-8"), format_process.returncode + + +def clang_tidy(files): + if not os.path.exists("compile_commands.json"): + logging.warning("Skipping clang-tidy: compile_commands.json not found") + logging.warning("To check with clang-tidy, run make with bear") + return "", 0 + + source_files_re = re.compile(r"(\.cpp|\.hpp|\.c|\.h)$") + files = [f for f in files if source_files_re.match(f)] + if not files: + return "No source files for clang-tidy check", 0 + + logging.debug("Files for clang-tidy: {}".format(files)) + tidy = subprocess.Popen(["clang-tidy"] + files, stdout=subprocess.PIPE) + stdout, _ = tidy.communicate() + return stdout.decode("utf-8"), tidy.returncode + + +def cpp_comment_style(files): + source_files_re = re.compile(r"^.*(\.cpp|\.hpp|\.c|\.h)$") + files = [f for f in files if source_files_re.match(f)] + if not files: + return "No source files for clang-format check", 0 + + returncode = 0 + stdout = "" + for file_name in files: + idx = clang.cindex.Index.create() + unit = idx.parse(file_name) + for token in unit.get_tokens(extent=unit.cursor.extent): + if token.kind != clang.cindex.TokenKind.COMMENT: + continue + if token.spelling: + words = token.spelling.split() + if words[0] != "/*" or words[-1] != "*/": + stdout += "Wrong comment style at {}\n{}\n".format( + token.extent, token.spelling + ) + returncode = 1 + return stdout, returncode + + +def python_blacken(files): + python_source_re = re.compile(r"^.*\.py$") + files = [f for f in files if python_source_re.match(f)] + if not files: + return "No python files to check for python-black", 0 + + black = subprocess.Popen( + ["black", "--check", "--diff"] + files, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + stdout, _ = black.communicate() + return stdout.decode("utf-8"), black.returncode + + +if __name__ == "__main__": + logging.basicConfig() + failures = [] + + checks = { + "cpp-comment-style": { + "func": cpp_comment_style, + }, + "clang-format": { + "func": clang_format, + }, + "clang-tidy": { + "func": clang_tidy, + }, + "python-black": { + "func": python_blacken, + }, + } + + git = subprocess.Popen( + [ + "git", + "diff", + "--name-only", + "--cached", + ], + stdout=subprocess.PIPE, + ) + stdout, stderr = git.communicate() + files = stdout.decode("utf-8").split() + + for rule_name, rule in checks.items(): + logging.info("Running rule '{}'".format(rule_name)) + stdout, returncode = rule["func"](files) + if returncode != 0: + failures.append(rule_name) + checks[rule_name]["stdout"] = stdout + checks[rule_name]["returncode"] = returncode + + for failure in failures: + logging.error( + "Failed rule '{}'\n{}".format( + failure, + checks[failure]["stdout"], + ) + ) + + # Summary + if failures: + logging.warning("Passed: {}".format(len(checks) - len(failures))) + logging.error("Failed: {}".format(len(failures))) + for failure in failures: + logging.error("Failed rule: {}".format(failure)) + + sys.exit(1 if failures else 0)