From ebeb12fedcb44257dcb573a07da93e5bb086aeb8 Mon Sep 17 00:00:00 2001 From: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com> Date: Fri, 12 Jun 2026 13:40:51 -0700 Subject: [PATCH] fix: distinguish remote project paths by SSH connection (#5279) Co-authored-by: Orca --- src/main/ipc/repos-remote.test.ts | 59 +++++++++++++++++++++++++++++-- src/main/ipc/repos.ts | 21 +++++++++-- 2 files changed, 74 insertions(+), 6 deletions(-) diff --git a/src/main/ipc/repos-remote.test.ts b/src/main/ipc/repos-remote.test.ts index 7226ebeff..6992d2193 100644 --- a/src/main/ipc/repos-remote.test.ts +++ b/src/main/ipc/repos-remote.test.ts @@ -89,7 +89,7 @@ vi.mock('./filesystem-auth', () => ({ vi.mock('../providers/ssh-git-dispatch', () => ({ getSshGitProvider: vi.fn().mockImplementation((id: string) => { - if (id === 'conn-1') { + if (id === 'conn-1' || id === 'conn-2') { return mockGitProvider } return undefined @@ -98,7 +98,7 @@ vi.mock('../providers/ssh-git-dispatch', () => ({ vi.mock('../providers/ssh-filesystem-dispatch', () => ({ getSshFilesystemProvider: vi.fn().mockImplementation((id: string) => { - if (id === 'conn-1') { + if (id === 'conn-1' || id === 'conn-2') { return mockFilesystemProvider } return undefined @@ -107,7 +107,7 @@ vi.mock('../providers/ssh-filesystem-dispatch', () => ({ vi.mock('./ssh', () => ({ getActiveMultiplexer: vi.fn().mockImplementation((id: string) => { - if (id === 'conn-1') { + if (id === 'conn-1' || id === 'conn-2') { return mockMultiplexer } return undefined @@ -886,6 +886,59 @@ describe('repos:addRemote', () => { expect(mockStore.addRepo).not.toHaveBeenCalled() }) + it('allows the same resolved remote path on a different SSH connection', async () => { + const existing = { + id: 'machine-1-project', + path: '/home/user/project', + connectionId: 'conn-1', + displayName: 'project', + badgeColor: '#fff', + addedAt: 1000, + kind: 'git' + } + mockStore.getRepos.mockReturnValue([existing]) + + const result = await handlers.get('repos:addRemote')!(null, { + connectionId: 'conn-2', + remotePath: '/home/user/project' + }) + + expect(mockStore.addRepo).toHaveBeenCalledWith( + expect.objectContaining({ + path: '/home/user/project', + connectionId: 'conn-2' + }) + ) + expect(result).toHaveProperty('repo.connectionId', 'conn-2') + expect(result).toHaveProperty('repo.id') + expect(result).not.toEqual({ repo: existing }) + }) + + it('dedupes remote projects after git root resolution on the same SSH connection', async () => { + const existing = { + id: 'existing-id', + path: '/home/user/project', + connectionId: 'conn-1', + displayName: 'project', + badgeColor: '#fff', + addedAt: 1000, + kind: 'git' + } + mockStore.getRepos.mockReturnValue([existing]) + mockGitProvider.isGitRepoAsync.mockResolvedValueOnce({ + isRepo: true, + rootPath: '/home/user/project' + }) + + const result = await handlers.get('repos:addRemote')!(null, { + connectionId: 'conn-1', + remotePath: '/home/user/project/src' + }) + + expect(result).toEqual({ repo: existing }) + expect(mockStore.addRepo).not.toHaveBeenCalled() + }) + it('throws when SSH connection is not found', async () => { const result = await handlers.get('repos:addRemote')!(null, { connectionId: 'unknown-conn', diff --git a/src/main/ipc/repos.ts b/src/main/ipc/repos.ts index d0bf84ce9..10f7da381 100644 --- a/src/main/ipc/repos.ts +++ b/src/main/ipc/repos.ts @@ -937,6 +937,16 @@ export function registerRepoHandlers(mainWindow: BrowserWindow, store: Store): v return { error: `SSH connection "${args.connectionId}" not found or not connected` } } + const findExistingRemoteRepo = (path: string): Repo | undefined => + store + .getRepos() + .find( + (repo) => + repo.connectionId === args.connectionId && + normalizeRuntimePathForComparison(repo.path) === + normalizeRuntimePathForComparison(path) + ) + let repoKind: 'git' | 'folder' = args.kind ?? 'git' let resolvedPath = args.remotePath @@ -959,9 +969,7 @@ export function registerRepoHandlers(mainWindow: BrowserWindow, store: Store): v // Why: check for duplicates after tilde resolution so that adding `~/` // when `/home/ubuntu` is already stored correctly detects the duplicate. - const existing = store - .getRepos() - .find((r) => r.connectionId === args.connectionId && r.path === resolvedPath) + const existing = findExistingRemoteRepo(resolvedPath) if (existing) { // Why: duplicate hit is suppressed by `emitRepoAdded` anyway, and for // remote adds git-ness isn't resolved until the isGitRepoAsync check @@ -982,6 +990,13 @@ export function registerRepoHandlers(mainWindow: BrowserWindow, store: Store): v if (check.rootPath) { resolvedPath = check.rootPath } + const existingAfterRootResolution = findExistingRemoteRepo(resolvedPath) + if (existingAfterRootResolution) { + // Why: users may browse inside a repo; store identity is the Git root, + // but different SSH targets can legitimately share that same path. + emitRepoAdded('folder_picker', true, true) + return { repo: existingAfterRootResolution } + } } else { return { error: `Not a valid git repository: ${args.remotePath}` } }