mirror of
https://github.com/stablyai/orca.git
synced 2026-10-27 16:02:42 +00:00
* fix(worktree): let a POSIX path keep its case on a Windows desktop `canonicalWorktreePath` folded case whenever `process.platform` was win32, without asking what the path itself was. A WSL or SSH checkout is spelled `/home/alice/ws/feature` on a Windows desktop too, and ext4 is case-sensitive, so `/home/alice/ws/Feature` and `/home/alice/ws/feature` — two real checkouts on two real branches — collapsed into one row. `removeWorktree` picks the row it is about to remove with that comparison and reads the branch off it. Requesting `/home/alice/ws/feature` removed the right directory (the path rides in argv) and then ran `git branch -d -- Feature`. The same wrong row feeds `assertWorktreeUnlockedForRemoval`, so a locked twin blocks an unlocked delete and an unlocked twin lets a locked one through. Whose filesystem a path names is a property of the path, not of the desktop reading it, so a POSIX-absolute path now takes POSIX rules at any platform and a POSIX/Windows pair is never equal — `win32.resolve` would otherwise give the POSIX path a drive root and manufacture the equality. Windows drive and UNC paths, including WSL UNC aliases, keep folding case as before. Two call sites already carried private copies of this rule (`isSameCommonDirPath`, `ipc/worktree-path-comparison`); this is the same rule at the source. The removal path is the one that never got one. * fix(worktree): keep a WSL checkout's case through the UNC spelling too The first commit gave POSIX-absolute paths POSIX case rules, which is right but does not reach the WSL case it claimed. `listWorktreesStrict` runs every listed path through `translateWorktreePath`, so git-in-the-distro's `/home/alice/ws/Feature` arrives as `\\wsl.localhost\Ubuntu\home\alice\ws\Feature` and the POSIX branch never sees it. The removal suite mocks `translateWslOutputPaths` to identity, which is why the end-to-end test passed without exercising the translation production always applies. Driving the real translator, the original defect survived unchanged: a request naming `...\ws\feature` ran `git worktree remove --force ...\ws\feature` and then `git branch -d -- Feature`. The filesystem behind `\\wsl.localhost` is ext4, so the UNC spelling is case-sensitive for the same reason the Linux spelling is — except where Windows genuinely folds: the `\\wsl$` share alias, the distro name, and a drvfs `/mnt/<letter>` tail, which really is a Windows volume. `foldWslUncPathCaseInsensitiveParts` already draws exactly that line and `git-fetch-head-lock` already depends on it, so this reuses it rather than writing a fourth copy of the rule. Windows drive paths keep folding whole. The end-to-end case now drives the real translator instead of the mock, so the translation cannot go missing again without the test noticing.
100 lines
4.4 KiB
TypeScript
100 lines
4.4 KiB
TypeScript
import { posix, win32 } from 'node:path'
|
|
import { foldWslUncPathCaseInsensitiveParts } from '../../shared/wsl-paths'
|
|
import type { GitWorktreeExecOptions } from './worktree-operation-options'
|
|
import { translateWslOutputPaths } from './runner'
|
|
|
|
/**
|
|
* Normalize a worktree path for cross-platform comparison/keying: resolved, and case-folded on
|
|
* Windows syntax.
|
|
*
|
|
* Why the path's own syntax outranks `platform`: whose filesystem a path names is a property of the
|
|
* path, not of the desktop reading it, and folding a case-sensitive filesystem merges two real
|
|
* checkouts into one row — enough for `removeWorktree` to pick the twin and delete its branch.
|
|
*
|
|
* Two syntaxes name a case-sensitive filesystem. A POSIX-absolute path is one. The other is the WSL
|
|
* UNC alias, which is the shape that actually reaches removal: `listWorktreesStrict` runs every
|
|
* listed path through `translateWorktreePath`, so git-in-the-distro's `/home/alice/Feature` arrives
|
|
* 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.
|
|
*/
|
|
export function canonicalWorktreePath(pathValue: string, platform = process.platform): string {
|
|
if (looksLikePosixAbsolutePath(pathValue)) {
|
|
return posix.normalize(posix.resolve(pathValue))
|
|
}
|
|
const wslKey = wslUncComparisonKey(pathValue)
|
|
if (wslKey) {
|
|
return wslKey
|
|
}
|
|
return platform === 'win32' || looksLikeWindowsPath(pathValue)
|
|
? win32.normalize(win32.resolve(pathValue)).toLowerCase()
|
|
: posix.normalize(posix.resolve(pathValue))
|
|
}
|
|
|
|
/**
|
|
* The comparison key for a WSL UNC path, or null when it is not one.
|
|
*
|
|
* Normalized through `win32` first so `..`/`.` segments and slash style collapse, then folded only
|
|
* where Windows really folds. The fold is unconditional on platform: a `\\wsl.localhost\...` string
|
|
* names the same distro filesystem whichever desktop is reading it.
|
|
*/
|
|
function wslUncComparisonKey(pathValue: string): string | null {
|
|
const folded = foldWslUncPathCaseInsensitiveParts(pathValue)
|
|
if (!folded) {
|
|
return null
|
|
}
|
|
return foldWslUncPathCaseInsensitiveParts(win32.normalize(pathValue)) ?? folded
|
|
}
|
|
|
|
export function areWorktreePathsEqual(
|
|
leftPath: string,
|
|
rightPath: string,
|
|
platform = process.platform
|
|
): boolean {
|
|
const leftIsPosix = looksLikePosixAbsolutePath(leftPath)
|
|
if (leftIsPosix || looksLikePosixAbsolutePath(rightPath)) {
|
|
// Why not fall through: `win32.resolve` gives a POSIX path a drive root, manufacturing an
|
|
// equality with a Windows path that names a different filesystem.
|
|
return (
|
|
leftIsPosix &&
|
|
looksLikePosixAbsolutePath(rightPath) &&
|
|
canonicalWorktreePath(leftPath, platform) === canonicalWorktreePath(rightPath, platform)
|
|
)
|
|
}
|
|
if (platform === 'win32' || looksLikeWindowsPath(leftPath) || looksLikeWindowsPath(rightPath)) {
|
|
return canonicalWorktreePath(leftPath, 'win32') === canonicalWorktreePath(rightPath, 'win32')
|
|
}
|
|
return canonicalWorktreePath(leftPath, platform) === canonicalWorktreePath(rightPath, platform)
|
|
}
|
|
|
|
function looksLikeWindowsPath(pathValue: string): boolean {
|
|
return /^[A-Za-z]:[\\/]/.test(pathValue) || pathValue.startsWith('\\\\')
|
|
}
|
|
|
|
// One leading slash only: `//server/share` and WSL UNC aliases are Windows roots, not POSIX paths.
|
|
function looksLikePosixAbsolutePath(pathValue: string): boolean {
|
|
return pathValue.startsWith('/') && !pathValue.startsWith('//')
|
|
}
|
|
|
|
export function resolveRevParsePath(repoPath: string, value: string): string {
|
|
if (posix.isAbsolute(value) || win32.isAbsolute(value)) {
|
|
return value
|
|
}
|
|
// Old git ignores `--path-format=absolute`, so resolve a relative toplevel/git-dir against the scanned repo path.
|
|
return looksLikeWindowsPath(repoPath)
|
|
? win32.resolve(repoPath, value)
|
|
: posix.resolve(repoPath, value)
|
|
}
|
|
|
|
export function translateWorktreePath(
|
|
worktreePath: string,
|
|
repoPath: string,
|
|
options: GitWorktreeExecOptions = {}
|
|
): string {
|
|
const prefix = 'worktree '
|
|
const translated = translateWslOutputPaths(`${prefix}${worktreePath}`, repoPath, options)
|
|
return translated.startsWith(prefix) ? translated.slice(prefix.length) : worktreePath
|
|
}
|