Skip to content

grip_hook.hooks

grip_hook.hooks

Install and remove native git hooks.

The generated hook is a tiny POSIX shell script that delegates to grip hook <stage>. It is marked with :data:MARKER so we never overwrite a hook we did not write. Users of the pre-commit framework should use .pre-commit-hooks.yaml instead; see the documentation.

The hook first runs the grip that installed it, by absolute path: IDEs and GUI git clients often run hooks with a PATH that does not have grip on it. PATH and python3 -m grip_hook are the fallbacks.

GripCommand dataclass

How a hook starts the grip that installed it.

Source code in src/grip_hook/hooks.py
@dataclass(frozen=True, slots=True)
class GripCommand:
    """How a hook starts the grip that installed it."""

    path: str
    """Absolute path with forward slashes, which git-bash on Windows understands too."""
    python: bool = False
    """``True`` when ``path`` is a Python interpreter to run ``-m grip_hook`` with."""

path instance-attribute

path: str

Absolute path with forward slashes, which git-bash on Windows understands too.

python class-attribute instance-attribute

python: bool = False

True when path is a Python interpreter to run -m grip_hook with.

HookStatus dataclass

What is installed for one stage.

Source code in src/grip_hook/hooks.py
@dataclass(frozen=True, slots=True)
class HookStatus:
    """What is installed for one stage."""

    stage: Stage
    path: Path
    exists: bool
    managed: bool
    """``True`` when the file was written entirely by grip."""
    appended: bool
    """``True`` when grip's snippet was appended to a pre-existing hook."""
    grip_path: str | None = None
    """The grip (or Python) the hook runs first; ``None`` for hooks that only use PATH."""

    @property
    def active(self) -> bool:
        """``True`` when grip runs at this stage."""
        return self.managed or self.appended

    @property
    def grip_missing(self) -> bool:
        """``True`` when the hook names a grip that is no longer there."""
        return self.active and self.grip_path is not None and not Path(self.grip_path).exists()

managed instance-attribute

managed: bool

True when the file was written entirely by grip.

appended instance-attribute

appended: bool

True when grip's snippet was appended to a pre-existing hook.

grip_path class-attribute instance-attribute

grip_path: str | None = None

The grip (or Python) the hook runs first; None for hooks that only use PATH.

active property

active: bool

True when grip runs at this stage.

grip_missing property

grip_missing: bool

True when the hook names a grip that is no longer there.

running_grip

running_grip(argv0: str | None = None, executable: str | None = None) -> GripCommand | None

The grip running now: its console script, else this interpreter with -m.

None when neither can be found (an embedded interpreter, a frozen app).

Source code in src/grip_hook/hooks.py
def running_grip(argv0: str | None = None, executable: str | None = None) -> GripCommand | None:
    """The grip running now: its console script, else this interpreter with ``-m``.

    ``None`` when neither can be found (an embedded interpreter, a frozen app).
    """
    argv0 = sys.argv[0] if argv0 is None else argv0
    script = Path(argv0)
    if script.stem.lower() in _CONSOLE_SCRIPTS:
        if not script.parent.parts:  # a bare name, as git passes for `git grip`
            found = shutil.which(argv0)
            script = Path(found) if found else script
        # Windows launchers may report the script without its ``.exe``.
        for candidate in (script, script.with_name(script.name + ".exe")):
            if candidate.is_file():
                return GripCommand(candidate.absolute().as_posix())
    executable = sys.executable if executable is None else executable
    if executable and Path(executable).is_file():
        return GripCommand(Path(executable).absolute().as_posix(), python=True)
    return None

status

status(git: Git, stage: Stage) -> HookStatus

Inspect the hook file for stage.

Source code in src/grip_hook/hooks.py
def status(git: Git, stage: Stage) -> HookStatus:
    """Inspect the hook file for ``stage``."""
    path = git.hooks_dir() / stage.value
    if not path.exists():
        return HookStatus(stage, path, exists=False, managed=False, appended=False)
    content = path.read_text("utf-8", errors="replace")
    managed = MARKER in content and content.startswith("#!/bin/sh\n" + MARKER)
    appended = MARKER in content and not managed
    grip_path = _recorded_grip(content) if managed or appended else None
    return HookStatus(stage, path, True, managed, appended, grip_path)

install

install(git: Git, stage: Stage, *, force: bool = False, append: bool = False, command: GripCommand | None = None) -> HookStatus

Write the hook for stage.

Parameters:

Name Type Description Default
git Git

Repository handle.

required
stage Stage

pre-commit or pre-push.

required
force bool

Overwrite a hook that grip did not write.

False
append bool

Append grip's snippet to an existing foreign hook instead of failing.

False
command GripCommand | None

The grip the hook runs first; defaults to :func:running_grip.

None

Raises:

Type Description
GripError

when a foreign hook exists and neither force nor append is set.

Source code in src/grip_hook/hooks.py
def install(
    git: Git,
    stage: Stage,
    *,
    force: bool = False,
    append: bool = False,
    command: GripCommand | None = None,
) -> HookStatus:
    """Write the hook for ``stage``.

    Args:
        git: Repository handle.
        stage: ``pre-commit`` or ``pre-push``.
        force: Overwrite a hook that grip did not write.
        append: Append grip's snippet to an existing foreign hook instead of failing.
        command: The grip the hook runs first; defaults to :func:`running_grip`.

    Raises:
        GripError: when a foreign hook exists and neither ``force`` nor ``append`` is set.
    """
    if stage not in INSTALLABLE_STAGES:
        raise GripError(f"cannot install a {stage.value} hook")
    command = command or running_grip()
    current = status(git, stage)
    path = current.path
    path.parent.mkdir(parents=True, exist_ok=True)

    if current.exists and not current.managed and not current.appended:
        if append:
            with path.open("a", encoding="utf-8") as fh:
                fh.write(_render(_APPEND_SNIPPET, stage, command))
            _make_executable(path)
            return status(git, stage)
        if not force:
            raise GripError(
                f"{path} already exists and was not written by grip. Re-run with --append to "
                "chain grip after it, or --force to replace it."
            )
    if current.appended and not force:
        # Re-running install refreshes the block, so it points at the grip running now.
        head = _strip_appended(path.read_text("utf-8"))
        path.write_text(head.rstrip("\n") + _render(_APPEND_SNIPPET, stage, command), "utf-8")
        return status(git, stage)

    path.write_text(_render(_SCRIPT, stage, command), "utf-8")
    _make_executable(path)
    if not _is_executable(path):  # pragma: no cover - filesystem without exec bits
        raise GripError(f"could not make {path} executable")
    return status(git, stage)

uninstall

uninstall(git: Git, stage: Stage) -> HookStatus

Remove grip from the hook for stage (deleting the file if grip owns it).

Source code in src/grip_hook/hooks.py
def uninstall(git: Git, stage: Stage) -> HookStatus:
    """Remove grip from the hook for ``stage`` (deleting the file if grip owns it)."""
    current = status(git, stage)
    if not current.exists:
        return current
    if current.managed:
        current.path.unlink()
    elif current.appended:
        content = current.path.read_text("utf-8")
        current.path.write_text(_strip_appended(content), "utf-8")
    return status(git, stage)