fix: qualify ssh fallback worktree paths (#7764)

This commit is contained in:
Yu Sun 2026-07-24 15:24:58 +08:00 committed by GitHub
parent 6d39e49480
commit 2cbcf03b0f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 96 additions and 55 deletions

View File

@ -242,13 +242,38 @@ describe('computeWorktreePath', () => {
).toBe('C:\\Projects\\app\\worktrees\\feature')
})
it('keeps legacy SSH sibling paths for global absolute workspace directories', () => {
it('qualifies SSH sibling paths with the repo name for global absolute workspace directories', () => {
expect(
computeRemoteWorktreePath('feature', '/remote/repo', {
computeRemoteWorktreePath('main', '/remote/bioinformatist.github.io', {
nestWorkspaces: false,
workspaceDir: '/local/workspaces'
})
).toBe('/remote/feature')
).toBe('/remote/bioinformatist.github.io-main')
expect(
computeRemoteWorktreePath('main-2', '/remote/dotfiles', {
nestWorkspaces: false,
workspaceDir: '/local/workspaces'
})
).toBe('/remote/dotfiles-main-2')
})
it('qualifies SSH sibling paths with the repo name on Windows remote paths', () => {
expect(
computeRemoteWorktreePath('main', 'C:\\Remote\\dotfiles', {
nestWorkspaces: false,
workspaceDir: 'C:\\Local\\workspaces'
})
).toBe('C:\\Remote\\dotfiles-main')
})
it('strips .git suffix from qualified SSH sibling paths', () => {
expect(
computeRemoteWorktreePath('main', '/remote/project.git', {
nestWorkspaces: false,
workspaceDir: '/local/workspaces'
})
).toBe('/remote/project-main')
})
it('applies repo-specific SSH workspace directories on the remote path', () => {
@ -275,6 +300,20 @@ describe('computeWorktreePath', () => {
)
).toBe('C:\\Remote\\worktrees\\feature')
})
it('keeps repo-specific absolute SSH workspace directories unqualified', () => {
expect(
computeRemoteWorktreePath(
'feature',
'/remote/project/repo',
{
nestWorkspaces: false,
workspaceDir: '/remote/worktrees'
},
{ useConfiguredAbsolutePath: true }
)
).toBe('/remote/worktrees/feature')
})
})
describe('areWorktreePathsEqual', () => {

View File

@ -125,9 +125,10 @@ export function computeRemoteWorktreePath(
return computeWorktreePath(sanitizedName, repoPath, settings)
}
// Why: absolute global workspaceDir values belong to the desktop machine.
// SSH worktrees keep the legacy repo-sibling root unless a repo-specific
// path opts into a remote-host location.
return getRuntimePathOps(repoPath, repoPath).join(repoPath, '..', sanitizedName)
// SSH falls back to repo-qualified sibling paths so origin/main is not shared.
const pathOps = getRuntimePathOps(repoPath, repoPath)
const repoName = pathOps.basename(repoPath).replace(/\.git$/, '')
return pathOps.join(repoPath, '..', `${repoName}-${sanitizedName}`)
}
export function getWorktreePathSettings(

View File

@ -3210,7 +3210,7 @@ describe('registerWorktreeHandlers', () => {
isMainWorktree: true
},
{
path: '/remote/improve-dashboard',
path: '/remote/repo-improve-dashboard',
head: 'abc123',
branch: 'refs/heads/improve-dashboard',
isBare: false,
@ -3250,7 +3250,7 @@ describe('registerWorktreeHandlers', () => {
expect(provider.listWorktrees).toHaveBeenCalledTimes(1)
expect(provider.worktreeIsClean).not.toHaveBeenCalled()
expect(store.setWorktreeMeta).toHaveBeenCalledWith(
'repo-ssh::/remote/improve-dashboard',
'repo-ssh::/remote/repo-improve-dashboard',
expect.objectContaining({
linkedIssue: 123,
linkedPR: 456,
@ -3308,7 +3308,7 @@ describe('registerWorktreeHandlers', () => {
])
.mockResolvedValueOnce([
{
path: '/remote/improve-dashboard',
path: '/remote/repo-improve-dashboard',
head: 'abc123',
branch: 'refs/heads/improve-dashboard',
isBare: false,
@ -3405,7 +3405,7 @@ describe('registerWorktreeHandlers', () => {
])
.mockResolvedValueOnce([
{
path: '/remote/improve-dashboard',
path: '/remote/repo-improve-dashboard',
head: 'abc123',
branch: 'refs/heads/improve-dashboard',
isBare: false,
@ -3512,7 +3512,7 @@ describe('registerWorktreeHandlers', () => {
isMainWorktree: true
},
{
path: '/remote/improve-dashboard',
path: '/remote/repo-improve-dashboard',
head: 'abc123',
branch: 'refs/heads/improve-dashboard',
isBare: false,
@ -3623,7 +3623,7 @@ describe('registerWorktreeHandlers', () => {
])
.mockResolvedValueOnce([
{
path: '/remote/improve-dashboard',
path: '/remote/repo-improve-dashboard',
head: 'abc123',
branch: 'refs/heads/improve-dashboard',
isBare: false,
@ -3675,7 +3675,7 @@ describe('registerWorktreeHandlers', () => {
}
if (args[0] === 'rev-parse' && args[1] === '--git-path') {
return {
stdout: '/remote/repo/.git/worktrees/improve-dashboard/orca/setup-runner.sh\n',
stdout: '/remote/repo/.git/worktrees/repo-improve-dashboard/orca/setup-runner.sh\n',
stderr: ''
}
}
@ -3688,7 +3688,7 @@ describe('registerWorktreeHandlers', () => {
addWorktree: vi.fn().mockResolvedValue(undefined),
listWorktrees: vi.fn().mockResolvedValue([
{
path: '/remote/improve-dashboard',
path: '/remote/repo-improve-dashboard',
head: 'abc123',
branch: 'refs/heads/improve-dashboard',
isBare: false,
@ -3725,25 +3725,26 @@ describe('registerWorktreeHandlers', () => {
})
expect(fsProvider.readFile).toHaveBeenCalledWith('/remote/repo/orca.yaml')
expect(fsProvider.readFile).toHaveBeenCalledWith('/remote/improve-dashboard/orca.yaml')
expect(fsProvider.readFile).toHaveBeenCalledWith('/remote/repo-improve-dashboard/orca.yaml')
expect(provider.exec).toHaveBeenCalledWith(
['rev-parse', '--git-path', 'orca/setup-runner.sh'],
'/remote/improve-dashboard'
'/remote/repo-improve-dashboard'
)
expect(fsProvider.createDir).toHaveBeenCalledWith(
'/remote/repo/.git/worktrees/improve-dashboard/orca'
'/remote/repo/.git/worktrees/repo-improve-dashboard/orca'
)
expect(fsProvider.writeFile).toHaveBeenCalledWith(
'/remote/repo/.git/worktrees/improve-dashboard/orca/setup-runner.sh',
'/remote/repo/.git/worktrees/repo-improve-dashboard/orca/setup-runner.sh',
'#!/usr/bin/env bash\nset -e\npnpm install\n'
)
expect(result).toEqual(
expect.objectContaining({
setup: {
runnerScriptPath: '/remote/repo/.git/worktrees/improve-dashboard/orca/setup-runner.sh',
runnerScriptPath:
'/remote/repo/.git/worktrees/repo-improve-dashboard/orca/setup-runner.sh',
envVars: expect.objectContaining({
ORCA_ROOT_PATH: '/remote/repo',
ORCA_WORKTREE_PATH: '/remote/improve-dashboard'
ORCA_WORKTREE_PATH: '/remote/repo-improve-dashboard'
})
}
})
@ -3772,7 +3773,7 @@ describe('registerWorktreeHandlers', () => {
removeWorktree: vi.fn().mockResolvedValue(undefined),
listWorktrees: vi.fn().mockResolvedValue([
{
path: '/remote/sparse-dashboard',
path: '/remote/repo-sparse-dashboard',
head: 'abc123',
branch: 'refs/heads/sparse-dashboard',
isBare: false,
@ -3813,23 +3814,23 @@ describe('registerWorktreeHandlers', () => {
expect(provider.addWorktree).toHaveBeenCalledWith(
'/remote/repo',
'sparse-dashboard',
'/remote/sparse-dashboard',
'/remote/repo-sparse-dashboard',
{ base: 'origin/main', noCheckout: true }
)
expect(provider.exec).toHaveBeenCalledWith(
['sparse-checkout', 'init', '--cone'],
'/remote/sparse-dashboard'
'/remote/repo-sparse-dashboard'
)
expect(provider.exec).toHaveBeenCalledWith(
['sparse-checkout', 'set', '--', 'apps/mobile', 'packages/shared'],
'/remote/sparse-dashboard'
'/remote/repo-sparse-dashboard'
)
expect(provider.exec).toHaveBeenCalledWith(
['checkout', 'sparse-dashboard'],
'/remote/sparse-dashboard'
'/remote/repo-sparse-dashboard'
)
expect(store.setWorktreeMeta).toHaveBeenCalledWith(
'repo-ssh::/remote/sparse-dashboard',
'repo-ssh::/remote/repo-sparse-dashboard',
expect.objectContaining({
sparseDirectories: ['apps/mobile', 'packages/shared'],
baseRef: 'refs/remotes/origin/main',
@ -3895,7 +3896,7 @@ describe('registerWorktreeHandlers', () => {
])
.mockResolvedValueOnce([
{
path: '/remote/fix-title-2',
path: '/remote/repo-fix-title-2',
head: 'abc123',
branch: 'refs/heads/feature/fix',
isBare: false,
@ -3905,7 +3906,7 @@ describe('registerWorktreeHandlers', () => {
}
const fsProvider = {
stat: vi.fn().mockImplementation(async (pathValue: string) => {
if (pathValue === '/remote/fix-title') {
if (pathValue === '/remote/repo-fix-title') {
return { size: 0, type: 'directory', mtime: 0 }
}
const error = new Error('missing') as Error & { code: string }
@ -3934,11 +3935,11 @@ describe('registerWorktreeHandlers', () => {
expect(provider.addWorktree).toHaveBeenCalledWith(
'/remote/repo',
'feature/fix',
'/remote/fix-title-2',
'/remote/repo-fix-title-2',
{ checkoutExistingBranch: true }
)
expect(mux.request).toHaveBeenCalledWith('session.registerRoot', {
rootPath: '/remote/fix-title-2'
rootPath: '/remote/repo-fix-title-2'
})
})
@ -3976,7 +3977,7 @@ describe('registerWorktreeHandlers', () => {
removeWorktree: vi.fn().mockResolvedValue(undefined),
listWorktrees: vi.fn().mockResolvedValue([
{
path: '/remote/feature-something-2',
path: '/remote/repo-feature-something-2',
head: 'abc123',
branch: 'refs/heads/feature/something-2',
isBare: false,
@ -4002,7 +4003,7 @@ describe('registerWorktreeHandlers', () => {
expect(provider.addWorktree).toHaveBeenCalledWith(
'/remote/repo',
'feature/something-2',
'/remote/feature-something-2',
'/remote/repo-feature-something-2',
{ base: 'origin/main' }
)
})
@ -4038,7 +4039,7 @@ describe('registerWorktreeHandlers', () => {
removeWorktree: vi.fn().mockResolvedValue(undefined),
listWorktrees: vi.fn().mockResolvedValue([
{
path: '/remote/feature-something-2',
path: '/remote/repo-feature-something-2',
head: 'abc123',
branch: 'refs/heads/feature/something-2',
isBare: false,
@ -4064,7 +4065,7 @@ describe('registerWorktreeHandlers', () => {
expect(provider.addWorktree).toHaveBeenCalledWith(
'/remote/repo',
'feature/something-2',
'/remote/feature-something-2',
'/remote/repo-feature-something-2',
{ base: 'origin/main' }
)
})
@ -4117,9 +4118,9 @@ describe('registerWorktreeHandlers', () => {
expect(provider.exec).toHaveBeenCalledWith(
['config', '--local', '--unset-all', 'branch.sparse-dashboard.base'],
'/remote/sparse-dashboard'
'/remote/repo-sparse-dashboard'
)
expect(provider.removeWorktree).toHaveBeenCalledWith('/remote/sparse-dashboard', true, {
expect(provider.removeWorktree).toHaveBeenCalledWith('/remote/repo-sparse-dashboard', true, {
deleteBranch: true,
forceBranchDelete: true
})
@ -4210,7 +4211,7 @@ describe('registerWorktreeHandlers', () => {
addWorktree: vi.fn().mockResolvedValue(undefined),
listWorktrees: vi.fn().mockResolvedValueOnce([
{
path: '/remote/improve-dashboard',
path: '/remote/repo-improve-dashboard',
head: 'abc123',
branch: 'refs/heads/improve-dashboard',
isBare: false,
@ -4244,7 +4245,7 @@ describe('registerWorktreeHandlers', () => {
expect(provider.addWorktree).toHaveBeenCalledWith(
'/remote/repo',
'improve-dashboard',
'/remote/improve-dashboard',
'/remote/repo-improve-dashboard',
{
base: 'origin/main'
}
@ -4282,7 +4283,7 @@ describe('registerWorktreeHandlers', () => {
addWorktree: vi.fn().mockResolvedValue(undefined),
listWorktrees: vi.fn().mockResolvedValue([
{
path: '/remote/local-branch-base',
path: '/remote/repo-local-branch-base',
head: 'develop-sha',
branch: 'refs/heads/local-branch-base',
isBare: false,
@ -4316,7 +4317,7 @@ describe('registerWorktreeHandlers', () => {
expect(provider.addWorktree).toHaveBeenCalledWith(
'/remote/repo',
'local-branch-base',
'/remote/local-branch-base',
'/remote/repo-local-branch-base',
{
base: 'develop'
}
@ -4360,7 +4361,7 @@ describe('registerWorktreeHandlers', () => {
addWorktree: vi.fn().mockResolvedValue(undefined),
listWorktrees: vi.fn().mockResolvedValue([
{
path: '/remote/slash-local-base',
path: '/remote/repo-slash-local-base',
head: 'team-feature-sha',
branch: 'refs/heads/slash-local-base',
isBare: false,
@ -4398,7 +4399,7 @@ describe('registerWorktreeHandlers', () => {
expect(provider.addWorktree).toHaveBeenCalledWith(
'/remote/repo',
'slash-local-base',
'/remote/slash-local-base',
'/remote/repo-slash-local-base',
{
base: 'team/feature'
}
@ -4428,7 +4429,7 @@ describe('registerWorktreeHandlers', () => {
.fn()
.mockResolvedValueOnce([
{
path: '/remote/first-worktree',
path: '/remote/repo-first-worktree',
head: 'abc123',
branch: 'refs/heads/first-worktree',
isBare: false,
@ -4437,7 +4438,7 @@ describe('registerWorktreeHandlers', () => {
])
.mockResolvedValueOnce([
{
path: '/remote/second-worktree',
path: '/remote/repo-second-worktree',
head: 'def456',
branch: 'refs/heads/second-worktree',
isBare: false,
@ -4506,7 +4507,7 @@ describe('registerWorktreeHandlers', () => {
addWorktree: vi.fn().mockResolvedValue(undefined),
listWorktrees: vi.fn().mockResolvedValue([
{
path: '/remote/fix-title',
path: '/remote/repo-fix-title',
head: sha,
branch: 'refs/heads/feature/fix',
isBare: false,
@ -4536,7 +4537,7 @@ describe('registerWorktreeHandlers', () => {
expect(provider.addWorktree).toHaveBeenCalledWith(
'/remote/repo',
'feature/fix',
'/remote/fix-title',
'/remote/repo-fix-title',
{ base: sha }
)
})
@ -4566,7 +4567,7 @@ describe('registerWorktreeHandlers', () => {
addWorktree: vi.fn().mockResolvedValue(undefined),
listWorktrees: vi.fn().mockResolvedValue([
{
path: '/remote/prefetched-worktree',
path: '/remote/repo-prefetched-worktree',
head: 'abc123',
branch: 'refs/heads/prefetched-worktree',
isBare: false,
@ -4642,7 +4643,7 @@ describe('registerWorktreeHandlers', () => {
addWorktree: vi.fn().mockResolvedValue(undefined),
listWorktrees: vi.fn().mockResolvedValue([
{
path: '/remote/prefetched-worktree',
path: '/remote/repo-prefetched-worktree',
head: 'abc123',
branch: 'refs/heads/prefetched-worktree',
isBare: false,
@ -4678,7 +4679,7 @@ describe('registerWorktreeHandlers', () => {
expect(provider.addWorktree).toHaveBeenCalledWith(
'/remote/repo',
'prefetched-worktree',
'/remote/prefetched-worktree',
'/remote/repo-prefetched-worktree',
{
base: 'origin/main'
}
@ -4718,7 +4719,7 @@ describe('registerWorktreeHandlers', () => {
addWorktree: vi.fn().mockResolvedValue(undefined),
listWorktrees: vi.fn().mockResolvedValue([
{
path: '/remote/slash-local-base',
path: '/remote/repo-slash-local-base',
head: 'team-feature-sha',
branch: 'refs/heads/slash-local-base',
isBare: false,
@ -4753,7 +4754,7 @@ describe('registerWorktreeHandlers', () => {
expect(provider.addWorktree).toHaveBeenCalledWith(
'/remote/repo',
'slash-local-base',
'/remote/slash-local-base',
'/remote/repo-slash-local-base',
{
base: 'team/feature'
}
@ -4785,7 +4786,7 @@ describe('registerWorktreeHandlers', () => {
addWorktree: vi.fn().mockResolvedValue(undefined),
listWorktrees: vi.fn().mockResolvedValue([
{
path: '/remote/prefetched-worktree',
path: '/remote/repo-prefetched-worktree',
head: 'abc123',
branch: 'refs/heads/prefetched-worktree',
isBare: false,
@ -4851,7 +4852,7 @@ describe('registerWorktreeHandlers', () => {
addWorktree: vi.fn().mockResolvedValue(undefined),
listWorktrees: vi.fn().mockResolvedValue([
{
path: '/remote/local-base-worktree',
path: '/remote/repo-local-base-worktree',
head: 'abc123',
branch: 'refs/heads/local-base-worktree',
isBare: false,

View File

@ -4677,7 +4677,7 @@ describe('OrcaRuntimeService', () => {
vi.mocked(listWorktrees).mockClear()
vi.mocked(addWorktree).mockClear()
const created = {
path: '/remote/mobile-feature',
path: '/remote/repo-mobile-feature',
head: 'def',
branch: 'refs/heads/mobile-feature',
isBare: false,
@ -4740,7 +4740,7 @@ describe('OrcaRuntimeService', () => {
expect(provider.addWorktree).toHaveBeenCalledWith(
'/remote/repo',
'mobile-feature',
'/remote/mobile-feature',
'/remote/repo-mobile-feature',
{ base: 'origin/main' }
)
expect(result.worktree).toMatchObject({