refactor(worktree): one worktree path equality; the ipc copy folded WSL ext4 tails (#30227)

src/main/ipc/worktree-path-comparison.ts kept its own areWorktreePathsEqual and
dedupeWorktreesByPath. Its Windows branch lowercased a WSL UNC path's ext4 tail, so two
case-twin WSL checkouts merged into one row in removal safety, folder-upgrade dedupe and
the SSH listing fallback. Every caller now uses git/worktree-path-comparison, which gains
the macOS /private/tmp fold the ipc copy had. worktreePathComparisonKey keeps its exact
bytes (retirement namespace keys persist it) in worktree-path-comparison-key.ts, and the
worktree-logic re-export goes.
This commit is contained in:
Neil
2026-10-18 00:02:29 -07:00
committed by GitHub
parent b21cda129f
commit f47337d852
31 changed files with 171 additions and 157 deletions
+1 -1
View File
@@ -2959,7 +2959,7 @@
"src/main/ipc/worktree-logic-wsl.test.ts": 95,
"src/main/ipc/worktree-logic.test.ts": 203,
"src/main/ipc/worktree-metadata-merge.test.ts": 42,
"src/main/ipc/worktree-path-deduplication.test.ts": 63,
"src/main/git/worktree-path-deduplication.test.ts": 63,
"src/main/ipc/worktree-push-target-cleanup.test.ts": 639,
"src/main/ipc/worktree-push-target-reconciliation-real-git.test.ts": 950,
"src/main/ipc/worktree-push-target-reconciliation.test.ts": 600,
-1
View File
@@ -39,7 +39,6 @@
"../src/main/ipc/worktree-display-name.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-availability.ts",
"../src/main/wsl-directory-probe-command.ts",
"../src/main/wsl-distro-list-output.ts",
@@ -3,7 +3,7 @@ import { tmpdir } from 'node:os'
import type * as NodeOs from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
import type * as WorktreeLogic from '../ipc/worktree-logic'
import type * as WorktreePathComparison from '../git/worktree-path-comparison'
const { getPathMock, homedirMock, worktreePathComparisons } = vi.hoisted(() => ({
getPathMock: vi.fn<(name: string) => string>(),
@@ -25,8 +25,8 @@ vi.mock('node:os', async () => {
}
})
vi.mock('../ipc/worktree-logic', async (importOriginal) => {
const actual = await importOriginal<typeof WorktreeLogic>()
vi.mock('../git/worktree-path-comparison', async (importOriginal) => {
const actual = await importOriginal<typeof WorktreePathComparison>()
return {
...actual,
areWorktreePathsEqual: (left: string, right: string) => {
+1 -1
View File
@@ -2,7 +2,7 @@ import { realpathSync } from 'node:fs'
import type { Repo } from '../shared/repo-types'
import type { GitWorktreeInfo } from '../shared/worktree/types'
import { getRepoExecutionHostId, LOCAL_EXECUTION_HOST_ID } from '../shared/execution-host'
import { areWorktreePathsEqual, dedupeWorktreesByPath } from './ipc/worktree-path-comparison'
import { areWorktreePathsEqual, dedupeWorktreesByPath } from './git/worktree-path-comparison'
function stillNamesRegisteredCheckout(repo: Repo, gitRoot: string): boolean {
if (areWorktreePathsEqual(repo.path, gitRoot)) {
+67 -6
View File
@@ -1,4 +1,5 @@
import { posix, win32 } from 'node:path'
import { isWindowsAbsolutePathLike } from '../../shared/cross-platform-path'
import { foldWslUncPathCaseInsensitiveParts } from '../../shared/wsl-paths'
import type { GitWorktreeExecOptions } from './worktree-operation-options'
import { translateWslOutputPaths } from './runner'
@@ -17,12 +18,11 @@ import { translateWslOutputPaths } from './runner'
* as `\\wsl.localhost\Ubuntu\home\alice\Feature` and a plain `toLowerCase` folded the ext4 tail.
* `foldWslUncPathCaseInsensitiveParts` already draws that line — Windows folds the share, the distro
* and a drvfs `/mnt/<letter>` tail, and nothing else — and `git-fetch-head-lock` already relies on
* it. `isSameCommonDirPath` and `ipc/worktree-path-comparison` carry local copies of the POSIX half;
* this is both halves at the source.
* it. `isSameCommonDirPath` carries a local copy of the POSIX half; this is both halves at the source.
*/
export function canonicalWorktreePath(pathValue: string, platform = process.platform): string {
if (looksLikePosixAbsolutePath(pathValue)) {
return posix.normalize(posix.resolve(pathValue))
return canonicalPosixPath(pathValue, platform)
}
const wslKey = wslUncComparisonKey(pathValue)
if (wslKey) {
@@ -30,7 +30,19 @@ export function canonicalWorktreePath(pathValue: string, platform = process.plat
}
return platform === 'win32' || looksLikeWindowsPath(pathValue)
? win32.normalize(win32.resolve(pathValue)).toLowerCase()
: posix.normalize(posix.resolve(pathValue))
: canonicalPosixPath(pathValue, platform)
}
// Why: macOS git reports a `/tmp` checkout as `/private/tmp`, the same directory.
function canonicalPosixPath(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
}
/**
@@ -45,7 +57,11 @@ function wslUncComparisonKey(pathValue: string): string | null {
if (!folded) {
return null
}
return foldWslUncPathCaseInsensitiveParts(win32.normalize(pathValue)) ?? folded
// A trailing separator names the same directory, as `resolve` already treats it elsewhere.
return (foldWslUncPathCaseInsensitiveParts(win32.normalize(pathValue)) ?? folded).replace(
/\/+$/,
''
)
}
export function areWorktreePathsEqual(
@@ -63,12 +79,57 @@ export function areWorktreePathsEqual(
canonicalWorktreePath(leftPath, platform) === canonicalWorktreePath(rightPath, platform)
)
}
if (platform === 'win32' || looksLikeWindowsPath(leftPath) || looksLikeWindowsPath(rightPath)) {
if (
platform === 'win32' ||
isWindowsAbsolutePathLike(leftPath) ||
isWindowsAbsolutePathLike(rightPath)
) {
return canonicalWorktreePath(leftPath, 'win32') === canonicalWorktreePath(rightPath, 'win32')
}
return canonicalWorktreePath(leftPath, platform) === canonicalWorktreePath(rightPath, platform)
}
export function dedupeWorktreesByPath<T extends { path: string }>(
worktrees: readonly T[],
platform = process.platform
): T[] {
// Why: large Git/relay listings key each absolute path once, keeping the first row under
// `areWorktreePathsEqual`; only relative rows, whose flavor depends on the peer, compare pairwise.
const unique: T[] = []
const posixKeys = new Set<string>()
const windowsKeys = new Set<string>()
const windowsPaths: string[] = []
const relativePaths: string[] = []
const matchesAny = (paths: string[], pathValue: string): boolean =>
paths.some((existing) => areWorktreePathsEqual(existing, pathValue, platform))
for (const worktree of worktrees) {
const pathValue = worktree.path
if (looksLikePosixAbsolutePath(pathValue)) {
const key = canonicalWorktreePath(pathValue, platform)
if (!posixKeys.has(key)) {
posixKeys.add(key)
unique.push(worktree)
}
continue
}
if (platform === 'win32' || isWindowsAbsolutePathLike(pathValue)) {
const key = canonicalWorktreePath(pathValue, 'win32')
if (!windowsKeys.has(key) && !matchesAny(relativePaths, pathValue)) {
windowsKeys.add(key)
windowsPaths.push(pathValue)
unique.push(worktree)
}
continue
}
if (!matchesAny(relativePaths, pathValue) && !matchesAny(windowsPaths, pathValue)) {
relativePaths.push(pathValue)
unique.push(worktree)
}
}
return unique
}
function looksLikeWindowsPath(pathValue: string): boolean {
return /^[A-Za-z]:[\\/]/.test(pathValue) || pathValue.startsWith('\\\\')
}
@@ -28,7 +28,10 @@ describe('dedupeWorktreesByPath', () => {
{ 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' }
{ id: 'relative-duplicate', path: 'workspaces/./feature' },
{ id: 'wsl-upper', path: '\\\\wsl.localhost\\Ubuntu\\home\\dev\\Feature' },
{ id: 'wsl-lower', path: '\\\\wsl$\\ubuntu\\home\\dev\\feature' },
{ id: 'wsl-alias', path: '//wsl$/UBUNTU/home/dev/feature' }
]
for (let offset = 0; offset < worktrees.length; offset += 1) {
@@ -44,6 +47,34 @@ describe('dedupeWorktreesByPath', () => {
}
)
// An ext4 tail is case-sensitive: folding it merged two real checkouts into one row.
it.each<NodeJS.Platform>(['darwin', 'linux', 'win32'])(
'keeps WSL worktrees that differ only in tail case on %s',
(platform) => {
const ids = dedupeWorktreesByPath(
[
{ id: 'upper', path: '\\\\wsl.localhost\\Ubuntu\\home\\dev\\Feature' },
{ id: 'lower', path: '\\\\wsl$\\ubuntu\\home\\dev\\feature' },
{ id: 'alias', path: '//wsl$/UBUNTU/home/dev/feature' }
],
platform
).map((worktree) => worktree.id)
expect(ids).toEqual(['upper', 'lower'])
}
)
it('treats a trailing separator on a WSL path as the same worktree', () => {
const ids = dedupeWorktreesByPath([
{ id: 'bare', path: '//wsl$/Ubuntu/home/dev/feature' },
{ id: 'slash', path: '//wsl$/Ubuntu/home/dev/feature/' },
{ id: 'backslash', path: '\\\\wsl.localhost\\Ubuntu\\home\\dev\\feature\\' }
]).map((worktree) => worktree.id)
expect(ids).toEqual(['bare'])
expect(
areWorktreePathsEqual('//wsl$/Ubuntu/home/dev/feature/', '//wsl$/Ubuntu/home/dev/feature')
).toBe(true)
})
it('reads each path once for a large unique list', () => {
let pathReads = 0
const worktrees = Array.from({ length: 1_000 }, (_, index) => ({
@@ -3,7 +3,7 @@ import { describeCreatedWorktree, listWorktreesSharedStrict } from '../git/workt
import { WORKTREE_LIST_TIMEOUT_MS } from '../git/worktree-operation-options'
import type { GitWorktreeExecOptions } from '../git/worktree'
import type { GitWorktreeInfo } from '../../shared/worktree/types'
import { areWorktreePathsEqual } from './worktree-path-comparison'
import { areWorktreePathsEqual } from '../git/worktree-path-comparison'
export function findCreatedWorktree<T extends { path: string; branch?: string }>(
worktrees: readonly T[],
+2 -2
View File
@@ -21,9 +21,9 @@ import {
formatWorktreeRemovalError,
isWindowsLongPathWorktreeRemovalError,
isOrphanCompatiblePreflightError,
isOrphanedWorktreeError,
areWorktreePathsEqual
isOrphanedWorktreeError
} from './worktree-logic'
import { areWorktreePathsEqual } from '../git/worktree-path-comparison'
describe('sanitizeWorktreeName', () => {
it('replaces spaces with hyphens', () => {
-1
View File
@@ -27,7 +27,6 @@ export {
computeValidatedBranchName
} 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.
-118
View File
@@ -1,118 +0,0 @@
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 worktreePathComparisonKey(pathValue: string, platform = process.platform): string {
if (looksLikePosixAbsolutePath(pathValue)) {
return `posix:${normalizePosixWorktreePathForComparison(pathValue, platform)}`
}
if (platform === 'win32' || isWindowsAbsolutePathLike(pathValue)) {
return `windows:${normalizeWindowsWorktreePathForComparison(pathValue)}`
}
return `posix:${normalizePosixWorktreePathForComparison(pathValue, platform)}`
}
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
}
@@ -3,7 +3,7 @@ import { mkdtemp, mkdir, realpath, rm, symlink, writeFile } from 'node:fs/promis
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { findCreatedWorktree } from './created-worktree-reconciliation'
import { areWorktreePathsEqual } from './worktree-path-comparison'
import { areWorktreePathsEqual } from '../git/worktree-path-comparison'
type ListedWorktree = { path: string; branch?: string }
@@ -40,7 +40,7 @@ const { createWorktreeVisibilitySourceMatcher, resolveCustomWorktreeVisibilitySo
await import('../../../../shared/worktree/visibility-sources')
const { resolveConfiguredWorktreeBasePaths } =
await import('../../../../shared/worktree/configured-worktree-base-path')
const { dedupeWorktreesByPath } = await import('../../worktree-path-comparison')
const { dedupeWorktreesByPath } = await import('../../../git/worktree-path-comparison')
const { readWorktreeMetaForHost } =
await import('../../../persistence/host-qualified-worktree-meta')
const { getRepoOwnedWorktreeMeta } = await import('../../../worktree-metadata-ownership')
@@ -1,6 +1,6 @@
import { preserveFolderUpgradeWorktreePath } from '../../../folder-upgrade-worktree-path'
import type { WorktreeMeta } from '../../../../shared/worktree/meta-types'
import { parseWorktreeId, areWorktreePathsEqual, mergeWorktree } from '../../worktree-logic'
import { parseWorktreeId, mergeWorktree } from '../../worktree-logic'
import {
getRepoIdFromWorktreeId,
getWorktreePathBasenameFromId
@@ -19,7 +19,7 @@ import {
isLegacyRepoForExternalWorktreeVisibility,
toDetectedWorktree
} from '../../../../shared/worktree/ownership'
import { dedupeWorktreesByPath } from '../../worktree-path-comparison'
import { areWorktreePathsEqual, dedupeWorktreesByPath } from '../../../git/worktree-path-comparison'
import {
createWorktreeVisibilitySourceMatcher,
resolveCustomWorktreeVisibilitySources
@@ -3,7 +3,8 @@ import { getLocalWorktreeCatalogVersion } from '../../../local-worktree-scan-gen
import type { RemoveWorktreeResult } from '../../../../shared/worktree/create-types'
import { getRepoExecutionHostId } from '../../../../shared/execution-host'
import { withWorktreeSpan } from '../../../observability/instrumentation'
import { areWorktreePathsEqual, parseWorktreeId } from '../../worktree-logic'
import { areWorktreePathsEqual } from '../../../git/worktree-path-comparison'
import { parseWorktreeId } from '../../worktree-logic'
import type { RemoveWorktreeArgs } from '../ipc-context-schemas'
import type { WorktreeIpcContext } from '../worktree-ipc-context'
import { executeWorktreeRemoval } from './execute-worktree-removal'
+2 -1
View File
@@ -1,7 +1,8 @@
import type { RemoveWorktreeResult } from '../shared/worktree/create-types'
import type { GitWorktreeInfo } from '../shared/worktree/types'
import { assertWorktreeUnlockedForRemoval } from '../shared/worktree/removal'
import { areWorktreePathsEqual, formatWorktreeRemovalError } from './ipc/worktree-logic'
import { areWorktreePathsEqual } from './git/worktree-path-comparison'
import { formatWorktreeRemovalError } from './ipc/worktree-logic'
import { gitExecFileAsync } from './git/runner'
import { listWorktreesStrict, type GitWorktreeExecOptions } from './git/worktree'
import { removeLocalWorktreePath } from './local-worktree-filesystem'
+1 -1
View File
@@ -1,7 +1,7 @@
import type { GitWorktreeInfo } from '../shared/worktree/types'
import type { NestedWorktreeRemovalApproval } from '../shared/worktree/nested-removal'
import { assertWorktreeUnlockedForRemoval } from '../shared/worktree/removal'
import { areWorktreePathsEqual } from './ipc/worktree-logic'
import { areWorktreePathsEqual } from './git/worktree-path-comparison'
import {
containsPath,
getPathOps,
+1 -1
View File
@@ -10,7 +10,7 @@ import {
import { isFolderRepo } from '../shared/repo-kind'
import { getRepoExecutionHostId, LOCAL_EXECUTION_HOST_ID } from '../shared/execution-host'
import { resolveGitRouteForHost } from './providers/execution-host-provider-dispatch'
import { areWorktreePathsEqual } from './ipc/worktree-logic'
import { areWorktreePathsEqual } from './git/worktree-path-comparison'
import { WorktreeCatalogUnavailableError } from '../shared/worktree/worktree-catalog-availability'
type LocalRepoWorktreeListOptions = {
@@ -98,7 +98,7 @@ import {
unregisterPty as unregisterLocalPtyMemoryRow
} from '../../memory/pty-registry'
import { inspectPtyProviderProcess } from '../../providers/pty-process-inspection'
import * as worktreePathComparison from '../../ipc/worktree-path-comparison'
import * as worktreePathComparison from '../../worktree-path-comparison-key'
import * as localWorktreeFilesystem from '../../local-worktree-filesystem'
import {
DEFAULT_REPO_BADGE_COLOR,
@@ -23,7 +23,8 @@ import type { WorktreeMeta } from '../../shared/worktree/meta-types'
import type { ProjectExecutionRuntimeResolution } from '../../shared/project-execution-runtime'
import type { RuntimeStore } from './runtime-store-contract'
import { areWorktreePathsEqual, mergeWorktree } from '../ipc/worktree-logic'
import { areWorktreePathsEqual } from '../git/worktree-path-comparison'
import { mergeWorktree } from '../ipc/worktree-logic'
import { pruneLineageForMissingRepoWorktrees } from '../worktree-lineage-pruning'
import { getRepoOwnedWorktreeMeta } from '../worktree-metadata-ownership'
import { resolveLocalProjectRuntimesForRepos } from '../project-runtime-git-options'
@@ -1,6 +1,6 @@
import type { RuntimeWorktreePsSummary } from '../../shared/runtime-types'
import { isWindowsAbsolutePathLike } from '../../shared/cross-platform-path'
import { worktreePathComparisonKey } from '../ipc/worktree-path-comparison'
import { worktreePathComparisonKey } from '../worktree-path-comparison-key'
import { parseRuntimeWorktreeId } from './runtime-worktree-path-identity'
type ResolvedWorktreePath = { id: string; repoId: string; path: string }
@@ -14,7 +14,8 @@ import type { WorkspaceKey } from '../../../shared/folder-workspace-types'
import type { Repo } from '../../../shared/repo-types'
import type { RuntimeStore } from '../runtime-store-contract'
import type { RuntimeWorkspaceCatalog } from './workspace-catalog'
import { areWorktreePathsEqual, mergeWorktree } from '../../ipc/worktree-logic'
import { areWorktreePathsEqual } from '../../git/worktree-path-comparison'
import { mergeWorktree } from '../../ipc/worktree-logic'
export abstract class RuntimeWorkspaceSelectors extends OrcaRuntimeWithResolveBrowserNetworkExecutionHostForWorktree {
protected abstract hasFreshResolvedWorktreeCache(
@@ -1,13 +1,13 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type * as WorktreeLogic from '../ipc/worktree-logic'
import type * as WorktreePathComparison from '../git/worktree-path-comparison'
import type { UsageScanWorktreeRef } from './usage-provider-contract'
const { worktreePathComparisons } = vi.hoisted(() => ({
worktreePathComparisons: { count: 0 }
}))
vi.mock('../ipc/worktree-logic', async (importOriginal) => {
const actual = await importOriginal<typeof WorktreeLogic>()
vi.mock('../git/worktree-path-comparison', async (importOriginal) => {
const actual = await importOriginal<typeof WorktreePathComparison>()
return {
...actual,
areWorktreePathsEqual: (left: string, right: string) => {
+1 -1
View File
@@ -1,6 +1,6 @@
import { realpath } from 'node:fs/promises'
import { posix, win32 } from 'node:path'
import { areWorktreePathsEqual } from '../ipc/worktree-logic'
import { areWorktreePathsEqual } from '../git/worktree-path-comparison'
import { canonicalizeUsageWorktreePaths } from '../usage-worktree-canonicalizer'
import { looksLikeWindowsPath, normalizeFsPath } from './usage-path-comparison'
import type { UsageScanWorktreeRef } from './usage-provider-contract'
+1 -1
View File
@@ -8,7 +8,7 @@ import {
projectResolvedWorktreeLineage
} from '../shared/resolved-worktree-lineage'
import { readAllWorktreeMetaForHost } from './persistence/host-qualified-worktree-meta'
import { areWorktreePathsEqual } from './ipc/worktree-path-comparison'
import { areWorktreePathsEqual } from './git/worktree-path-comparison'
import { mergeWorktree } from './ipc/worktree-metadata-merge'
import { resolveRepoOwnershipEvidence } from './ipc/worktrees/listing/worktree-host-ownership'
import {
+1 -1
View File
@@ -20,7 +20,7 @@ import {
getWorktreePathSettings,
hasRepoWorktreeBasePath
} from './ipc/worktree-logic'
import { worktreePathComparisonKey } from './ipc/worktree-path-comparison'
import { worktreePathComparisonKey } from './worktree-path-comparison-key'
import {
retirementHostIdentity,
retirementNamespaceKey,
@@ -0,0 +1,23 @@
import { describe, expect, it } from 'vitest'
import { worktreePathComparisonKey } from './worktree-path-comparison-key'
// Retirement namespace keys persist these bytes; a change orphans saved retired names.
describe('worktreePathComparisonKey bytes', () => {
it.each<[string, NodeJS.Platform, string]>([
['/home/dev/Repo/', 'linux', 'posix:/home/dev/Repo'],
['/home/dev/./a/../Repo', 'win32', 'posix:/home/dev/Repo'],
['/private/tmp/orca/feature', 'darwin', 'posix:/tmp/orca/feature'],
['/private/tmp', 'darwin', 'posix:/tmp'],
['/private/tmp/orca/feature', 'linux', 'posix:/private/tmp/orca/feature'],
['C:/Workspaces/Feature', 'darwin', 'windows:c:\\workspaces\\feature'],
['\\\\Server\\Share\\Feature', 'linux', 'windows:\\\\server\\share\\feature'],
['//server/share/Feature', 'darwin', 'windows:\\\\server\\share\\feature'],
[
'\\\\wsl.localhost\\Ubuntu\\home\\dev\\Feature',
'win32',
'windows:\\\\wsl.localhost\\ubuntu\\home\\dev\\feature'
]
])('keys %s on %s', (pathValue, platform, expected) => {
expect(worktreePathComparisonKey(pathValue, platform)).toBe(expected)
})
})
+15
View File
@@ -0,0 +1,15 @@
import { win32 } from 'node:path'
import { isWindowsAbsolutePathLike } from '../shared/cross-platform-path'
import { canonicalWorktreePath } from './git/worktree-path-comparison'
/**
* Bytes frozen: retirement namespace keys persist this, so a changed fold would orphan saved
* retired names. That is why a WSL UNC tail still folds here; equality uses `areWorktreePathsEqual`.
*/
export function worktreePathComparisonKey(pathValue: string, platform = process.platform): string {
const isPosixAbsolute = pathValue.startsWith('/') && !pathValue.startsWith('//')
if (!isPosixAbsolute && (platform === 'win32' || isWindowsAbsolutePathLike(pathValue))) {
return `windows:${win32.normalize(win32.resolve(pathValue)).toLowerCase()}`
}
return `posix:${canonicalWorktreePath(pathValue, platform)}`
}
+1 -1
View File
@@ -2,7 +2,7 @@ import { lstat, readFile } from 'node:fs/promises'
import type { WorktreeMeta } from '../shared/worktree/meta-types'
import type { GitWorktreeInfo } from '../shared/worktree/types'
import { NESTED_WORKTREE_REMOVAL_PREFIX } from '../shared/worktree/nested-removal'
import { areWorktreePathsEqual } from './ipc/worktree-logic'
import { areWorktreePathsEqual } from './git/worktree-path-comparison'
import {
containsPath,
getPathOps,
@@ -1,5 +1,5 @@
import { splitWorktreeId } from '../shared/worktree/id'
import { worktreePathComparisonKey } from './ipc/worktree-path-comparison'
import { worktreePathComparisonKey } from './worktree-path-comparison-key'
/** Comparison keys used only to retain state; canonical equivalence must never select a delete target. */
export function worktreeRetentionPathComparisonKey(
+1 -1
View File
@@ -4,7 +4,7 @@ import {
type RetiredNameRegistry
} from '../shared/worktree/retired-name-registry'
import type { Repo } from '../shared/repo-types'
import { worktreePathComparisonKey } from './ipc/worktree-path-comparison'
import { worktreePathComparisonKey } from './worktree-path-comparison-key'
import { sshEndpointKey, type SshIdentityFields } from './ssh/ssh-target-identity'
/**
@@ -3,7 +3,7 @@ import path from 'node:path'
import type { Page } from '@stablyai/playwright-test'
import { gitExecFileAsync } from '../../src/main/git/runner'
import { listWorktreesStrict } from '../../src/main/git/worktree'
import { areWorktreePathsEqual } from '../../src/main/ipc/worktree-path-comparison'
import { areWorktreePathsEqual } from '../../src/main/git/worktree-path-comparison'
import { expect, test } from './helpers/orca-app'
import {
createRuntimeDesktopPairingOffer,