Skip to content

Regression: hooks fail to install in a git worktree (ENOTDIR) since 2.13.0 #148

Description

@wmaurer

Summary

Installing hooks from inside a git worktree fails with ENOTDIR. This is a regression: worktrees were fixed in #88 (which closed #58), but commit 8bb9818 broke them again about two years later.

Affected versions: 2.13.0 and 2.13.1. Version 2.12.x and earlier work correctly.

This looks similar to #58, but the error is different. #58 reported ENOENT on .git/worktrees/<name>/hooks/pre-commit. The current error is ENOTDIR on <worktree>/.git/hooks. So I opened a new issue instead of reopening the old one.

How to reproduce

git init main
cd main
git config user.email t@t.t && git config user.name t
echo '{"name":"x","simple-git-hooks":{"pre-commit":"echo hi"}}' > package.json
git add -A && git commit -m init
git worktree add ../wt -b feat
cd ../wt
npx simple-git-hooks

Result:

ENOTDIR: not a directory, mkdir '.../wt/.git/hooks'

No hook is installed.

Cause

getGitProjectRoot() still handles worktrees correctly. In a worktree, .git is a file, not a directory, so the function reads the gitdir: line and follows commondir back to the main repository. This is the logic that was added in #88, and it still returns the right path today.

The problem is that _setHook() no longer uses this value:

function _setHook(hook, command, projectRoot=process.cwd()) {
    const gitRoot = getGitProjectRoot(projectRoot)   // correct path

    if (!gitRoot) { ... }                            // but only used as a null check

    const hookDirectory = _getHooksDirPath(projectRoot)   // projectRoot, not gitRoot

And _getHooksDirPath() builds the default path like this:

const defaultHooksDirPath = path.join(projectRoot, '.git', 'hooks')

In a worktree, <worktree>/.git is a file. Joining hooks onto a file path and calling mkdirSync gives ENOTDIR.

Commit 8bb9818 ("feat: optimize the migration experience from husky", #127) introduced this. It added core.hooksPath support and changed the path computation:

-    const hookDirectory = gitRoot + '/hooks/'
+    const hookDirectory = _getHooksDirPath(projectRoot)

The old line used the resolved gitRoot from #88. The new one does not, so the worktree fix became dead code.

I verified this by checking out 8bb9818^ and running the steps above: the install succeeds and the hook is written to main/.git/hooks/pre-commit, which is the correct place.

_removeHook() was changed in the same commit and has the same bug, so uninstall is also broken in a worktree.

One more small hint that this was not intended: the JSDoc of _getHooksDirPath still documents the old contract:

 * @param {string} gitRoot - The absolute path to the Git project root

but the parameter is named projectRoot and every caller passes projectRoot.

Suggested fix

Use gitRoot as the base for the default hooks directory again, and keep core.hooksPath support.

One detail worth keeping in mind: a relative core.hooksPath should still be resolved against the worktree working directory, because that is what git itself does. Only the default <gitdir>/hooks path needs to use gitRoot.

Workaround

Set an absolute core.hooksPath before installing:

git config --local core.hooksPath /abs/path/to/hooks

This skips the broken default path.


I will send a PR with the fix and a regression test for worktrees.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions