]> git.lttng.org Git - lttng-tools.git/commitdiff
misc: Add pre-commit hook for common checks
authorKienan Stewart <kstewart@efficios.com>
Tue, 10 Dec 2024 17:11:37 +0000 (12:11 -0500)
committerJérémie Galarneau <jeremie.galarneau@efficios.com>
Thu, 12 Dec 2024 19:54:13 +0000 (19:54 +0000)
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 <SourceRange start <SourceLocation file 'src/bin/lttng-sessiond/main.cpp', line 109, column 1>, end <SourceLocation file 'src/bin/lttng-sessiond/main.cpp', line 109, column 6>>
// hi
Wrong comment style at <SourceRange start <SourceLocation file 'src/bin/lttng-sessiond/main.cpp', line 111, column 1>, end <SourceLocation file 'src/bin/lttng-sessiond/main.cpp', line 111, column 12>>
/** hey **/
Wrong comment style at <SourceRange start <SourceLocation file 'src/bin/lttng-sessiond/main.cpp', line 113, column 1>, end <SourceLocation file 'src/bin/lttng-sessiond/main.cpp', line 113, column 11>>
/* sup **/
Wrong comment style at <SourceRange start <SourceLocation file 'src/bin/lttng-sessiond/main.cpp', line 115, column 1>, end <SourceLocation file 'src/bin/lttng-sessiond/main.cpp', line 115, column 3>>
//
Wrong comment style at <SourceRange start <SourceLocation file 'src/bin/lttng-sessiond/main.cpp', line 117, column 1>, end <SourceLocation file 'src/bin/lttng-sessiond/main.cpp', line 117, column 5>>
/**/

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 <kstewart@efficios.com>
Change-Id: Icc5e7c9324ebc937cf295619f53557649797d914
Signed-off-by: Jérémie Galarneau <jeremie.galarneau@efficios.com>
CONTRIBUTING.md
extras/pre-commit.py [new file with mode: 0755]

index 3eaec7489867da038c54c8895d652af492d58a05..b5b38e3f132dd882657e0de46387c59349b3fd4d 100644 (file)
@@ -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 (executable)
index 0000000..788ea72
--- /dev/null
@@ -0,0 +1,142 @@
+#!/usr/bin/env python3
+#
+# SPDX-License-Identifier: GPL-2.0-only
+# SPDX-FileCopyrightText: 2024 Kienan Stewart <kstewart@efficios.com>
+#
+
+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)
This page took 0.031092 seconds and 4 git commands to generate.