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
5 changes: 3 additions & 2 deletions README.es.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
6 changes: 4 additions & 2 deletions documentation/usage.es.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.)
Expand Down
7 changes: 5 additions & 2 deletions documentation/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.)
Expand Down
51 changes: 46 additions & 5 deletions src/becwright/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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")
Expand Down
93 changes: 93 additions & 0 deletions tests/test_hook_managers.py
Original file line number Diff line number Diff line change
@@ -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
Loading