Skip to content

Fix Git hooks installation in linked worktrees and submodules - #360

Merged
repolevedavaj merged 1 commit into
mainfrom
fix/git-hooks-in-linked-worktrees
Aug 14, 2026
Merged

Fix Git hooks installation in linked worktrees and submodules#360
repolevedavaj merged 1 commit into
mainfrom
fix/git-hooks-in-linked-worktrees

Conversation

@repolevedavaj

Copy link
Copy Markdown
Collaborator

Problem

project-env-cli cannot install anything in a project whose .git is a file instead of a directory. That is the case in a linked Git worktree and in a submodule.

GitSupport copied the hooks into a hardcoded <projectRoot>/.git/hooks, so the copy failed:

Installing Java (Temurin) 25.0.3+9...
Installing NodeJS 22.22.3...
Installing Git hooks...
Failed to install tools: IOException: Cannot create directory '<worktree>/.git/hooks'.

The impact is larger than the hooks. Because the CLI aborts the whole run on a tool failure, no tool is installed or activated at all in such a project, and the CLI exits 1 without writing an activation script. Any project with a [git] section is affected in every worktree.

Fix

The target directory is resolved from the referenced Git directory:

<projectRoot>/.git target
directory, or missing <projectRoot>/.git/hooks, as before
file, referenced Git directory has a commondir file the common Git directory, because a linked worktree shares its hooks with the main worktree
file, no commondir file the referenced Git directory, because a submodule holds its own hooks

A .git file that references a Git directory which does not exist now fails with a clear message instead of silently creating that path outside the project. Git rejects the same input with fatal: not a git repository: ....

Paths are resolved through the file system rather than normalised lexically. Path.normalize() removes .. segments lexically, which gives the wrong directory when a symlink sits at the traversed depth, and Git resolves such a path through the OS.

Resolution uses only java.io and java.nio. No git executable is required, so the native image is unaffected. The three added dependencies are test scope only.

Verification

git-support had no tests. GitSupportTest is new and covers every case with file system fixtures. It never invokes git, so it is deterministic on CI and on Windows.

Tests run: 10, Failures: 0, Errors: 0, Skipped: 0

Beyond the unit tests, the behaviour was checked against real Git in seven layouts: plain repository, worktree with an absolute gitdir, worktree created with --relative-paths, submodule, submodule inside a linked worktree, bare repository with a worktree, and git init --separate-git-dir. In each one the resolved target equals what git rev-parse --git-common-dir reports, and Git executed the installed hook.

The central design decision was confirmed by experiment. With competing hooks in both <main>/.git/hooks and <main>/.git/worktrees/<name>/hooks, committing from the linked worktree runs the one in the common directory.

Finally the built CLI was run against a real linked worktree of a Maven project. Before: exit 1 and no activation script. After: exit 0, a complete activation script for the worktree's own JDK, Maven and Node, and the hooks in the common Git directory.

Not addressed

Two pre-existing limitations are untouched and out of scope: core.hooksPath is still ignored, and a project root below the repository root still resolves to <projectRoot>/.git/hooks.

🤖 Generated with Claude Code

GitSupport copied the hooks into a hardcoded <projectRoot>/.git/hooks.
That assumes .git is a directory, which is only true in a plain
repository. In a linked worktree and in a submodule, .git is a regular
file that references the real Git directory, so the copy failed with
"Cannot create directory '<projectRoot>/.git/hooks'". Because the CLI
aborts the whole run on a tool failure, no tool was installed or
activated at all in such a project, not only the Git hooks.

The target directory is now resolved from the referenced Git directory:

- .git is a directory, or missing: <projectRoot>/.git/hooks, as before.
- .git is a file and the referenced Git directory has a commondir file:
  the common Git directory, because a linked worktree shares its hooks
  with the main worktree. Verified against Git: a hook in the common
  directory is executed from a linked worktree, one in the per-worktree
  administrative directory is not.
- .git is a file without a commondir file: the referenced Git directory,
  because a submodule holds its own hooks.

A .git file that references a Git directory which does not exist now
fails with a clear message instead of silently creating that path
outside the project. Git rejects the same input.

Paths are resolved through the file system rather than normalized
lexically, so a relative reference stays correct when a symlink sits on
the path. Resolution uses only java.io and java.nio, so no Git
executable is required and the native image is unaffected.

The new GitSupportTest covers all cases with file system fixtures and
never invokes Git.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@repolevedavaj repolevedavaj added the enhancement New feature or request label Aug 14, 2026
@repolevedavaj
repolevedavaj merged commit b13c5a8 into main Aug 14, 2026
0 of 5 checks passed
@repolevedavaj
repolevedavaj deleted the fix/git-hooks-in-linked-worktrees branch August 14, 2026 19:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant