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:
Neil
2026-10-29 00:32:19 -07:00
committed by GitHub
co-authored by Claude OrcaWin
parent 3d236c3b3a
commit 71f030fcd6
3 changed files with 246 additions and 0 deletions
@@ -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
}
}