From 4cd3fbb5fab2a1c6c800e0052876c8fb64cf48dd Mon Sep 17 00:00:00 2001 From: D-Majumder <98733892+D-Majumder@users.noreply.github.com> Date: Fri, 11 Sep 2026 12:37:01 +0530 Subject: [PATCH 1/2] Route git linked-worktree files to the correct window (#290708, #299540) findWindowOnFile matched windows by simple parent-folder containment of a file's path. Git linked worktrees store their private per-worktree files (COMMIT_EDITMSG, rebase-merge/git-rebase-todo, etc.) inside the *main* worktree's .git/worktrees/ directory rather than under the linked worktree's own folder, so such files always resolved to the main worktree's window instead of the correct linked-worktree window (e.g. when core.editor is "code --wait"). Add findWindowOnGitWorktreeFile, checked before the existing parent-folder matching: it recognizes .git/worktrees/ metadata paths and, by resolving each candidate window's own .git pointer file, matches against the full worktree directory path rather than just the trailing segment, so two unrelated repositories that happen to share a worktree name cannot be confused with one another. Ordinary repositories (.git as a directory) and non-worktree files are unaffected and never trigger the new filesystem read. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01TzTiUkg9L2rajTg9eb9zxY --- .../windows/electron-main/windowsFinder.ts | 85 +++++++++++++++++- .../test/electron-main/windowsFinder.test.ts | 90 +++++++++++++++++++ 2 files changed, 174 insertions(+), 1 deletion(-) diff --git a/src/vs/platform/windows/electron-main/windowsFinder.ts b/src/vs/platform/windows/electron-main/windowsFinder.ts index 1fc60b1355431..5a8cac011292c 100644 --- a/src/vs/platform/windows/electron-main/windowsFinder.ts +++ b/src/vs/platform/windows/electron-main/windowsFinder.ts @@ -3,14 +3,97 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as fs from 'fs'; +import { Schemas } from '../../../base/common/network.js'; +import { resolve } from '../../../base/common/path.js'; import { extUriBiasedIgnorePathCase } from '../../../base/common/resources.js'; import { URI } from '../../../base/common/uri.js'; import { ICodeWindow } from '../../window/electron-main/window.js'; import { IResolvedWorkspace, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IWorkspaceIdentifier } from '../../workspace/common/workspace.js'; +// Matches paths pointing into a git linked worktree's private metadata +// directory, e.g. `/.git/worktrees//COMMIT_EDITMSG`. +// Captures the full path of the worktree's metadata directory itself, i.e. +// everything up to and including `.git/worktrees/`. Accepts both `/` +// and `\` as path separators. +const gitWorktreeFilePathRegex = /^(.*[\\/]\.git[\\/]worktrees[\\/][^\\/]+)[\\/]/; + +/** + * Git linked worktrees store their private per-worktree files (such as + * `COMMIT_EDITMSG` or `rebase-merge/git-rebase-todo`) inside the *main* + * worktree's `.git/worktrees/` directory, even though the corresponding + * working directory lives elsewhere on disk. A plain parent-folder match on + * such a file's path therefore always resolves to the window that has the + * main worktree open, never the window with the linked worktree the file + * actually belongs to. + * + * This detects that case and, if the linked worktree is open in one of the + * candidate windows, returns that window instead. Returns `undefined` when + * the path is not a git worktree metadata path, or when no candidate window + * has the corresponding linked worktree open, so callers can fall back to + * their normal matching logic. + */ +async function findWindowOnGitWorktreeFile(windows: ICodeWindow[], fileUri: URI): Promise { + if (fileUri.scheme !== Schemas.file) { + return undefined; + } + + const worktreeMatch = gitWorktreeFilePathRegex.exec(fileUri.fsPath); + if (!worktreeMatch) { + return undefined; + } + + // The exact directory the file's worktree metadata lives in, e.g. + // `/path/to/main/.git/worktrees/linked`. Candidate windows are matched + // against this full path rather than just the trailing `` segment, + // so that two unrelated repositories that happen to use the same worktree + // name (e.g. both named `linked`) cannot be confused with one another. + const worktreeGitDir = URI.file(worktreeMatch[1]); + + for (const window of windows) { + const openedFolder = isSingleFolderWorkspaceIdentifier(window.openedWorkspace) ? window.openedWorkspace.uri : undefined; + if (!openedFolder || openedFolder.scheme !== Schemas.file) { + continue; + } + + // A linked worktree's working directory contains a plain-text `.git` + // *file* (not a directory) of the form `gitdir: /path/to/main/.git/worktrees/`. + // Reading this will fail (and is safely skipped) for ordinary repositories, + // where `.git` is a directory, and for folders that are not a git repository at all. + let gitFileContents: string; + try { + gitFileContents = await fs.promises.readFile(URI.joinPath(openedFolder, '.git').fsPath, 'utf8'); + } catch { + continue; + } + + const gitDirMatch = /^gitdir:\s*(.+)$/m.exec(gitFileContents); + if (!gitDirMatch) { + continue; + } + + // The pointer is usually absolute, but resolve it relative to the + // worktree's own folder in case it is ever written as a relative path. + const resolvedGitDir = URI.file(resolve(openedFolder.fsPath, gitDirMatch[1].trim())); + if (extUriBiasedIgnorePathCase.isEqual(resolvedGitDir, worktreeGitDir)) { + return window; + } + } + + return undefined; +} + export async function findWindowOnFile(windows: ICodeWindow[], fileUri: URI, localWorkspaceResolver: (workspace: IWorkspaceIdentifier) => Promise): Promise { - // First check for windows with workspaces that have a parent folder of the provided path opened + // First, check whether the file is a git linked worktree's private metadata + // file and, if so, prefer the window that has that linked worktree open + // (see `findWindowOnGitWorktreeFile` for why this needs special handling) + const gitWorktreeWindow = await findWindowOnGitWorktreeFile(windows, fileUri); + if (gitWorktreeWindow) { + return gitWorktreeWindow; + } + + // Then check for windows with workspaces that have a parent folder of the provided path opened for (const window of windows) { const workspace = window.openedWorkspace; if (isWorkspaceIdentifier(workspace)) { diff --git a/src/vs/platform/windows/test/electron-main/windowsFinder.test.ts b/src/vs/platform/windows/test/electron-main/windowsFinder.test.ts index 85214ff775b89..2d7a810a11f5d 100644 --- a/src/vs/platform/windows/test/electron-main/windowsFinder.test.ts +++ b/src/vs/platform/windows/test/electron-main/windowsFinder.test.ts @@ -4,6 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; +import * as fs from 'fs'; +import { tmpdir } from 'os'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { Event } from '../../../../base/common/event.js'; import { join } from '../../../../base/common/path.js'; @@ -17,7 +19,9 @@ import { findWindowOnFile } from '../../electron-main/windowsFinder.js'; import { toWorkspaceFolders } from '../../../workspaces/common/workspaces.js'; import { IWorkspaceIdentifier } from '../../../workspace/common/workspace.js'; import { FileAccess } from '../../../../base/common/network.js'; +import { Promises } from '../../../../base/node/pfs.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; +import { getRandomTestPath } from '../../../../base/test/node/testUtils.js'; import { FocusMode, IApplicationBadge } from '../../../native/common/native.js'; suite('WindowsFinder', () => { @@ -115,5 +119,91 @@ suite('WindowsFinder', () => { assert.strictEqual(await findWindowOnFile([window], URI.file(join(fixturesFolder, 'vscode_workspace_2_folder', 'nested_vscode_folder', 'subfolder', 'file.txt')), localWorkspaceResolver), window); }); + suite('Git linked worktree', () => { + + let testDir: string; + let mainWorktreeFolder: string; + let linkedWorktreeFolder: string; + + setup(async () => { + testDir = getRandomTestPath(tmpdir(), 'vsctests', 'windowsfinder-worktree'); + mainWorktreeFolder = join(testDir, 'main'); + linkedWorktreeFolder = join(testDir, 'linked'); + + // Simulate a main worktree with a real `.git` directory that + // contains the linked worktree's private metadata directory + await fs.promises.mkdir(join(mainWorktreeFolder, '.git', 'worktrees', 'linked'), { recursive: true }); + await fs.promises.writeFile(join(mainWorktreeFolder, '.git', 'worktrees', 'linked', 'COMMIT_EDITMSG'), ''); + + // Simulate the linked worktree's folder with a plain-text `.git` file + // pointing back at the main worktree's `worktrees/linked` directory + await fs.promises.mkdir(linkedWorktreeFolder, { recursive: true }); + await fs.promises.writeFile(join(linkedWorktreeFolder, '.git'), `gitdir: ${join(mainWorktreeFolder, '.git', 'worktrees', 'linked')}\n`); + }); + + teardown(() => { + return Promises.rm(testDir); + }); + + test('Linked worktree window wins for worktree metadata file over main worktree window', async () => { + const mainWindow: ICodeWindow = createTestCodeWindow({ lastFocusTime: 1, openedFolderUri: URI.file(mainWorktreeFolder) }); + const linkedWindow: ICodeWindow = createTestCodeWindow({ lastFocusTime: 2, openedFolderUri: URI.file(linkedWorktreeFolder) }); + + const worktreeFile = URI.file(join(mainWorktreeFolder, '.git', 'worktrees', 'linked', 'COMMIT_EDITMSG')); + + // Without worktree-awareness, this file is a child of `mainWorktreeFolder` + // (but not of `linkedWorktreeFolder`), so a plain parent-folder match + // would incorrectly resolve to `mainWindow` here. + assert.ok(extUriBiasedIgnorePathCase.isEqualOrParent(worktreeFile, URI.file(mainWorktreeFolder))); + assert.ok(!extUriBiasedIgnorePathCase.isEqualOrParent(worktreeFile, URI.file(linkedWorktreeFolder))); + + assert.strictEqual(await findWindowOnFile([mainWindow, linkedWindow], worktreeFile, localWorkspaceResolver), linkedWindow); + }); + + test('Falls back to normal matching when no window has the linked worktree open', async () => { + const mainWindow: ICodeWindow = createTestCodeWindow({ lastFocusTime: 1, openedFolderUri: URI.file(mainWorktreeFolder) }); + + const worktreeFile = URI.file(join(mainWorktreeFolder, '.git', 'worktrees', 'linked', 'COMMIT_EDITMSG')); + assert.strictEqual(await findWindowOnFile([mainWindow], worktreeFile, localWorkspaceResolver), mainWindow); + }); + + test('Does not misroute when a path merely looks like a worktree metadata path', async () => { + // A folder that happens to contain a `.git/worktrees/` path segment + // but is not connected to any window's real `.git` pointer file: no window + // should be preferred based on that coincidence, and normal parent-folder + // matching should still apply. + const coincidentalFolder = join(testDir, 'not-a-worktree'); + await fs.promises.mkdir(join(coincidentalFolder, '.git', 'worktrees', 'coincidence'), { recursive: true }); + + const coincidentalWindow: ICodeWindow = createTestCodeWindow({ lastFocusTime: 1, openedFolderUri: URI.file(coincidentalFolder) }); + const linkedWindow: ICodeWindow = createTestCodeWindow({ lastFocusTime: 2, openedFolderUri: URI.file(linkedWorktreeFolder) }); + + const filePath = URI.file(join(coincidentalFolder, '.git', 'worktrees', 'coincidence', 'file.txt')); + assert.strictEqual(await findWindowOnFile([linkedWindow, coincidentalWindow], filePath, localWorkspaceResolver), coincidentalWindow); + }); + + test('Does not confuse two unrelated repositories that use the same worktree name', async () => { + // A second, entirely unrelated main worktree that also happens to have + // a linked worktree named `linked` (a common, unremarkable name). Its + // `.git/worktrees/linked` metadata directory must not be confused with + // `mainWorktreeFolder`'s own `linked` worktree just because the trailing + // path segment is identical. + const otherMainWorktreeFolder = join(testDir, 'other-main'); + const otherLinkedWorktreeFolder = join(testDir, 'other-linked'); + await fs.promises.mkdir(join(otherMainWorktreeFolder, '.git', 'worktrees', 'linked'), { recursive: true }); + await fs.promises.mkdir(otherLinkedWorktreeFolder, { recursive: true }); + await fs.promises.writeFile(join(otherLinkedWorktreeFolder, '.git'), `gitdir: ${join(otherMainWorktreeFolder, '.git', 'worktrees', 'linked')}\n`); + + const linkedWindow: ICodeWindow = createTestCodeWindow({ lastFocusTime: 1, openedFolderUri: URI.file(linkedWorktreeFolder) }); + const otherLinkedWindow: ICodeWindow = createTestCodeWindow({ lastFocusTime: 2, openedFolderUri: URI.file(otherLinkedWorktreeFolder) }); + + const worktreeFile = URI.file(join(mainWorktreeFolder, '.git', 'worktrees', 'linked', 'COMMIT_EDITMSG')); + + // The unrelated window is listed first so that a basename-only match + // (rather than a full-path match) would incorrectly win here. + assert.strictEqual(await findWindowOnFile([otherLinkedWindow, linkedWindow], worktreeFile, localWorkspaceResolver), linkedWindow); + }); + }); + ensureNoDisposablesAreLeakedInTestSuite(); }); From 3c4e4de8ed71e15de1d571022a8212f5662bda0f Mon Sep 17 00:00:00 2001 From: D-Majumder <98733892+D-Majumder@users.noreply.github.com> Date: Fri, 11 Sep 2026 14:25:53 +0530 Subject: [PATCH 2/2] git: resolve linked worktrees in multi-root workspaces too findWindowOnGitWorktreeFile only checked single-folder windows when matching a linked worktree's .git pointer file, so a linked worktree opened as one folder of a multi-root workspace was skipped and could still route to the main worktree's window. Resolve IWorkspaceIdentifier folders through the existing localWorkspaceResolver, matching how the surrounding findWindowOnFile fallback logic already handles multi-root workspaces. Unresolved workspaces are skipped gracefully, as before. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01TzTiUkg9L2rajTg9eb9zxY --- .../windows/electron-main/windowsFinder.ts | 77 ++++++++++++------- .../test/electron-main/windowsFinder.test.ts | 15 ++++ 2 files changed, 63 insertions(+), 29 deletions(-) diff --git a/src/vs/platform/windows/electron-main/windowsFinder.ts b/src/vs/platform/windows/electron-main/windowsFinder.ts index 5a8cac011292c..a360cc2f14372 100644 --- a/src/vs/platform/windows/electron-main/windowsFinder.ts +++ b/src/vs/platform/windows/electron-main/windowsFinder.ts @@ -18,6 +18,23 @@ import { IResolvedWorkspace, ISingleFolderWorkspaceIdentifier, isSingleFolderWor // and `\` as path separators. const gitWorktreeFilePathRegex = /^(.*[\\/]\.git[\\/]worktrees[\\/][^\\/]+)[\\/]/; +// Returns the folder(s) a window has open, for both single-folder windows and +// (resolved) multi-root workspace windows. Returns an empty array when the +// window has no folders opened, or its workspace could not be resolved. +async function getOpenedFolderUris(window: ICodeWindow, localWorkspaceResolver: (workspace: IWorkspaceIdentifier) => Promise): Promise { + const workspace = window.openedWorkspace; + if (isSingleFolderWorkspaceIdentifier(workspace)) { + return [workspace.uri]; + } + + if (isWorkspaceIdentifier(workspace)) { + const resolvedWorkspace = await localWorkspaceResolver(workspace); + return resolvedWorkspace ? resolvedWorkspace.folders.map(folder => folder.uri) : []; + } + + return []; +} + /** * Git linked worktrees store their private per-worktree files (such as * `COMMIT_EDITMSG` or `rebase-merge/git-rebase-todo`) inside the *main* @@ -28,12 +45,13 @@ const gitWorktreeFilePathRegex = /^(.*[\\/]\.git[\\/]worktrees[\\/][^\\/]+)[\\/] * actually belongs to. * * This detects that case and, if the linked worktree is open in one of the - * candidate windows, returns that window instead. Returns `undefined` when - * the path is not a git worktree metadata path, or when no candidate window - * has the corresponding linked worktree open, so callers can fall back to - * their normal matching logic. + * candidate windows (as a single-folder window or as one folder of a + * multi-root workspace window), returns that window instead. Returns + * `undefined` when the path is not a git worktree metadata path, or when no + * candidate window has the corresponding linked worktree open, so callers + * can fall back to their normal matching logic. */ -async function findWindowOnGitWorktreeFile(windows: ICodeWindow[], fileUri: URI): Promise { +async function findWindowOnGitWorktreeFile(windows: ICodeWindow[], fileUri: URI, localWorkspaceResolver: (workspace: IWorkspaceIdentifier) => Promise): Promise { if (fileUri.scheme !== Schemas.file) { return undefined; } @@ -51,32 +69,33 @@ async function findWindowOnGitWorktreeFile(windows: ICodeWindow[], fileUri: URI) const worktreeGitDir = URI.file(worktreeMatch[1]); for (const window of windows) { - const openedFolder = isSingleFolderWorkspaceIdentifier(window.openedWorkspace) ? window.openedWorkspace.uri : undefined; - if (!openedFolder || openedFolder.scheme !== Schemas.file) { - continue; - } + for (const openedFolder of await getOpenedFolderUris(window, localWorkspaceResolver)) { + if (openedFolder.scheme !== Schemas.file) { + continue; + } - // A linked worktree's working directory contains a plain-text `.git` - // *file* (not a directory) of the form `gitdir: /path/to/main/.git/worktrees/`. - // Reading this will fail (and is safely skipped) for ordinary repositories, - // where `.git` is a directory, and for folders that are not a git repository at all. - let gitFileContents: string; - try { - gitFileContents = await fs.promises.readFile(URI.joinPath(openedFolder, '.git').fsPath, 'utf8'); - } catch { - continue; - } + // A linked worktree's working directory contains a plain-text `.git` + // *file* (not a directory) of the form `gitdir: /path/to/main/.git/worktrees/`. + // Reading this will fail (and is safely skipped) for ordinary repositories, + // where `.git` is a directory, and for folders that are not a git repository at all. + let gitFileContents: string; + try { + gitFileContents = await fs.promises.readFile(URI.joinPath(openedFolder, '.git').fsPath, 'utf8'); + } catch { + continue; + } - const gitDirMatch = /^gitdir:\s*(.+)$/m.exec(gitFileContents); - if (!gitDirMatch) { - continue; - } + const gitDirMatch = /^gitdir:\s*(.+)$/m.exec(gitFileContents); + if (!gitDirMatch) { + continue; + } - // The pointer is usually absolute, but resolve it relative to the - // worktree's own folder in case it is ever written as a relative path. - const resolvedGitDir = URI.file(resolve(openedFolder.fsPath, gitDirMatch[1].trim())); - if (extUriBiasedIgnorePathCase.isEqual(resolvedGitDir, worktreeGitDir)) { - return window; + // The pointer is usually absolute, but resolve it relative to the + // worktree's own folder in case it is ever written as a relative path. + const resolvedGitDir = URI.file(resolve(openedFolder.fsPath, gitDirMatch[1].trim())); + if (extUriBiasedIgnorePathCase.isEqual(resolvedGitDir, worktreeGitDir)) { + return window; + } } } @@ -88,7 +107,7 @@ export async function findWindowOnFile(windows: ICodeWindow[], fileUri: URI, loc // First, check whether the file is a git linked worktree's private metadata // file and, if so, prefer the window that has that linked worktree open // (see `findWindowOnGitWorktreeFile` for why this needs special handling) - const gitWorktreeWindow = await findWindowOnGitWorktreeFile(windows, fileUri); + const gitWorktreeWindow = await findWindowOnGitWorktreeFile(windows, fileUri, localWorkspaceResolver); if (gitWorktreeWindow) { return gitWorktreeWindow; } diff --git a/src/vs/platform/windows/test/electron-main/windowsFinder.test.ts b/src/vs/platform/windows/test/electron-main/windowsFinder.test.ts index 2d7a810a11f5d..a26ff441f93c0 100644 --- a/src/vs/platform/windows/test/electron-main/windowsFinder.test.ts +++ b/src/vs/platform/windows/test/electron-main/windowsFinder.test.ts @@ -203,6 +203,21 @@ suite('WindowsFinder', () => { // (rather than a full-path match) would incorrectly win here. assert.strictEqual(await findWindowOnFile([otherLinkedWindow, linkedWindow], worktreeFile, localWorkspaceResolver), linkedWindow); }); + + test('Linked worktree window wins when opened as part of a multi-root workspace', async () => { + const linkedWorkspace: IWorkspaceIdentifier = { + id: Date.now().toString(), + configPath: URI.file(join(testDir, 'linked.code-workspace')) + }; + const linkedWorkspaceFolders = toWorkspaceFolders([{ path: linkedWorktreeFolder }, { path: join(fixturesFolder, 'vscode_folder') }], linkedWorkspace.configPath, extUriBiasedIgnorePathCase); + const linkedWorkspaceResolver = async (workspace: IWorkspaceIdentifier) => { return workspace === linkedWorkspace ? { id: linkedWorkspace.id, configPath: workspace.configPath, folders: linkedWorkspaceFolders } : undefined; }; + + const mainWindow: ICodeWindow = createTestCodeWindow({ lastFocusTime: 1, openedFolderUri: URI.file(mainWorktreeFolder) }); + const multiRootWindow: ICodeWindow = createTestCodeWindow({ lastFocusTime: 2, openedWorkspace: linkedWorkspace }); + + const worktreeFile = URI.file(join(mainWorktreeFolder, '.git', 'worktrees', 'linked', 'COMMIT_EDITMSG')); + assert.strictEqual(await findWindowOnFile([mainWindow, multiRootWindow], worktreeFile, linkedWorkspaceResolver), multiRootWindow); + }); }); ensureNoDisposablesAreLeakedInTestSuite();