Skip to content

Commit 716c2c9

Browse files
authored
Merge pull request #1150 from GitGuardian/gg-hh/1143/husky_install
feat(install): allow to install hook locally with husky
2 parents 7218450 + 0e73973 commit 716c2c9

File tree

3 files changed

+75
-2
lines changed

3 files changed

+75
-2
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
### Fixed
2+
3+
- Install `ggshield` hooks inside `.husky/` when the repository uses Husky-managed hooks so local installs work out of the box. (#1143)

ggshield/cmd/install.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,15 +103,52 @@ def get_global_hook_dir_path() -> Optional[Path]:
103103
def install_local(hook_type: str, force: bool, append: bool) -> int:
104104
"""Local pre-commit/pre-push hook installation."""
105105
check_git_dir()
106+
hook_dir_path = get_local_hook_dir_path()
106107
return create_hook(
107-
hook_dir_path=Path(".git/hooks"),
108+
hook_dir_path=hook_dir_path,
108109
force=force,
109110
local_hook_support=False,
110111
hook_type=hook_type,
111112
append=append,
112113
)
113114

114115

116+
def get_local_hook_dir_path() -> Path:
117+
"""
118+
Return the directory where local hooks should be installed.
119+
120+
If core.hooksPath is configured, honor it and detect Husky-managed repositories
121+
to avoid overwriting Husky's shim scripts.
122+
"""
123+
hooks_path = get_git_local_hooks_path()
124+
if hooks_path is None:
125+
return Path(".git/hooks")
126+
127+
if is_husky_hooks_path(hooks_path):
128+
return hooks_path.parent
129+
130+
return hooks_path
131+
132+
133+
def get_git_local_hooks_path() -> Optional[Path]:
134+
"""Return the hooks path defined in the repository config, if any."""
135+
try:
136+
out = git(
137+
["config", "--local", "--get", "core.hooksPath"], ignore_git_config=False
138+
)
139+
except subprocess.CalledProcessError:
140+
return None
141+
return Path(click.format_filename(out)).expanduser()
142+
143+
144+
def is_husky_hooks_path(path: Path) -> bool:
145+
"""Detect Husky-generated hooks directories (.husky/_)."""
146+
try:
147+
return path.name == "_" and path.parent.name == ".husky"
148+
except IndexError:
149+
return False
150+
151+
115152
def create_hook(
116153
hook_dir_path: Path,
117154
force: bool,

tests/unit/cmd/test_install.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from click.testing import CliRunner
77

88
from ggshield.__main__ import cli
9-
from ggshield.cmd.install import get_default_global_hook_dir_path
9+
from ggshield.cmd.install import get_default_global_hook_dir_path, install_local
1010
from ggshield.core.errors import ExitCode
1111
from tests.unit.conftest import assert_invoke_exited_with, assert_invoke_ok
1212

@@ -177,6 +177,39 @@ def test_prepush_install(
177177
assert f"pre-push successfully added in {hook_path}\n" in result.output
178178
assert_invoke_ok(result)
179179

180+
@patch("ggshield.cmd.install.check_git_dir")
181+
@patch("ggshield.cmd.install.git")
182+
def test_install_local_detects_husky(
183+
self,
184+
git_mock: Mock,
185+
check_dir_mock: Mock,
186+
cli_fs_runner: CliRunner,
187+
):
188+
"""
189+
GIVEN a repository configured with Husky (.husky/_ directory as hooks path)
190+
WHEN install_local is called
191+
THEN it should create the hook in .husky/pre-commit instead of .git/hooks
192+
"""
193+
husky_dir = Path(".husky")
194+
husky_hooks_dir = husky_dir / "_"
195+
husky_hooks_dir.mkdir(parents=True)
196+
197+
# Mock git to return .husky/_ as the local hooks path
198+
git_mock.return_value = ".husky/_"
199+
200+
return_code = install_local(hook_type="pre-commit", force=False, append=False)
201+
202+
assert return_code == 0
203+
204+
# Hook should be in .husky/pre-commit, not .husky/_/pre-commit
205+
husky_hook = husky_dir / "pre-commit"
206+
assert husky_hook.is_file()
207+
assert 'ggshield secret scan pre-commit "$@"' in husky_hook.read_text()
208+
209+
# Hook should NOT be in .git/hooks/pre-commit
210+
default_hook = Path(".git/hooks/pre-commit")
211+
assert not default_hook.exists()
212+
180213

181214
@pytest.fixture()
182215
def custom_global_git_config_path(tmp_path, monkeypatch):

0 commit comments

Comments
 (0)