Skip to content

Commit 3668b10

Browse files
committed
fix: unbreak installing of global git hooks
Problem: the fix from 1.32.1 to ignore the user git configuration when running git commands broke `ggshield install -m global` because this command modifies the global git config so it must not ignore the user git configuration. Solution: - Add a `ignore_git_config` optional argument to `git_shell.git()`, which defaults to True. Set it to False in calls from cmd.install. - Make `install_global()` install the git hooks in a sub-directory of the dir returned by `get_data_dir()`, this is required to be able to have tests which fail in the situation of 1.32.1. - Rework test_install to be able to reproduce the bug. Add more tests. chore: add changelog entry
1 parent 80e46c9 commit 3668b10

File tree

4 files changed

+101
-39
lines changed

4 files changed

+101
-39
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
### Fixed
2+
3+
- Fixed a regression introduced in ggshield 1.32.1, which made `ggshield install -m global` crash (#972).

ggshield/cmd/install.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from click import UsageError
88

99
from ggshield.cmd.utils.common_options import add_common_options
10+
from ggshield.core.dirs import get_data_dir
1011
from ggshield.core.errors import UnexpectedError
1112
from ggshield.utils.git_shell import check_git_dir, git
1213

@@ -66,8 +67,11 @@ def install_global(hook_type: str, force: bool, append: bool) -> int:
6667
hook_dir_path = get_global_hook_dir_path()
6768

6869
if not hook_dir_path:
69-
hook_dir_path = Path("~/.git/hooks").expanduser()
70-
git(["config", "--global", "core.hooksPath", str(hook_dir_path)])
70+
hook_dir_path = get_default_global_hook_dir_path()
71+
git(
72+
["config", "--global", "core.hooksPath", str(hook_dir_path)],
73+
ignore_git_config=False,
74+
)
7175

7276
return create_hook(
7377
hook_dir_path=hook_dir_path,
@@ -78,10 +82,19 @@ def install_global(hook_type: str, force: bool, append: bool) -> int:
7882
)
7983

8084

85+
def get_default_global_hook_dir_path() -> Path:
86+
"""
87+
Returns the directory in which ggshield creates its global hooks
88+
"""
89+
return get_data_dir() / "git-hooks"
90+
91+
8192
def get_global_hook_dir_path() -> Optional[Path]:
82-
"""Return the default hooks path (if it exists)."""
93+
"""Return the default hooks path defined in git global config (if it exists)."""
8394
try:
84-
out = git(["config", "--global", "--get", "core.hooksPath"])
95+
out = git(
96+
["config", "--global", "--get", "core.hooksPath"], ignore_git_config=False
97+
)
8598
except subprocess.CalledProcessError:
8699
return None
87100
return Path(click.format_filename(out)).expanduser()

ggshield/utils/git_shell.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -186,15 +186,17 @@ def git(
186186
check: bool = True,
187187
cwd: Optional[Union[str, Path]] = None,
188188
log_stderr: bool = True,
189+
ignore_git_config: bool = True,
189190
) -> str:
190191
"""Calls git with the given arguments, returns stdout as a string"""
191192
env = os.environ.copy()
192193
# Ensure git messages are in English
193194
env["LANG"] = "C"
194195
# Ensure git behavior is not affected by the user git configuration, but give us a
195196
# way to set some configuration (useful for safe.directory)
196-
env["GIT_CONFIG_GLOBAL"] = os.getenv("GG_GIT_CONFIG", "")
197-
env["GIT_CONFIG_SYSTEM"] = ""
197+
if ignore_git_config:
198+
env["GIT_CONFIG_GLOBAL"] = os.getenv("GG_GIT_CONFIG", "")
199+
env["GIT_CONFIG_SYSTEM"] = ""
198200

199201
if cwd is None:
200202
cwd = Path.cwd()

tests/unit/cmd/test_install.py

Lines changed: 77 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import os
22
from pathlib import Path
3-
from unittest import mock
43
from unittest.mock import Mock, patch
54

65
import pytest
76
from click.testing import CliRunner
87

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

@@ -20,15 +20,6 @@
2020
"""
2121

2222

23-
@pytest.fixture(scope="class")
24-
def mockHookDirPath():
25-
with mock.patch(
26-
"ggshield.cmd.install.get_global_hook_dir_path",
27-
return_value=Path("global/hooks"),
28-
):
29-
yield
30-
31-
3223
class TestInstallLocal:
3324
def test_local_exist_is_dir(self, cli_fs_runner):
3425
os.system("git init")
@@ -187,69 +178,122 @@ def test_prepush_install(
187178
assert_invoke_ok(result)
188179

189180

181+
@pytest.fixture()
182+
def custom_global_git_config_path(tmp_path, monkeypatch):
183+
config_path = tmp_path / "global-git-config"
184+
monkeypatch.setenv("GIT_CONFIG_GLOBAL", str(config_path))
185+
yield config_path
186+
187+
190188
class TestInstallGlobal:
191-
def test_global_exist_is_dir(self, cli_fs_runner, mockHookDirPath):
192-
global_hook_path = Path("global/hooks/pre-commit")
193-
global_hook_path.mkdir(parents=True)
189+
"""
190+
These tests use the cli_runner fixture and not the cli_fs_runner one. The reason for
191+
this is they execute git commands and git commands are not run in the fake
192+
filesystem created by cli_fs_runner so the fake filesystem is useless here.
193+
"""
194+
195+
def test_global_exist_is_dir(
196+
self, cli_runner: CliRunner, custom_global_git_config_path: Path
197+
):
198+
global_pre_commit_hook_path = get_default_global_hook_dir_path() / "pre-commit"
199+
global_pre_commit_hook_path.mkdir(parents=True)
194200

195-
result = cli_fs_runner.invoke(cli, ["install", "-m", "global"])
201+
result = cli_runner.invoke(cli, ["install", "-m", "global"])
196202
assert_invoke_exited_with(result, ExitCode.USAGE_ERROR)
197203
assert result.exception
198204

199-
def test_global_not_exist(self, cli_fs_runner, mockHookDirPath):
200-
global_hook_path = Path("global/hooks/pre-commit")
201-
assert not global_hook_path.exists()
205+
def test_global_not_exist(self, cli_runner, custom_global_git_config_path: Path):
206+
global_pre_commit_hook_path = get_default_global_hook_dir_path() / "pre-commit"
207+
assert not global_pre_commit_hook_path.exists()
208+
209+
result = cli_runner.invoke(cli, ["install", "-m", "global"])
210+
assert global_pre_commit_hook_path.is_file()
211+
assert_invoke_ok(result)
212+
assert (
213+
f"pre-commit successfully added in {global_pre_commit_hook_path}"
214+
in result.output
215+
)
216+
217+
def test_install_custom_global_hook_dir(
218+
self, cli_runner: CliRunner, tmp_path: Path, custom_global_git_config_path: Path
219+
):
220+
"""
221+
GIVEN an existing global git config
222+
AND a custom value for core.hooksPath in the global git config
223+
WHEN `install -m global` is called
224+
THEN it installs the hook in the custom core.hooksPath dir
225+
"""
226+
custom_hooks_dir = tmp_path / "custom-hooks-dir"
227+
custom_pre_commit_path = custom_hooks_dir / "pre-commit"
228+
custom_global_git_config_path.write_text(
229+
f"[core]\nhooksPath = {custom_hooks_dir.as_posix()}\n", encoding="utf-8"
230+
)
202231

203-
result = cli_fs_runner.invoke(cli, ["install", "-m", "global"])
204-
assert global_hook_path.is_file()
232+
result = cli_runner.invoke(cli, ["install", "-m", "global"])
205233
assert_invoke_ok(result)
206-
assert f"pre-commit successfully added in {global_hook_path}" in result.output
234+
assert custom_pre_commit_path.is_file()
235+
assert (
236+
f"pre-commit successfully added in {custom_pre_commit_path}"
237+
in result.output
238+
)
207239

208240
@pytest.mark.parametrize("hook_type", ["pre-push", "pre-commit"])
209-
@patch("ggshield.cmd.install.get_global_hook_dir_path")
210241
def test_install_global(
211242
self,
212-
get_global_hook_dir_path_mock: Mock,
213243
hook_type: str,
214-
cli_fs_runner: CliRunner,
244+
cli_runner: CliRunner,
245+
custom_global_git_config_path: Path,
215246
):
216247
"""
217248
GIVEN None
218249
WHEN the command is run
219250
THEN it should create a pre-push git hook script in the global path
220251
"""
221-
global_hooks_dir = Path("global_hooks")
222-
get_global_hook_dir_path_mock.return_value = global_hooks_dir
223-
result = cli_fs_runner.invoke(
252+
253+
result = cli_runner.invoke(
224254
cli,
225255
["install", "-m", "global", "-t", hook_type],
226256
)
227257

228-
hook_path = global_hooks_dir / hook_type
258+
hook_path = get_default_global_hook_dir_path() / hook_type
229259
hook_str = hook_path.read_text()
230260
assert f"if [ -f .git/hooks/{hook_type} ]; then" in hook_str
231261
assert f"ggshield secret scan {hook_type}" in hook_str
232262

233263
assert f"{hook_type} successfully added in {hook_path}\n" in result.output
234264
assert_invoke_ok(result)
235265

236-
def test_global_exist_not_force(self, cli_fs_runner, mockHookDirPath):
237-
hook_path = Path("global/hooks/pre-commit")
266+
def test_global_exist_not_force(
267+
self, cli_runner: CliRunner, custom_global_git_config_path: Path
268+
):
269+
"""
270+
GIVEN a global hook dir with an exising pre-commit script
271+
WHEN install is called
272+
THEN it fails
273+
"""
274+
hook_path = get_default_global_hook_dir_path() / "pre-commit"
238275
hook_path.parent.mkdir(parents=True, exist_ok=True)
239276
hook_path.write_text("pre-commit file")
240277
assert hook_path.is_file()
241278

242-
result = cli_fs_runner.invoke(cli, ["install", "-m", "global"])
279+
result = cli_runner.invoke(cli, ["install", "-m", "global"])
243280
assert_invoke_exited_with(result, ExitCode.UNEXPECTED_ERROR)
244281
assert result.exception
245282
assert f"Error: {hook_path} already exists." in result.output
246283

247-
def test_global_exist_force(self, cli_fs_runner, mockHookDirPath):
248-
hook_path = Path("global/hooks/pre-commit")
284+
def test_global_exist_force(
285+
self, cli_runner: CliRunner, custom_global_git_config_path: Path
286+
):
287+
"""
288+
GIVEN a global hook dir with an exising pre-commit script
289+
WHEN install is called with -f
290+
THEN it ignores the fact that the pre-commit script exists and succeeds
291+
"""
292+
hook_path = get_default_global_hook_dir_path() / "pre-commit"
249293
hook_path.parent.mkdir(parents=True, exist_ok=True)
250294
hook_path.write_text("pre-commit file")
251295
assert hook_path.is_file()
252296

253-
result = cli_fs_runner.invoke(cli, ["install", "-m", "global", "-f"])
297+
result = cli_runner.invoke(cli, ["install", "-m", "global", "-f"])
254298
assert_invoke_ok(result)
255299
assert f"pre-commit successfully added in {hook_path}" in result.output

0 commit comments

Comments
 (0)