Repository navigation
Keep worktree ids and status cache path out of the shared git config - #2129
Merged
Tyrie Vella (tyrielv) merged 2 commits intoNov 5, 2026
Merged
Conversation
Tyrie Vella (tyrielv)
force-pushed
the
tyrielv/worktree-mount-ids
branch
from
October 12, 2026 21:10
ff59aec to
1009e08
Compare
Tyrie Vella (tyrielv)
force-pushed
the
tyrielv/worktree-mount-ids
branch
from
November 3, 2026 17:02
1009e08 to
272204c
Compare
Tyrie Vella (tyrielv)
force-pushed
the
tyrielv/worktree-mount-ids
branch
4 times, most recently
from
November 5, 2026 18:42
538cce1 to
31371b4
Compare
A linked worktree mount wrote its ids and status.deserializePath with git config --local. Git resolves --local config for a linked worktree to the config that all worktrees share. So each worktree mount overwrote the ids and the status cache path of the primary enlistment. The status cache path then pointed every enlistment at the cache of the last mount. Git rejects a cache that does not match its own index, so every other enlistment lost the status cache. Required config: - One required-config helper is now used by mount, clone, and repair. - For a linked worktree, the helper enables extensions.worktreeConfig in the shared config. It writes status.deserializePath to the config.worktree of the worktree with git config --file. An unattended worktree writes an empty value, which disables the status cache. - The shared config keeps the status cache path of the primary. A worktree mount restores that value if an older version overwrote it. - A config.worktree value overrides the shared value. So the helper removes every other required setting from the config.worktree of the enlistment, for the primary too. - The primary also enables the extension when a linked worktree has a config.worktree, so repair keeps it. - The helper reads every config entry with git config --list -z. A required setting is correct only when it has exactly one entry with the required value. Git uses the last entry of a key, so the helper rewrites a key with several entries, or with an entry that has no value, with git config --replace-all. - The helper reads the local config and config.worktree with the shared config read retry. The mount passes its tracer, so retries are traced. - A failed shared write is retried up to five times. It counts as done when a new read shows the value, because concurrent worktree mounts write the same values. This also applies to the primary. - Setting names with a quoted subsection, such as credential."https://dev.azure.com".useHttpPath, now match the output of git config --list. Before, every mount rewrote that setting in the shared config. - Optional settings use a smaller helper that sets only missing keys. Its behavior does not change. - The repair job shows the error of a failed required-settings write. Ids: - The mount id is created before the mount tracer. Every event of a mount now carries the id of that mount, not of the previous mount. - A worktree writes gvfs.mount-id to its config.worktree. - The worktree enlistment id is derived from the primary enlistment id and the worktree name. It does not change when the worktree moves. A worktree that is removed and added again with the same name gets the same id. A worktree mount restores the primary enlistment id in the shared config if an older version overwrote it. - The mount reads the primary enlistment id one time, before the tracer. The tracer and the worktree metadata use the same derived id. - For the primary, GetEnlistmentId reads the repo metadata first, and then the shared config. - EnlistmentInfo reports PrimaryEnlistmentId for a worktree. - Warnings about a missing primary enlistment id, a failed mount id write, and a failed shared id restore go to telemetry. Assisted-by: Claude Opus 4.8 and Claude Opus 5.5 Signed-off-by: Tyrie Vella <tyrielv@gmail.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Tyrie Vella (tyrielv)
force-pushed
the
tyrielv/worktree-mount-ids
branch
from
November 5, 2026 19:23
31371b4 to
5bdfb37
Compare
Tyrie Vella (tyrielv)
marked this pull request as ready for review
November 5, 2026 19:48
|
[KeithBot]: Validation
🤖 KeithBot · claude-opus-5.5, gpt-6.1-sol, gemini-4-argon · reply on a thread to dispute |
… mount A mount without a GitStatusCache (unattended, or a platform without the status cache) does not update or delete GitStatusCache.dat. Git still reads that file when status.deserializePath names it, so git status can report a clean tree over real edits. This happens when an attended mount of a linked worktree writes the path of the primary to the shared config while the primary runs unattended. It also happens after an upgrade, when the shared config still names the cache of an unattended worktree. The mount now deletes the status cache file of its enlistment when it does not run a GitStatusCache. It retries the delete five times, and then fails the mount. Git skips a path that does not exist. An empty status.deserializePath in config.worktree does not disable a readable shared path: microsoft/git keeps the last readable path. The comment in RequiredGitConfig now says what the empty value does. Add the functional test WorktreeTests.UnattendedMountDeletesTheStatusCacheThatItDoesNotUpdate. The test builds an attended cache, remounts the primary unattended, checks that the cache file is gone, writes the shared path again, and checks that git status shows a new file. Assisted-by: Claude Opus 5.5 Signed-off-by: Tyrie Vella <tyrielv@gmail.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
[KeithBot]: ✅ Resolved · iteration 8 (
Validation
Approving. 🤖 KeithBot · claude-opus-5.5, gpt-6.1-sol, gemini-4-argon · reply on a thread to dispute |
Keith Klein (KeithIsSleeping)
approved these changes
Nov 5, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem and Context
Each linked git worktree of a GVFS enlistment runs its own
GVFS.Mount.exe. At mount, the worktree wrotegvfs.mount-id,gvfs.enlistment-id, and the required settingstatus.deserializePathwithgit config --local.Git resolves
--localconfig for a linked worktree to the shared.git/config, not to a per-worktree file. So each worktree mount overwrote values that belong to the primary enlistment:gvfs.mount-idandgvfs.enlistment-id, which broke telemetry correlation.git statusto a cache under its own.gvfsdirectory. The sharedstatus.deserializePathpointed every enlistment at the cache of whichever enlistment mounted last. microsoft/git rejects a cache that does not match the index of the reader, so every other enlistment, the primary too, silently lost the status cache.unable to access '.git/config': Permission denied.Changes
Required config
RequiredGitConfig.TrySetRequiredSettingsis now the single helper for required settings. Mount,gvfs clone, andgvfs repairuse it, which replaces two copies of the same loop.extensions.worktreeConfigin the shared config. It writesstatus.deserializePathto.git/worktrees/<name>/config.worktreewithgit config --file. Git then uses the worktree value for that worktree and the shared value for the primary.status.deserializePathto itsconfig.worktree. The empty value replaces a path that an earlier attended mount wrote. It does not disable a readable shared path, because microsoft/git uses the last readable path. Git rejects the cache of the primary in a worktree, because that cache does not match the worktree index.GitStatusCache.datof its enlistment before it reports ready. Nothing updates that file while the mount runs, but git reads it whenstatus.deserializePathnames it. A worktree mount writes the path of the primary to the shared config again, so an unattended primary otherwise reads an old cache. The mount retries the delete five times, and then fails.config.worktreevalue overrides the shared value when the extension is on. So the helper removes every other required setting from theconfig.worktreeof the enlistment, for the primary too.config.worktree. Sogvfs repairon the primary keeps the extension.git config --list -z. A required setting is correct only when it has exactly one entry with the required value. Git uses the last entry of a key. So the helper rewrites a key that has several entries, or an entry without a value, with--replace-all. This also applies toextensions.worktreeConfig.config.worktreealso finds a required key that has no value.config.worktreewith the config read retry from Retry transient Git config reads #2130. The mount passes its tracer to the helper, so the retries are traced.credential."https://dev.azure.com".useHttpPath, now match the output ofgit config --list. Before this change, every mount of every enlistment rewrote that setting in the shared config.TrySetMissingConfig, that sets only missing keys. Its behavior does not change.Ids
GVFS.Mountcreates the mount id before it creates its tracer. Every telemetry event of a mount now carries the id of that mount. Before this change, the tracer read the id of the previous mount from git config. This applies to the primary too.GetEnlistmentIdreads the repo metadata first, and then the shared config.gvfs.mount-idto itsconfig.worktree.EnlistmentInfoevent of a worktree mount addsPrimaryEnlistmentId, so telemetry can relate a worktree to its primary.gvfs.enlistment-idof the primary when an older version overwrote it.Why the ids do not use
config.worktreegit worktree add, when it runs inside a linked worktree, copies theconfig.worktreeof that worktree into the new worktree. If a worktree stored its enlistment id there, the new worktree gets the same id. The mount rewritesgvfs.mount-idandstatus.deserializePathon every mount, so a copy of those values does no harm. The enlistment id is derived instead of stored.Tests
WorktreeEnlistmentTeststhat run real git against a worktree layout. The tests check:config.worktree, and duplicate extension values;config.worktree, and a corruptconfig.worktree;WorktreeEnlistmentTeststhat pin the derived enlistment id to a known value. These tests also cover worktree-name case and the fallback to the stored id when the primary metadata is missing or locked.RequiredGitConfigTeststhat useMockGitProcess. They cover a failed shared write that another process completed, a retry that succeeds, a write that keeps failing, an unreadable local config, a quoted-subsection setting that is already present, a key with several values, a key without a value, and a failed write whose new read shows a second value.WorktreeTests.WorktreeMountUsesPerWorktreeConfigAndKeepsPrimaryValues. It mounts and remounts a real worktree and checks that the primary keeps its ids and status cache path. It also checks that the worktree has its own status cache path, a stable enlistment id, and a new mount id on each mount. The test reads theEnlistmentInfoevent from the mount log of the worktree and checks itsmountId,EnlistmentId, andPrimaryEnlistmentIdagainst the stored values.WorktreeTests.WorktreeMountRepairsConfigLeftByPreviousVersion. It recreates the state that an older version leaves, mounts only the worktree, and checks that the shared values of the primary and the worktree values are repaired. The test restores the shared values when it ends.WorktreeTests.UnattendedMountDeletesTheStatusCacheThatItDoesNotUpdate. The test builds an attended status cache, remounts the primary unattended, and checks that the cache file is gone. It then writes the shared path again, as a worktree mount does, and checks thatgit statusshows a new file.Known gaps and deliberate decisions
.git/configwhile GVFS replaces it can fail withPermission denied. GVFS readers retry this failure since Retry transient Git config reads #2130. This change limits shared writes to the repair of wrong values, but git processes outside GVFS do not retry. No follow-up planned.config.worktreeand leavesextensions.worktreeConfigenabled. Inside the worktree,git config gvfs.mount-idthen returns the last mount id that this version wrote. The older version reads the id with--local, so GVFS itself is not affected. No follow-up planned.core.worktree— When the extension is enabled, git applies a sharedcore.worktreeto linked worktrees too. GVFS clones do not setcore.worktree, so this change does not migrate it. No follow-up planned.InitializeWorktreeMetadatasaves repo metadata before it takes the per-worktree mount lock. A second concurrent mount of the same worktree can retry the metadata replace for a long time instead of reporting that a mount is already running. This behavior predates this change. No follow-up planned.gvfs.mount-id, the value stays wrong until the primary mounts again. Only telemetry tooling reads that value.git config --list --localand--list --filedo not expandinclude.pathorincludeIf. A required setting that comes from an included file is not checked or removed. This gap already exists for the shared.git/config. GVFS does not write includes. No follow-up planned.status.deserializePathof the primary. The next mount of the primary, or the next attended worktree mount, restores it. No follow-up planned.gvfs.mount-idinconfig.worktreeon each mount. A git process in that worktree that reads its config at the same moment can fail. The window is one small file write per mount. GVFS readers retry this failure. No follow-up planned.