summaryrefslogtreecommitdiffstats
path: root/tools/clang_format.py
diff options
context:
space:
mode:
authorJan Holthuis <jan.holthuis@ruhr-uni-bochum.de>2020-07-31 18:19:12 +0200
committerJan Holthuis <jan.holthuis@ruhr-uni-bochum.de>2020-07-31 18:52:56 +0200
commit1d77aa3032e8575712821149d82e594920f05cee (patch)
tree96686112e5703469e5cb92e76548182080fee36e /tools/clang_format.py
parent0cf2ee06970022f14b3a76b7c354191840c67415 (diff)
tools: Refactor python clang-format wrapping
This merges the clang-format and line-length hooks into a single one. Semantically, this makes way more sense. Also, this prevent multiple attempted commit that are rejected by pre-commit (pre-commit might skip the line-length hook if old clang-format hook already failed). Also, this removes any dependency on git-clang-format and implements the whole mechanism itself. It retrieves all added lines from the unified diff instead and is now capable of taking PRE_COMMIT_FROM_REF into account. By moving the git-related code into a separate githelper library, it's way easier to write additional python wrappers for new hooks, e.g. for clang-tidy.
Diffstat (limited to 'tools/clang_format.py')
-rwxr-xr-xtools/clang_format.py124
1 files changed, 124 insertions, 0 deletions
diff --git a/tools/clang_format.py b/tools/clang_format.py
new file mode 100755
index 0000000000..4447cb70aa
--- /dev/null
+++ b/tools/clang_format.py
@@ -0,0 +1,124 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+import argparse
+import logging
+import os
+import re
+import subprocess
+import sys
+import tempfile
+import typing
+
+import githelper
+
+
+# We recommend a maximum line length of 80, but do allow up to 100 characters
+# if deemed necessary by the developer. Lines that exceed that limit will
+# be wrapped after 80 characters automatically.
+LINE_LENGTH_THRESHOLD = 100
+BREAK_BEFORE = 80
+
+
+def get_clang_format_config_with_columnlimit(rootdir, limit):
+ cpp_file = os.path.join(rootdir, "src/mixxx.cpp")
+ proc = subprocess.run(
+ ["clang-format", "--dump-config", cpp_file],
+ capture_output=True,
+ text=True,
+ )
+ proc.check_returncode()
+ return re.sub(r"(ColumnLimit:\s*)\d+", r"\g<1>{}".format(80), proc.stdout,)
+
+
+def run_clang_format_on_lines(rootdir, changed_file, assume_filename=None):
+ logger = logging.getLogger(__name__)
+
+ line_arguments = [
+ "--lines={}:{}".format(start, end) for start, end in changed_file.lines
+ ]
+ assert line_arguments
+
+ logger.info("Reformatting %s...", changed_file.filename)
+ filename = os.path.join(rootdir, changed_file.filename)
+ cmd = [
+ "clang-format",
+ "--style=file",
+ "--assume-filename={}".format(
+ assume_filename if assume_filename else filename
+ ),
+ *line_arguments,
+ ]
+
+ with open(filename) as fp:
+ logger.debug("Executing: %r", cmd)
+ proc = subprocess.run(cmd, stdin=fp, capture_output=True, text=True)
+ try:
+ proc.check_returncode()
+ except subprocess.CalledProcessError:
+ logger.error(
+ "Error while executing command %s: %s", cmd, proc.stderr,
+ )
+ raise
+
+ if proc.stderr:
+ logger.error(proc.stderr)
+ with open(filename, mode="w+") as fp:
+ fp.write(proc.stdout)
+
+
+def main(argv: typing.Optional[typing.List[str]] = None) -> int:
+ logging.basicConfig(
+ format="[%(levelname)s] %(message)s", level=logging.INFO
+ )
+
+ logger = logging.getLogger(__name__)
+
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--from-ref", help="compare against git reference")
+ parser.add_argument("files", nargs="*", help="only check these files")
+ args = parser.parse_args(argv)
+
+ if not args.from_ref:
+ args.from_ref = os.getenv("PRE_COMMIT_FROM_REF") or os.getenv(
+ "PRE_COMMIT_SOURCE"
+ )
+
+ # Filter filenames
+ rootdir = githelper.get_toplevel_path()
+
+ # First pass: Format added lines using clang-format
+ logger.info("First pass: Reformat files...")
+ files_with_added_lines = githelper.get_changed_lines_grouped(
+ from_ref=args.from_ref,
+ filter_lines=lambda line: line.added,
+ include_files=args.files,
+ )
+ for changed_file in files_with_added_lines:
+ run_clang_format_on_lines(rootdir, changed_file)
+
+ # Second pass: Wrap long added lines using clang-format
+ logger.info("Second pass: Reformat files with long lines...")
+ files_with_long_added_lines = githelper.get_changed_lines_grouped(
+ from_ref=args.from_ref,
+ filter_lines=lambda line: line.added
+ and LINE_LENGTH_THRESHOLD < (len(line.text) - 1),
+ include_files=args.files,
+ )
+ config = get_clang_format_config_with_columnlimit(rootdir, BREAK_BEFORE)
+ with tempfile.TemporaryDirectory(prefix="clang-format") as tempdir:
+ # Create temporary config with ColumnLimit enabled
+ configfile = os.path.join(tempdir, ".clang-format")
+ with open(configfile, mode="w") as configfp:
+ configfp.write(config)
+
+ for changed_file in files_with_long_added_lines:
+ run_clang_format_on_lines(
+ rootdir,
+ changed_file,
+ assume_filename=os.path.join(tempdir, changed_file.filename),
+ )
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())