From 879c05894db669ddd48da9a132c2960bc742871a Mon Sep 17 00:00:00 2001 From: Matias Duarte Date: Mon, 29 Jun 2026 22:33:06 -0300 Subject: [PATCH] fix(hooks): limit Windows hook rebuild workers --- graphify/hooks.py | 17 +++++++++++++++++ tests/test_hooks.py | 14 ++++++++++++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/graphify/hooks.py b/graphify/hooks.py index c85c48f7..a2a3325f 100644 --- a/graphify/hooks.py +++ b/graphify/hooks.py @@ -1,6 +1,7 @@ # git hook integration - install/uninstall graphify post-commit and post-checkout hooks from __future__ import annotations import configparser +import os import re import sys from pathlib import Path @@ -224,6 +225,13 @@ _HOOK_SCRIPT = """\ # churn run-to-run. Pinning it makes graphify-out reproducible. export PYTHONHASHSEED=0 +# Git for Windows/MSYS hooks can inherit fragile pipe handles from GUI clients +# and agent shells. Keep hook-triggered rebuilds sequential by default there; +# explicit GRAPHIFY_MAX_WORKERS still wins for users who want parallelism. +if [ -n "${WINDIR:-}" ] || [ -n "${MSYSTEM:-}" ]; then + export GRAPHIFY_MAX_WORKERS="${GRAPHIFY_MAX_WORKERS:-1}" +fi + # Skip during rebase/merge/cherry-pick to avoid blocking --continue with unstaged changes GIT_DIR=$(git rev-parse --git-dir 2>/dev/null) [ -d "$GIT_DIR/rebase-merge" ] && exit 0 @@ -269,6 +277,13 @@ _CHECKOUT_SCRIPT = """\ # churn run-to-run. Pinning it makes graphify-out reproducible. export PYTHONHASHSEED=0 +# Git for Windows/MSYS hooks can inherit fragile pipe handles from GUI clients +# and agent shells. Keep hook-triggered rebuilds sequential by default there; +# explicit GRAPHIFY_MAX_WORKERS still wins for users who want parallelism. +if [ -n "${WINDIR:-}" ] || [ -n "${MSYSTEM:-}" ]; then + export GRAPHIFY_MAX_WORKERS="${GRAPHIFY_MAX_WORKERS:-1}" +fi + PREV_HEAD=$1 NEW_HEAD=$2 BRANCH_SWITCH=$3 @@ -319,6 +334,8 @@ def _reject_windows_path(value: str, source: str) -> None: junk directory (backslashes and all), while install reports success and the real ``.git/hooks`` gets nothing. Fail loudly instead so the user can fix it. """ + if os.name == "nt": + return if _WINDOWS_DRIVE_RE.match(value) or "\\" in value: raise RuntimeError( f"git hooks path from {source} looks like a Windows path: {value!r}. " diff --git a/tests/test_hooks.py b/tests/test_hooks.py index 3abb4df8..49f2c1da 100644 --- a/tests/test_hooks.py +++ b/tests/test_hooks.py @@ -254,6 +254,15 @@ def test_hooks_use_cross_platform_detach(name, script): assert "0x00000200" in script, f"{name} missing CREATE_NEW_PROCESS_GROUP flag" +@pytest.mark.parametrize("name,script", _HOOK_SCRIPTS) +def test_hooks_limit_windows_workers_by_default(name, script): + """Git for Windows/MSYS hooks can expose fragile pipe handles to spawned + ProcessPoolExecutor children. Hook-triggered rebuilds should default to one + worker there, while still allowing explicit user overrides.""" + assert '[ -n "${WINDIR:-}" ] || [ -n "${MSYSTEM:-}" ]' in script + assert 'export GRAPHIFY_MAX_WORKERS="${GRAPHIFY_MAX_WORKERS:-1}"' in script + + def _launcher_payload(script: str) -> str: """Extract the `python -c ""` the hook hands to GRAPHIFY_PYTHON. @@ -351,9 +360,10 @@ def _set_hookspath(repo: Path, value: str) -> None: r"D:\hooks", r"some\back\slashed\path", ]) -def test_windows_hookspath_rejected_no_junk_dir(tmp_path, winpath): +def test_windows_hookspath_rejected_no_junk_dir_on_posix(tmp_path, monkeypatch, winpath): """A Windows-style core.hooksPath must raise (loud failure), not silently - create a backslash-named junk directory and report success (#1385).""" + create a backslash-named junk directory and report success on POSIX/WSL (#1385).""" + monkeypatch.setattr("graphify.hooks.os.name", "posix") repo = _make_git_repo(tmp_path) _set_hookspath(repo, winpath) with pytest.raises(RuntimeError, match="Windows path"):