perf: unblock open_project from gh pr status fetch
Placement only ever needed local git data; fetching GitHub PR status was incidental coupling through getSnapshot, blocking the response ~5s with no feedback in the desktop app. Split it: getCheckout for placement (local only), getSnapshot for the watch path. classify is now a pure function over a checkout. The observer emits the full gitRuntime/githubRuntime via workspace_update when the background snapshot lands.
This commit is contained in:
@@ -1596,12 +1596,8 @@ export class Session {
|
|||||||
return normalizePersistedWorkspaceId(snapshot?.git.repoRoot ?? normalizedCwd);
|
return normalizePersistedWorkspaceId(snapshot?.git.repoRoot ?? normalizedCwd);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const checkout = await this.workspaceGitService.getCheckout(normalizedCwd);
|
||||||
const snapshot = await this.workspaceGitService.getSnapshot(normalizedCwd);
|
return normalizePersistedWorkspaceId(checkout.worktreeRoot ?? normalizedCwd);
|
||||||
return normalizePersistedWorkspaceId(snapshot.git.repoRoot ?? normalizedCwd);
|
|
||||||
} catch {
|
|
||||||
return normalizedCwd;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async buildProjectPlacementForWorkspace(
|
private async buildProjectPlacementForWorkspace(
|
||||||
@@ -6577,10 +6573,8 @@ export class Session {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async createWorkspaceForDirectory(cwd: string): Promise<PersistedWorkspaceRecord> {
|
private async createWorkspaceForDirectory(cwd: string): Promise<PersistedWorkspaceRecord> {
|
||||||
const membership = await classifyDirectoryForProjectMembership({
|
const checkout = await this.workspaceGitService.getCheckout(cwd);
|
||||||
cwd,
|
const membership = classifyDirectoryForProjectMembership({ cwd, checkout });
|
||||||
workspaceGitService: this.workspaceGitService,
|
|
||||||
});
|
|
||||||
const timestamp = new Date().toISOString();
|
const timestamp = new Date().toISOString();
|
||||||
|
|
||||||
const projectRecord = await this.resolveProjectRecordForPlacement({
|
const projectRecord = await this.resolveProjectRecordForPlacement({
|
||||||
@@ -6607,10 +6601,8 @@ export class Session {
|
|||||||
project: PersistedProjectRecord | null;
|
project: PersistedProjectRecord | null;
|
||||||
cwd: string;
|
cwd: string;
|
||||||
}): Promise<PersistedWorkspaceRecord> {
|
}): Promise<PersistedWorkspaceRecord> {
|
||||||
const membership = await classifyDirectoryForProjectMembership({
|
const checkout = await this.workspaceGitService.getCheckout(input.cwd);
|
||||||
cwd: input.cwd,
|
const membership = classifyDirectoryForProjectMembership({ cwd: input.cwd, checkout });
|
||||||
workspaceGitService: this.workspaceGitService,
|
|
||||||
});
|
|
||||||
const timestamp = new Date().toISOString();
|
const timestamp = new Date().toISOString();
|
||||||
const projectRecord = await this.resolveProjectRecordForPlacement({
|
const projectRecord = await this.resolveProjectRecordForPlacement({
|
||||||
membership,
|
membership,
|
||||||
@@ -6646,7 +6638,7 @@ export class Session {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async resolveProjectRecordForPlacement(input: {
|
private async resolveProjectRecordForPlacement(input: {
|
||||||
membership: Awaited<ReturnType<typeof classifyDirectoryForProjectMembership>>;
|
membership: ReturnType<typeof classifyDirectoryForProjectMembership>;
|
||||||
timestamp: string;
|
timestamp: string;
|
||||||
}): Promise<PersistedProjectRecord> {
|
}): Promise<PersistedProjectRecord> {
|
||||||
const rootPath = input.membership.projectRootPath;
|
const rootPath = input.membership.projectRootPath;
|
||||||
@@ -7063,8 +7055,9 @@ export class Session {
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const workspace = await this.findOrCreateWorkspaceForDirectory(request.cwd);
|
const workspace = await this.findOrCreateWorkspaceForDirectory(request.cwd);
|
||||||
|
await this.syncWorkspaceGitObserverForWorkspace(workspace);
|
||||||
|
const descriptor = await this.describeWorkspaceRecord(workspace);
|
||||||
await this.emitWorkspaceUpdateForCwd(workspace.cwd);
|
await this.emitWorkspaceUpdateForCwd(workspace.cwd);
|
||||||
const descriptor = await this.describeWorkspaceRecordWithGitData(workspace);
|
|
||||||
this.emit({
|
this.emit({
|
||||||
type: "open_project_response",
|
type: "open_project_response",
|
||||||
payload: {
|
payload: {
|
||||||
@@ -7073,6 +7066,18 @@ export class Session {
|
|||||||
error: null,
|
error: null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
void this.workspaceGitService
|
||||||
|
.getSnapshot(workspace.cwd, {
|
||||||
|
force: true,
|
||||||
|
includeGitHub: true,
|
||||||
|
reason: "open_project",
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
this.sessionLogger.warn(
|
||||||
|
{ err: error, cwd: workspace.cwd },
|
||||||
|
"Background snapshot refresh failed after open_project",
|
||||||
|
);
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : "Failed to open project";
|
const message = error instanceof Error ? error.message : "Failed to open project";
|
||||||
this.sessionLogger.error({ err: error, cwd: request.cwd }, "Failed to open project");
|
this.sessionLogger.error({ err: error, cwd: request.cwd }, "Failed to open project");
|
||||||
|
|||||||
@@ -65,6 +65,12 @@ interface SessionTestAccess {
|
|||||||
terminalManager: {
|
terminalManager: {
|
||||||
killTerminal(id: string): unknown;
|
killTerminal(id: string): unknown;
|
||||||
} | null;
|
} | null;
|
||||||
|
workspaceGitService: {
|
||||||
|
getCheckout: (cwd: string) => Promise<unknown>;
|
||||||
|
getSnapshot: (cwd: string, options?: unknown) => Promise<WorkspaceGitRuntimeSnapshot>;
|
||||||
|
peekSnapshot: (cwd: string) => WorkspaceGitRuntimeSnapshot | null;
|
||||||
|
registerWorkspace: (params: { cwd: string }, listener: unknown) => { unsubscribe: () => void };
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ListFetchResult {
|
interface ListFetchResult {
|
||||||
@@ -2094,6 +2100,174 @@ test("open_project_request registers a workspace before any agent exists", async
|
|||||||
expect(response?.payload.workspace?.id).toBe("/tmp/repo");
|
expect(response?.payload.workspace?.id).toBe("/tmp/repo");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("open_project_response returns immediately even when the GitHub fetch is slow", async () => {
|
||||||
|
const emitted: Array<{ type: string; payload: unknown }> = [];
|
||||||
|
const session = createSessionForWorkspaceTests();
|
||||||
|
const projects = new Map<string, ReturnType<typeof createPersistedProjectRecord>>();
|
||||||
|
const workspaces = new Map<string, ReturnType<typeof createPersistedWorkspaceRecord>>();
|
||||||
|
const cwd = "/tmp/slow-github-repo";
|
||||||
|
|
||||||
|
session.emit = (message) => emitted.push(message as { type: string; payload: unknown });
|
||||||
|
session.projectRegistry.get = async (projectId: string) => projects.get(projectId) ?? null;
|
||||||
|
session.projectRegistry.upsert = async (
|
||||||
|
record: ReturnType<typeof createPersistedProjectRecord>,
|
||||||
|
) => {
|
||||||
|
projects.set(record.projectId, record);
|
||||||
|
};
|
||||||
|
session.workspaceRegistry.get = async (workspaceId: string) =>
|
||||||
|
workspaces.get(workspaceId) ?? null;
|
||||||
|
session.workspaceRegistry.upsert = async (
|
||||||
|
record: ReturnType<typeof createPersistedWorkspaceRecord>,
|
||||||
|
) => {
|
||||||
|
workspaces.set(record.workspaceId, record);
|
||||||
|
};
|
||||||
|
session.projectRegistry.list = async () => Array.from(projects.values());
|
||||||
|
session.workspaceRegistry.list = async () => Array.from(workspaces.values());
|
||||||
|
session.workspaceGitService.getCheckout = async (requestedCwd: string) => ({
|
||||||
|
cwd: requestedCwd,
|
||||||
|
isGit: true,
|
||||||
|
currentBranch: "main",
|
||||||
|
remoteUrl: "https://github.com/acme/slow.git",
|
||||||
|
worktreeRoot: requestedCwd,
|
||||||
|
isPaseoOwnedWorktree: false,
|
||||||
|
mainRepoRoot: null,
|
||||||
|
});
|
||||||
|
let resolveSnapshot: (snapshot: WorkspaceGitRuntimeSnapshot) => void = () => {};
|
||||||
|
const snapshotPromise = new Promise<WorkspaceGitRuntimeSnapshot>((resolve) => {
|
||||||
|
resolveSnapshot = resolve;
|
||||||
|
});
|
||||||
|
session.workspaceGitService.getSnapshot = (requestedCwd: string) => {
|
||||||
|
void requestedCwd;
|
||||||
|
return snapshotPromise;
|
||||||
|
};
|
||||||
|
|
||||||
|
const start = Date.now();
|
||||||
|
await session.handleMessage({
|
||||||
|
type: "open_project_request",
|
||||||
|
cwd,
|
||||||
|
requestId: "req-slow-github",
|
||||||
|
});
|
||||||
|
const elapsedMs = Date.now() - start;
|
||||||
|
|
||||||
|
expect(elapsedMs).toBeLessThan(500);
|
||||||
|
|
||||||
|
const response = emitted.find((message) => message.type === "open_project_response") as
|
||||||
|
| {
|
||||||
|
payload: {
|
||||||
|
error: unknown;
|
||||||
|
workspace?: {
|
||||||
|
id: string;
|
||||||
|
gitRuntime?: unknown;
|
||||||
|
githubRuntime?: unknown;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
| undefined;
|
||||||
|
expect(response?.payload.error).toBeNull();
|
||||||
|
expect(response?.payload.workspace?.id).toBe(cwd);
|
||||||
|
expect(response?.payload.workspace?.gitRuntime).toBeUndefined();
|
||||||
|
expect(response?.payload.workspace?.githubRuntime).toBeUndefined();
|
||||||
|
|
||||||
|
resolveSnapshot(createWorkspaceRuntimeSnapshot(cwd));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("open_project_request emits a workspace_update with githubRuntime once the snapshot resolves", async () => {
|
||||||
|
const emitted: Array<{ type: string; payload: unknown }> = [];
|
||||||
|
const session = createSessionForWorkspaceTests();
|
||||||
|
const projects = new Map<string, ReturnType<typeof createPersistedProjectRecord>>();
|
||||||
|
const workspaces = new Map<string, ReturnType<typeof createPersistedWorkspaceRecord>>();
|
||||||
|
const cwd = "/tmp/github-runtime-repo";
|
||||||
|
const snapshot = createWorkspaceRuntimeSnapshot(cwd);
|
||||||
|
|
||||||
|
let listener: ((snapshot: WorkspaceGitRuntimeSnapshot) => void) | null = null;
|
||||||
|
const peeked = { value: null as WorkspaceGitRuntimeSnapshot | null };
|
||||||
|
|
||||||
|
session.emit = (message) => emitted.push(message as { type: string; payload: unknown });
|
||||||
|
session.projectRegistry.get = async (projectId: string) => projects.get(projectId) ?? null;
|
||||||
|
session.projectRegistry.upsert = async (
|
||||||
|
record: ReturnType<typeof createPersistedProjectRecord>,
|
||||||
|
) => {
|
||||||
|
projects.set(record.projectId, record);
|
||||||
|
};
|
||||||
|
session.workspaceRegistry.get = async (workspaceId: string) =>
|
||||||
|
workspaces.get(workspaceId) ?? null;
|
||||||
|
session.workspaceRegistry.upsert = async (
|
||||||
|
record: ReturnType<typeof createPersistedWorkspaceRecord>,
|
||||||
|
) => {
|
||||||
|
workspaces.set(record.workspaceId, record);
|
||||||
|
};
|
||||||
|
session.projectRegistry.list = async () => Array.from(projects.values());
|
||||||
|
session.workspaceRegistry.list = async () => Array.from(workspaces.values());
|
||||||
|
session.workspaceGitService.getCheckout = async (requestedCwd: string) => ({
|
||||||
|
cwd: requestedCwd,
|
||||||
|
isGit: true,
|
||||||
|
currentBranch: "main",
|
||||||
|
remoteUrl: "https://github.com/acme/repo.git",
|
||||||
|
worktreeRoot: requestedCwd,
|
||||||
|
isPaseoOwnedWorktree: false,
|
||||||
|
mainRepoRoot: null,
|
||||||
|
});
|
||||||
|
session.workspaceGitService.peekSnapshot = () => peeked.value;
|
||||||
|
session.workspaceGitService.registerWorkspace = (_params, incomingListener) => {
|
||||||
|
listener = incomingListener as (snapshot: WorkspaceGitRuntimeSnapshot) => void;
|
||||||
|
return { unsubscribe: () => {} };
|
||||||
|
};
|
||||||
|
session.workspaceGitService.getSnapshot = async () => {
|
||||||
|
peeked.value = snapshot;
|
||||||
|
listener?.(snapshot);
|
||||||
|
return snapshot;
|
||||||
|
};
|
||||||
|
session.workspaceUpdatesSubscription = {
|
||||||
|
subscriptionId: "sub-open-project",
|
||||||
|
filter: undefined,
|
||||||
|
isBootstrapping: false,
|
||||||
|
pendingUpdatesByWorkspaceId: new Map(),
|
||||||
|
lastEmittedByWorkspaceId: new Map(),
|
||||||
|
};
|
||||||
|
session.reconcileActiveWorkspaceRecords = async () => new Set();
|
||||||
|
|
||||||
|
await session.handleMessage({
|
||||||
|
type: "open_project_request",
|
||||||
|
cwd,
|
||||||
|
requestId: "req-runtime-update",
|
||||||
|
});
|
||||||
|
|
||||||
|
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||||
|
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||||
|
|
||||||
|
const updates = emitted.filter((message) => message.type === "workspace_update") as Array<{
|
||||||
|
type: "workspace_update";
|
||||||
|
payload: WorkspaceUpdatePayloadShape;
|
||||||
|
}>;
|
||||||
|
const upsertedWithGitHub = updates
|
||||||
|
.map((update) => update.payload)
|
||||||
|
.filter(
|
||||||
|
(payload): payload is WorkspaceUpsertPayload =>
|
||||||
|
payload.kind === "upsert" && payload.workspace.id === cwd,
|
||||||
|
)
|
||||||
|
.find((payload) => payload.workspace.githubRuntime?.pullRequest);
|
||||||
|
expect(upsertedWithGitHub?.workspace.githubRuntime?.pullRequest).toEqual(
|
||||||
|
expect.objectContaining({ url: "https://github.com/acme/repo/pull/123" }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
interface WorkspaceUpsertPayload {
|
||||||
|
kind: "upsert";
|
||||||
|
workspace: {
|
||||||
|
id: string;
|
||||||
|
githubRuntime?: {
|
||||||
|
pullRequest?: { url?: string } | null;
|
||||||
|
} | null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WorkspaceRemovePayload {
|
||||||
|
kind: "remove";
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type WorkspaceUpdatePayloadShape = WorkspaceUpsertPayload | WorkspaceRemovePayload;
|
||||||
|
|
||||||
test("open_project_request does not match a new child directory to an existing parent workspace", async () => {
|
test("open_project_request does not match a new child directory to an existing parent workspace", async () => {
|
||||||
const emitted: Array<{ type: string; payload: unknown }> = [];
|
const emitted: Array<{ type: string; payload: unknown }> = [];
|
||||||
const session = createSessionForWorkspaceTests();
|
const session = createSessionForWorkspaceTests();
|
||||||
@@ -2278,6 +2452,15 @@ test("open_project_request reclassifies an archived directory workspace when git
|
|||||||
};
|
};
|
||||||
session.projectRegistry.list = async () => Array.from(projects.values());
|
session.projectRegistry.list = async () => Array.from(projects.values());
|
||||||
session.workspaceRegistry.list = async () => Array.from(workspaces.values());
|
session.workspaceRegistry.list = async () => Array.from(workspaces.values());
|
||||||
|
session.workspaceGitService.getCheckout = async () => ({
|
||||||
|
cwd,
|
||||||
|
isGit: true,
|
||||||
|
currentBranch: "feature/desktop-daemon-settings",
|
||||||
|
remoteUrl: "git@github.com:getpaseo/paseo.git",
|
||||||
|
worktreeRoot: cwd,
|
||||||
|
isPaseoOwnedWorktree: false,
|
||||||
|
mainRepoRoot: repoRoot,
|
||||||
|
});
|
||||||
session.workspaceGitService.getSnapshot = async () =>
|
session.workspaceGitService.getSnapshot = async () =>
|
||||||
createWorkspaceRuntimeSnapshot(cwd, {
|
createWorkspaceRuntimeSnapshot(cwd, {
|
||||||
git: {
|
git: {
|
||||||
@@ -2389,6 +2572,15 @@ test("open_project_request reclassifies an active directory workspace when git m
|
|||||||
};
|
};
|
||||||
session.projectRegistry.list = async () => Array.from(projects.values());
|
session.projectRegistry.list = async () => Array.from(projects.values());
|
||||||
session.workspaceRegistry.list = async () => Array.from(workspaces.values());
|
session.workspaceRegistry.list = async () => Array.from(workspaces.values());
|
||||||
|
session.workspaceGitService.getCheckout = async (requestedCwd: string) => ({
|
||||||
|
cwd: requestedCwd,
|
||||||
|
isGit: true,
|
||||||
|
currentBranch: requestedCwd === repoRoot ? "main" : "feature/desktop-daemon-settings",
|
||||||
|
remoteUrl: "git@github.com:getpaseo/paseo.git",
|
||||||
|
worktreeRoot: requestedCwd,
|
||||||
|
isPaseoOwnedWorktree: false,
|
||||||
|
mainRepoRoot: requestedCwd === repoRoot ? null : repoRoot,
|
||||||
|
});
|
||||||
session.workspaceGitService.getSnapshot = async (requestedCwd: string) =>
|
session.workspaceGitService.getSnapshot = async (requestedCwd: string) =>
|
||||||
createWorkspaceRuntimeSnapshot(requestedCwd, {
|
createWorkspaceRuntimeSnapshot(requestedCwd, {
|
||||||
git: {
|
git: {
|
||||||
@@ -2476,6 +2668,15 @@ test("open_project_request groups a plain git worktree under an existing repo pr
|
|||||||
};
|
};
|
||||||
session.projectRegistry.list = async () => Array.from(projects.values());
|
session.projectRegistry.list = async () => Array.from(projects.values());
|
||||||
session.workspaceRegistry.list = async () => Array.from(workspaces.values());
|
session.workspaceRegistry.list = async () => Array.from(workspaces.values());
|
||||||
|
session.workspaceGitService.getCheckout = async (requestedCwd: string) => ({
|
||||||
|
cwd: requestedCwd,
|
||||||
|
isGit: true,
|
||||||
|
currentBranch: requestedCwd === repoRoot ? "main" : "feature/desktop-daemon-settings",
|
||||||
|
remoteUrl: "git@github.com:getpaseo/paseo.git",
|
||||||
|
worktreeRoot: requestedCwd,
|
||||||
|
isPaseoOwnedWorktree: false,
|
||||||
|
mainRepoRoot: requestedCwd === repoRoot ? null : repoRoot,
|
||||||
|
});
|
||||||
session.workspaceGitService.getSnapshot = async (requestedCwd: string) =>
|
session.workspaceGitService.getSnapshot = async (requestedCwd: string) =>
|
||||||
createWorkspaceRuntimeSnapshot(requestedCwd, {
|
createWorkspaceRuntimeSnapshot(requestedCwd, {
|
||||||
git: {
|
git: {
|
||||||
|
|||||||
@@ -40,6 +40,15 @@ export function createNoopWorkspaceGitService(
|
|||||||
unsubscribe: () => {},
|
unsubscribe: () => {},
|
||||||
}),
|
}),
|
||||||
peekSnapshot: () => null,
|
peekSnapshot: () => null,
|
||||||
|
getCheckout: async (cwd: string) => ({
|
||||||
|
cwd,
|
||||||
|
isGit: false,
|
||||||
|
currentBranch: null,
|
||||||
|
remoteUrl: null,
|
||||||
|
worktreeRoot: null,
|
||||||
|
isPaseoOwnedWorktree: false,
|
||||||
|
mainRepoRoot: null,
|
||||||
|
}),
|
||||||
getSnapshot: async (cwd: string) => createNoGitWorkspaceRuntimeSnapshot(cwd),
|
getSnapshot: async (cwd: string) => createNoGitWorkspaceRuntimeSnapshot(cwd),
|
||||||
getCheckoutDiff: async (): Promise<CheckoutDiffResult> => ({ diff: "" }),
|
getCheckoutDiff: async (): Promise<CheckoutDiffResult> => ({ diff: "" }),
|
||||||
validateBranchRef: async () => ({ kind: "not-found" }),
|
validateBranchRef: async () => ({ kind: "not-found" }),
|
||||||
|
|||||||
@@ -102,6 +102,15 @@ function createFallbackWorkspaceGitService(): WorkspaceGitService {
|
|||||||
unsubscribe: () => {},
|
unsubscribe: () => {},
|
||||||
}),
|
}),
|
||||||
peekSnapshot: () => null,
|
peekSnapshot: () => null,
|
||||||
|
getCheckout: async (cwd: string) => ({
|
||||||
|
cwd,
|
||||||
|
isGit: false,
|
||||||
|
currentBranch: null,
|
||||||
|
remoteUrl: null,
|
||||||
|
worktreeRoot: null,
|
||||||
|
isPaseoOwnedWorktree: false,
|
||||||
|
mainRepoRoot: null,
|
||||||
|
}),
|
||||||
getSnapshot: async (cwd: string) => createFallbackWorkspaceGitSnapshot(cwd),
|
getSnapshot: async (cwd: string) => createFallbackWorkspaceGitSnapshot(cwd),
|
||||||
getCheckoutDiff: async () => ({ diff: "" }),
|
getCheckoutDiff: async () => ({ diff: "" }),
|
||||||
validateBranchRef: async () => ({ kind: "not-found" }),
|
validateBranchRef: async () => ({ kind: "not-found" }),
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { readFile, readdir } from "node:fs/promises";
|
|||||||
import { join, resolve } from "node:path";
|
import { join, resolve } from "node:path";
|
||||||
import pLimit from "p-limit";
|
import pLimit from "p-limit";
|
||||||
import type pino from "pino";
|
import type pino from "pino";
|
||||||
|
import type { ProjectCheckoutLitePayload } from "../shared/messages.js";
|
||||||
import type { CheckoutContext } from "../utils/checkout-git.js";
|
import type { CheckoutContext } from "../utils/checkout-git.js";
|
||||||
import {
|
import {
|
||||||
type BranchCheckoutResolution,
|
type BranchCheckoutResolution,
|
||||||
@@ -28,7 +29,7 @@ import {
|
|||||||
buildWorkspaceGitMetadataFromSnapshot,
|
buildWorkspaceGitMetadataFromSnapshot,
|
||||||
type WorkspaceGitMetadata,
|
type WorkspaceGitMetadata,
|
||||||
} from "./workspace-git-metadata.js";
|
} from "./workspace-git-metadata.js";
|
||||||
import { normalizeWorkspaceId } from "./workspace-registry-model.js";
|
import { checkoutLiteFromGitSnapshot, normalizeWorkspaceId } from "./workspace-registry-model.js";
|
||||||
|
|
||||||
const WORKSPACE_GIT_WATCH_DEBOUNCE_MS = 500;
|
const WORKSPACE_GIT_WATCH_DEBOUNCE_MS = 500;
|
||||||
const BACKGROUND_GIT_FETCH_INTERVAL_MS = 180_000;
|
const BACKGROUND_GIT_FETCH_INTERVAL_MS = 180_000;
|
||||||
@@ -94,6 +95,7 @@ export interface WorkspaceGitService {
|
|||||||
): WorkspaceGitSubscription;
|
): WorkspaceGitSubscription;
|
||||||
|
|
||||||
peekSnapshot(cwd: string): WorkspaceGitRuntimeSnapshot | null;
|
peekSnapshot(cwd: string): WorkspaceGitRuntimeSnapshot | null;
|
||||||
|
getCheckout(cwd: string): Promise<ProjectCheckoutLitePayload>;
|
||||||
getSnapshot(
|
getSnapshot(
|
||||||
cwd: string,
|
cwd: string,
|
||||||
options?: WorkspaceGitSnapshotOptions,
|
options?: WorkspaceGitSnapshotOptions,
|
||||||
@@ -390,6 +392,42 @@ export class WorkspaceGitServiceImpl implements WorkspaceGitService {
|
|||||||
return this.requestWorkspaceSnapshot(target, request);
|
return this.requestWorkspaceSnapshot(target, request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getCheckout(cwd: string): Promise<ProjectCheckoutLitePayload> {
|
||||||
|
const normalizedCwd = normalizeWorkspaceId(cwd);
|
||||||
|
try {
|
||||||
|
const status = await this.deps.getCheckoutStatus(normalizedCwd, {
|
||||||
|
paseoHome: this.paseoHome,
|
||||||
|
});
|
||||||
|
if (!status.isGit) {
|
||||||
|
return checkoutLiteFromGitSnapshot(normalizedCwd, {
|
||||||
|
isGit: false,
|
||||||
|
currentBranch: null,
|
||||||
|
remoteUrl: null,
|
||||||
|
repoRoot: null,
|
||||||
|
isPaseoOwnedWorktree: false,
|
||||||
|
mainRepoRoot: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return checkoutLiteFromGitSnapshot(normalizedCwd, {
|
||||||
|
isGit: true,
|
||||||
|
currentBranch: status.currentBranch,
|
||||||
|
remoteUrl: status.remoteUrl,
|
||||||
|
repoRoot: status.repoRoot,
|
||||||
|
isPaseoOwnedWorktree: status.isPaseoOwnedWorktree,
|
||||||
|
mainRepoRoot: status.mainRepoRoot,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return checkoutLiteFromGitSnapshot(normalizedCwd, {
|
||||||
|
isGit: false,
|
||||||
|
currentBranch: null,
|
||||||
|
remoteUrl: null,
|
||||||
|
repoRoot: null,
|
||||||
|
isPaseoOwnedWorktree: false,
|
||||||
|
mainRepoRoot: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
peekSnapshot(cwd: string): WorkspaceGitRuntimeSnapshot | null {
|
peekSnapshot(cwd: string): WorkspaceGitRuntimeSnapshot | null {
|
||||||
cwd = normalizeWorkspaceId(cwd);
|
cwd = normalizeWorkspaceId(cwd);
|
||||||
return this.workspaceTargets.get(cwd)?.latestSnapshot ?? null;
|
return this.workspaceTargets.get(cwd)?.latestSnapshot ?? null;
|
||||||
|
|||||||
@@ -68,16 +68,17 @@ export async function bootstrapWorkspaceRegistries(options: {
|
|||||||
const recordsByWorkspaceId = new Map<
|
const recordsByWorkspaceId = new Map<
|
||||||
string,
|
string,
|
||||||
{
|
{
|
||||||
membership: Awaited<ReturnType<typeof classifyDirectoryForProjectMembership>>;
|
membership: ReturnType<typeof classifyDirectoryForProjectMembership>;
|
||||||
records: StoredAgentRecord[];
|
records: StoredAgentRecord[];
|
||||||
}
|
}
|
||||||
>();
|
>();
|
||||||
const placements = await Promise.all(
|
const placements = await Promise.all(
|
||||||
activeRecords.map(async (record) => {
|
activeRecords.map(async (record) => {
|
||||||
const normalizedCwd = normalizeWorkspaceId(record.cwd);
|
const normalizedCwd = normalizeWorkspaceId(record.cwd);
|
||||||
const membership = await classifyDirectoryForProjectMembership({
|
const checkout = await options.workspaceGitService.getCheckout(normalizedCwd);
|
||||||
|
const membership = classifyDirectoryForProjectMembership({
|
||||||
cwd: normalizedCwd,
|
cwd: normalizedCwd,
|
||||||
workspaceGitService: options.workspaceGitService,
|
checkout,
|
||||||
});
|
});
|
||||||
return { record, membership, workspaceId: membership.workspaceId };
|
return { record, membership, workspaceId: membership.workspaceId };
|
||||||
}),
|
}),
|
||||||
@@ -91,7 +92,7 @@ export async function bootstrapWorkspaceRegistries(options: {
|
|||||||
const projectRanges = new Map<string, { createdAt: string | null; updatedAt: string | null }>();
|
const projectRanges = new Map<string, { createdAt: string | null; updatedAt: string | null }>();
|
||||||
const workspaceUpsertInputs: {
|
const workspaceUpsertInputs: {
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
membership: Awaited<ReturnType<typeof classifyDirectoryForProjectMembership>>;
|
membership: ReturnType<typeof classifyDirectoryForProjectMembership>;
|
||||||
workspaceCwd: string;
|
workspaceCwd: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
|
|||||||
@@ -109,34 +109,18 @@ describe("deriveWorkspaceId", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("git worktree grouping", () => {
|
describe("git worktree grouping", () => {
|
||||||
test("classifies plain git worktrees for project membership from git facts", async () => {
|
test("classifies plain git worktrees for project membership from git facts", () => {
|
||||||
const membership = await classifyDirectoryForProjectMembership({
|
const membership = classifyDirectoryForProjectMembership({
|
||||||
cwd: "/tmp/repo-feature",
|
cwd: "/tmp/repo-feature",
|
||||||
workspaceGitService: {
|
checkout: {
|
||||||
getSnapshot: async () => ({
|
cwd: "/tmp/repo-feature",
|
||||||
cwd: "/tmp/repo-feature",
|
isGit: true,
|
||||||
git: {
|
currentBranch: "feature/plain",
|
||||||
isGit: true,
|
remoteUrl: "https://github.com/acme/repo.git",
|
||||||
repoRoot: "/tmp/repo-feature",
|
worktreeRoot: "/tmp/repo-feature",
|
||||||
mainRepoRoot: "/tmp/repo",
|
isPaseoOwnedWorktree: false,
|
||||||
currentBranch: "feature/plain",
|
mainRepoRoot: "/tmp/repo",
|
||||||
remoteUrl: "https://github.com/acme/repo.git",
|
},
|
||||||
isPaseoOwnedWorktree: false,
|
|
||||||
isDirty: false,
|
|
||||||
baseRef: null,
|
|
||||||
aheadBehind: null,
|
|
||||||
aheadOfOrigin: null,
|
|
||||||
behindOfOrigin: null,
|
|
||||||
hasRemote: true,
|
|
||||||
diffStat: null,
|
|
||||||
},
|
|
||||||
github: {
|
|
||||||
featuresEnabled: false,
|
|
||||||
pullRequest: null,
|
|
||||||
error: null,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
} as never,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(membership).toMatchObject({
|
expect(membership).toMatchObject({
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { resolve } from "node:path";
|
|||||||
|
|
||||||
import type { ProjectCheckoutLitePayload, ProjectPlacementPayload } from "../shared/messages.js";
|
import type { ProjectCheckoutLitePayload, ProjectPlacementPayload } from "../shared/messages.js";
|
||||||
import { parseGitRevParsePath } from "../utils/git-rev-parse-path.js";
|
import { parseGitRevParsePath } from "../utils/git-rev-parse-path.js";
|
||||||
import type { WorkspaceGitService } from "./workspace-git-service.js";
|
|
||||||
import type { PersistedWorkspaceRecord } from "./workspace-registry.js";
|
import type { PersistedWorkspaceRecord } from "./workspace-registry.js";
|
||||||
|
|
||||||
export type PersistedProjectKind = "git" | "non_git";
|
export type PersistedProjectKind = "git" | "non_git";
|
||||||
@@ -215,11 +214,11 @@ export async function detectStaleWorkspaces(
|
|||||||
return staleWorkspaceIds;
|
return staleWorkspaceIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function buildProjectPlacementForCwd(input: {
|
export function buildProjectPlacementForCwd(input: {
|
||||||
cwd: string;
|
cwd: string;
|
||||||
workspaceGitService: WorkspaceGitService;
|
checkout: ProjectCheckoutLitePayload;
|
||||||
}): Promise<ProjectPlacementPayload> {
|
}): ProjectPlacementPayload {
|
||||||
const membership = await classifyDirectoryForProjectMembership(input);
|
const membership = classifyDirectoryForProjectMembership(input);
|
||||||
return {
|
return {
|
||||||
projectKey: membership.projectKey,
|
projectKey: membership.projectKey,
|
||||||
projectName: membership.projectName,
|
projectName: membership.projectName,
|
||||||
@@ -227,28 +226,15 @@ export async function buildProjectPlacementForCwd(input: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function classifyDirectoryForProjectMembership(input: {
|
export function classifyDirectoryForProjectMembership(input: {
|
||||||
cwd: string;
|
cwd: string;
|
||||||
workspaceGitService: WorkspaceGitService;
|
checkout: ProjectCheckoutLitePayload;
|
||||||
}): Promise<DirectoryProjectMembership> {
|
}): DirectoryProjectMembership {
|
||||||
const normalizedCwd = normalizeWorkspaceId(input.cwd);
|
const normalizedCwd = normalizeWorkspaceId(input.cwd);
|
||||||
const checkout = await input.workspaceGitService
|
const checkout: ProjectCheckoutLitePayload = {
|
||||||
.getSnapshot(normalizedCwd)
|
...input.checkout,
|
||||||
.then(
|
cwd: normalizedCwd,
|
||||||
(snapshot): ProjectCheckoutLitePayload =>
|
};
|
||||||
checkoutLiteFromGitSnapshot(normalizedCwd, snapshot.git),
|
|
||||||
)
|
|
||||||
.catch(
|
|
||||||
(): ProjectCheckoutLitePayload => ({
|
|
||||||
cwd: normalizedCwd,
|
|
||||||
isGit: false,
|
|
||||||
currentBranch: null,
|
|
||||||
remoteUrl: null,
|
|
||||||
worktreeRoot: null,
|
|
||||||
isPaseoOwnedWorktree: false,
|
|
||||||
mainRepoRoot: null,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const projectKey = deriveProjectGroupingKey({
|
const projectKey = deriveProjectGroupingKey({
|
||||||
cwd: checkout.worktreeRoot ?? normalizedCwd,
|
cwd: checkout.worktreeRoot ?? normalizedCwd,
|
||||||
|
|||||||
Reference in New Issue
Block a user