Does this issue occur when all extensions are disabled?: Yes (the cause is in the built-in Git extension, see analysis below)
- VS Code Version: (behavior verified in the Git extension source on
main, 1.131.0, commit 396ff629)
- OS Version: macOS (Darwin 25.5.0)
Steps to Reproduce:
- Open a workspace whose root folder is a git repository (the "parent").
- Have a second git repository nested in a subfolder beneath it (any depth below the parent root).
- Case A: open a file from the nested repo, then reload the window. The nested repo shows up as its own source-control provider.
- Case B: from a clean load with no nested-repo file open, open that same file afterwards. The nested repo does not show up.
Same file, same repository, different result depending only on whether it was open at startup.
Summary
Whether a nested git repository (one whose root sits below an already-open workspace/parent repository) gets registered depends on timing:
- If a file belonging to the nested repo is already open when the window loads, the nested repo appears in Source Control.
- If the same file is opened later, the nested repo never appears.
The behavior is inconsistent.
Cause (from the Git extension source + trace)
Model.doInitialScan() in extensions/git/src/model.ts runs three tasks concurrently:
const initialScanFn = () => Promise.all([
this.onDidChangeWorkspaceFolders({ added: workspace.workspaceFolders || [], removed: [] }),
this.onDidChangeVisibleTextEditors(window.visibleTextEditors),
this.scanWorkspaceFolders()
]);
-
At load: onDidChangeVisibleTextEditors processes files already open. For a visible file, the parent repo may not be registered yet (opening it requires spawning git), so getRepository(uri) returns undefined and openRepository(path.dirname(uri)) is called. That resolves via git rev-parse --show-toplevel (Git.getRepositoryRoot() in extensions/git/src/git.ts) to the nested repo root and opens it.
-
Opened later: the parent repo is already registered, so getRepository(uri) returns it and the handler short-circuits:
[Model][onDidChangeVisibleTextEditors] Repository for editor resource
…/<nested-repo>/<file>
already exists: /…/<parent-repo>
openRepository is never called for the nested repo.
getRepository() matches purely by path ancestry (isDescendant over this.openRepositories, deepest root first) and only considers already-open repositories. Once the parent is open it always shadows a not-yet-open nested repo; the only carve-out is submodules of an open repository, which does not apply to plain nested repos.
Impact
Nested-repo detection is timing-dependent and inconsistent. In practice a nested repo can only be surfaced reliably by keeping one of its files open across reloads, or by declaring it explicitly in git.scanRepositories. Opening a file on demand, which works at startup, silently does nothing afterwards.
Note that Case A working at all is itself a race: the visible-editors task usually wins because opening the parent repo requires spawning a git process, but nothing guarantees the ordering.
Request
Make open-editors detection consistent: when a file is opened, open the deepest repository that contains it, even if an ancestor repository is already open (or provide an opt-in setting for this). The outcome should not depend on whether the file happened to be open at startup.
Workaround
git.scanRepositories with the nested repo's path. Only paths relative to a workspace folder are accepted; absolute paths are explicitly rejected (Model.scanWorkspaceFolders()).
- Keeping a nested-repo file open across window reloads.
Related issues
None of these fixed the open-editors code path described above; the short-circuit still exists on current main (1.131.0).
- #133577: umbrella feature request for nested repository detection. Closed when workspace folder scanning shipped; the openEditors gap remained.
- #322859: same symptom (nested repo appears only after opening a file and reloading the window). Closed by the reporter; the root cause there was the
git.repositoryScanMaxDepth default. The reload behavior it describes is exactly the startup race explained here.
- #198744: same symptom (editing a nested-repo file no longer opens the repo). Turned out to be a GitLens regression and was closed by the reporter. It shows the open-on-edit behavior users expect comes from GitLens, not the built-in Git extension.
- #189287 (
bug, git, confirmed, still open): a different symptom of the same root cause, purely path-based attribution in getRepository(), in the opposite direction. A diff for a file tracked by the ancestor repo resolves its original content against the deepest open repo, where the file is ignored, so the original side shows empty. This report is the detection/registration counterpart: there the deepest open repo wrongly wins, here the already-open ancestor wrongly wins.
Does this issue occur when all extensions are disabled?: Yes (the cause is in the built-in Git extension, see analysis below)
main, 1.131.0, commit396ff629)Steps to Reproduce:
Same file, same repository, different result depending only on whether it was open at startup.
Summary
Whether a nested git repository (one whose root sits below an already-open workspace/parent repository) gets registered depends on timing:
The behavior is inconsistent.
Cause (from the Git extension source + trace)
Model.doInitialScan()inextensions/git/src/model.tsruns three tasks concurrently:At load:
onDidChangeVisibleTextEditorsprocesses files already open. For a visible file, the parent repo may not be registered yet (opening it requires spawning git), sogetRepository(uri)returnsundefinedandopenRepository(path.dirname(uri))is called. That resolves viagit rev-parse --show-toplevel(Git.getRepositoryRoot()inextensions/git/src/git.ts) to the nested repo root and opens it.Opened later: the parent repo is already registered, so
getRepository(uri)returns it and the handler short-circuits:openRepositoryis never called for the nested repo.getRepository()matches purely by path ancestry (isDescendantoverthis.openRepositories, deepest root first) and only considers already-open repositories. Once the parent is open it always shadows a not-yet-open nested repo; the only carve-out is submodules of an open repository, which does not apply to plain nested repos.Impact
Nested-repo detection is timing-dependent and inconsistent. In practice a nested repo can only be surfaced reliably by keeping one of its files open across reloads, or by declaring it explicitly in
git.scanRepositories. Opening a file on demand, which works at startup, silently does nothing afterwards.Note that Case A working at all is itself a race: the visible-editors task usually wins because opening the parent repo requires spawning a git process, but nothing guarantees the ordering.
Request
Make open-editors detection consistent: when a file is opened, open the deepest repository that contains it, even if an ancestor repository is already open (or provide an opt-in setting for this). The outcome should not depend on whether the file happened to be open at startup.
Workaround
git.scanRepositorieswith the nested repo's path. Only paths relative to a workspace folder are accepted; absolute paths are explicitly rejected (Model.scanWorkspaceFolders()).Related issues
None of these fixed the open-editors code path described above; the short-circuit still exists on current
main(1.131.0).git.repositoryScanMaxDepthdefault. The reload behavior it describes is exactly the startup race explained here.bug,git,confirmed, still open): a different symptom of the same root cause, purely path-based attribution ingetRepository(), in the opposite direction. A diff for a file tracked by the ancestor repo resolves its original content against the deepest open repo, where the file is ignored, so the original side shows empty. This report is the detection/registration counterpart: there the deepest open repo wrongly wins, here the already-open ancestor wrongly wins.