mirror of
https://github.com/stablyai/orca.git
synced 2026-11-03 18:47:40 +00:00
feat(wsl): default a new Windows-drive project under a WSL default to Git for Windows (D1c-5) (#33122)
* feat(wsl): default a new Windows-drive project under a WSL default to Git for Windows * fix(wsl): judge a new project's worktree links with the prune guard's test, so a //wsl.localhost link counts as Windows Git's Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -32,6 +32,7 @@ import { pruneDeregisteredRepoUiResidue } from './repo-lifecycle-ui-residue'
|
||||
import { isWslMoveProjectSessionFrozen } from '../moving-wsl-projects/wsl-move-scope-guard'
|
||||
import { dropOrcadTerminalRestoreHintsForWorktrees } from '../migrating-orcad-catalog/orcad-terminal-restore-hint-store'
|
||||
import { getRepoIdFromWorktreeId } from '../../../shared/worktree/id'
|
||||
import { applyNewWindowsDriveProjectDefault } from '../../wsl/wsl-drive-project-new-default'
|
||||
|
||||
// A paused WSL move keeps its local rows for a downgrade.
|
||||
function removeLocalRepoSession(
|
||||
@@ -68,6 +69,10 @@ export class RepoLifecycleOperations {
|
||||
addRepo(repo: Repo): void {
|
||||
bumpLocalWorktreeScanGeneration(repo.id)
|
||||
getRepoOrderOperations(this).addRepo(repo)
|
||||
const context = this[repoLifecycleOperationsContext]
|
||||
if (applyNewWindowsDriveProjectDefault(context.runtime.state, repo)) {
|
||||
scheduleSave(context.scheduling)
|
||||
}
|
||||
}
|
||||
|
||||
reorderRepos(orderedIds: string[]): boolean {
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { LocalWindowsRuntimePreference } from '../../shared/project-execution-runtime'
|
||||
import {
|
||||
applyNewWindowsDriveProjectDefault,
|
||||
newWindowsDriveProjectRuntimePreference,
|
||||
readMainCheckoutWorktreeLinks,
|
||||
type NewWindowsDriveProjectDefaultArgs
|
||||
} from './wsl-drive-project-new-default'
|
||||
|
||||
const gate = vi.hoisted(() => ({ open: true }))
|
||||
vi.mock('./wsl-host-gate', () => ({ wslHostsGateOpen: () => gate.open }))
|
||||
|
||||
const WSL_DEFAULT = { kind: 'wsl', distro: 'Ubuntu' }
|
||||
|
||||
function args(overrides: Partial<NewWindowsDriveProjectDefaultArgs> = {}) {
|
||||
return {
|
||||
platform: 'win32',
|
||||
repo: { path: 'C:\\src\\app', kind: 'git' as const },
|
||||
globalWindowsRuntimeDefault: WSL_DEFAULT,
|
||||
readWorktreeLinks: () => [],
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
describe('newWindowsDriveProjectRuntimePreference', () => {
|
||||
it('pins the global distro with Git for Windows for a drive project', () => {
|
||||
expect(newWindowsDriveProjectRuntimePreference(args())).toEqual({
|
||||
kind: 'wsl',
|
||||
distro: 'Ubuntu',
|
||||
git: 'windows-host'
|
||||
})
|
||||
})
|
||||
|
||||
it.each([
|
||||
['off Windows', { platform: 'darwin' }],
|
||||
['under a Windows default', { globalWindowsRuntimeDefault: { kind: 'windows-host' } }],
|
||||
['with no default distro', { globalWindowsRuntimeDefault: { kind: 'wsl', distro: null } }],
|
||||
['inside a distro', { repo: { path: '\\\\wsl.localhost\\Ubuntu\\home\\me\\app' } }],
|
||||
['for a folder project', { repo: { path: 'C:\\src\\app', kind: 'folder' as const } }],
|
||||
['on another host', { repo: { path: 'C:\\src\\app', executionHostId: 'ssh:box' as const } }],
|
||||
['with WSL-authored worktrees', { readWorktreeLinks: () => ['/mnt/c/src/feature/.git'] }],
|
||||
['when its worktree links are unreadable', { readWorktreeLinks: () => null }]
|
||||
])('leaves the project inheriting %s', (_name, overrides) => {
|
||||
expect(newWindowsDriveProjectRuntimePreference(args(overrides))).toBeNull()
|
||||
})
|
||||
|
||||
it('accepts worktrees Windows Git made', () => {
|
||||
expect(
|
||||
newWindowsDriveProjectRuntimePreference(
|
||||
args({
|
||||
readWorktreeLinks: () => [
|
||||
'C:/src/feature/.git',
|
||||
'//wsl.localhost/Ubuntu/home/me/fix/.git'
|
||||
]
|
||||
})
|
||||
)
|
||||
).not.toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('applyNewWindowsDriveProjectDefault', () => {
|
||||
beforeEach(() => {
|
||||
gate.open = true
|
||||
})
|
||||
|
||||
function state(localWindowsRuntimePreference?: LocalWindowsRuntimePreference) {
|
||||
return {
|
||||
projects: [{ sourceRepoIds: ['r1'], localWindowsRuntimePreference }],
|
||||
settings: {
|
||||
localWindowsRuntimeDefault: { kind: 'wsl' as const, distro: 'Ubuntu' }
|
||||
}
|
||||
}
|
||||
}
|
||||
const repo = { id: 'r1', path: 'C:\\src\\app', kind: 'git' as const }
|
||||
const env = { platform: 'win32', readWorktreeLinks: () => [] }
|
||||
|
||||
it("writes the default onto the repo's new project", () => {
|
||||
const current = state()
|
||||
expect(applyNewWindowsDriveProjectDefault(current, repo, env)).toBe(true)
|
||||
expect(current.projects[0].localWindowsRuntimePreference).toEqual({
|
||||
kind: 'wsl',
|
||||
distro: 'Ubuntu',
|
||||
git: 'windows-host'
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps a preference the project already has', () => {
|
||||
const current = state({ kind: 'windows-host' })
|
||||
expect(applyNewWindowsDriveProjectDefault(current, repo, env)).toBe(false)
|
||||
expect(current.projects[0].localWindowsRuntimePreference).toEqual({
|
||||
kind: 'windows-host'
|
||||
})
|
||||
})
|
||||
|
||||
it('does nothing while the WSL hosts gate is closed', () => {
|
||||
gate.open = false
|
||||
const current = state()
|
||||
expect(applyNewWindowsDriveProjectDefault(current, repo, env)).toBe(false)
|
||||
expect(current.projects[0].localWindowsRuntimePreference).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('readMainCheckoutWorktreeLinks', () => {
|
||||
let dir: string
|
||||
|
||||
beforeEach(() => {
|
||||
dir = mkdtempSync(join(tmpdir(), 'orca-drive-default-'))
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
it("reads each linked worktree's gitdir link", () => {
|
||||
mkdirSync(join(dir, '.git', 'worktrees', 'feature'), { recursive: true })
|
||||
writeFileSync(join(dir, '.git', 'worktrees', 'feature', 'gitdir'), '/mnt/c/src/feature/.git\n')
|
||||
expect(readMainCheckoutWorktreeLinks(dir)).toEqual(['/mnt/c/src/feature/.git'])
|
||||
})
|
||||
|
||||
it('reads a checkout with no linked worktrees as none', () => {
|
||||
mkdirSync(join(dir, '.git'))
|
||||
expect(readMainCheckoutWorktreeLinks(dir)).toEqual([])
|
||||
})
|
||||
|
||||
it('reads a linked worktree or missing checkout as unknown', () => {
|
||||
expect(readMainCheckoutWorktreeLinks(dir)).toBeNull()
|
||||
writeFileSync(join(dir, '.git'), 'gitdir: C:/src/app/.git/worktrees/x\n')
|
||||
expect(readMainCheckoutWorktreeLinks(dir)).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* A brand-new project on a Windows drive (`C:\…`) added while the global default runtime is WSL
|
||||
* gets Git for Windows, keeping its terminals and agents in WSL: WSL Git there is ~300x slower and
|
||||
* misses Windows-side edits. Existing projects convert through wsl-drive-project-windows-git.ts.
|
||||
*/
|
||||
import { readdirSync, readFileSync, statSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { getRepoExecutionHostId, LOCAL_EXECUTION_HOST_ID } from '../../shared/execution-host'
|
||||
import {
|
||||
normalizeGlobalWindowsRuntimeDefault,
|
||||
type LocalWindowsRuntimePreference
|
||||
} from '../../shared/project-execution-runtime'
|
||||
import type { GlobalSettings } from '../../shared/global-settings-types'
|
||||
import type { Project } from '../../shared/project-types'
|
||||
import { isFolderRepo } from '../../shared/repo-kind'
|
||||
import type { Repo } from '../../shared/repo-types'
|
||||
import { isForeignLink } from '../git/worktree-prune-guard'
|
||||
import { wslHostsGateOpen } from './wsl-host-gate'
|
||||
|
||||
export type NewWindowsDriveProjectDefaultArgs = {
|
||||
platform: string
|
||||
repo: Pick<Repo, 'path' | 'kind' | 'executionHostId' | 'connectionId'>
|
||||
globalWindowsRuntimeDefault: unknown
|
||||
/** `gitdir` links of the repo's linked worktrees; null when they can't be read. */
|
||||
readWorktreeLinks: (repoPath: string) => string[] | null
|
||||
}
|
||||
|
||||
export function newWindowsDriveProjectRuntimePreference(
|
||||
args: NewWindowsDriveProjectDefaultArgs
|
||||
): LocalWindowsRuntimePreference | null {
|
||||
const { repo } = args
|
||||
if (
|
||||
args.platform !== 'win32' ||
|
||||
getRepoExecutionHostId(repo) !== LOCAL_EXECUTION_HOST_ID ||
|
||||
isFolderRepo(repo) ||
|
||||
!/^[A-Za-z]:[\\/]/.test(repo.path)
|
||||
) {
|
||||
return null
|
||||
}
|
||||
const globalDefault = normalizeGlobalWindowsRuntimeDefault(args.globalWindowsRuntimeDefault)
|
||||
if (globalDefault.kind !== 'wsl' || !globalDefault.distro) {
|
||||
return null
|
||||
}
|
||||
// WSL Git wrote Linux-path links that Windows Git can't follow; the preflight refuses these too.
|
||||
const links = args.readWorktreeLinks(repo.path)
|
||||
if (!links || links.some((link) => isForeignLink(link, false))) {
|
||||
return null
|
||||
}
|
||||
// Pinned like a converted project, so a later global change can't silently switch its Git.
|
||||
return { kind: 'wsl', distro: globalDefault.distro, git: 'windows-host' }
|
||||
}
|
||||
|
||||
/** Sets the default on the project `addRepo` just made for `repo`; true when it wrote. */
|
||||
export function applyNewWindowsDriveProjectDefault(
|
||||
state: {
|
||||
projects: Pick<Project, 'sourceRepoIds' | 'localWindowsRuntimePreference'>[]
|
||||
settings: Pick<GlobalSettings, 'localWindowsRuntimeDefault'>
|
||||
},
|
||||
repo: NewWindowsDriveProjectDefaultArgs['repo'] & Pick<Repo, 'id'>,
|
||||
env: Pick<NewWindowsDriveProjectDefaultArgs, 'platform' | 'readWorktreeLinks'> = {
|
||||
platform: process.platform,
|
||||
readWorktreeLinks: readMainCheckoutWorktreeLinks
|
||||
}
|
||||
): boolean {
|
||||
if (!wslHostsGateOpen()) {
|
||||
return false
|
||||
}
|
||||
const project = state.projects.find((entry) => entry.sourceRepoIds.includes(repo.id))
|
||||
if (!project || project.localWindowsRuntimePreference) {
|
||||
return false
|
||||
}
|
||||
const preference = newWindowsDriveProjectRuntimePreference({
|
||||
...env,
|
||||
repo,
|
||||
globalWindowsRuntimeDefault: state.settings.localWindowsRuntimeDefault
|
||||
})
|
||||
if (!preference) {
|
||||
return false
|
||||
}
|
||||
project.localWindowsRuntimePreference = preference
|
||||
return true
|
||||
}
|
||||
|
||||
/** Only a main checkout's `.git` folder; a linked worktree or submodule reads as unknown. */
|
||||
export function readMainCheckoutWorktreeLinks(repoPath: string): string[] | null {
|
||||
try {
|
||||
const gitDir = join(repoPath, '.git')
|
||||
if (!statSync(gitDir).isDirectory()) {
|
||||
return null
|
||||
}
|
||||
const root = join(gitDir, 'worktrees')
|
||||
let names: string[]
|
||||
try {
|
||||
names = readdirSync(root)
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
return names.flatMap((name) => {
|
||||
try {
|
||||
return [readFileSync(join(root, name, 'gitdir'), 'utf8').trim()]
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
})
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user