mirror of
https://github.com/stablyai/orca.git
synced 2026-11-02 18:17:36 +00:00
fix(wsl): refuse a drive/UNC conversion that would split a nested duplicate project (#36155)
* fix(wsl): refuse a drive/UNC conversion that would split a nested duplicate project A refused duplicate inside a converting folder had its path rewritten while its ids stayed, so its terminal history and sessions no longer matched. Another spelling of the same folder still converts with it. Co-Authored-By: Claude <noreply@anthropic.com> * fix(wsl): refuse only duplicates inside a converting folder, as an overlap Review: an enclosing duplicate never splits, the same-spelling exemption keys on the main folder, the blocker reads as an overlap instead of naming the clicked row, and the scan is skipped when nothing is a duplicate. Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -137,6 +137,42 @@ describe('startWslDriveUncConversion', () => {
|
||||
}
|
||||
)
|
||||
|
||||
/** A Windows project and the UNC spelling of its folder, which the scope refuses as a duplicate. */
|
||||
const withDuplicate = (windowsPath: string, duplicate: string): PersistedState => {
|
||||
const value = state()
|
||||
value.repos.push(
|
||||
{ id: 'windows', path: windowsPath, displayName: 'win', badgeColor: '', addedAt: 0 },
|
||||
{ id: 'repo-dup', path: duplicate, displayName: 'dup', badgeColor: '', addedAt: 0 }
|
||||
)
|
||||
return value
|
||||
}
|
||||
|
||||
it('refuses when a duplicate inside the folder would move without its ids', async () => {
|
||||
const value = withDuplicate(String.raw`C:\src\app\sub`, `${UNC}\\sub`)
|
||||
const run = deps({ readProfileState: async () => value })
|
||||
expect(await startWslDriveUncConversion(run, REQUEST)).toMatchObject({
|
||||
blockers: [{ kind: 'not-convertible', id: 'repo-dup', reason: 'overlaps-refused-project' }]
|
||||
})
|
||||
expect(run.pause).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it.each([
|
||||
{
|
||||
shape: 'another spelling of the folder',
|
||||
windowsPath: String.raw`C:\src\app\sub`,
|
||||
duplicate: String.raw`\\wsl$\Ubuntu\mnt\c\src\app`
|
||||
},
|
||||
{
|
||||
shape: 'a folder around it',
|
||||
windowsPath: String.raw`C:\src`,
|
||||
duplicate: String.raw`\\wsl$\Ubuntu\mnt\c\src`
|
||||
}
|
||||
])('converts beside a duplicate of $shape', async ({ windowsPath, duplicate }) => {
|
||||
const value = withDuplicate(windowsPath, duplicate)
|
||||
const run = deps({ readProfileState: async () => value })
|
||||
expect(await startWslDriveUncConversion(run, REQUEST)).toMatchObject({ outcome: 'restarting' })
|
||||
})
|
||||
|
||||
it('refuses, pausing nothing, when the distro does not show /mnt/c as drive C:', async () => {
|
||||
for (const mounts of [null, [{ mountPoint: '/mnt/c', windowsRoot: null }]]) {
|
||||
const run = deps({ readDriveMounts: async () => mounts })
|
||||
|
||||
@@ -27,6 +27,7 @@ import { WslDriveUncKeyCollisionError } from './wsl-drive-unc-identity-rewrite'
|
||||
import {
|
||||
collectWslDriveUncScope,
|
||||
withNestedWslDriveUncProjects,
|
||||
wslDriveUncDuplicatesSplitBy,
|
||||
wslUncDrivePathToWindows
|
||||
} from './wsl-drive-unc-project-scope'
|
||||
import { rewriteWslDriveUncState, wslDriveUncRewriteRoots } from './wsl-drive-unc-state-rewrite'
|
||||
@@ -142,15 +143,30 @@ function checkScope(
|
||||
if (repoIds.length + folderWorkspaceIds.length === 0 && blockers.length === 0) {
|
||||
blockers.push({ kind: 'not-convertible', id: '', reason: 'not-in-scope' })
|
||||
}
|
||||
if (blockers.length > 0) {
|
||||
return { blockers }
|
||||
}
|
||||
const rewriteRoots = (project: { repoIds: string[]; folderWorkspaceIds: string[] }): string[] =>
|
||||
wslDriveUncRewriteRoots(state, { distro: request.distro, ...project })
|
||||
// Bare paths are re-keyed by folder, so a nested project's rows would move without its ids.
|
||||
return blockers.length > 0
|
||||
? { blockers }
|
||||
: withNestedWslDriveUncProjects(
|
||||
scope,
|
||||
request.distro,
|
||||
{ repoIds, folderWorkspaceIds },
|
||||
(project) => wslDriveUncRewriteRoots(state, { distro: request.distro, ...project })
|
||||
)
|
||||
const converting = withNestedWslDriveUncProjects(
|
||||
scope,
|
||||
request.distro,
|
||||
{ repoIds, folderWorkspaceIds },
|
||||
rewriteRoots
|
||||
)
|
||||
const split = wslDriveUncDuplicatesSplitBy(state, scope, request.distro, converting, () =>
|
||||
rewriteRoots(converting)
|
||||
)
|
||||
return split.length > 0
|
||||
? {
|
||||
blockers: split.map((id) => ({
|
||||
kind: 'not-convertible',
|
||||
id,
|
||||
reason: 'overlaps-refused-project'
|
||||
}))
|
||||
}
|
||||
: converting
|
||||
}
|
||||
|
||||
/** Windows Git runs in the drive path, which is the same folder the distro shows. */
|
||||
|
||||
@@ -121,7 +121,7 @@ export function collectWslDriveUncScope(
|
||||
|
||||
/** A bare path is re-keyed by the folder it lies in, so nested projects must convert together. */
|
||||
function refuseOverlaps(scope: WslDriveUncScope, repos: readonly Repo[], distro: string): void {
|
||||
// A duplicate's folder is the project it duplicates, so it never splits a converted folder.
|
||||
// A same-folder duplicate is the project it duplicates; nested ones: wslDriveUncDuplicatesSplitBy.
|
||||
const refusedIds = new Set(
|
||||
scope.refused
|
||||
.filter((entry) => entry.reason !== 'duplicate-windows-project')
|
||||
@@ -183,6 +183,46 @@ export function withNestedWslDriveUncProjects(
|
||||
return { repoIds: [...repoIds], folderWorkspaceIds: [...folderWorkspaceIds] }
|
||||
}
|
||||
|
||||
/**
|
||||
* Refused duplicates a conversion rewriting these folders would split: their paths inside them
|
||||
* would move while their ids stay. A second spelling of a converting project is that project.
|
||||
*/
|
||||
export function wslDriveUncDuplicatesSplitBy(
|
||||
state: WslDriveUncScopeState,
|
||||
scope: WslDriveUncScope,
|
||||
distro: string,
|
||||
converting: { repoIds: readonly string[]; folderWorkspaceIds: readonly string[] },
|
||||
rewriteRoots: () => readonly string[]
|
||||
): string[] {
|
||||
const duplicates = scope.refused.filter((entry) => entry.reason === 'duplicate-windows-project')
|
||||
if (duplicates.length === 0) {
|
||||
return []
|
||||
}
|
||||
const convertingFolders = new Set(
|
||||
driveRoots(
|
||||
[
|
||||
...state.repos.filter(({ id }) => converting.repoIds.includes(id)).map(({ path }) => path),
|
||||
...state.folderWorkspaces
|
||||
.filter(({ id }) => converting.folderWorkspaceIds.includes(id))
|
||||
.map(({ folderPath }) => folderPath)
|
||||
],
|
||||
distro
|
||||
)
|
||||
)
|
||||
const roots = driveRoots(rewriteRoots(), distro)
|
||||
const splits = (folder: string, paths: readonly string[]): boolean =>
|
||||
!driveRoots([folder], distro).some((path) => convertingFolders.has(path)) &&
|
||||
driveRoots(paths, distro).some((path) => roots.some((root) => isInside(path, root)))
|
||||
return duplicates.flatMap(({ kind, id }) => {
|
||||
if (kind === 'repo') {
|
||||
const repo = state.repos.find((entry) => entry.id === id)
|
||||
return repo && splits(repo.path, wslMoveRepoPaths(repo)) ? [id] : []
|
||||
}
|
||||
const folder = state.folderWorkspaces.find((entry) => entry.id === id)
|
||||
return folder && splits(folder.folderPath, [folder.folderPath]) ? [id] : []
|
||||
})
|
||||
}
|
||||
|
||||
function driveRoots(paths: readonly string[], distro: string): string[] {
|
||||
return paths.flatMap((path) => {
|
||||
const drive = wslUncDrivePathToWindows(path, distro)
|
||||
|
||||
Reference in New Issue
Block a user