Current behavior 😯
When a submodule's worktree .git is a gitlink file (modern form), Submodule::git_dir_try_old_form() (gix/src/submodule/mod.rs:239) skips the file's gitdir: pointer entirely and falls back to the name-derived path <common_dir>/modules/<name>. When the gitlink target and the name-derived path resolve to different directories — which can happen after a submodule rename or a manual relocation of .git/modules/<name> — gix opens the wrong repository.
Every consumer of Submodule::open() is affected. The most user-visible symptom is gix status reporting a SubmoduleModification against the superproject's index when real git status reports clean. (I noticed it via starship, which uses gix internally for prompt status.)
Expected behavior 🤔
Follow the gitlink's gitdir: pointer, like git does (setup.c::read_gitfile_gently). The pointer is authoritative; the name-derived path is a guess that's only correct in the typical case where the submodule was added with its current name and never moved.
Git behavior
Git reads the worktree's .git gitlink, parses gitdir: <relative-path>, resolves it against the worktree's .git location, and opens that directory. The name-derived <common_dir>/modules/<name> path is not consulted at open time.
Steps to reproduce 🕹
Minimal shell repro that builds the divergent state from scratch:
set -e
cd /tmp && rm -rf gix-submod-bug && mkdir gix-submod-bug && cd gix-submod-bug
# Upstream module to use as a submodule.
git init -q upstream && cd upstream
git config user.email t@e && git config user.name t
echo a > f && git add f && git commit -q -m a
echo b > f && git commit -qam b
cd ..
# Superproject with the submodule under a nested name.
git init -q super && cd super
git config user.email t@e && git config user.name t
git -c protocol.file.allow=always submodule add -q ../upstream outer/inner
git commit -q -m "add submodule outer/inner"
# Relocate the gitdir to a name-collision path and rewrite the gitlink.
mv .git/modules/outer/inner .git/modules/inner
rmdir .git/modules/outer
git config --file .git/modules/inner/config core.worktree ../../../outer/inner
printf 'gitdir: ../../.git/modules/inner\n' > outer/inner/.git
# Plant a different repo at the original (name-derived) path with a
# different HEAD so the divergence is observable.
git clone -q --bare ../upstream .git/modules/outer/inner
git -C .git/modules/outer/inner update-ref HEAD "$(git -C ../upstream rev-parse @~1)"
# Real git: clean.
git status --porcelain=v2
git submodule status outer/inner
# gix: reports `SubmoduleModification` for outer/inner against the parent index.
A self-contained reproducer test against this exact state is in https://github.com/tymcauley/gitoxide/tree/fix/submodule-gitlink-resolution:
gix/tests/fixtures/make_submodules.sh — submodule-with-divergent-gitlink fixture.
gix/tests/gix/submodule.rs — gitlink_target_takes_precedence_over_name_in_git_dir_resolution.
Root cause
git_dir_try_old_form() was designed as a binary classifier — old form vs modern — not a generic gitdir resolver. Its body branches on worktree_gitdir.is_dir(): directory → use it; otherwise → fall back to the name-derived git_dir(). The "otherwise" case lumps "uninitialized submodule" and "modern gitlink file" together, even though they need different answers; the gitlink file's contents are never read.
state() compounds the issue: is_old_form is derived as maybe_old_path != git_dir, which works only because the buggy resolver returns identical values for modern submodules. Once the resolver is fixed to follow gitlinks, divergent modern forms would otherwise be misclassified as old form, so the heuristic needs replacing too.
A fix is on the same branch — git_dir_try_old_form() follows the gitlink via gix_discover::path::from_gitdir_file(), and state().is_old_form derives directly from worktree_git.is_dir(). Happy to open a PR if useful.
Related
Issue drafted with AI assistance (Claude Code) on top of my own analysis.
Current behavior 😯
When a submodule's worktree
.gitis a gitlink file (modern form),Submodule::git_dir_try_old_form()(gix/src/submodule/mod.rs:239) skips the file'sgitdir:pointer entirely and falls back to the name-derived path<common_dir>/modules/<name>. When the gitlink target and the name-derived path resolve to different directories — which can happen after a submodule rename or a manual relocation of.git/modules/<name>— gix opens the wrong repository.Every consumer of
Submodule::open()is affected. The most user-visible symptom isgix statusreporting aSubmoduleModificationagainst the superproject's index when realgit statusreports clean. (I noticed it viastarship, which uses gix internally for prompt status.)Expected behavior 🤔
Follow the gitlink's
gitdir:pointer, likegitdoes (setup.c::read_gitfile_gently). The pointer is authoritative; the name-derived path is a guess that's only correct in the typical case where the submodule was added with its current name and never moved.Git behavior
Git reads the worktree's
.gitgitlink, parsesgitdir: <relative-path>, resolves it against the worktree's.gitlocation, and opens that directory. The name-derived<common_dir>/modules/<name>path is not consulted at open time.Steps to reproduce 🕹
Minimal shell repro that builds the divergent state from scratch:
A self-contained reproducer test against this exact state is in https://github.com/tymcauley/gitoxide/tree/fix/submodule-gitlink-resolution:
gix/tests/fixtures/make_submodules.sh—submodule-with-divergent-gitlinkfixture.gix/tests/gix/submodule.rs—gitlink_target_takes_precedence_over_name_in_git_dir_resolution.Root cause
git_dir_try_old_form()was designed as a binary classifier — old form vs modern — not a generic gitdir resolver. Its body branches onworktree_gitdir.is_dir(): directory → use it; otherwise → fall back to the name-derivedgit_dir(). The "otherwise" case lumps "uninitialized submodule" and "modern gitlink file" together, even though they need different answers; the gitlink file's contents are never read.state()compounds the issue:is_old_formis derived asmaybe_old_path != git_dir, which works only because the buggy resolver returns identical values for modern submodules. Once the resolver is fixed to follow gitlinks, divergent modern forms would otherwise be misclassified as old form, so the heuristic needs replacing too.A fix is on the same branch —
git_dir_try_old_form()follows the gitlink viagix_discover::path::from_gitdir_file(), andstate().is_old_formderives directly fromworktree_git.is_dir(). Happy to open a PR if useful.Related
git_dir_try_old_form()shape, with the binary "old-form or not" mental model that this issue refines.Issue drafted with AI assistance (Claude Code) on top of my own analysis.