Import linked worktrees under one project (#6441)

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Brennan Benson 2026-06-26 13:26:58 -07:00 committed by GitHub
parent 996ab4a224
commit 4a98b11e77
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 471 additions and 14 deletions

View File

@ -21,6 +21,7 @@ vi.mock('./runner', () => ({
import {
addSparseWorktree,
addWorktree,
listWorktreeGraph,
moveWorktree,
parseWorktreeList,
removeWorktree
@ -235,6 +236,49 @@ bare
})
})
describe('listWorktreeGraph', () => {
it('returns the worktree graph without sparse-checkout annotation probes', async () => {
gitExecFileAsyncMock.mockResolvedValueOnce({
stdout: `worktree /repo
HEAD abc123
branch refs/heads/main
worktree /repo-feature
HEAD def456
branch refs/heads/feature/test
`
})
await expect(listWorktreeGraph('/repo')).resolves.toEqual([
{
path: '/repo',
head: 'abc123',
branch: 'refs/heads/main',
isBare: false,
isMainWorktree: true
},
{
path: '/repo-feature',
head: 'def456',
branch: 'refs/heads/feature/test',
isBare: false,
isMainWorktree: false
}
])
expect(gitExecFileAsyncMock).toHaveBeenCalledTimes(1)
expect(gitExecFileAsyncMock).toHaveBeenCalledWith(['worktree', 'list', '--porcelain', '-z'], {
cwd: '/repo'
})
})
it('returns an empty graph for paths Git reports as non-repositories', async () => {
gitExecFileAsyncMock.mockRejectedValueOnce(new Error('fatal: not a git repository'))
await expect(listWorktreeGraph('/not-a-repo')).resolves.toEqual([])
})
})
describe('addWorktree', () => {
const resolveRemoteBase = () => {
gitExecFileAsyncMock.mockResolvedValueOnce({ stdout: 'abc123\n' }) // rev-parse refs/remotes/origin/main^{commit}

View File

@ -457,6 +457,41 @@ async function readWorktreeList(
return parseWorktreeList(stdout)
}
async function readTranslatedWorktreeGraph(
repoPath: string,
options: GitWorktreeExecOptions = {}
): Promise<GitWorktreeInfo[]> {
return (await readWorktreeList(repoPath, options)).map((worktree) => {
const translatedPath = translateWorktreePath(worktree.path, repoPath, options)
return translatedPath === worktree.path ? worktree : { ...worktree, path: translatedPath }
})
}
export async function listWorktreeGraph(
repoPath: string,
options: GitWorktreeExecOptions = {}
): Promise<GitWorktreeInfo[]> {
try {
return await readTranslatedWorktreeGraph(repoPath, options)
} catch (err) {
if (getErrorCode(err) === 'ENOENT') {
try {
await stat(repoPath)
} catch (statErr) {
if (getErrorCode(statErr) === 'ENOENT') {
console.warn(`[git/worktree] repo path missing; skipping worktree list: ${repoPath}`)
return []
}
}
}
if (isNotGitRepositoryError(err)) {
return []
}
console.warn(`[git/worktree] listWorktreeGraph failed for ${repoPath}:`, err)
return []
}
}
/**
* List all worktrees for a git repo at the given path.
*/
@ -465,10 +500,7 @@ export async function listWorktrees(
options: GitWorktreeExecOptions = {}
): Promise<GitWorktreeInfo[]> {
try {
const worktrees = (await readWorktreeList(repoPath, options)).map((worktree) => {
const translatedPath = translateWorktreePath(worktree.path, repoPath, options)
return translatedPath === worktree.path ? worktree : { ...worktree, path: translatedPath }
})
const worktrees = await readTranslatedWorktreeGraph(repoPath, options)
return annotateSparseCheckoutStatus(worktrees)
} catch (err) {
if (getErrorCode(err) === 'ENOENT') {

View File

@ -20,6 +20,7 @@ const {
mockFilesystemProvider,
mockMultiplexer,
gitSpawnMock,
listWorktreeGraphMock,
invalidateAuthorizedRootsCacheMock,
prepareLocalWorktreeRootForRepoMock
} = vi.hoisted(() => ({
@ -45,6 +46,7 @@ const {
isGitRepoAsync: vi.fn().mockResolvedValue({ isRepo: true, rootPath: null }),
exec: vi.fn().mockResolvedValue({ stdout: '', stderr: '' }),
clone: vi.fn().mockResolvedValue({ stdout: '', stderr: '' }),
listWorktrees: vi.fn().mockResolvedValue([]),
getHostPlatform: vi.fn().mockReturnValue({
relayPlatform: 'linux-x64',
os: 'linux',
@ -68,6 +70,7 @@ const {
notify: vi.fn()
},
gitSpawnMock: vi.fn(),
listWorktreeGraphMock: vi.fn(),
invalidateAuthorizedRootsCacheMock: vi.fn(),
prepareLocalWorktreeRootForRepoMock: vi.fn()
}))
@ -103,6 +106,10 @@ vi.mock('../git/runner', () => ({
gitSpawn: gitSpawnMock
}))
vi.mock('../git/worktree', () => ({
listWorktreeGraph: listWorktreeGraphMock
}))
vi.mock('./filesystem-auth', () => ({
invalidateAuthorizedRootsCache: invalidateAuthorizedRootsCacheMock
}))
@ -158,6 +165,7 @@ describe('projectGroups IPC validation', () => {
mockStore.updateProjectGroup.mockReset()
mockStore.deleteProjectGroup.mockReset()
mockStore.moveProjectToGroup.mockReset()
mockStore.addRepo.mockReset()
mockStore.getProjects.mockReset().mockReturnValue([])
mockStore.getProjectHostSetups.mockReset().mockReturnValue([])
mockStore.updateProjectHostSetup.mockReset()
@ -171,6 +179,10 @@ describe('projectGroups IPC validation', () => {
mockFilesystemProvider.stat.mockRejectedValue(new Error('not found'))
mockGitProvider.isGitRepoAsync.mockReset()
mockGitProvider.isGitRepoAsync.mockResolvedValue({ isRepo: true, rootPath: null })
mockGitProvider.listWorktrees.mockReset()
mockGitProvider.listWorktrees.mockResolvedValue([])
listWorktreeGraphMock.mockReset()
listWorktreeGraphMock.mockResolvedValue([])
vi.mocked(isGitRepo).mockReset()
vi.mocked(isGitRepo).mockReturnValue(true)
mockMultiplexer.notify.mockReset()
@ -643,6 +655,75 @@ describe('projectGroups IPC validation', () => {
})
})
it('resolves SSH linked worktree imports through the SSH provider worktree graph', async () => {
const selectedPath = '/srv/platform/demo/brash-binder'
const secondSelectedPath = '/srv/platform/demo/quick-howler'
const mainPath = '/srv/source/demo-project'
mockGitProvider.isGitRepoAsync.mockImplementation(async (path: string) => ({
isRepo: path === selectedPath || path === secondSelectedPath,
rootPath: '/srv/provider/root'
}))
mockGitProvider.listWorktrees.mockResolvedValue([
{
path: mainPath,
head: 'main-head',
branch: 'refs/heads/main',
isBare: false,
isMainWorktree: true
},
{
path: selectedPath,
head: 'feature-head',
branch: 'refs/heads/brash-binder',
isBare: false,
isMainWorktree: false
},
{
path: secondSelectedPath,
head: 'feature-head',
branch: 'refs/heads/quick-howler',
isBare: false,
isMainWorktree: false
}
])
mockFilesystemProvider.stat.mockImplementation(async (path: string) => {
if (path === `${selectedPath}/.git` || path === `${secondSelectedPath}/.git`) {
return { type: 'directory', size: 0, mtime: 0 }
}
throw new Error('not found')
})
mockFilesystemProvider.readDir.mockImplementation(async (dirPath: string) =>
dirPath === '/srv/platform/demo'
? [
{ name: 'brash-binder', isDirectory: true, isSymlink: false },
{ name: 'quick-howler', isDirectory: true, isSymlink: false }
]
: []
)
const result = await handlers.get('projectGroups:importNested')!(null, {
parentPath: '/srv/platform/demo',
groupName: '',
projectPaths: [selectedPath, secondSelectedPath],
connectionId: 'conn-1',
mode: 'separate'
})
expect(result).toMatchObject({ importedCount: 1, alreadyKnownCount: 1, failedCount: 0 })
expect(mockGitProvider.listWorktrees).toHaveBeenCalledWith(selectedPath)
expect(mockGitProvider.listWorktrees).toHaveBeenCalledTimes(1)
expect(listWorktreeGraphMock).not.toHaveBeenCalled()
expect(mockStore.addRepo).toHaveBeenCalledWith(
expect.objectContaining({
path: mainPath,
connectionId: 'conn-1'
})
)
expect(mockMultiplexer.notify).toHaveBeenCalledWith('session.registerRoot', {
rootPath: mainPath
})
})
it('imports a small selection from a large nested SSH scan', async () => {
const group = {
id: 'group-1',
@ -709,6 +790,67 @@ describe('projectGroups IPC validation', () => {
)
})
it('imports selected local linked worktrees as one project rooted at the main worktree', async () => {
const tempRoot = await mkdtemp(join(tmpdir(), 'orca-nested-linked-worktrees-'))
try {
const parentPath = join(tempRoot, 'paseo-worktrees', 'demo-project')
const mainPath = join(tempRoot, 'source', 'demo-project')
const firstWorktreePath = join(parentPath, 'brash-binder')
const secondWorktreePath = join(parentPath, 'quick-howler')
await mkdir(join(firstWorktreePath, '.git'), { recursive: true })
await mkdir(join(secondWorktreePath, '.git'), { recursive: true })
await mkdir(mainPath, { recursive: true })
vi.mocked(isGitRepo).mockReturnValue(false)
vi.mocked(isGitRepo).mockImplementation((path: string) =>
[firstWorktreePath, secondWorktreePath, mainPath].includes(path)
)
listWorktreeGraphMock.mockResolvedValue([
{
path: mainPath,
head: 'main-head',
branch: 'refs/heads/main',
isBare: false,
isMainWorktree: true
},
{
path: firstWorktreePath,
head: 'feature-head',
branch: 'refs/heads/brash-binder',
isBare: false,
isMainWorktree: false
},
{
path: secondWorktreePath,
head: 'feature-head',
branch: 'refs/heads/quick-howler',
isBare: false,
isMainWorktree: false
}
])
const result = await handlers.get('projectGroups:importNested')!(null, {
parentPath,
groupName: '',
projectPaths: [firstWorktreePath, secondWorktreePath],
mode: 'separate'
})
expect(result).toMatchObject({
importedCount: 1,
alreadyKnownCount: 1,
failedCount: 0
})
expect(mockStore.addRepo).toHaveBeenCalledTimes(1)
expect(mockStore.addRepo).toHaveBeenCalledWith(expect.objectContaining({ path: mainPath }))
expect(listWorktreeGraphMock).toHaveBeenCalledTimes(1)
expect((result as { projects: { projectId?: string }[] }).projects[0].projectId).toBe(
(result as { projects: { projectId?: string }[] }).projects[1].projectId
)
} finally {
await rm(tempRoot, { recursive: true, force: true })
}
})
it('sanitizes unexpected nested import errors before returning results', async () => {
const group = {
id: 'group-1',

View File

@ -57,6 +57,7 @@ import {
createNestedProjectGroupResolver,
resolveNestedRepoSelection
} from '../project-groups/nested-repo-import'
import { createNestedRepoImportTargetResolver } from '../project-groups/nested-repo-import-target'
import {
isGitRepo,
getGitUsername,
@ -1498,12 +1499,16 @@ export function registerRepoHandlers(mainWindow: BrowserWindow, store: Store): v
error: 'Repository was not found in the nested repo scan result'
})
)
const importedProjectIdsByRepoPath = new Map<string, string>()
const importTargetResolver = createNestedRepoImportTargetResolver()
for (const [projectGroupOrder, repoPath] of selection.selectedPaths.entries()) {
try {
let importRepoPath = repoPath
if (args.connectionId) {
const gitProvider = getSshGitProvider(args.connectionId)
if (!gitProvider || !(await gitProvider.isGitRepoAsync(repoPath)).isRepo) {
const check = gitProvider ? await gitProvider.isGitRepoAsync(repoPath) : null
if (!gitProvider || !check?.isRepo) {
results.push({
path: repoPath,
status: 'failed',
@ -1511,35 +1516,49 @@ export function registerRepoHandlers(mainWindow: BrowserWindow, store: Store): v
})
continue
}
importRepoPath = await importTargetResolver.resolveSsh(repoPath, gitProvider)
} else if (!isGitRepo(repoPath)) {
results.push({ path: repoPath, status: 'failed', error: 'Not a valid git repository' })
continue
} else {
importRepoPath = await importTargetResolver.resolveLocal(repoPath)
}
const normalizedImportRepoPath = normalizeRuntimePathForComparison(importRepoPath)
const alreadyImportedProjectId =
importedProjectIdsByRepoPath.get(normalizedImportRepoPath)
if (alreadyImportedProjectId) {
results.push({
path: repoPath,
projectId: alreadyImportedProjectId,
status: 'already-known'
})
continue
}
const existing = store
.getRepos()
.find(
(repo) =>
(repo.connectionId ?? null) === (args.connectionId ?? null) &&
normalizeRuntimePathForComparison(repo.path) ===
normalizeRuntimePathForComparison(repoPath)
normalizeRuntimePathForComparison(repo.path) === normalizedImportRepoPath
)
const group = groupResolver.getGroupForRepo(repoPath)
if (existing) {
if (group) {
store.moveProjectToGroup(existing.id, group.id, projectGroupOrder)
}
importedProjectIdsByRepoPath.set(normalizedImportRepoPath, existing.id)
results.push({ path: repoPath, projectId: existing.id, status: 'already-known' })
continue
}
const detected = await detectRepoIconAndUpstream({
repoPath,
repoPath: importRepoPath,
kind: 'git',
connectionId: args.connectionId
})
const repo: Repo = {
id: randomUUID(),
path: repoPath,
displayName: getRepoName(repoPath),
path: importRepoPath,
displayName: getRepoName(importRepoPath),
badgeColor: DEFAULT_REPO_BADGE_COLOR,
...detected,
addedAt: Date.now(),
@ -1559,9 +1578,10 @@ export function registerRepoHandlers(mainWindow: BrowserWindow, store: Store): v
await prepareLocalWorktreeRootForRepo(store, repo)
if (args.connectionId) {
getActiveMultiplexer(args.connectionId)?.notify('session.registerRoot', {
rootPath: repoPath
rootPath: importRepoPath
})
}
importedProjectIdsByRepoPath.set(normalizedImportRepoPath, repo.id)
results.push({ path: repoPath, projectId: repo.id, status: 'imported' })
// Why: nested-repo import only reaches here after the isGitRepo /
// isGitRepoAsync guard above confirmed a git repo, so always `true`.

View File

@ -0,0 +1,116 @@
import { describe, expect, it, vi, beforeEach } from 'vitest'
import { join } from 'path'
import type { GitWorktreeInfo } from '../../shared/types'
import { listWorktreeGraph } from '../git/worktree'
import {
createNestedRepoImportTargetResolver,
resolveLocalNestedRepoImportTargetPath,
resolveSshNestedRepoImportTargetPath
} from './nested-repo-import-target'
vi.mock('../git/worktree', () => ({
listWorktreeGraph: vi.fn()
}))
function worktree(overrides: Partial<GitWorktreeInfo>): GitWorktreeInfo {
return {
path: join('/workspace', 'repo'),
head: 'abc',
branch: 'refs/heads/main',
isBare: false,
isMainWorktree: false,
...overrides
}
}
describe('nested repo import target resolution', () => {
beforeEach(() => {
vi.mocked(listWorktreeGraph).mockReset()
})
it('canonicalizes a selected linked worktree to its non-bare main worktree', async () => {
const mainPath = join('/workspace', 'source', 'demo')
const selectedPath = `${join('/workspace', 'paseo', 'demo', 'brash-binder')}/`
const selectedPathInGraph = join('/workspace', 'paseo', 'demo', 'brash-binder')
vi.mocked(listWorktreeGraph).mockResolvedValue([
worktree({ path: mainPath, isMainWorktree: true }),
worktree({ path: selectedPathInGraph, branch: 'refs/heads/brash-binder' })
])
await expect(resolveLocalNestedRepoImportTargetPath(selectedPath)).resolves.toBe(mainPath)
expect(listWorktreeGraph).toHaveBeenCalledWith(selectedPath)
})
it('warms the local resolver cache for sibling worktrees in the same graph', async () => {
const mainPath = join('/workspace', 'source', 'demo')
const firstPath = join('/workspace', 'paseo', 'demo', 'brash-binder')
const secondPath = join('/workspace', 'paseo', 'demo', 'quick-howler')
vi.mocked(listWorktreeGraph).mockResolvedValue([
worktree({ path: mainPath, isMainWorktree: true }),
worktree({ path: firstPath, branch: 'refs/heads/brash-binder' }),
worktree({ path: secondPath, branch: 'refs/heads/quick-howler' })
])
const resolver = createNestedRepoImportTargetResolver()
await expect(resolver.resolveLocal(firstPath)).resolves.toBe(mainPath)
await expect(resolver.resolveLocal(secondPath)).resolves.toBe(mainPath)
expect(listWorktreeGraph).toHaveBeenCalledTimes(1)
})
it('falls back when the worktree list is empty', async () => {
const selectedPath = join('/workspace', 'paseo', 'demo', 'brash-binder')
vi.mocked(listWorktreeGraph).mockResolvedValue([])
await expect(resolveLocalNestedRepoImportTargetPath(selectedPath)).resolves.toBe(selectedPath)
})
it('falls back when the worktree lister throws', async () => {
const selectedPath = join('/workspace', 'paseo', 'demo', 'brash-binder')
vi.mocked(listWorktreeGraph).mockRejectedValue(new Error('git failed'))
await expect(resolveLocalNestedRepoImportTargetPath(selectedPath)).resolves.toBe(selectedPath)
})
it('falls back when Git returns a stale graph that omits the selected path', async () => {
const selectedPath = join('/workspace', 'paseo', 'demo', 'brash-binder')
vi.mocked(listWorktreeGraph).mockResolvedValue([
worktree({ path: join('/other', 'source', 'demo'), isMainWorktree: true }),
worktree({ path: join('/other', 'linked', 'quick-howler') })
])
await expect(resolveLocalNestedRepoImportTargetPath(selectedPath)).resolves.toBe(selectedPath)
})
it('falls back when the only main worktree is bare', async () => {
const selectedPath = join('/workspace', 'paseo', 'demo', 'brash-binder')
vi.mocked(listWorktreeGraph).mockResolvedValue([
worktree({
path: join('/workspace', 'source', 'demo.git'),
isBare: true,
isMainWorktree: true
}),
worktree({ path: selectedPath, branch: 'refs/heads/brash-binder' })
])
await expect(resolveLocalNestedRepoImportTargetPath(selectedPath)).resolves.toBe(selectedPath)
})
it('uses the SSH provider worktree list for remote import targets', async () => {
const mainPath = join('/srv', 'source', 'demo')
const selectedPath = join('/srv', 'paseo', 'demo', 'brash-binder')
const gitProvider = {
listWorktrees: vi
.fn()
.mockResolvedValue([
worktree({ path: mainPath, isMainWorktree: true }),
worktree({ path: selectedPath, branch: 'refs/heads/brash-binder' })
])
}
await expect(resolveSshNestedRepoImportTargetPath(selectedPath, gitProvider)).resolves.toBe(
mainPath
)
expect(gitProvider.listWorktrees).toHaveBeenCalledWith(selectedPath)
expect(listWorktreeGraph).not.toHaveBeenCalled()
})
})

View File

@ -0,0 +1,87 @@
import type { GitWorktreeInfo } from '../../shared/types'
import { normalizeRuntimePathForComparison } from '../../shared/cross-platform-path'
import { listWorktreeGraph } from '../git/worktree'
type WorktreeLister = {
listWorktrees: (repoPath: string) => Promise<GitWorktreeInfo[]>
}
export type NestedRepoImportTargetResolver = {
resolveLocal: (repoPath: string) => Promise<string>
resolveSsh: (repoPath: string, gitProvider: WorktreeLister) => Promise<string>
}
function findImportTarget(
selectedPath: string,
worktrees: readonly GitWorktreeInfo[]
): { targetPath: string; graphPaths: string[] } | null {
const selectedPathKey = normalizeRuntimePathForComparison(selectedPath)
const graphContainsSelectedPath = worktrees.some(
(worktree) => normalizeRuntimePathForComparison(worktree.path) === selectedPathKey
)
if (!graphContainsSelectedPath) {
return null
}
// Why: a linked worktree may only collapse to its owner when Git proves the
// selected path belongs to that same non-bare worktree graph.
const mainWorktree = worktrees.find((worktree) => worktree.isMainWorktree && !worktree.isBare)
return mainWorktree
? { targetPath: mainWorktree.path, graphPaths: worktrees.map((worktree) => worktree.path) }
: null
}
async function resolveWithCache(
repoPath: string,
cache: Map<string, string>,
readWorktreeGraph: (path: string) => Promise<GitWorktreeInfo[]>
): Promise<string> {
const repoPathKey = normalizeRuntimePathForComparison(repoPath)
const cachedPath = cache.get(repoPathKey)
if (cachedPath) {
return cachedPath
}
try {
const target = findImportTarget(repoPath, await readWorktreeGraph(repoPath))
if (target) {
for (const graphPath of target.graphPaths) {
cache.set(normalizeRuntimePathForComparison(graphPath), target.targetPath)
}
return target.targetPath
}
} catch {
// Fall through to selected-path compatibility behavior.
}
cache.set(repoPathKey, repoPath)
return repoPath
}
export function createNestedRepoImportTargetResolver(): NestedRepoImportTargetResolver {
const localCache = new Map<string, string>()
const sshCaches = new WeakMap<WorktreeLister, Map<string, string>>()
return {
resolveLocal: (repoPath) =>
resolveWithCache(repoPath, localCache, (path) => listWorktreeGraph(path)),
resolveSsh: (repoPath, gitProvider) => {
let cache = sshCaches.get(gitProvider)
if (!cache) {
cache = new Map()
sshCaches.set(gitProvider, cache)
}
return resolveWithCache(repoPath, cache, (path) => gitProvider.listWorktrees(path))
}
}
}
export async function resolveLocalNestedRepoImportTargetPath(repoPath: string): Promise<string> {
return createNestedRepoImportTargetResolver().resolveLocal(repoPath)
}
export async function resolveSshNestedRepoImportTargetPath(
repoPath: string,
gitProvider: WorktreeLister
): Promise<string> {
return createNestedRepoImportTargetResolver().resolveSsh(repoPath, gitProvider)
}

View File

@ -663,6 +663,7 @@ import {
createNestedProjectGroupResolver,
resolveNestedRepoSelection
} from '../project-groups/nested-repo-import'
import { createNestedRepoImportTargetResolver } from '../project-groups/nested-repo-import-target'
function sanitizeNestedRepoRuntimeImportError(context: string, error: unknown): string {
console.warn(`[project-groups] ${context}`, error)
@ -9288,27 +9289,41 @@ export class OrcaRuntimeService {
error: 'Repository was not found in the nested repo scan result'
})
)
const importedProjectIdsByRepoPath = new Map<string, string>()
const importTargetResolver = createNestedRepoImportTargetResolver()
for (const [projectGroupOrder, repoPath] of selection.selectedPaths.entries()) {
try {
if (!isGitRepo(repoPath)) {
results.push({ path: repoPath, status: 'failed', error: 'Not a valid git repository' })
continue
}
const importRepoPath = await importTargetResolver.resolveLocal(repoPath)
const normalizedImportRepoPath = normalizeRuntimePathForComparison(importRepoPath)
const alreadyImportedProjectId = importedProjectIdsByRepoPath.get(normalizedImportRepoPath)
if (alreadyImportedProjectId) {
results.push({
path: repoPath,
projectId: alreadyImportedProjectId,
status: 'already-known'
})
continue
}
const existing = this.store
.getRepos()
.find((repo) => runtimePathsEqual(repo.path, repoPath))
.find((repo) => normalizeRuntimePathForComparison(repo.path) === normalizedImportRepoPath)
const group = groupResolver.getGroupForRepo(repoPath)
if (existing) {
if (group) {
this.store.moveProjectToGroup(existing.id, group.id, projectGroupOrder)
}
importedProjectIdsByRepoPath.set(normalizedImportRepoPath, existing.id)
results.push({ path: repoPath, projectId: existing.id, status: 'already-known' })
continue
}
const repo: Repo = {
id: randomUUID(),
path: repoPath,
displayName: getRepoName(repoPath),
path: importRepoPath,
displayName: getRepoName(importRepoPath),
badgeColor: DEFAULT_REPO_BADGE_COLOR,
addedAt: Date.now(),
kind: 'git',
@ -9322,6 +9337,7 @@ export class OrcaRuntimeService {
: {})
}
this.store.addRepo(repo)
importedProjectIdsByRepoPath.set(normalizedImportRepoPath, repo.id)
results.push({ path: repoPath, projectId: repo.id, status: 'imported' })
} catch (error) {
results.push({