SCM view keeps only one repository visible after a new repository is discovered before a previously-visible one
- Dominant language
- TypeScript
- Stars
- 193k
- Forks
- 42.4k
- PR merge metrics
- PR metrics pending
Description
Does this issue occur when all extensions are disabled?: **Yes** (reproduced with `--disable-extensions` and a clean `--user-data-dir`)
- VS Code Version: 1.136.1 (commit a44adf7f53e00964ab890f9f8758a334f1fc15bc)
- OS Version: Linux x64
## Summary
In a multi-root workspace, if a repository that was **not** present in the previous session is discovered **before** a repository that **was** visible in the previous session, the Source Control view ends up showing **only that one previously-visible repository**. Every other repository is silently marked not-visible, even though the Git extension opened all of them.
The bad state is then written back to `scm:view:visibleRepositories`, so it survives every subsequent restart — the workspace never recovers on its own.
It is also close to invisible to the user: with a single visible repository the Source Control view does not draw repository headers at all (`repositoryCount === 1 && !scm.alwaysShowRepositories`), and the **Source Control Repositories** view is `hideByDefault: true`. The only hint is the status bar, which starts showing the branch of the wrong repository.
## Steps to Reproduce
Three empty git repos, no worktrees, no submodules, `--disable-extensions`, clean profile.
```bash
R=/tmp/scm-repro; mkdir -p $R && cd $R
for r in A B C; do git init -q $r && git -C $r commit -q --allow-empty -m init; done
# 1) seed session: workspace with A and B
printf '{ "folders": [ {"path":"A"}, {"path":"B"} ] }\n' > ws.code-workspace
code --user-data-dir $R/udd --extensions-dir $R/ext --disable-extensions --new-window $R/ws.code-workspace
# wait ~60s so the state is flushed, then close the window
# workspaceStorage now holds: {"all":[A,B],"visible":[0,1],"sortKey":"discoveryTime"}
# 2) add a NEW repository C *before* B in the folder list, reopen
printf '{ "folders": [ {"path":"A"}, {"path":"C"}, {"path":"B"} ] }\n' > ws.code-workspace
code --user-data-dir $R/udd --extensions-dir $R/ext --disable-extensions --new-window $R/ws.code-workspace
```
**Expected:** all three repositories visible in Source Control.
**Actual:** only **B** is visible. A and C are not.
`scm:view:visibleRepositories` after step 2:
```json
{"all":["git:Git:file:///tmp/scm-repro/A","git:Git:file:///tmp/scm-repro/C","git:Git:file:///tmp/scm-repro/B"],"visible":[2],"sortKey":"discoveryTime"}
```
The Git extension log confirms all three were opened normally:
```
[Model][openRepository] Opened repository (path): /tmp/scm-repro/A (kind): repository
[Model][openRepository] Opened repository (path): /tmp/scm-repro/C (kind): repository
[Model][openRepository] Opened repository (path): /tmp/scm-repro/B (kind): repository
[Model][doInitialScan] Initial repository scan completed - repositories (3), closed repositories (0), ...
```
## Matrix (all measured on 1.136.1, same profile)
| # | Folder order | Previous persisted state | Resulting `visible` | |
|---|---|---|---|---|
| 1 | A, B | none | `[0,1]` → A, B | ok |
| 2 | A, **C (new)**, B | `all=[A,B] visible=[0,1]` | **`[2]` → only B** | **bug** |
| 3 | A, C, B | `all=[A,C,B] visible=[2]` | `[2]` → only B | bad state is self-perpetuating |
| 4 | A, B, **C (new)** | `all=[A,B] visible=[0,1]` | `[0,1,2]` → all | ok (new repo discovered last) |
| 5 | C added at runtime | `all=[A,B] visible=[0,1]` | `[0,1,2]` → all | ok (restore path not taken) |
So the trigger is purely **the discovery position of a new repository relative to a previously-visible one**.
## Cause
`src/vs/workbench/contrib/scm/browser/scmViewService.ts`, `SCMViewService.onDidAddRepository`:
```ts
if (index === -1) {
// ... select all repositories ...
this.didSelectRepository = false; // (A)
return;
}
if (this.previousState.visible.indexOf(index) === -1) {
// Explicit selection started
if (this.didSelectRepository) { ...; return; } // (B)
} else {
// First visible repository
if (!this.didSelectRepository) { // (C)
removed = [...this.visibleRepositories];
this._repositories.forEach(r => { r.focused = false; r.selectionIndex = -1; });
this.didSelectRepository = true;
}
}
```
Branch (A) makes everything visible for a repository that is new to this workspace, but it also resets `didSelectRepository` to `false` while keeping `previousState` in effect. Any repository discovered afterwards that *was* visible last session therefore takes branch (C) and wipes `selectionIndex`/`focused` on **everything added so far**, leaving itself as the only visible repository.
Walking through case 2 (order A → C → B):
| step | repo | branch | result |
|---|---|---|---|
| 1 | A | in `all`, not in `visible` → (B) fall-through | visible, focused |
| 2 | C | not in `all` → (A) select all, `didSelectRepository = false` | A and C visible |
| 3 | B | in `visible`, `didSelectRepository === false` → (C) | **A and C wiped**, only B visible and focused |
## How I hit this in real life
A multi-root workspace with two repositories, one of which contains nested linked worktrees in its working tree. Those worktrees get opened by the ordinary depth-1 subfolder scan (this happens with `git.detectWorktrees: false` as well). Creating one more worktree whose directory name sorts before an existing one is enough: on the next window open the workspace collapses to that one older worktree, and because the branch status bar now targets the worktree, `Git: Checkout to...` fails with
```
fatal: 'dev' is already used by worktree at '/home/u/repo'
```
which is a fairly confusing way to find out that three of your four repositories have been hidden.
## Suggested fix
Once branch (A) has decided to select everything, `previousState` should no longer be consulted for the remainder of this load, e.g. `this.previousState = undefined;` (or calling `finishLoading()`) before its `return`. Replaying the algorithm with that change gives "all visible" for cases 2 and 3 while leaving cases 1, 4 and 5 unchanged, and it keeps the genuine hide-on-purpose case working when no new repository is involved.
Related: #301028 describes a different failure of the same branch (visibility of intentionally hidden repositories being lost). Both come from branch (A) resetting the restore state, so a single change may cover them.
I have opened #334845 with the suggested change.
It does not carry a unit test: there is currently no `scmViewService.test.ts`, and adding one needs a fair amount of service scaffolding (`ISCMService`, `SCMMenus`, storage, configuration, workspace), which I could not run in my environment. I verified the change instead by applying the equivalent patch to a copy of the shipped 1.136.1 build and re-running the scenarios above against it; the details are in the PR. Happy to add a unit test covering the discovery-order cases if you would like it in that PR.
## Screenshot
Two windows of the same 1.136.1 build, `--disable-extensions`, same settings, each seeded with the same previous state (`A` and `B` visible). The only difference is where the new repository `C` sits in the workspace folder list.
Left, folder order `A, C, B`: the Repositories view lists all three repositories but only **B** is selected, Changes has no repository headers and shows only B's files, and the status bar reads `B master*`.
Right, folder order `A, B, C`: the same three repositories, all selected, with a header and a commit box for each.
Contributor guide
Assessment
This issue has not been assessed yet.