mirror of
https://github.com/stablyai/orca.git
synced 2026-10-22 00:03:47 +00:00
Persist project host setup method
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
+12
-3
@@ -99,7 +99,8 @@ function buildProjectHostSetupResult(store: Store, repo: Repo): ProjectHostSetup
|
||||
function alignRepoWithRequestedProject(
|
||||
store: Store,
|
||||
repo: Repo,
|
||||
projectId: string
|
||||
projectId: string,
|
||||
setupMethod: ProjectHostSetupExistingFolderArgs['setupMethod'] = 'imported-existing-folder'
|
||||
): ProjectHostSetupResult {
|
||||
let setup = getProjectHostSetupForRepo(store.getProjectHostSetups(), repo)
|
||||
if (setup.projectId !== projectId) {
|
||||
@@ -122,6 +123,13 @@ function alignRepoWithRequestedProject(
|
||||
repo = updated
|
||||
setup = getProjectHostSetupForRepo(store.getProjectHostSetups(), repo)
|
||||
}
|
||||
const updated = store.updateRepo(repo.id, { projectHostSetupMethod: setupMethod })
|
||||
if (!updated) {
|
||||
throw new Error(
|
||||
`Project setup repo disappeared before setup metadata could be linked: ${repo.id}`
|
||||
)
|
||||
}
|
||||
repo = updated
|
||||
return buildProjectHostSetupResult(store, repo)
|
||||
}
|
||||
|
||||
@@ -329,7 +337,8 @@ const ProjectHostSetupExistingFolderIpcArgs = z.object({
|
||||
hostId: z.string().min(1),
|
||||
path: z.string().min(1),
|
||||
kind: z.enum(['git', 'folder']).optional(),
|
||||
displayName: z.string().min(1).optional()
|
||||
displayName: z.string().min(1).optional(),
|
||||
setupMethod: z.enum(['imported-existing-folder', 'cloned']).optional()
|
||||
})
|
||||
|
||||
const ProjectGroupScanNestedArgs = z.object({
|
||||
@@ -691,7 +700,7 @@ export function registerRepoHandlers(mainWindow: BrowserWindow, store: Store): v
|
||||
invalidateAuthorizedRootsCache()
|
||||
notifyReposChanged(mainWindow)
|
||||
emitRepoAdded('folder_picker', result.alreadyExisted)
|
||||
return alignRepoWithRequestedProject(store, result.repo, args.projectId)
|
||||
return alignRepoWithRequestedProject(store, result.repo, args.projectId, args.setupMethod)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -2329,6 +2329,21 @@ describe('Store', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('updateRepo preserves repo-backed project host setup method', async () => {
|
||||
const store = await createStore()
|
||||
store.addRepo(makeRepo())
|
||||
|
||||
store.updateRepo('r1', { projectHostSetupMethod: 'cloned' })
|
||||
|
||||
expect(store.getRepo('r1')?.projectHostSetupMethod).toBe('cloned')
|
||||
expect(store.getProjectHostSetups()).toEqual([
|
||||
expect.objectContaining({
|
||||
id: 'r1',
|
||||
setupMethod: 'cloned'
|
||||
})
|
||||
])
|
||||
})
|
||||
|
||||
it('updateRepo drops repo icons that fail shared sanitization', async () => {
|
||||
const store = await createStore()
|
||||
store.addRepo(makeRepo())
|
||||
|
||||
+25
-1
@@ -36,6 +36,7 @@ import type {
|
||||
PersistedState,
|
||||
Project,
|
||||
ProjectHostSetup,
|
||||
ProjectHostSetupMethod,
|
||||
Repo,
|
||||
ProjectGroup,
|
||||
SparsePreset,
|
||||
@@ -741,8 +742,19 @@ function sanitizeRepoUpstream(value: unknown): Repo['upstream'] | undefined {
|
||||
return owner && repo ? { owner, repo } : undefined
|
||||
}
|
||||
|
||||
function sanitizeRepoProjectHostSetupMethod(
|
||||
value: unknown
|
||||
): Exclude<ProjectHostSetupMethod, 'legacy-repo'> | undefined {
|
||||
return value === 'imported-existing-folder' || value === 'cloned' ? value : undefined
|
||||
}
|
||||
|
||||
function sanitizeRepoUpdatesForPersistence<
|
||||
T extends Partial<Pick<Repo, 'badgeColor' | 'repoIcon' | 'upstream' | 'worktreeBasePath'>>
|
||||
T extends Partial<
|
||||
Pick<
|
||||
Repo,
|
||||
'badgeColor' | 'repoIcon' | 'upstream' | 'worktreeBasePath' | 'projectHostSetupMethod'
|
||||
>
|
||||
>
|
||||
>(updates: T): T {
|
||||
const sanitized = { ...updates }
|
||||
if ('badgeColor' in sanitized) {
|
||||
@@ -777,6 +789,14 @@ function sanitizeRepoUpdatesForPersistence<
|
||||
delete sanitized.worktreeBasePath
|
||||
}
|
||||
}
|
||||
if ('projectHostSetupMethod' in sanitized) {
|
||||
const setupMethod = sanitizeRepoProjectHostSetupMethod(sanitized.projectHostSetupMethod)
|
||||
if (setupMethod === undefined) {
|
||||
delete sanitized.projectHostSetupMethod
|
||||
} else {
|
||||
sanitized.projectHostSetupMethod = setupMethod
|
||||
}
|
||||
}
|
||||
return sanitized
|
||||
}
|
||||
|
||||
@@ -2696,6 +2716,7 @@ export class Store {
|
||||
| 'externalWorktreeVisibilityPromptDismissedAt'
|
||||
| 'projectGroupId'
|
||||
| 'projectGroupOrder'
|
||||
| 'projectHostSetupMethod'
|
||||
>
|
||||
> & { sourceControlAi?: Repo['sourceControlAi'] | null }
|
||||
): Repo | null {
|
||||
@@ -2780,11 +2801,13 @@ export class Store {
|
||||
repoIcon: rawRepoIcon,
|
||||
upstream: rawUpstream,
|
||||
sourceControlAi: rawSourceControlAi,
|
||||
projectHostSetupMethod: rawProjectHostSetupMethod,
|
||||
...repoWithoutIcon
|
||||
} = repo
|
||||
const repoIcon = sanitizeRepoIcon(rawRepoIcon)
|
||||
const upstream = sanitizeRepoUpstream(rawUpstream)
|
||||
const sourceControlAi = normalizeRepoSourceControlAiOverrides(rawSourceControlAi)
|
||||
const projectHostSetupMethod = sanitizeRepoProjectHostSetupMethod(rawProjectHostSetupMethod)
|
||||
const gitUsername = isFolderRepo(repo)
|
||||
? ''
|
||||
: (this.gitUsernameCache.get(repo.path) ??
|
||||
@@ -2799,6 +2822,7 @@ export class Store {
|
||||
...(repoIcon !== undefined ? { repoIcon } : {}),
|
||||
...(upstream !== undefined ? { upstream } : {}),
|
||||
...(sourceControlAi !== undefined ? { sourceControlAi } : {}),
|
||||
...(projectHostSetupMethod !== undefined ? { projectHostSetupMethod } : {}),
|
||||
kind: isFolderRepo(repo) ? 'folder' : 'git',
|
||||
gitUsername,
|
||||
hookSettings: {
|
||||
|
||||
@@ -6484,6 +6484,15 @@ export class OrcaRuntimeService {
|
||||
repo = updated
|
||||
setup = getProjectHostSetupForRepo(this.listProjectHostSetups(), repo)
|
||||
}
|
||||
const setupMethod = args.setupMethod ?? 'imported-existing-folder'
|
||||
const updated = this.store.updateRepo(repo.id, { projectHostSetupMethod: setupMethod })
|
||||
if (!updated) {
|
||||
throw new Error(
|
||||
`Project setup repo disappeared before setup metadata could be linked: ${repo.id}`
|
||||
)
|
||||
}
|
||||
repo = updated
|
||||
setup = getProjectHostSetupForRepo(this.listProjectHostSetups(), repo)
|
||||
const project = this.listProjects().find((entry) => entry.id === setup.projectId)
|
||||
if (!project) {
|
||||
throw new Error(`Project setup was created without a project record: ${setup.projectId}`)
|
||||
|
||||
@@ -15,7 +15,8 @@ const ProjectHostSetupExistingFolder = z.object({
|
||||
}),
|
||||
path: requiredString('Missing project path'),
|
||||
kind: z.enum(['git', 'folder']).optional(),
|
||||
displayName: OptionalString
|
||||
displayName: OptionalString,
|
||||
setupMethod: z.enum(['imported-existing-folder', 'cloned']).optional()
|
||||
})
|
||||
|
||||
export const PROJECT_RUNTIME_METHODS: RpcMethod[] = [
|
||||
|
||||
@@ -388,7 +388,8 @@ describe('repo RPC methods', () => {
|
||||
projectId: 'project-1',
|
||||
hostId: 'runtime:env-1',
|
||||
path: '/repo',
|
||||
kind: 'git'
|
||||
kind: 'git',
|
||||
setupMethod: 'cloned'
|
||||
})
|
||||
)
|
||||
|
||||
@@ -396,7 +397,8 @@ describe('repo RPC methods', () => {
|
||||
projectId: 'project-1',
|
||||
hostId: 'runtime:env-1',
|
||||
path: '/repo',
|
||||
kind: 'git'
|
||||
kind: 'git',
|
||||
setupMethod: 'cloned'
|
||||
})
|
||||
expect(response).toMatchObject({ ok: true, result: { result } })
|
||||
})
|
||||
|
||||
@@ -431,7 +431,7 @@ describe('repo slice runtime routing', () => {
|
||||
path: clonedRepo.path,
|
||||
displayName: clonedRepo.displayName,
|
||||
setupState: 'ready',
|
||||
setupMethod: 'legacy-repo',
|
||||
setupMethod: 'cloned',
|
||||
createdAt: 1,
|
||||
updatedAt: 1
|
||||
}
|
||||
@@ -462,7 +462,8 @@ describe('repo slice runtime routing', () => {
|
||||
hostId: 'local',
|
||||
path: clonedRepo.path,
|
||||
kind: 'git',
|
||||
displayName: 'Project'
|
||||
displayName: 'Project',
|
||||
setupMethod: 'cloned'
|
||||
})
|
||||
})
|
||||
|
||||
@@ -484,7 +485,7 @@ describe('repo slice runtime routing', () => {
|
||||
path: clonedRepo.path,
|
||||
displayName: clonedRepo.displayName,
|
||||
setupState: 'ready',
|
||||
setupMethod: 'legacy-repo',
|
||||
setupMethod: 'cloned',
|
||||
createdAt: 1,
|
||||
updatedAt: 1
|
||||
}
|
||||
@@ -534,7 +535,8 @@ describe('repo slice runtime routing', () => {
|
||||
hostId: 'runtime:env-1',
|
||||
path: clonedRepo.path,
|
||||
kind: 'git',
|
||||
displayName: 'Project'
|
||||
displayName: 'Project',
|
||||
setupMethod: 'cloned'
|
||||
},
|
||||
timeoutMs: 15_000
|
||||
})
|
||||
|
||||
@@ -747,7 +747,8 @@ export const createRepoSlice: StateCreator<AppState, [], [], RepoSlice> = (set,
|
||||
hostId: args.hostId,
|
||||
path: repo.path,
|
||||
kind: 'git',
|
||||
displayName: args.displayName
|
||||
displayName: args.displayName,
|
||||
setupMethod: 'cloned'
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('Failed to clone project on host:', err)
|
||||
|
||||
@@ -71,6 +71,19 @@ describe('project host setup projection', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('preserves repo-backed setup method metadata', () => {
|
||||
const projection = projectHostSetupProjectionFromRepos([
|
||||
repo({
|
||||
id: 'repo-1',
|
||||
path: '/Users/alice/orca',
|
||||
displayName: 'orca',
|
||||
projectHostSetupMethod: 'cloned'
|
||||
})
|
||||
])
|
||||
|
||||
expect(projection.setups[0]?.setupMethod).toBe('cloned')
|
||||
})
|
||||
|
||||
it('groups repo checkouts with the same provider identity under one project', () => {
|
||||
const projection = projectHostSetupProjectionFromRepos([
|
||||
repo({
|
||||
|
||||
@@ -68,6 +68,7 @@ function mergeProjectRepo(project: Project, repo: Repo): Project {
|
||||
function createSetupFromRepo(repo: Repo, projectId: string, now: number): ProjectHostSetup {
|
||||
const hostId = getRepoExecutionHostId(repo)
|
||||
const createdAt = repo.addedAt || now
|
||||
const setupMethod = repo.projectHostSetupMethod ?? 'legacy-repo'
|
||||
return {
|
||||
id: repo.id,
|
||||
projectId,
|
||||
@@ -83,7 +84,7 @@ function createSetupFromRepo(repo: Repo, projectId: string, now: number): Projec
|
||||
...(repo.gitUsername ? { gitUsername: repo.gitUsername } : {}),
|
||||
...(repo.sourceControlAi ? { sourceControlAi: repo.sourceControlAi } : {}),
|
||||
setupState: 'ready',
|
||||
setupMethod: 'legacy-repo',
|
||||
setupMethod,
|
||||
createdAt,
|
||||
updatedAt: createdAt
|
||||
}
|
||||
|
||||
@@ -131,6 +131,7 @@ export type ProjectHostSetupExistingFolderArgs = {
|
||||
path: string
|
||||
kind?: RepoKind
|
||||
displayName?: string
|
||||
setupMethod?: Exclude<ProjectHostSetupMethod, 'legacy-repo'>
|
||||
}
|
||||
|
||||
export type ProjectHostSetupCloneArgs = {
|
||||
@@ -193,6 +194,8 @@ export type Repo = {
|
||||
projectGroupOrder?: number
|
||||
/** Repo-specific source-control AI overrides. Missing fields inherit global settings. */
|
||||
sourceControlAi?: RepoSourceControlAiOverrides
|
||||
/** Transitional source for ProjectHostSetup.setupMethod while Repo remains compatibility storage. */
|
||||
projectHostSetupMethod?: Exclude<ProjectHostSetupMethod, 'legacy-repo'>
|
||||
}
|
||||
|
||||
export type ProjectGroupCreatedFrom = 'manual' | 'folder-scan' | 'migration'
|
||||
|
||||
Reference in New Issue
Block a user