Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,19 @@ repos:
hooks:
- id: codespell

# Python linting and formatting
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.9.9
hooks:
- id: ruff
- id: ruff-format

- repo: https://github.com/asottile/pyupgrade
rev: v3.9.0
hooks:
- id: pyupgrade
args: [--py310-plus]

# CMake linting and formatting
- repo: https://github.com/BlankSpruce/gersemi
rev: 0.26.1
Expand Down
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ By default, you get pre-commit behavior without annoying
failing commits (except markdown or things not fixable automatically)
and manual amends or separate formatting commits.

The shipped example config also includes Python hooks (ruff + pyupgrade).

One drawback: the custom commit hook won't work properly for code
changes within submodules. To help a bit with that, repos
typically developed as submodules can be configured to get
Expand Down Expand Up @@ -138,15 +140,15 @@ On **CMake configure**, `mb_pre_commit_setup()` can:
- Ensure a **project-local Python venv** exists (default: `<project>/.venv`).
- **Pin and install** the `pre-commit` package with pip (`PRE_COMMIT_VERSION`, default `4.5.1`), upgrading only when the
version does not match.
- Install a **Git `pre-commit` hook** under `.git/hooks/` in the project you point at (`PROJECT_SOURCE_DIR`, default
`CMAKE_SOURCE_DIR`).
- Install a **Git `pre-commit` hook** into the effective hooks directory reported by Git (works for normal checkouts and
worktrees) in the project you point at (`PROJECT_SOURCE_DIR`, default `CMAKE_SOURCE_DIR`).
- Register a **`mb-pre-commit-sweep`** CMake target that runs **pre-commit on all files**
(`pre-commit run --all-files`) from `PROJECT_SOURCE_DIR` (see above).
- Optionally, register **another** sweep target (`PRE_COMMIT_TOOL_SWEEP_TARGET`) when **this** checkout is not the same
tree as `PROJECT_SOURCE_DIR` (or when the main sweep is off); see **Optional: sweep this package’s checkout** under
[Run pre-commit on all files](#run-pre-commit-on-all-files-mb-pre-commit-sweep).

If the tree is not a normal Git checkout (no `.git` or `.git/hooks`), setup is **skipped** with a status
If the tree is not a Git checkout (or Git cannot resolve a hooks directory there), setup is **skipped** with a status
message—configure still succeeds.

**Requirements:** `find_package(Python3 REQUIRED COMPONENTS Interpreter)` must succeed (used to create the venv). Git
Expand Down Expand Up @@ -177,8 +179,9 @@ Relative paths for `PROJECT_SOURCE_DIR` / `PROJECT_BINARY_DIR` / `PRE_COMMIT_VEN

### `CUSTOM` (default)

CMake **configures** `cmake/pre-commit.in` into `${PROJECT_BINARY_DIR}/pre-commit`, then **copies** it to
`${PROJECT_SOURCE_DIR}/.git/hooks/pre-commit` when the content differs. On non-Windows hosts the hook is marked
CMake **configures** `cmake/pre-commit.in` into `${PROJECT_BINARY_DIR}/pre-commit`, then **copies** it to the hooks
directory from `git rev-parse --git-path hooks` (for example `${PROJECT_SOURCE_DIR}/.git/hooks/pre-commit`) when the
content differs. On non-Windows hosts the hook is marked
executable.

The hook is a small Bash script that:
Expand Down
13 changes: 13 additions & 0 deletions configs/v4/.pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,19 @@ repos:
hooks:
- id: codespell

# Python linting and formatting
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.9.9
hooks:
- id: ruff
- id: ruff-format

- repo: https://github.com/asottile/pyupgrade
rev: v3.9.0
hooks:
- id: pyupgrade
args: [--py310-plus]

# CMake linting and formatting
- repo: https://github.com/BlankSpruce/gersemi
rev: 0.26.1
Expand Down
10 changes: 8 additions & 2 deletions python/mb-pre-commit-clang-format.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@ def _iter_repo_dirs(cache_root: Path) -> list[Path]:
if not cache_root.is_dir():
return []
return sorted(
child for child in cache_root.iterdir() if child.is_dir() and child.name.startswith("repo")
child
for child in cache_root.iterdir()
if child.is_dir() and child.name.startswith("repo")
)


Expand Down Expand Up @@ -109,7 +111,11 @@ def _iter_executables(repo_dir: Path) -> list[Path]:
for child in sorted(repo_dir.iterdir()):
if not child.is_dir() or not child.name.startswith("py_env-"):
continue
for rel in ("bin/clang-format", "Scripts/clang-format.exe", "Scripts/clang-format"):
for rel in (
"bin/clang-format",
"Scripts/clang-format.exe",
"Scripts/clang-format",
):
candidate = child / rel
if candidate.is_file():
executables.append(candidate)
Expand Down
56 changes: 40 additions & 16 deletions python/mb-pre-commit-setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ def _onexc(func, apath: str, exc: BaseException) -> None:
except OSError as e2:
raise exc from e2

def _onerror(func: object, apath: str, exc_info: tuple[type, BaseException, object]) -> None:
def _onerror(
func: object, apath: str, exc_info: tuple[type, BaseException, object]
) -> None:
_onexc(func, apath, exc_info[1]) # type: ignore[arg-type]

try:
Expand All @@ -63,7 +65,9 @@ def _onerror(func: object, apath: str, exc_info: tuple[type, BaseException, obje
pass
if os.path.exists(p):
if os.name != "nt":
subprocess.run(["/bin/chmod", "-R", "u+w", p], check=False, capture_output=True)
subprocess.run(
["/bin/chmod", "-R", "u+w", p], check=False, capture_output=True
)
subprocess.run(["/bin/rm", "-rf", p], check=False)
else:
subprocess.run(
Expand Down Expand Up @@ -190,13 +194,32 @@ def _which_git() -> str | None:
return shutil.which("git")


def _git_hooks_dir(project_source: Path, git_exe: str) -> Path | None:
"""Resolve the effective hooks directory, including Git worktrees."""
r = subprocess.run(
[git_exe, "rev-parse", "--git-path", "hooks"],
cwd=str(project_source),
capture_output=True,
text=True,
check=False,
)
if r.returncode != 0:
return None
raw = (r.stdout or "").strip()
if raw == "":
return None
hooks = Path(raw)
if not hooks.is_absolute():
hooks = (project_source / hooks).resolve()
return hooks


def _git_ok(project_source: Path) -> bool:
git = _which_git()
if not git:
return False
git_dir = project_source / ".git"
hooks_dir = git_dir / "hooks"
return git_dir.is_dir() and hooks_dir.is_dir()
hooks_dir = _git_hooks_dir(project_source, git)
return hooks_dir is not None and hooks_dir.is_dir()


def _resolve_path(p: str | Path, base: Path) -> Path:
Expand Down Expand Up @@ -366,7 +389,10 @@ def _install_custom_hook(
"(As it wouldn't make sense to install something that won't be used.)\n"
"Hint: `git config --unset-all core.hooksPath`"
)
hook_target = project_source / ".git" / "hooks" / "pre-commit"
hooks_dir = _git_hooks_dir(project_source, git_exe)
if hooks_dir is None:
_fatal(f"Could not resolve Git hooks directory in {project_source}")
hook_target = hooks_dir / "pre-commit"
_copy_if_different(generated_hook, hook_target)
_chmod_exec_unix(hook_target)
_status(f"Installed custom pre-commit hook: {hook_target}")
Expand Down Expand Up @@ -410,7 +436,9 @@ def _clang_format_launcher_path(venv_dir: Path) -> Path:
return bindir / "mb-pre-commit-clang-format"


def _install_clang_format_launcher(tool_root: Path, venv_dir: Path, venv_python: Path) -> Path:
def _install_clang_format_launcher(
tool_root: Path, venv_dir: Path, venv_python: Path
) -> Path:
wrapper = _clang_format_wrapper_path(tool_root)
if not wrapper.is_file():
_fatal(f"mb_pre_commit_setup: clang-format wrapper not found: {wrapper}")
Expand All @@ -419,15 +447,15 @@ def _install_clang_format_launcher(tool_root: Path, venv_dir: Path, venv_python:
if os.name == "nt":
text = (
"@echo off\r\n"
f"\"{_cmd_quote(str(venv_python))}\" "
f"\"{_cmd_quote(str(wrapper))}\" %*\r\n"
f'"{_cmd_quote(str(venv_python))}" '
f'"{_cmd_quote(str(wrapper))}" %*\r\n'
)
wrote = _write_text_if_different(launcher, text, newline="\r\n")
else:
text = (
"#!/usr/bin/env sh\n"
f"exec {_sh_single_quote(str(venv_python))} "
f"{_sh_single_quote(str(wrapper))} \"$@\"\n"
f'{_sh_single_quote(str(wrapper))} "$@"\n'
)
wrote = _write_text_if_different(launcher, text, newline="\n")
_chmod_exec_unix(launcher)
Expand Down Expand Up @@ -484,9 +512,7 @@ def _install_example_configs(
best_n, best_src = picked
dest = project_source / ".pre-commit-config.yaml"
shutil.copyfile(best_src, dest)
_status(
f"Installed example pre-commit config (configs/v{best_n}) -> {dest}"
)
_status(f"Installed example pre-commit config (configs/v{best_n}) -> {dest}")
md_src = best_src.parent / ".markdownlint.yaml"
if md_src.is_file():
md_dest = project_source / ".markdownlint.yaml"
Expand Down Expand Up @@ -624,9 +650,7 @@ def main(argv: list[str] | None = None) -> int:
args = _build_arg_parser().parse_args(argv)

tool_root = (
Path(args.tool_root).resolve()
if args.tool_root
else _default_tool_root()
Path(args.tool_root).resolve() if args.tool_root else _default_tool_root()
)
python_for_venv = (
Path(args.python_for_venv).resolve()
Expand Down
2 changes: 1 addition & 1 deletion scripts/git-tag
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def main() -> int:
"(`git tag -d TAG`) or push again when the network is healthy."
)
)
p.add_argument("tag", help='e.g. v1.2.0')
p.add_argument("tag", help="e.g. v1.2.0")
p.add_argument(
"--no-push",
action="store_true",
Expand Down
Loading