Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 103 additions & 1 deletion src/vs/platform/windows/electron-main/windowsFinder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,116 @@
* 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. `<main-worktree>/.git/worktrees/<name>/COMMIT_EDITMSG`.
// Captures the full path of the worktree's metadata directory itself, i.e.
// everything up to and including `.git/worktrees/<name>`. Accepts both `/`
// 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<IResolvedWorkspace | undefined>): Promise<URI[]> {
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*
* worktree's `.git/worktrees/<name>` 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 (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, localWorkspaceResolver: (workspace: IWorkspaceIdentifier) => Promise<IResolvedWorkspace | undefined>): Promise<ICodeWindow | undefined> {
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 `<name>` 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) {
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/<name>`.
// 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<IResolvedWorkspace | undefined>): Promise<ICodeWindow | undefined> {

// 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, localWorkspaceResolver);
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)) {
Expand Down
105 changes: 105 additions & 0 deletions src/vs/platform/windows/test/electron-main/windowsFinder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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', () => {
Expand Down Expand Up @@ -115,5 +119,106 @@ 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/<name>` 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);
});

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();
});