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 backgroundFetchTimer: ReturnType<typeof setInterval> | undefined;
|
||||||
let backgroundRepoStatusTimer: ReturnType<typeof setInterval> | undefined;
|
let backgroundRepoStatusTimer: ReturnType<typeof setInterval> | undefined;
|
||||||
let backgroundFetchInFlight = false;
|
let backgroundFetchInFlight = false;
|
||||||
|
const backgroundCommitNotesFetches = new Map<string, Promise<boolean>>();
|
||||||
let backgroundRepoStatusInFlight = false;
|
let backgroundRepoStatusInFlight = false;
|
||||||
let backgroundRepoStatusIndex = 0;
|
let backgroundRepoStatusIndex = 0;
|
||||||
let appShuttingDown = false;
|
let appShuttingDown = false;
|
||||||
@@ -672,6 +673,15 @@
|
|||||||
// Keep the cached tab data if this repo is unavailable at startup.
|
// 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 {
|
} finally {
|
||||||
backgroundFetchInFlight = false;
|
backgroundFetchInFlight = false;
|
||||||
@@ -717,18 +727,91 @@
|
|||||||
// ahead/behind (and the taskbar badge) stay accurate without the user pulling manually.
|
// 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/
|
// Errors are swallowed here — auth/network failures surface via the manual Fetch/Pull/
|
||||||
// Push buttons instead, not as a background popup.
|
// 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() {
|
async function backgroundFetchTick() {
|
||||||
if (appShuttingDown || !autoRefreshEnabled) return;
|
if (appShuttingDown || !autoRefreshEnabled) return;
|
||||||
|
|
||||||
if (activeView === "repository" && activeRepoPath && !backgroundFetchInFlight
|
if (activeView === "repository" && activeRepoPath && !backgroundFetchInFlight
|
||||||
&& !isBusy && Date.now() - lastRepoSwitchAt >= BACKGROUND_FETCH_AFTER_SWITCH_GRACE_MS) {
|
&& !isBusy && Date.now() - lastRepoSwitchAt >= BACKGROUND_FETCH_AFTER_SWITCH_GRACE_MS) {
|
||||||
|
const path = activeRepoPath;
|
||||||
backgroundFetchInFlight = true;
|
backgroundFetchInFlight = true;
|
||||||
try {
|
try {
|
||||||
await fetchRemote(activeRepoPath);
|
let refsFetched = false;
|
||||||
applyStatus(await getStatus(activeRepoPath));
|
try {
|
||||||
await refreshRefsAndCommitGraph(activeRepoPath);
|
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 {
|
} catch {
|
||||||
// ignore — see comment above
|
// ignore transient refresh failures
|
||||||
} finally {
|
} finally {
|
||||||
backgroundFetchInFlight = false;
|
backgroundFetchInFlight = false;
|
||||||
}
|
}
|
||||||
@@ -742,14 +825,26 @@
|
|||||||
|
|
||||||
backgroundFetchInFlight = true;
|
backgroundFetchInFlight = true;
|
||||||
try {
|
try {
|
||||||
await fetchRemote(path);
|
let refsFetched = false;
|
||||||
const nextStatus = await getStatus(path);
|
try {
|
||||||
if (sameRepoPath(path, activeRepoPath)) {
|
await fetchRemote(path);
|
||||||
applyStatus(nextStatus);
|
refsFetched = true;
|
||||||
await refreshRefsAndCommitGraph(path);
|
} catch {
|
||||||
} else updateRepoManagementStatus(path, nextStatus);
|
// 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 {
|
} catch {
|
||||||
// ignore; manual Fetch/Pull surfaces auth or network problems
|
// ignore transient refresh failures
|
||||||
} finally {
|
} finally {
|
||||||
backgroundFetchInFlight = false;
|
backgroundFetchInFlight = false;
|
||||||
}
|
}
|
||||||
@@ -2180,7 +2275,8 @@
|
|||||||
changed_files: bundle.status.files.length,
|
changed_files: bundle.status.files.length,
|
||||||
has_upstream: bundle.status.upstream ? 1 : 0,
|
has_upstream: bundle.status.upstream ? 1 : 0,
|
||||||
});
|
});
|
||||||
void backgroundFetchRepo(activeRepoPath);
|
if (backgroundFetchInFlight) void backgroundFetchCommitNotesAndRefresh(activeRepoPath);
|
||||||
|
else void backgroundFetchRepo(activeRepoPath);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3011,6 +3107,7 @@
|
|||||||
commitNoteError = "";
|
commitNoteError = "";
|
||||||
commitNoteStatus = "";
|
commitNoteStatus = "";
|
||||||
try {
|
try {
|
||||||
|
await waitForBackgroundCommitNotes(repo);
|
||||||
await setCommitNote(repo, commit.hash, note);
|
await setCommitNote(repo, commit.hash, note);
|
||||||
commitNoteText = note;
|
commitNoteText = note;
|
||||||
commits = commits.map((item) => item.hash === commit.hash ? { ...item, has_note: true } : item);
|
commits = commits.map((item) => item.hash === commit.hash ? { ...item, has_note: true } : item);
|
||||||
@@ -3033,6 +3130,7 @@
|
|||||||
commitNoteError = "";
|
commitNoteError = "";
|
||||||
commitNoteStatus = "";
|
commitNoteStatus = "";
|
||||||
try {
|
try {
|
||||||
|
await waitForBackgroundCommitNotes(repo);
|
||||||
await deleteCommitNote(repo, commit.hash);
|
await deleteCommitNote(repo, commit.hash);
|
||||||
commitNoteText = "";
|
commitNoteText = "";
|
||||||
commits = commits.map((item) => item.hash === commit.hash ? { ...item, has_note: false } : item);
|
commits = commits.map((item) => item.hash === commit.hash ? { ...item, has_note: false } : item);
|
||||||
@@ -3068,6 +3166,7 @@
|
|||||||
commitNoteError = "";
|
commitNoteError = "";
|
||||||
commitNoteStatus = "";
|
commitNoteStatus = "";
|
||||||
try {
|
try {
|
||||||
|
await waitForBackgroundCommitNotes(repo);
|
||||||
const credential = await storedCredentialForNoteRemote(remote, direction);
|
const credential = await storedCredentialForNoteRemote(remote, direction);
|
||||||
if (direction === "fetch") {
|
if (direction === "fetch") {
|
||||||
await fetchCommitNotes(repo, remote, credential?.username, credential?.password);
|
await fetchCommitNotes(repo, remote, credential?.username, credential?.password);
|
||||||
|
|||||||
Reference in New Issue
Block a user