diff --git a/src/App.svelte b/src/App.svelte index 4ffd5d0..4ac0e7f 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -427,6 +427,7 @@ let backgroundFetchTimer: ReturnType | undefined; let backgroundRepoStatusTimer: ReturnType | undefined; let backgroundFetchInFlight = false; + const backgroundCommitNotesFetches = new Map>(); let backgroundRepoStatusInFlight = false; let backgroundRepoStatusIndex = 0; let appShuttingDown = false; @@ -672,6 +673,15 @@ // Keep the cached tab data if this repo is unavailable at startup. } } + + const notesFetched = await backgroundFetchCommitNotes(path); + if (notesFetched && sameRepoPath(path, activeRepoPath)) { + try { + await refreshCommitHistory(path); + } catch { + // The active repository may still be opening; its own background pass retries. + } + } } } finally { backgroundFetchInFlight = false; @@ -717,18 +727,91 @@ // ahead/behind (and the taskbar badge) stay accurate without the user pulling manually. // Errors are swallowed here — auth/network failures surface via the manual Fetch/Pull/ // Push buttons instead, not as a background popup. + function preferredNotesRemote(remotes: GitRemote[]): GitRemote | undefined { + return remotes.find((remote) => remote.name === selectedRemote) + ?? remotes.find((remote) => remote.name === "origin") + ?? remotes[0]; + } + + async function backgroundFetchCommitNotesCore(path: string): Promise { + if (appShuttingDown || !autoRefreshEnabled || !path) return false; + if (commitNoteTarget && sameRepoPath(path, commitNoteRepoPath)) return false; + + let credentialKey: string | null = null; + try { + const remote = preferredNotesRemote(await listRemotes(path)); + if (!remote) return false; + + credentialKey = orgKeyFromUrl(remote.fetch_url); + if (credentialKey && rejectedCredentialKeys.has(credentialKey)) return false; + const credential = await loadStoredCredential(credentialKey); + if (/^https?:\/\//i.test(remote.fetch_url) && !credential) return false; + await fetchCommitNotes(path, remote.name, credential?.username, credential?.password); + if (credentialKey) rejectedCredentialKeys.delete(credentialKey); + trackEvent("commit_notes_background_fetched"); + return true; + } catch (error) { + if (credentialKey && isAuthError(errorToMessage(error))) { + rejectedCredentialKeys.add(credentialKey); + } + if (import.meta.env.DEV) console.info("[Gitty notes] Background fetch skipped", errorToMessage(error)); + return false; + } + } + + function backgroundFetchCommitNotes(path: string): Promise { + const key = repoKey(path); + const current = backgroundCommitNotesFetches.get(key); + if (current) return current; + + const request = backgroundFetchCommitNotesCore(path).finally(() => { + backgroundCommitNotesFetches.delete(key); + }); + backgroundCommitNotesFetches.set(key, request); + return request; + } + + async function waitForBackgroundCommitNotes(path: string) { + await backgroundCommitNotesFetches.get(repoKey(path)); + } + + async function backgroundFetchCommitNotesAndRefresh(path: string) { + if (!await backgroundFetchCommitNotes(path)) return; + if (!sameRepoPath(path, activeRepoPath)) return; + try { + await refreshCommitHistory(path); + } catch { + // Repository changes can invalidate this best-effort background refresh. + } + } + async function backgroundFetchTick() { if (appShuttingDown || !autoRefreshEnabled) return; if (activeView === "repository" && activeRepoPath && !backgroundFetchInFlight && !isBusy && Date.now() - lastRepoSwitchAt >= BACKGROUND_FETCH_AFTER_SWITCH_GRACE_MS) { + const path = activeRepoPath; backgroundFetchInFlight = true; try { - await fetchRemote(activeRepoPath); - applyStatus(await getStatus(activeRepoPath)); - await refreshRefsAndCommitGraph(activeRepoPath); + let refsFetched = false; + try { + await fetchRemote(path); + refsFetched = true; + } catch { + // Manual Fetch/Pull surfaces remote errors; background work stays silent. + } + + const notesFetched = await backgroundFetchCommitNotes(path); + if (sameRepoPath(path, activeRepoPath)) { + if (refsFetched) { + applyStatus(await getStatus(path)); + await refreshRefsAndCommitGraph(path); + } else if (notesFetched) { + await refreshCommitHistory(path); + } + } } catch { - // ignore — see comment above + // ignore transient refresh failures } finally { backgroundFetchInFlight = false; } @@ -742,14 +825,26 @@ backgroundFetchInFlight = true; try { - await fetchRemote(path); - const nextStatus = await getStatus(path); - if (sameRepoPath(path, activeRepoPath)) { - applyStatus(nextStatus); - await refreshRefsAndCommitGraph(path); - } else updateRepoManagementStatus(path, nextStatus); + let refsFetched = false; + try { + await fetchRemote(path); + refsFetched = true; + } catch { + // Manual Fetch/Pull surfaces remote errors; background work stays silent. + } + + const notesFetched = await backgroundFetchCommitNotes(path); + if (refsFetched) { + const nextStatus = await getStatus(path); + if (sameRepoPath(path, activeRepoPath)) { + applyStatus(nextStatus); + await refreshRefsAndCommitGraph(path); + } else updateRepoManagementStatus(path, nextStatus); + } else if (notesFetched && sameRepoPath(path, activeRepoPath)) { + await refreshCommitHistory(path); + } } catch { - // ignore; manual Fetch/Pull surfaces auth or network problems + // ignore transient refresh failures } finally { backgroundFetchInFlight = false; } @@ -2180,7 +2275,8 @@ changed_files: bundle.status.files.length, has_upstream: bundle.status.upstream ? 1 : 0, }); - void backgroundFetchRepo(activeRepoPath); + if (backgroundFetchInFlight) void backgroundFetchCommitNotesAndRefresh(activeRepoPath); + else void backgroundFetchRepo(activeRepoPath); }); } @@ -3011,6 +3107,7 @@ commitNoteError = ""; commitNoteStatus = ""; try { + await waitForBackgroundCommitNotes(repo); await setCommitNote(repo, commit.hash, note); commitNoteText = note; commits = commits.map((item) => item.hash === commit.hash ? { ...item, has_note: true } : item); @@ -3033,6 +3130,7 @@ commitNoteError = ""; commitNoteStatus = ""; try { + await waitForBackgroundCommitNotes(repo); await deleteCommitNote(repo, commit.hash); commitNoteText = ""; commits = commits.map((item) => item.hash === commit.hash ? { ...item, has_note: false } : item); @@ -3068,6 +3166,7 @@ commitNoteError = ""; commitNoteStatus = ""; try { + await waitForBackgroundCommitNotes(repo); const credential = await storedCredentialForNoteRemote(remote, direction); if (direction === "fetch") { await fetchCommitNotes(repo, remote, credential?.username, credential?.password);