Reduce workspace git refresh polling (#1102)
* Reduce workspace git refresh polling * Keep workspace git snapshot reads passive * Update workspace git refresh tests
This commit is contained in:
@@ -269,7 +269,7 @@ describe("WorkspaceGitServiceImpl primitive refresh entrypoint", () => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
test("getSnapshot returns cached warm value without shelling out within consumer TTL", async () => {
|
||||
test("getSnapshot returns the current snapshot without shelling out", async () => {
|
||||
let nowMs = Date.parse("2026-04-12T00:00:00.000Z");
|
||||
const getCheckoutStatus = vi.fn(async (cwd: string) => createCheckoutStatus(cwd));
|
||||
const service = createService({
|
||||
@@ -411,7 +411,7 @@ describe("WorkspaceGitServiceImpl primitive refresh entrypoint", () => {
|
||||
service.dispose();
|
||||
});
|
||||
|
||||
test("warm non-forced getSnapshot calls attach to an in-flight refresh instead of returning stale cache", async () => {
|
||||
test("non-forced getSnapshot returns the current snapshot during an in-flight refresh", async () => {
|
||||
let nowMs = Date.parse("2026-04-12T00:00:00.000Z");
|
||||
const refreshStatus = createDeferred<CheckoutStatusGit>();
|
||||
const getCheckoutStatus = vi
|
||||
@@ -434,24 +434,34 @@ describe("WorkspaceGitServiceImpl primitive refresh entrypoint", () => {
|
||||
}),
|
||||
);
|
||||
|
||||
nowMs += 16_000;
|
||||
const first = service.getSnapshot(REPO_CWD);
|
||||
await flushPromises();
|
||||
const second = service.getSnapshot(REPO_CWD);
|
||||
const initialSnapshot = createSnapshot(REPO_CWD, {
|
||||
git: { diffStat: { additions: 4, deletions: 2 } },
|
||||
});
|
||||
|
||||
nowMs += 3_000;
|
||||
const refresh = service.refresh(REPO_CWD);
|
||||
await flushPromises();
|
||||
const directRead = service.getSnapshot(REPO_CWD);
|
||||
|
||||
expect(getCheckoutStatus).toHaveBeenCalledTimes(2);
|
||||
expect(getCheckoutShortstat).toHaveBeenCalledTimes(1);
|
||||
await expect(directRead).resolves.toEqual(initialSnapshot);
|
||||
|
||||
refreshStatus.resolve(createCheckoutStatus(REPO_CWD, { currentBranch: "feature" }));
|
||||
|
||||
const freshSnapshot = createSnapshot(REPO_CWD, {
|
||||
git: {
|
||||
currentBranch: "feature",
|
||||
diffStat: { additions: 4, deletions: 2 },
|
||||
},
|
||||
});
|
||||
await expect(Promise.all([first, second])).resolves.toEqual([freshSnapshot, freshSnapshot]);
|
||||
await refresh;
|
||||
expect(service.peekSnapshot(REPO_CWD)).toEqual(
|
||||
createSnapshot(REPO_CWD, {
|
||||
git: {
|
||||
currentBranch: "feature",
|
||||
diffStat: { additions: 4, deletions: 2 },
|
||||
},
|
||||
github: {
|
||||
featuresEnabled: false,
|
||||
pullRequest: null,
|
||||
error: null,
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(getCheckoutStatus).toHaveBeenCalledTimes(2);
|
||||
expect(getCheckoutShortstat).toHaveBeenCalledTimes(2);
|
||||
|
||||
@@ -543,10 +553,9 @@ describe("WorkspaceGitServiceImpl primitive refresh entrypoint", () => {
|
||||
.fn<() => Promise<CheckoutStatusGit>>()
|
||||
.mockImplementationOnce(async () => forcedGitRefresh.promise)
|
||||
.mockImplementation(async () => createCheckoutStatus(REPO_CWD));
|
||||
const getPullRequestStatus = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce(createPullRequestStatusResult("Stale cached PR"))
|
||||
.mockResolvedValueOnce(createPullRequestStatusResult("Fresh validation PR"));
|
||||
const getPullRequestStatus = vi.fn(async () =>
|
||||
createPullRequestStatusResult("Fresh validation PR"),
|
||||
);
|
||||
const service = createService({ getCheckoutStatus, getPullRequestStatus });
|
||||
|
||||
const gitRefresh = service.getSnapshot(REPO_CWD, {
|
||||
@@ -584,7 +593,8 @@ describe("WorkspaceGitServiceImpl primitive refresh entrypoint", () => {
|
||||
await gitRefresh;
|
||||
|
||||
expect(getCheckoutStatus).toHaveBeenCalledTimes(2);
|
||||
expect(getPullRequestStatus).toHaveBeenNthCalledWith(2, REPO_CWD, expect.anything(), {
|
||||
expect(getPullRequestStatus).toHaveBeenCalledTimes(1);
|
||||
expect(getPullRequestStatus).toHaveBeenCalledWith(REPO_CWD, expect.anything(), {
|
||||
force: true,
|
||||
reason: "merge-pr-validation",
|
||||
});
|
||||
@@ -634,7 +644,7 @@ describe("WorkspaceGitServiceImpl primitive refresh entrypoint", () => {
|
||||
service.dispose();
|
||||
});
|
||||
|
||||
test("consumer TTL triggers cold-load after expiry on getSnapshot", async () => {
|
||||
test("non-forced getSnapshot keeps returning the current snapshot after time passes", async () => {
|
||||
let nowMs = 0;
|
||||
const getCheckoutStatus = vi.fn(async (cwd: string) => createCheckoutStatus(cwd));
|
||||
const service = createService({
|
||||
@@ -646,12 +656,12 @@ describe("WorkspaceGitServiceImpl primitive refresh entrypoint", () => {
|
||||
nowMs = 16_000;
|
||||
await service.getSnapshot(REPO_CWD);
|
||||
|
||||
expect(getCheckoutStatus).toHaveBeenCalledTimes(2);
|
||||
expect(getCheckoutStatus).toHaveBeenCalledTimes(1);
|
||||
|
||||
service.dispose();
|
||||
});
|
||||
|
||||
test("self-heal timer fires at the interval and calls getSnapshot without force", async () => {
|
||||
test("self-heal timer refreshes git without refreshing GitHub", async () => {
|
||||
let nowMs = 0;
|
||||
const getCheckoutStatus = vi.fn(async (cwd: string) => createCheckoutStatus(cwd));
|
||||
const getPullRequestStatus = vi.fn(async () => createPullRequestStatusResult());
|
||||
@@ -668,9 +678,10 @@ describe("WorkspaceGitServiceImpl primitive refresh entrypoint", () => {
|
||||
await flushPromises();
|
||||
|
||||
expect(getCheckoutStatus).toHaveBeenCalledTimes(2);
|
||||
expect(getPullRequestStatus).toHaveBeenLastCalledWith(REPO_CWD, expect.anything(), {
|
||||
expect(getPullRequestStatus).toHaveBeenCalledTimes(1);
|
||||
expect(getPullRequestStatus).toHaveBeenCalledWith(REPO_CWD, expect.anything(), {
|
||||
force: false,
|
||||
reason: "self-heal-git",
|
||||
reason: "initial",
|
||||
});
|
||||
|
||||
subscription.unsubscribe();
|
||||
@@ -803,8 +814,12 @@ describe("WorkspaceGitServiceImpl primitive refresh entrypoint", () => {
|
||||
github,
|
||||
now: () => new Date(nowMs),
|
||||
});
|
||||
const subscription = service.registerWorkspace({ cwd: REPO_CWD }, vi.fn());
|
||||
const listener = vi.fn();
|
||||
const subscription = service.registerWorkspace({ cwd: REPO_CWD }, listener);
|
||||
await flushPromises();
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
await flushPromises();
|
||||
const gitReadsAfterInitialSnapshot = getCheckoutStatus.mock.calls.length;
|
||||
|
||||
nowMs = 20_000;
|
||||
await vi.advanceTimersByTimeAsync(20_000);
|
||||
@@ -814,6 +829,73 @@ describe("WorkspaceGitServiceImpl primitive refresh entrypoint", () => {
|
||||
reason: "self-heal-github",
|
||||
tickMs: 20_000,
|
||||
});
|
||||
expect(getCheckoutStatus).toHaveBeenCalledTimes(gitReadsAfterInitialSnapshot);
|
||||
expect(listener).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
github: expect.objectContaining({
|
||||
pullRequest: expect.objectContaining({
|
||||
checksStatus: "pending",
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
subscription.unsubscribe();
|
||||
service.dispose();
|
||||
github.dispose?.();
|
||||
});
|
||||
|
||||
test("settled GitHub self-heal reads stay on the slow poll window without refreshing git", async () => {
|
||||
let nowMs = 0;
|
||||
const githubReadCalls: Array<{ reason: string | undefined; tickMs: number }> = [];
|
||||
const github = createGitHubService({
|
||||
ttlMs: 0,
|
||||
runner: vi.fn(async () => ({
|
||||
stdout: currentPullRequestJson(),
|
||||
stderr: "",
|
||||
})),
|
||||
resolveGhPath: async () => "/usr/bin/gh",
|
||||
now: () => nowMs,
|
||||
});
|
||||
const getCurrentPullRequestStatus = github.getCurrentPullRequestStatus.bind(github);
|
||||
github.getCurrentPullRequestStatus = vi.fn(
|
||||
async (options): Promise<GitHubCurrentPullRequestStatus | null> => {
|
||||
githubReadCalls.push({ reason: options.reason, tickMs: nowMs });
|
||||
return getCurrentPullRequestStatus(options);
|
||||
},
|
||||
);
|
||||
const getCheckoutStatus = vi.fn(async (cwd: string) =>
|
||||
createCheckoutStatus(cwd, { currentBranch: "feature" }),
|
||||
);
|
||||
const service = createService({
|
||||
getCheckoutStatus,
|
||||
github,
|
||||
now: () => new Date(nowMs),
|
||||
});
|
||||
const subscription = service.registerWorkspace({ cwd: REPO_CWD }, vi.fn());
|
||||
await flushPromises();
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
await flushPromises();
|
||||
const gitReadsAfterInitialSnapshot = getCheckoutStatus.mock.calls.length;
|
||||
|
||||
nowMs = 20_000;
|
||||
await vi.advanceTimersByTimeAsync(20_000);
|
||||
await flushPromises();
|
||||
|
||||
expect(githubReadCalls).not.toContainEqual({
|
||||
reason: "self-heal-github",
|
||||
tickMs: 20_000,
|
||||
});
|
||||
expect(getCheckoutStatus).toHaveBeenCalledTimes(gitReadsAfterInitialSnapshot);
|
||||
|
||||
nowMs = 120_000;
|
||||
await vi.advanceTimersByTimeAsync(100_000);
|
||||
await flushPromises();
|
||||
|
||||
expect(githubReadCalls).toContainEqual({
|
||||
reason: "self-heal-github",
|
||||
tickMs: 120_000,
|
||||
});
|
||||
|
||||
subscription.unsubscribe();
|
||||
service.dispose();
|
||||
@@ -929,7 +1011,7 @@ describe("WorkspaceGitServiceImpl primitive refresh entrypoint", () => {
|
||||
expect(getCheckoutStatus).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
test("self-heal poll coalesces with a concurrent direct getSnapshot call", async () => {
|
||||
test("direct getSnapshot returns current snapshot during a self-heal refresh", async () => {
|
||||
let nowMs = 0;
|
||||
const selfHealRefresh = createDeferred<CheckoutStatusGit>();
|
||||
const getCheckoutStatus = vi
|
||||
@@ -950,9 +1032,10 @@ describe("WorkspaceGitServiceImpl primitive refresh entrypoint", () => {
|
||||
await flushPromises();
|
||||
|
||||
expect(getCheckoutStatus).toHaveBeenCalledTimes(2);
|
||||
await expect(directRead).resolves.toEqual(createSnapshot(REPO_CWD));
|
||||
|
||||
selfHealRefresh.resolve(createCheckoutStatus(REPO_CWD));
|
||||
await directRead;
|
||||
await flushPromises();
|
||||
|
||||
expect(getCheckoutStatus).toHaveBeenCalledTimes(2);
|
||||
|
||||
|
||||
@@ -341,7 +341,7 @@ describe("WorkspaceGitServiceImpl", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("non-forced GitHub refresh does not emit when pull request state is unchanged", async () => {
|
||||
test("non-forced workspace refresh does not reload GitHub or emit when state is unchanged", async () => {
|
||||
let nowMs = Date.parse("2026-04-12T00:00:00.000Z");
|
||||
const getPullRequestStatus = vi.fn(async () => createPullRequestStatusResult());
|
||||
const service = createService({
|
||||
@@ -355,7 +355,7 @@ describe("WorkspaceGitServiceImpl", () => {
|
||||
nowMs += 3_000;
|
||||
await service.refresh(REPO_CWD);
|
||||
|
||||
expect(getPullRequestStatus).toHaveBeenCalledTimes(2);
|
||||
expect(getPullRequestStatus).toHaveBeenCalledTimes(1);
|
||||
expect(listener).not.toHaveBeenCalled();
|
||||
|
||||
subscription.unsubscribe();
|
||||
@@ -401,7 +401,7 @@ describe("WorkspaceGitServiceImpl", () => {
|
||||
service.dispose();
|
||||
});
|
||||
|
||||
test("multiple listeners on the same workspace share one GitHub pull request lookup", async () => {
|
||||
test("multiple listeners on the same workspace share one observation setup", async () => {
|
||||
const getPullRequestStatus = vi.fn(async () => createPullRequestStatusResult());
|
||||
const resolveAbsoluteGitDir = vi.fn(async () => join(REPO_CWD, ".git"));
|
||||
|
||||
@@ -416,7 +416,7 @@ describe("WorkspaceGitServiceImpl", () => {
|
||||
const second = service.registerWorkspace({ cwd: REPO_CWD }, vi.fn());
|
||||
await flushPromises();
|
||||
|
||||
expect(getPullRequestStatus).toHaveBeenCalledTimes(1);
|
||||
expect(getPullRequestStatus).toHaveBeenCalledTimes(0);
|
||||
expect(resolveAbsoluteGitDir).toHaveBeenCalledTimes(1);
|
||||
|
||||
first.unsubscribe();
|
||||
@@ -448,7 +448,7 @@ describe("WorkspaceGitServiceImpl", () => {
|
||||
createSnapshot(REPO_CWD),
|
||||
);
|
||||
|
||||
expect(getPullRequestStatus).toHaveBeenCalledTimes(2);
|
||||
expect(getPullRequestStatus).toHaveBeenCalledTimes(1);
|
||||
expect(resolveAbsoluteGitDir).toHaveBeenCalledTimes(1);
|
||||
|
||||
subscription.unsubscribe();
|
||||
@@ -489,7 +489,7 @@ describe("WorkspaceGitServiceImpl", () => {
|
||||
service.dispose();
|
||||
});
|
||||
|
||||
test("explicit refresh recomputes github state and notifies listeners", async () => {
|
||||
test("explicit forced snapshot refresh recomputes github state and notifies listeners", async () => {
|
||||
const getPullRequestStatus = vi
|
||||
.fn<() => Promise<PullRequestStatusResult>>()
|
||||
.mockResolvedValueOnce(
|
||||
@@ -529,7 +529,10 @@ describe("WorkspaceGitServiceImpl", () => {
|
||||
|
||||
expect(initialSnapshot.github.pullRequest?.title).toBe("Before refresh");
|
||||
|
||||
await service.refresh(REPO_CWD);
|
||||
await service.getSnapshot(REPO_CWD, {
|
||||
force: true,
|
||||
reason: "test-force-github-refresh",
|
||||
});
|
||||
await flushPromises();
|
||||
|
||||
expect(getPullRequestStatus).toHaveBeenCalledTimes(2);
|
||||
|
||||
@@ -41,8 +41,8 @@ const WORKSPACE_GIT_WATCH_DEBOUNCE_MS = 500;
|
||||
const BACKGROUND_GIT_FETCH_INTERVAL_MS = 180_000;
|
||||
export const WORKSPACE_GIT_SELF_HEAL_INTERVAL_MS = 60_000;
|
||||
const WORKING_TREE_WATCH_FALLBACK_REFRESH_MS = 5_000;
|
||||
// Consumer reads may reuse cached values within this window; older peeks cold-load through the service.
|
||||
const WORKSPACE_GIT_CONSUMER_TTL_MS = 15_000;
|
||||
// Auxiliary reads may reuse cached values within this window; snapshots do not expire on read.
|
||||
const WORKSPACE_GIT_AUXILIARY_READ_TTL_MS = 15_000;
|
||||
// Non-forced refresh triggers share this minimum gap to absorb watcher/self-heal bursts; force bypasses it.
|
||||
const WORKSPACE_GIT_INTERNAL_MIN_GAP_MS = 2_000;
|
||||
const LINUX_WATCH_MAX_DIRS = 5_000;
|
||||
@@ -262,6 +262,10 @@ interface WorkspaceGitTarget {
|
||||
githubPollSubscription: { unsubscribe: () => void } | null;
|
||||
githubPollHeadRef: string | null;
|
||||
refreshState: WorkspaceGitRefreshState;
|
||||
latestGit: WorkspaceGitRuntimeSnapshot["git"] | null;
|
||||
latestGitLoadedAtMs: number | null;
|
||||
latestGithub: WorkspaceGitRuntimeSnapshot["github"] | null;
|
||||
latestGithubLoadedAtMs: number | null;
|
||||
latestSnapshot: WorkspaceGitRuntimeSnapshot | null;
|
||||
latestSnapshotLoadedAtMs: number | null;
|
||||
latestFingerprint: string | null;
|
||||
@@ -409,7 +413,7 @@ export class WorkspaceGitServiceImpl implements WorkspaceGitService {
|
||||
cwd = normalizeWorkspaceId(cwd);
|
||||
const request = this.normalizeRefreshRequest(options, "getSnapshot", true);
|
||||
const target = this.ensureWorkspaceTarget(cwd);
|
||||
if (!request.force && this.isSnapshotWarm(target)) {
|
||||
if (!request.force && target.latestSnapshot) {
|
||||
return target.latestSnapshot;
|
||||
}
|
||||
|
||||
@@ -698,7 +702,7 @@ export class WorkspaceGitServiceImpl implements WorkspaceGitService {
|
||||
const nowMs = this.deps.now().getTime();
|
||||
if (!options?.force && entry.value !== null && entry.loadedAtMs !== null) {
|
||||
const ageMs = nowMs - entry.loadedAtMs;
|
||||
if (ageMs <= WORKSPACE_GIT_CONSUMER_TTL_MS) {
|
||||
if (ageMs <= WORKSPACE_GIT_AUXILIARY_READ_TTL_MS) {
|
||||
return Promise.resolve(entry.value);
|
||||
}
|
||||
if (
|
||||
@@ -773,6 +777,10 @@ export class WorkspaceGitServiceImpl implements WorkspaceGitService {
|
||||
githubPollSubscription: null,
|
||||
githubPollHeadRef: null,
|
||||
refreshState: { status: "idle" },
|
||||
latestGit: null,
|
||||
latestGitLoadedAtMs: null,
|
||||
latestGithub: null,
|
||||
latestGithubLoadedAtMs: null,
|
||||
latestSnapshot: null,
|
||||
latestSnapshotLoadedAtMs: null,
|
||||
latestFingerprint: null,
|
||||
@@ -1045,7 +1053,12 @@ export class WorkspaceGitServiceImpl implements WorkspaceGitService {
|
||||
if (!target.selfHealTimer) {
|
||||
target.selfHealTimer = setInterval(() => {
|
||||
this.scheduleWorkspaceObservationSetup(target);
|
||||
this.getSnapshot(target.cwd, { reason: "self-heal-git" }).catch((error) => {
|
||||
this.refreshWorkspaceTarget(target, {
|
||||
force: false,
|
||||
includeGitHub: false,
|
||||
reason: "self-heal-git",
|
||||
notify: true,
|
||||
}).catch((error) => {
|
||||
this.logger.warn(
|
||||
{ err: error, cwd: target.cwd, reason: "self-heal-git" },
|
||||
"Failed to run workspace git self-heal refresh",
|
||||
@@ -1063,15 +1076,15 @@ export class WorkspaceGitServiceImpl implements WorkspaceGitService {
|
||||
return;
|
||||
}
|
||||
|
||||
const snapshot = target.latestSnapshot;
|
||||
if (!snapshot || !this.deps.github.retainCurrentPullRequestStatusPoll) {
|
||||
const git = target.latestGit;
|
||||
if (!git || !this.deps.github.retainCurrentPullRequestStatusPoll) {
|
||||
this.stopGitHubPollForTarget(target);
|
||||
return;
|
||||
}
|
||||
|
||||
const headRef = snapshot.git.currentBranch;
|
||||
const headRef = git.currentBranch;
|
||||
const hasGitHubRemote =
|
||||
target.cachedGitHubRemote?.remoteUrl === snapshot.git.remoteUrl &&
|
||||
target.cachedGitHubRemote?.remoteUrl === git.remoteUrl &&
|
||||
target.cachedGitHubRemote.identity !== null;
|
||||
if (!headRef || !hasGitHubRemote) {
|
||||
this.stopGitHubPollForTarget(target);
|
||||
@@ -1086,14 +1099,11 @@ export class WorkspaceGitServiceImpl implements WorkspaceGitService {
|
||||
target.githubPollSubscription = this.deps.github.retainCurrentPullRequestStatusPoll({
|
||||
cwd: target.cwd,
|
||||
headRef,
|
||||
onStatus: () => {
|
||||
onStatus: (status) => {
|
||||
if (!this.isActiveObservedWorkspaceTarget(target)) {
|
||||
return;
|
||||
}
|
||||
void this.refreshWorkspaceTarget(target, {
|
||||
force: false,
|
||||
includeGitHub: true,
|
||||
reason: "self-heal-github",
|
||||
this.rememberGitHubSnapshot(target, buildGitHubSnapshotFromStatus(status), {
|
||||
notify: true,
|
||||
});
|
||||
},
|
||||
@@ -1381,25 +1391,12 @@ export class WorkspaceGitServiceImpl implements WorkspaceGitService {
|
||||
const force = options?.force === true;
|
||||
return {
|
||||
force,
|
||||
includeGitHub: options?.includeGitHub ?? force,
|
||||
includeGitHub: options?.includeGitHub ?? true,
|
||||
reason: options?.reason ?? defaultReason,
|
||||
notify,
|
||||
};
|
||||
}
|
||||
|
||||
private isSnapshotWarm(target: WorkspaceGitTarget): target is WorkspaceGitTarget & {
|
||||
latestSnapshot: WorkspaceGitRuntimeSnapshot;
|
||||
latestSnapshotLoadedAtMs: number;
|
||||
} {
|
||||
if (!target.latestSnapshot || target.latestSnapshotLoadedAtMs === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
this.deps.now().getTime() - target.latestSnapshotLoadedAtMs <= WORKSPACE_GIT_CONSUMER_TTL_MS
|
||||
);
|
||||
}
|
||||
|
||||
private async resolveGitHubRemoteForTarget(
|
||||
target: WorkspaceGitTarget,
|
||||
remoteUrl: string | null,
|
||||
@@ -1484,57 +1481,128 @@ export class WorkspaceGitServiceImpl implements WorkspaceGitService {
|
||||
target: WorkspaceGitTarget,
|
||||
request: WorkspaceGitRefreshRequest,
|
||||
): Promise<WorkspaceGitRuntimeSnapshot> {
|
||||
await this.refreshGitSnapshot(target, request);
|
||||
if (request.includeGitHub) {
|
||||
await this.refreshGitHubSnapshot(target, request);
|
||||
}
|
||||
|
||||
const snapshot = this.combineSnapshot(target);
|
||||
target.latestSnapshotLoadedAtMs = this.deps.now().getTime();
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
private async refreshGitSnapshot(
|
||||
target: WorkspaceGitTarget,
|
||||
request: WorkspaceGitRefreshRequest,
|
||||
): Promise<void> {
|
||||
const now = this.deps.now();
|
||||
target.lastShellOutAtMs = now.getTime();
|
||||
|
||||
const cwd = target.cwd;
|
||||
const previousGitHubPollKey = this.getGitHubPollKey(target);
|
||||
const context: CheckoutContext = { paseoHome: this.paseoHome, logger: this.logger };
|
||||
const checkoutStatus = await this.deps.getCheckoutStatus(cwd, context);
|
||||
if (!checkoutStatus.isGit) {
|
||||
target.latestGit = buildNotGitSnapshot(cwd).git;
|
||||
target.latestGitLoadedAtMs = this.deps.now().getTime();
|
||||
target.cachedGitHubRemote = null;
|
||||
target.latestGithub = buildGitHubUnavailableSnapshot();
|
||||
target.latestGithubLoadedAtMs = target.latestGitLoadedAtMs;
|
||||
return;
|
||||
}
|
||||
|
||||
await this.resolveGitHubRemoteForTarget(target, checkoutStatus.remoteUrl);
|
||||
const diffStat = await this.deps
|
||||
.getCheckoutShortstat(cwd, context, { force: request.force })
|
||||
.catch(() => null);
|
||||
|
||||
target.latestGit = {
|
||||
isGit: true,
|
||||
repoRoot: checkoutStatus.repoRoot,
|
||||
mainRepoRoot: checkoutStatus.mainRepoRoot,
|
||||
currentBranch: checkoutStatus.currentBranch,
|
||||
remoteUrl: checkoutStatus.remoteUrl,
|
||||
isPaseoOwnedWorktree: checkoutStatus.isPaseoOwnedWorktree,
|
||||
isDirty: checkoutStatus.isDirty,
|
||||
baseRef: checkoutStatus.baseRef,
|
||||
aheadBehind: checkoutStatus.aheadBehind,
|
||||
aheadOfOrigin: checkoutStatus.aheadOfOrigin,
|
||||
behindOfOrigin: checkoutStatus.behindOfOrigin,
|
||||
hasRemote: checkoutStatus.hasRemote,
|
||||
diffStat,
|
||||
};
|
||||
target.latestGitLoadedAtMs = this.deps.now().getTime();
|
||||
|
||||
if (previousGitHubPollKey !== this.getGitHubPollKey(target)) {
|
||||
target.latestGithub = buildGitHubUnavailableSnapshot();
|
||||
target.latestGithubLoadedAtMs = target.latestGitLoadedAtMs;
|
||||
}
|
||||
}
|
||||
|
||||
private async refreshGitHubSnapshot(
|
||||
target: WorkspaceGitTarget,
|
||||
request: WorkspaceGitRefreshRequest,
|
||||
): Promise<void> {
|
||||
const githubRemote = target.cachedGitHubRemote?.identity ?? null;
|
||||
const forceGitHub = request.force && request.includeGitHub;
|
||||
if (forceGitHub) {
|
||||
this.deps.github.invalidate({ cwd: target.cwd });
|
||||
}
|
||||
|
||||
const cwd = target.cwd;
|
||||
const context: CheckoutContext = { paseoHome: this.paseoHome, logger: this.logger };
|
||||
const checkoutStatus = await this.deps.getCheckoutStatus(cwd, context);
|
||||
if (!checkoutStatus.isGit) {
|
||||
target.latestSnapshotLoadedAtMs = now.getTime();
|
||||
return buildNotGitSnapshot(cwd);
|
||||
target.latestGithub = await loadGitHubSnapshot({
|
||||
cwd: target.cwd,
|
||||
githubRemote,
|
||||
now: this.deps.now(),
|
||||
deps: this.deps,
|
||||
force: forceGitHub,
|
||||
reason: request.reason,
|
||||
});
|
||||
target.latestGithubLoadedAtMs = this.deps.now().getTime();
|
||||
}
|
||||
|
||||
private combineSnapshot(target: WorkspaceGitTarget): WorkspaceGitRuntimeSnapshot {
|
||||
if (!target.latestGit) {
|
||||
return target.latestSnapshot ?? buildNotGitSnapshot(target.cwd);
|
||||
}
|
||||
|
||||
const githubRemote = await this.resolveGitHubRemoteForTarget(target, checkoutStatus.remoteUrl);
|
||||
|
||||
const [diffStat, github] = await Promise.all([
|
||||
this.deps.getCheckoutShortstat(cwd, context, { force: request.force }).catch(() => null),
|
||||
loadGitHubSnapshot({
|
||||
cwd,
|
||||
githubRemote,
|
||||
now,
|
||||
deps: this.deps,
|
||||
force: forceGitHub,
|
||||
reason: request.reason,
|
||||
}),
|
||||
]);
|
||||
|
||||
target.latestSnapshotLoadedAtMs = now.getTime();
|
||||
return {
|
||||
cwd,
|
||||
git: {
|
||||
isGit: true,
|
||||
repoRoot: checkoutStatus.repoRoot,
|
||||
mainRepoRoot: checkoutStatus.mainRepoRoot,
|
||||
currentBranch: checkoutStatus.currentBranch,
|
||||
remoteUrl: checkoutStatus.remoteUrl,
|
||||
isPaseoOwnedWorktree: checkoutStatus.isPaseoOwnedWorktree,
|
||||
isDirty: checkoutStatus.isDirty,
|
||||
baseRef: checkoutStatus.baseRef,
|
||||
aheadBehind: checkoutStatus.aheadBehind,
|
||||
aheadOfOrigin: checkoutStatus.aheadOfOrigin,
|
||||
behindOfOrigin: checkoutStatus.behindOfOrigin,
|
||||
hasRemote: checkoutStatus.hasRemote,
|
||||
diffStat,
|
||||
},
|
||||
github,
|
||||
cwd: target.cwd,
|
||||
git: target.latestGit,
|
||||
github: target.latestGithub ?? buildGitHubUnavailableSnapshot(),
|
||||
};
|
||||
}
|
||||
|
||||
private getGitHubPollKey(target: WorkspaceGitTarget): string | null {
|
||||
const git = target.latestGit;
|
||||
if (!git?.currentBranch || !git.remoteUrl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const githubRemote = target.cachedGitHubRemote;
|
||||
if (!githubRemote || githubRemote.remoteUrl !== git.remoteUrl || !githubRemote.identity) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return JSON.stringify([git.remoteUrl, git.currentBranch]);
|
||||
}
|
||||
|
||||
private rememberGitHubSnapshot(
|
||||
target: WorkspaceGitTarget,
|
||||
github: WorkspaceGitRuntimeSnapshot["github"],
|
||||
options?: { notify?: boolean },
|
||||
): void {
|
||||
if (target.closed || this.workspaceTargets.get(target.cwd) !== target) {
|
||||
return;
|
||||
}
|
||||
|
||||
target.latestGithub = github;
|
||||
target.latestGithubLoadedAtMs = this.deps.now().getTime();
|
||||
this.rememberSnapshot(target, this.combineSnapshot(target), {
|
||||
notify: options?.notify,
|
||||
forceEmit: false,
|
||||
});
|
||||
}
|
||||
|
||||
private rememberSnapshot(
|
||||
target: WorkspaceGitTarget,
|
||||
snapshot: WorkspaceGitRuntimeSnapshot,
|
||||
@@ -1794,11 +1862,25 @@ function buildNotGitSnapshot(cwd: string): WorkspaceGitRuntimeSnapshot {
|
||||
hasRemote: false,
|
||||
diffStat: null,
|
||||
},
|
||||
github: {
|
||||
featuresEnabled: false,
|
||||
pullRequest: null,
|
||||
error: null,
|
||||
},
|
||||
github: buildGitHubUnavailableSnapshot(),
|
||||
};
|
||||
}
|
||||
|
||||
function buildGitHubUnavailableSnapshot(): WorkspaceGitRuntimeSnapshot["github"] {
|
||||
return {
|
||||
featuresEnabled: false,
|
||||
pullRequest: null,
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
|
||||
function buildGitHubSnapshotFromStatus(
|
||||
status: WorkspaceGitRuntimeSnapshot["github"]["pullRequest"],
|
||||
): WorkspaceGitRuntimeSnapshot["github"] {
|
||||
return {
|
||||
featuresEnabled: true,
|
||||
pullRequest: status,
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user