mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 08:37:49 +00:00
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:
@@ -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,
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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('\\\\')
|
||||
}
|
||||
|
||||
+32
-1
@@ -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[],
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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,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,
|
||||
|
||||
@@ -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,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'
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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)}`
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user