feat(app): add background commit notes fetch and refresh logic
This change adds a background path for fetching commit notes per repo and a shared cache to avoid duplicate work. When notes are fetched and the active repo is visible, history is refreshed to reflect notes without blocking user actions. - Adds per-path backgroundCommitNotesFetches cache to debounce fetches - Integrates note fetch into the background tick and refresh flow - Handles credential errors and shutdown gracefully during fetches
This commit is contained in:
+111
-12
@@ -427,6 +427,7 @@
|
||||
let backgroundFetchTimer: ReturnType<typeof setInterval> | undefined;
|
||||
let backgroundRepoStatusTimer: ReturnType<typeof setInterval> | undefined;
|
||||
let backgroundFetchInFlight = false;
|
||||
const backgroundCommitNotesFetches = new Map<string, Promise<boolean>>();
|
||||
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<boolean> {
|
||||
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<boolean> {
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user