diff --git a/README.es.md b/README.es.md index c1065ae..702f63a 100644 --- a/README.es.md +++ b/README.es.md @@ -225,8 +225,9 @@ npx becwright check ``` En ambos casos becwright igual lee `.bec/rules.yaml` y frena el commit ante una -regla bloqueante rota. Corré `becwright init` una vez para generar las reglas -(salteá su instalación del hook si otra herramienta lo administra). +regla bloqueante rota. Corré `becwright init` una vez para generar las reglas — +detecta Husky, el framework pre-commit o un `core.hooksPath` custom, saltea su +propio hook e imprime la línea exacta a agregar en su lugar. ## Como check obligatorio de CI (GitHub Action) diff --git a/README.md b/README.md index 191b390..cd36cfd 100644 --- a/README.md +++ b/README.md @@ -221,8 +221,9 @@ npx becwright check ``` Either way becwright still reads `.bec/rules.yaml` and blocks the commit on a -broken blocking rule. Run `becwright init` once to scaffold the rules (skip its -hook install if another tool owns the hook). +broken blocking rule. Run `becwright init` once to scaffold the rules — +it detects Husky, the pre-commit framework, or a custom `core.hooksPath`, skips +its own hook, and prints the exact line to add instead. ## As a required CI check (GitHub Action) diff --git a/documentation/usage.es.md b/documentation/usage.es.md index 3ac1941..767c4d3 100644 --- a/documentation/usage.es.md +++ b/documentation/usage.es.md @@ -25,8 +25,10 @@ becwright init # genera .bec/rules.yaml (según el lenguaje) e inst `init` detecta si el repo tiene archivos Python o JS/TS y escribe un `.bec/rules.yaml` de arranque con reglas acordes, y luego instala el hook -pre-commit. Revisá las reglas generadas y corré `becwright check --all` para ver -el estado actual. +pre-commit. Si un hook manager ya administra los hooks (Husky, el framework +pre-commit o un `core.hooksPath` custom), `init` saltea su propio hook e +imprime la línea exacta a agregar a ese manager en su lugar. Revisá las reglas +generadas y corré `becwright check --all` para ver el estado actual. A partir de ahí, cada `git commit` corre los checks. (También podés configurarlo a mano: `becwright install` más un `.bec/rules.yaml` que escribas vos.) diff --git a/documentation/usage.md b/documentation/usage.md index 8f22551..59ac159 100644 --- a/documentation/usage.md +++ b/documentation/usage.md @@ -24,8 +24,11 @@ becwright init # scaffolds .bec/rules.yaml (language-aware) and ins ``` `init` detects whether the repo has Python or JS/TS files and writes a starter -`.bec/rules.yaml` with matching rules, then installs the pre-commit hook. Review -the generated rules and run `becwright check --all` to see the current state. +`.bec/rules.yaml` with matching rules, then installs the pre-commit hook. If a +hook manager already owns the hooks (Husky, the pre-commit framework, or a +custom `core.hooksPath`), `init` skips its own hook and prints the exact line +to add to that manager instead. Review the generated rules and run +`becwright check --all` to see the current state. From then on, every `git commit` runs the checks. (You can also set up by hand: `becwright install` plus a `.bec/rules.yaml` you write yourself.) diff --git a/src/becwright/cli.py b/src/becwright/cli.py index bff85db..fc7f409 100644 --- a/src/becwright/cli.py +++ b/src/becwright/cli.py @@ -310,11 +310,49 @@ def _cmd_doctor(_: argparse.Namespace) -> int: return 0 -def _cmd_install(_: argparse.Namespace) -> int: - root = git.repo_root() +def _hook_guidance(root: Path) -> str | None: + """When a hook manager owns this repo's hooks, a native hook in .git/hooks is + dead code (core.hooksPath redirects git away from it) or a second owner of the + same moment. Returns the integration to print instead of installing, or None + when the native hook is the right move.""" + manager = git.hook_manager(root) + override = git.hooks_path_override(root) + if manager == "husky": + return ("Husky manages this repo's hooks. Add this line to .husky/pre-commit " + "instead:\n npx becwright check") + if override: + return (f"core.hooksPath = {override}: git ignores .git/hooks entirely, so " + "the native hook would never run. Wire `becwright check` into that " + "hook path.") + if manager == "pre-commit": + return ("The pre-commit framework manages this repo's hooks. Add becwright to " + ".pre-commit-config.yaml instead:\n" + " - repo: https://github.com/DataDave-Dev/becwright\n" + f" rev: v{__version__}\n" + " hooks:\n" + " - id: becwright") + return None + + +def _install_native_hooks(root: Path) -> None: for install in (git.install_hook, git.install_msg_hook): ok, msg = install(root) print(_style(msg, GREEN if ok else YELLOW)) + + +def _cmd_install(_: argparse.Namespace) -> int: + root = git.repo_root() + if git.hooks_path_override(root): + # A native hook would be dead on arrival — refuse rather than half-install. + print(_style("Not installing: this hook would never run.", RED, BOLD), + file=sys.stderr) + print(_style(f" {_hook_guidance(root)}", DIM), file=sys.stderr) + return 2 + guidance = _hook_guidance(root) + if guidance: + print(_style(f"Note: {guidance}", YELLOW)) + print(_style(" Installing the native hook anyway, as asked.", DIM)) + _install_native_hooks(root) return 0 @@ -792,9 +830,12 @@ def _cmd_init(args: argparse.Namespace) -> int: _print_claude_summary(derived) if args.baseline: _print_baseline(rules, downgraded) - for install in (git.install_hook, git.install_msg_hook): - ok, msg = install(root) - print(_style(msg, GREEN if ok else YELLOW)) + guidance = _hook_guidance(root) + if guidance: + print(_style("Hook manager detected — not installing the native git hook.", YELLOW)) + print(f" {guidance}") + else: + _install_native_hooks(root) print() print(_style("Next steps:", BOLD)) print(f" 1. {_style('Look at your rules:', DIM)} .bec/rules.yaml") diff --git a/tests/test_hook_managers.py b/tests/test_hook_managers.py new file mode 100644 index 0000000..84df159 --- /dev/null +++ b/tests/test_hook_managers.py @@ -0,0 +1,93 @@ +import subprocess + +from becwright import cli + + +def _git(root, *args): + subprocess.run(["git", *args], cwd=root, check=True, capture_output=True, text=True) + + +def _init_repo(path): + _git(path, "init") + _git(path, "config", "user.email", "t@t.t") + _git(path, "config", "user.name", "t") + return path + + +def _native_hook(root): + return root / ".git" / "hooks" / "pre-commit" + + +# --- init --- + +def test_init_plain_repo_installs_native_hook(tmp_path, monkeypatch): + _init_repo(tmp_path) + (tmp_path / "a.py").write_text("x = 1\n", encoding="utf-8") + monkeypatch.chdir(tmp_path) + assert cli.main(["init"]) == 0 + assert _native_hook(tmp_path).exists() + + +def test_init_with_husky_skips_native_hook(tmp_path, monkeypatch, capsys): + _init_repo(tmp_path) + (tmp_path / "a.py").write_text("x = 1\n", encoding="utf-8") + (tmp_path / ".husky").mkdir() + monkeypatch.chdir(tmp_path) + assert cli.main(["init"]) == 0 + out = capsys.readouterr().out + assert not _native_hook(tmp_path).exists() + assert "npx becwright check" in out + assert (tmp_path / ".bec" / "rules.yaml").exists() # rules still scaffolded + + +def test_init_with_precommit_config_skips_native_hook(tmp_path, monkeypatch, capsys): + _init_repo(tmp_path) + (tmp_path / "a.py").write_text("x = 1\n", encoding="utf-8") + (tmp_path / ".pre-commit-config.yaml").write_text("repos: []\n", encoding="utf-8") + monkeypatch.chdir(tmp_path) + assert cli.main(["init"]) == 0 + out = capsys.readouterr().out + assert not _native_hook(tmp_path).exists() + assert ".pre-commit-config.yaml" in out + assert "id: becwright" in out + + +def test_init_with_hooks_path_override_skips_native_hook(tmp_path, monkeypatch, capsys): + _init_repo(tmp_path) + (tmp_path / "a.py").write_text("x = 1\n", encoding="utf-8") + _git(tmp_path, "config", "core.hooksPath", ".custom-hooks") + monkeypatch.chdir(tmp_path) + assert cli.main(["init"]) == 0 + out = capsys.readouterr().out + assert not _native_hook(tmp_path).exists() + assert "core.hooksPath" in out + + +# --- install --- + +def test_install_refuses_under_hooks_path_override(tmp_path, monkeypatch, capsys): + _init_repo(tmp_path) + _git(tmp_path, "config", "core.hooksPath", ".husky/_") + (tmp_path / ".husky").mkdir() + monkeypatch.chdir(tmp_path) + assert cli.main(["install"]) == 2 + assert not _native_hook(tmp_path).exists() + assert "never run" in capsys.readouterr().err + + +def test_install_proceeds_with_precommit_config_but_warns(tmp_path, monkeypatch, capsys): + _init_repo(tmp_path) + (tmp_path / ".pre-commit-config.yaml").write_text("repos: []\n", encoding="utf-8") + monkeypatch.chdir(tmp_path) + assert cli.main(["install"]) == 0 + out = capsys.readouterr().out + assert _native_hook(tmp_path).exists() + assert "pre-commit framework" in out + + +def test_install_plain_repo_stays_quietly_native(tmp_path, monkeypatch, capsys): + _init_repo(tmp_path) + monkeypatch.chdir(tmp_path) + assert cli.main(["install"]) == 0 + assert _native_hook(tmp_path).exists() + assert "Note:" not in capsys.readouterr().out