perf(main): make worktree path dedupe linear (#8177)

This commit is contained in:
Neil 2026-07-10 20:52:24 -07:00 committed by GitHub
parent 435de94102
commit 2a01b41638
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 175 additions and 70 deletions

View File

@ -10,6 +10,7 @@
"../src/main/ipc/worktree-logic.ts",
"../src/main/ipc/worktree-linked-work-item-metadata.ts",
"../src/main/ipc/worktree-metadata-merge.ts",
"../src/main/ipc/worktree-path-comparison.ts",
"../src/main/wsl.ts"
],
"compilerOptions": {

View File

@ -1,6 +1,6 @@
import { resolve, relative, isAbsolute, posix, sep, win32 } from 'node:path'
import type { GlobalSettings, OrcaWorkspaceLayout, Repo } from '../../shared/types'
import { resolveRuntimePath } from '../../shared/cross-platform-path'
import { isWindowsAbsolutePathLike, resolveRuntimePath } from '../../shared/cross-platform-path'
import { isWslUncPath } from '../../shared/wsl-paths'
import { splitWorktreeId } from '../../shared/worktree-id'
import { getWslHome, parseWslPath } from '../wsl'
@ -10,6 +10,7 @@ type WorktreeBasePathRepo = Pick<Repo, 'path' | 'worktreeBasePath'>
export { computeBranchName, getConfiguredBranchPrefix } from './worktree-branch-name'
export { mergeWorktree } from './worktree-metadata-merge'
export { areWorktreePathsEqual } from './worktree-path-comparison'
/**
* Sanitize a worktree name for use in branch names and directory paths.
@ -153,65 +154,13 @@ export function hasRepoWorktreeBasePath(repo: Pick<Repo, 'worktreeBasePath'>): b
return getRepoWorktreeBasePath(repo) !== undefined
}
export function areWorktreePathsEqual(
leftPath: string,
rightPath: string,
platform = process.platform
): boolean {
if (looksLikePosixAbsolutePath(leftPath) || looksLikePosixAbsolutePath(rightPath)) {
// Why: local WSL projects run POSIX paths on a Windows desktop; comparing
// them with win32 rules can delete or dedupe the wrong runtime-owned path.
if (!looksLikePosixAbsolutePath(leftPath) || !looksLikePosixAbsolutePath(rightPath)) {
return false
}
const left = normalizePosixWorktreePathForComparison(leftPath, platform)
const right = normalizePosixWorktreePathForComparison(rightPath, platform)
return left === right
}
if (platform === 'win32' || looksLikeWindowsPath(leftPath) || looksLikeWindowsPath(rightPath)) {
const left = win32.normalize(win32.resolve(leftPath))
const right = win32.normalize(win32.resolve(rightPath))
// Why: `git worktree list` can report the same Windows path with different
// slash styles or drive-letter casing than the path we computed before
// creation. Orca must treat those as the same worktree or a successful
// create spuriously fails until the next full reload repopulates state.
return left.toLowerCase() === right.toLowerCase()
}
const left = normalizePosixWorktreePathForComparison(leftPath, platform)
const right = normalizePosixWorktreePathForComparison(rightPath, platform)
return left === right
}
function looksLikeWindowsPath(pathValue: string): boolean {
return (
/^[A-Za-z]:[\\/]/.test(pathValue) || pathValue.startsWith('\\\\') || pathValue.startsWith('//')
)
}
function looksLikePosixAbsolutePath(pathValue: string): boolean {
return pathValue.startsWith('/') && !pathValue.startsWith('//')
}
function normalizePosixWorktreePathForComparison(
pathValue: string,
platform: NodeJS.Platform
): string {
const normalized = posix.normalize(posix.resolve(pathValue))
if (platform !== 'darwin') {
return normalized
}
if (normalized === '/private/tmp') {
return '/tmp'
}
return normalized.startsWith('/private/tmp/') ? normalized.slice('/private'.length) : normalized
}
function getRuntimePathOps(
repoPath: string,
workspaceDir: string
): Pick<typeof posix, 'basename' | 'isAbsolute' | 'join' | 'normalize'> {
return looksLikeWindowsPath(repoPath) || looksLikeWindowsPath(workspaceDir) ? win32 : posix
return isWindowsAbsolutePathLike(repoPath) || isWindowsAbsolutePathLike(workspaceDir)
? win32
: posix
}
function resolveWorkspaceDirForRepo(repoPath: string, workspaceDir: string): string {

View File

@ -0,0 +1,108 @@
import { posix, win32 } from 'node:path'
import { isWindowsAbsolutePathLike } from '../../shared/cross-platform-path'
export function areWorktreePathsEqual(
leftPath: string,
rightPath: string,
platform = process.platform
): boolean {
if (looksLikePosixAbsolutePath(leftPath) || looksLikePosixAbsolutePath(rightPath)) {
// Why: local WSL projects run POSIX paths on a Windows desktop; comparing
// them with win32 rules can delete or dedupe the wrong runtime-owned path.
if (!looksLikePosixAbsolutePath(leftPath) || !looksLikePosixAbsolutePath(rightPath)) {
return false
}
const left = normalizePosixWorktreePathForComparison(leftPath, platform)
const right = normalizePosixWorktreePathForComparison(rightPath, platform)
return left === right
}
if (
platform === 'win32' ||
isWindowsAbsolutePathLike(leftPath) ||
isWindowsAbsolutePathLike(rightPath)
) {
const left = normalizeWindowsWorktreePathForComparison(leftPath)
const right = normalizeWindowsWorktreePathForComparison(rightPath)
// Why: Git can report the same Windows path with different slash styles or
// drive-letter casing; treating them as distinct creates duplicate worktrees.
return left === right
}
const left = normalizePosixWorktreePathForComparison(leftPath, platform)
const right = normalizePosixWorktreePathForComparison(rightPath, platform)
return left === right
}
export function dedupeWorktreesByPath<T extends { path: string }>(
worktrees: readonly T[],
platform = process.platform
): T[] {
// Why: large Git/relay listings should normalize each path once while still
// preserving the first row under the cross-platform equality contract above.
const unique: T[] = []
const posixAbsoluteKeys = new Set<string>()
const windowsKeys = new Set<string>()
const windowsPaths: string[] = []
const relativePaths: string[] = []
for (const worktree of worktrees) {
const pathValue = worktree.path
if (looksLikePosixAbsolutePath(pathValue)) {
const key = normalizePosixWorktreePathForComparison(pathValue, platform)
if (posixAbsoluteKeys.has(key)) {
continue
}
posixAbsoluteKeys.add(key)
unique.push(worktree)
continue
}
const windowsKey = normalizeWindowsWorktreePathForComparison(pathValue)
if (platform === 'win32' || isWindowsAbsolutePathLike(pathValue)) {
if (
windowsKeys.has(windowsKey) ||
relativePaths.some((existing) => areWorktreePathsEqual(existing, pathValue, platform))
) {
continue
}
windowsKeys.add(windowsKey)
windowsPaths.push(pathValue)
unique.push(worktree)
continue
}
// Why: Git normally reports absolute paths. Retain pair-aware comparison
// only for malformed/legacy relative rows whose flavor depends on its peer.
if (
relativePaths.some((existing) => areWorktreePathsEqual(existing, pathValue, platform)) ||
windowsPaths.some((existing) => areWorktreePathsEqual(existing, pathValue, platform))
) {
continue
}
relativePaths.push(pathValue)
unique.push(worktree)
}
return unique
}
function looksLikePosixAbsolutePath(pathValue: string): boolean {
return pathValue.startsWith('/') && !pathValue.startsWith('//')
}
function normalizeWindowsWorktreePathForComparison(pathValue: string): string {
return win32.normalize(win32.resolve(pathValue)).toLowerCase()
}
function normalizePosixWorktreePathForComparison(
pathValue: string,
platform: NodeJS.Platform
): string {
const normalized = posix.normalize(posix.resolve(pathValue))
if (platform !== 'darwin') {
return normalized
}
if (normalized === '/private/tmp') {
return '/tmp'
}
return normalized.startsWith('/private/tmp/') ? normalized.slice('/private'.length) : normalized
}

View File

@ -0,0 +1,59 @@
import { describe, expect, it } from 'vitest'
import { areWorktreePathsEqual, dedupeWorktreesByPath } from './worktree-path-comparison'
function dedupeWithComparator<T extends { path: string }>(
worktrees: readonly T[],
platform: NodeJS.Platform
): T[] {
const unique: T[] = []
for (const worktree of worktrees) {
if (!unique.some((existing) => areWorktreePathsEqual(existing.path, worktree.path, platform))) {
unique.push(worktree)
}
}
return unique
}
describe('dedupeWorktreesByPath', () => {
it.each<NodeJS.Platform>(['darwin', 'linux', 'win32'])(
'preserves the comparator result on %s',
(platform) => {
const worktrees = [
{ id: 'posix-upper', path: '/home/dev/Repo' },
{ id: 'posix-lower', path: '/home/dev/repo' },
{ id: 'windows-first', path: 'C:\\Workspaces\\Feature' },
{ id: 'windows-duplicate', path: 'c:/workspaces/feature' },
{ id: 'unc-first', path: '\\\\Server\\Share\\Feature' },
{ id: 'unc-duplicate', path: '//server/share/feature' },
{ id: 'temp-private', path: '/private/tmp/orca/feature' },
{ id: 'temp-short', path: '/tmp/orca/feature' },
{ id: 'relative-first', path: 'workspaces/feature' },
{ id: 'relative-duplicate', path: 'workspaces/./feature' }
]
for (let offset = 0; offset < worktrees.length; offset += 1) {
const rotated = [...worktrees.slice(offset), ...worktrees.slice(0, offset)]
expect(dedupeWorktreesByPath(rotated, platform)).toEqual(
dedupeWithComparator(rotated, platform)
)
const reversed = rotated.toReversed()
expect(dedupeWorktreesByPath(reversed, platform)).toEqual(
dedupeWithComparator(reversed, platform)
)
}
}
)
it('reads each path once for a large unique list', () => {
let pathReads = 0
const worktrees = Array.from({ length: 1_000 }, (_, index) => ({
get path(): string {
pathReads += 1
return `/workspaces/feature-${index}`
}
}))
expect(dedupeWorktreesByPath(worktrees)).toHaveLength(1_000)
expect(pathReads).toBe(1_000)
})
})

View File

@ -72,6 +72,7 @@ import {
isOrphanCompatiblePreflightError,
isOrphanedWorktreeError
} from './worktree-logic'
import { dedupeWorktreesByPath } from './worktree-path-comparison'
import { joinWorktreeRelativePath } from '../runtime/runtime-relative-paths'
import {
createLocalWorktree,
@ -208,19 +209,6 @@ async function closeLocalWatcherForRemoval(worktreePath: string): Promise<void>
})
}
function dedupeGitWorktreesByPath(gitWorktrees: GitWorktreeInfo[]): GitWorktreeInfo[] {
const uniqueGitWorktrees: GitWorktreeInfo[] = []
for (const gitWorktree of gitWorktrees) {
if (
uniqueGitWorktrees.some((existing) => areWorktreePathsEqual(existing.path, gitWorktree.path))
) {
continue
}
uniqueGitWorktrees.push(gitWorktree)
}
return uniqueGitWorktrees
}
function getProjectHostSetupMetaUpdates(
store: Store,
repo: Repo,
@ -718,7 +706,7 @@ function buildDetectedGitWorktrees(
const settings = store.getSettings()
const knownOrcaLayouts = buildKnownOrcaWorkspaceLayouts(settings, repo)
const isLegacyRepoForVisibility = isLegacyRepoForExternalWorktreeVisibility(repo)
return dedupeGitWorktreesByPath(gitWorktrees).map((gitWorktree) => {
return dedupeWorktreesByPath(gitWorktrees).map((gitWorktree) => {
const worktreeId = `${repo.id}::${gitWorktree.path}`
let meta = store.getWorktreeMeta(worktreeId)
const worktree = mergeWorktree(repo.id, gitWorktree, meta, repo.displayName)