Skip to content

Status() and Diff() throw GitRepositoryNotFoundException for a valid repository when one submodule's git directory is missing #238

Open

Description

@matt-edmondson

What's wrong

GitCommandBuilder.CreateException classifies any failure whose diagnostic contains not a git repository as GitRepositoryNotFoundException, documented as "The path given is not inside a git working tree":

string message = $"git exited with code {result.ExitCode}: {diagnostic.Trim()}";
// Git reports a missing working tree with a stable phrase and exit code 128. Surfacing it
// as a distinct type lets callers distinguish "wrong directory" from "command failed".
//
// The phrase match relies on an English git, which RunCommandGitProcessRunner guarantees by
// running every invocation with LC_ALL=C. That guarantee is the whole reason the match is
// dependable: git translates its messages wherever the host has the catalogs installed, so
// without a forced locale this would silently miss on a non-English machine and degrade to
// a generic GitCommandException. Any alternative IGitProcessRunner implementation must
// force the locale too, or accept that degradation.
return diagnostic.Contains("not a git repository", StringComparison.OrdinalIgnoreCase)
? new GitRepositoryNotFoundException(message, result.ExitCode, result.Arguments, diagnostic)
: new GitCommandException(message, result.ExitCode, result.Arguments, diagnostic);

git uses the same phrase for a submodule's broken gitfile, for example fatal: not a git repository: sub/../.git/modules/sub. In that case the superproject is a perfectly good repository, but the library tells the caller it is not one.

Reproduction (git 2.43, verified through the library)

git init up && git -C up commit --allow-empty -m base
git clone up down
git -c protocol.file.allow=always -C down submodule add ../up sub
git -C down commit -m sub
rm -rf down/.git/modules/sub      # e.g. a copy or bind mount that left out .git/modules

Then, against down:

OpenAsync(down)  -> succeeds (rev-parse does not touch submodules)
Status()         -> GitRepositoryNotFoundException: git exited with code 128: fatal: not a git repository: sub/../.git/modules/sub
Diff()           -> GitRepositoryNotFoundException: (same)
Submodules()     -> ok
Log()            -> ok

Why it matters

The same GitRepository is reported as found by OpenAsync and as not found by Status(). A caller that does the obvious thing with GitRepositoryNotFoundException (show "this folder is not a git repository", offer Init, drop it from a list of repositories) acts on a false statement, and the real problem, one broken submodule, is hidden behind the wrong type.

Suggested fix

Only classify as GitRepositoryNotFoundException when git is talking about the repository the command was scoped to:

  • the discovery failure forms, not a git repository (or any of the parent directories) and not a git repository (or any parent up to mount point …), or
  • a not a git repository: <path> whose path is the repository's own git directory (for example a linked worktree whose admin directory has gone).

Any other not a git repository: <path>, such as one naming …/.git/modules/…, should stay a plain GitCommandException carrying git's message.

Acceptance criteria

  • With the reproduction above, Status() and Diff() throw GitCommandException and not GitRepositoryNotFoundException, and the message still names the broken submodule path.
  • Running a verb against a directory that is not inside any repository still throws GitRepositoryNotFoundException.
  • A unit test on CreateException covers both diagnostic forms.

Activity

  1. matt-edmondson commented on Oct 17, 2026

    @matt-edmondson
    ContributorAuthor

    Triage

    • Category: Bug
    • Priority: Medium
    • Area: GitIntegration/Builders/GitCommandBuilder.CreateException, which classifies git diagnostics
    • Suggested assignment: The owner of the git command layer and its exception mapping.
    • Duplicates / in progress: Not a duplicate. No open PR covers it. Open PR Pin --ignore-submodules=none in Status, Diff and Patch [patch] #222 ("Pin --ignore-submodules=none in Status, Diff and Patch") touches the same verbs and may change how often this path is hit, but it does not fix the misclassification.

    Notes: The exception type is wrong only in an uncommon setup: a submodule whose .git/modules/<name> is missing. The consequence is misleading, though, because callers may offer Init or drop the repository. The fix is contained: match only the discovery forms and the repository's own git directory, and add unit tests on CreateException for both diagnostic shapes.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions