Skip to content

Store worktree mount and enlistment ids outside the shared git config - #2129

Draft
Tyrie Vella (tyrielv) wants to merge 1 commit into
microsoft:masterfrom
tyrielv:tyrielv/worktree-mount-ids
Draft

Tyrie Vella (tyrielv) wants to merge 1 commit into
microsoft:masterfrom
tyrielv:tyrielv/worktree-mount-ids

Conversation

@tyrielv

@tyrielv Tyrie Vella (tyrielv) commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Problem and Context

A linked git worktree runs its own GVFS.Mount.exe. During mount, the code wrote gvfs.mount-id and gvfs.enlistment-id with git config --local.

Git resolves --local config for a linked worktree to the shared .git/config. So a worktree mount overwrote the ids of the primary enlistment. The mount id must be unique for each mount of an enlistment, so this corrupted telemetry correlation. The two writes also added unconditional writes of the shared config to every worktree mount. On Windows, git writes config through config.lock and a rename, and a concurrent reader can fail with Permission denied.

The worktree identity was also not stable. Every worktree mount called RepoMetadata.SaveCloneMetadata, which generated a new EnlistmentId in the worktree metadata. Heartbeat and lock telemetry report that value.

Changes

  • A worktree keeps its ids in its own repo metadata (.git/worktrees/<name>/.gvfs/databases/RepoMetadata.dat). The worktree mount writes no git config for the ids.
  • RepoMetadata.SaveWorktreeMetadata replaces SaveCloneMetadata on the worktree mount path. It keeps an existing enlistment id, so each worktree has one stable enlistment id across mounts. That id is separate from the primary enlistment id.
  • RepoMetadata adds a MountId key. The worktree mount writes a new mount id to it on each mount.
  • RepoMetadata.TryReadIds and TryEnsureEnlistmentId read and create the ids without initializing RepoMetadata.Instance. For a worktree, GVFSEnlistment.GetEnlistmentId and GetMountId use TryReadIds.
  • InProcessMountVerb creates the worktree enlistment id before it creates the tracer. This gives the first mount of a new worktree an enlistment id in its telemetry.
  • InProcessMount.LogEnlistmentInfoAndSetConfigValues emits the same EnlistmentInfo event for both enlistment types. For a worktree, it writes the mount id to repo metadata instead of git config.
  • WorktreeEnlistmentTests checks that worktree ids come from the worktree repo metadata and not from the shared config. It also checks that a read does not create metadata, that a locked metadata file gives empty ids, and that the enlistment id is created only once.
  • WorktreeTests.WorktreeMountKeepsPrimaryIdsAndHasStableWorktreeEnlistmentId mounts and remounts a real worktree. It checks that the config ids of the primary do not change, that the worktree enlistment id stays the same, and that the worktree mount id changes.

The primary enlistment behavior does not change.

Why not extensions.worktreeConfig

git config --worktree can store per-worktree values in git config, but it has three costs here:

  • The extension must be enabled with a write to the shared config.
  • --local from a worktree still writes the shared config, so every worktree write site must change to --worktree.
  • git worktree add run from a linked worktree copies the config.worktree of that worktree into the new worktree. A new worktree would start with the ids of its sibling.

Repo metadata has none of these problems, and only the mount process reads the ids.

Known gaps and deliberate decisions

  • status.deserializePath still writes the shared config from every worktree mount — RequiredGitConfig builds this value from the per-worktree .gvfs path. TrySetRequiredGitConfigSettings writes it with --local when the value differs, so each worktree mount rewrites the shared value. Parallel worktree mounts can still race on config.lock. Deferred because a correct fix needs per-worktree git config. A follow-up PR will address it.
  • Worktree metadata is written before the mount lock — InitializeWorktreeMetadata saves repo metadata before it takes the per-worktree mount lock. A second concurrent mount of the same worktree can retry the metadata replace indefinitely, instead of reporting that a mount is already running. This behavior is pre-existing and this PR does not change it. No follow-up planned.
  • Worktree telemetry has no link to the primary enlistment id — A worktree reports its own enlistment id, and EnlistmentInfo does not include the id of the primary. Deferred because separate identity is the intended design. No follow-up planned.
  • The telemetry envelope carries the previous mount id — The mount process creates its tracer before it generates the new mount id, so its events carry the id of the previous mount. This behavior is pre-existing and also applies to the primary enlistment. No follow-up planned.

A linked worktree mount wrote gvfs.mount-id and gvfs.enlistment-id with
git config --local. Git resolves --local config for a linked worktree to
the shared config, so each worktree mount overwrote the primary
enlistment's ids. That corrupted telemetry correlation, because the mount
id must be unique per mount of a given enlistment. It also added two
unconditional writes of the shared .git/config to every worktree mount.

A worktree now keeps its ids in its own repo metadata
(.git/worktrees/<name>/.gvfs/databases/RepoMetadata.dat) and writes no
git config for them:

- SaveWorktreeMetadata replaces SaveCloneMetadata on the worktree mount
  path. It keeps an existing enlistment id, so the worktree enlistment id
  is stable across mounts. Before, every worktree mount generated a new
  one, and heartbeat and lock telemetry reported it.
- The mount id is written to the new MountId key on each mount.
- GVFSEnlistment.GetEnlistmentId and GetMountId read the worktree repo
  metadata without initializing RepoMetadata.Instance.
- The mount process creates the worktree enlistment id before it creates
  its tracer, so the first mount of a new worktree has an enlistment id
  in its telemetry.

The primary enlistment behavior does not change.

Assisted-by: Claude Opus 4.8
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant