Skip to content

Commit 86ef049

Browse files
authored
fix: run git fetch for all SSH workspaces (#431)
## Problem Git fetch was only running once per project per polling cycle. This optimization worked for local workspaces that share a git repository, but broke for SSH workspaces where each workspace has its own independent git repository. ## Solution Changed the fetch key logic to distinguish between local and SSH workspaces: - **Local workspaces**: Use `projectName` as fetch key → one fetch serves all workspaces (efficient) - **SSH workspaces**: Use `workspaceId` as fetch key → each workspace gets its own fetch (correct) ## Changes - Add `getFetchKey()` to determine fetch key based on runtime type - Rename `tryFetchNextProject` → `tryFetchWorkspaces` - Rename `fetchProject` → `fetchWorkspace` - Remove `groupWorkspacesByProject` (no longer needed) ## Impact ### Before (Bug) ``` Polling Cycle 1: SSH Workspace A (project-x) ───┐ SSH Workspace B (project-x) ───┼─→ Fetch key: "project-x" SSH Workspace C (project-x) ───┘ ❌ Only ONE fetch runs ``` ### After (Fixed) ``` Polling Cycle 1: SSH Workspace A (id: abc123) ──→ Fetch key: "abc123" ✓ Polling Cycle 2: SSH Workspace B (id: def456) ──→ Fetch key: "def456" ✓ Polling Cycle 3: SSH Workspace C (id: ghi789) ──→ Fetch key: "ghi789" ✓ ``` This ensures SSH workspaces get fresh git status updates while maintaining efficient fetch behavior for local workspaces. ## Testing The existing tests in `GitStatusStore.test.ts` continue to pass. The changes only affect internal fetch scheduling logic and maintain backward compatibility for local workspaces.
1 parent 2ee355f commit 86ef049

File tree

1 file changed

+36
-44
lines changed

1 file changed

+36
-44
lines changed

src/stores/GitStatusStore.ts

Lines changed: 36 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -145,11 +145,8 @@ export class GitStatusStore {
145145
return;
146146
}
147147

148-
// Group workspaces by project for fetch management
149-
const projectGroups = this.groupWorkspacesByProject(this.workspaceMetadata);
150-
151-
// Try to fetch one project per cycle (background, non-blocking)
152-
this.tryFetchNextProject(projectGroups);
148+
// Try to fetch workspaces that need it (background, non-blocking)
149+
this.tryFetchWorkspaces(this.workspaceMetadata);
153150

154151
// Query git status for each workspace
155152
// Rate limit: Process in batches to prevent bash process explosion
@@ -256,59 +253,52 @@ export class GitStatusStore {
256253
}
257254

258255
/**
259-
* Group workspaces by project name.
256+
* Get a unique fetch key for a workspace.
257+
* For local workspaces: project name (shared git repo)
258+
* For SSH workspaces: workspace ID (each has its own git repo)
260259
*/
261-
private groupWorkspacesByProject(
262-
metadata: Map<string, FrontendWorkspaceMetadata>
263-
): Map<string, FrontendWorkspaceMetadata[]> {
264-
const groups = new Map<string, FrontendWorkspaceMetadata[]>();
265-
266-
for (const m of metadata.values()) {
267-
const projectName = m.projectName;
268-
269-
if (!groups.has(projectName)) {
270-
groups.set(projectName, []);
271-
}
272-
groups.get(projectName)!.push(m);
273-
}
274-
275-
return groups;
260+
private getFetchKey(metadata: FrontendWorkspaceMetadata): string {
261+
const isSSH = metadata.runtimeConfig?.type === "ssh";
262+
return isSSH ? metadata.id : metadata.projectName;
276263
}
277264

278265
/**
279-
* Try to fetch the project that needs it most urgently.
266+
* Try to fetch workspaces that need it most urgently.
267+
* For SSH workspaces: each workspace has its own repo, so fetch each one.
268+
* For local workspaces: workspaces share a repo, so fetch once per project.
280269
*/
281-
private tryFetchNextProject(projectGroups: Map<string, FrontendWorkspaceMetadata[]>): void {
282-
let targetProject: string | null = null;
270+
private tryFetchWorkspaces(workspaces: Map<string, FrontendWorkspaceMetadata>): void {
271+
// Find the workspace that needs fetching most urgently
272+
let targetFetchKey: string | null = null;
283273
let targetWorkspaceId: string | null = null;
284274
let oldestTime = Date.now();
285275

286-
for (const [projectName, workspaces] of projectGroups) {
287-
if (workspaces.length === 0) continue;
276+
for (const metadata of workspaces.values()) {
277+
const fetchKey = this.getFetchKey(metadata);
288278

289-
if (this.shouldFetch(projectName)) {
290-
const cache = this.fetchCache.get(projectName);
279+
if (this.shouldFetch(fetchKey)) {
280+
const cache = this.fetchCache.get(fetchKey);
291281
const lastFetch = cache?.lastFetch ?? 0;
292282

293283
if (lastFetch < oldestTime) {
294284
oldestTime = lastFetch;
295-
targetProject = projectName;
296-
targetWorkspaceId = workspaces[0].id;
285+
targetFetchKey = fetchKey;
286+
targetWorkspaceId = metadata.id;
297287
}
298288
}
299289
}
300290

301-
if (targetProject && targetWorkspaceId) {
291+
if (targetFetchKey && targetWorkspaceId) {
302292
// Fetch in background (don't await - don't block status checks)
303-
void this.fetchProject(targetProject, targetWorkspaceId);
293+
void this.fetchWorkspace(targetFetchKey, targetWorkspaceId);
304294
}
305295
}
306296

307297
/**
308-
* Check if project should be fetched.
298+
* Check if a workspace/project should be fetched.
309299
*/
310-
private shouldFetch(projectName: string): boolean {
311-
const cached = this.fetchCache.get(projectName);
300+
private shouldFetch(fetchKey: string): boolean {
301+
const cached = this.fetchCache.get(fetchKey);
312302
if (!cached) return true;
313303
if (cached.inProgress) return false;
314304

@@ -321,15 +311,17 @@ export class GitStatusStore {
321311
}
322312

323313
/**
324-
* Fetch updates for a project (one workspace is sufficient).
314+
* Fetch updates for a workspace.
315+
* For local workspaces: fetches the shared project repo.
316+
* For SSH workspaces: fetches the workspace's individual repo.
325317
*/
326-
private async fetchProject(projectName: string, workspaceId: string): Promise<void> {
318+
private async fetchWorkspace(fetchKey: string, workspaceId: string): Promise<void> {
327319
// Defensive: Return early if window.api is unavailable (e.g., test environment)
328320
if (typeof window === "undefined" || !window.api) {
329321
return;
330322
}
331323

332-
const cache = this.fetchCache.get(projectName) ?? {
324+
const cache = this.fetchCache.get(fetchKey) ?? {
333325
lastFetch: 0,
334326
inProgress: false,
335327
consecutiveFailures: 0,
@@ -338,7 +330,7 @@ export class GitStatusStore {
338330
if (cache.inProgress) return;
339331

340332
// Mark as in progress
341-
this.fetchCache.set(projectName, { ...cache, inProgress: true });
333+
this.fetchCache.set(fetchKey, { ...cache, inProgress: true });
342334

343335
try {
344336
const result = await window.api.workspace.executeBash(workspaceId, GIT_FETCH_SCRIPT, {
@@ -355,15 +347,15 @@ export class GitStatusStore {
355347
}
356348

357349
// Success - reset failure counter
358-
console.debug(`[fetch] Success for ${projectName}`);
359-
this.fetchCache.set(projectName, {
350+
console.debug(`[fetch] Success for ${fetchKey}`);
351+
this.fetchCache.set(fetchKey, {
360352
lastFetch: Date.now(),
361353
inProgress: false,
362354
consecutiveFailures: 0,
363355
});
364356
} catch (error) {
365357
// All errors logged to console, never shown to user
366-
console.debug(`[fetch] Failed for ${projectName}:`, error);
358+
console.debug(`[fetch] Failed for ${fetchKey}:`, error);
367359

368360
const newFailures = cache.consecutiveFailures + 1;
369361
const nextDelay = Math.min(
@@ -372,11 +364,11 @@ export class GitStatusStore {
372364
);
373365

374366
console.debug(
375-
`[fetch] Will retry ${projectName} after ${Math.round(nextDelay / 1000)}s ` +
367+
`[fetch] Will retry ${fetchKey} after ${Math.round(nextDelay / 1000)}s ` +
376368
`(failure #${newFailures})`
377369
);
378370

379-
this.fetchCache.set(projectName, {
371+
this.fetchCache.set(fetchKey, {
380372
lastFetch: Date.now(),
381373
inProgress: false,
382374
consecutiveFailures: newFailures,

0 commit comments

Comments
 (0)