From 4a181325bf583d0df197f9d03f8cadb389801906 Mon Sep 17 00:00:00 2001 From: jcardonne <50085165+jcardonne@users.noreply.github.com> Date: Sun, 12 Jul 2026 23:15:13 +0200 Subject: [PATCH] fix(perf): run network git ops off the main thread push/push_force/pull/fetch were bare #[tauri::command], so their blocking remote IO ran on the main/IPC thread and froze the webview for seconds - visible as a UI stutter when picking a pull/fetch action. Mark them (async) like the other heavy commands so they run on a worker thread. That removes the implicit backend serialization those blocking commands had, so add a synchronous re-entrancy guard in RepoView.run() (busyRef, not the render-lagged busy state) so a rapid double-trigger - e.g. the pull/push keyboard shortcut hit twice - can't launch two concurrent git ops racing on the same .git lock files. The now-redundant busy->busyRef mirror effect is dropped; run() owns busyRef directly. --- src-tauri/src/lib.rs | 10 ++++++---- src/components/RepoView.tsx | 18 +++++++++++------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 61406d0..0cedf04 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -183,22 +183,24 @@ fn create_branch(repo: String, name: String, checkout: bool) -> AppResult<()> { branch::create_branch(&open(&repo)?, &name, checkout) } -#[tauri::command] +// Network ops block for seconds (remote IO). `(async)` runs them on a worker +// thread so they don't freeze the UI/IPC - same rationale as the reads above. +#[tauri::command(async)] fn push(repo: String) -> AppResult { ops::push(&open(&repo)?) } -#[tauri::command] +#[tauri::command(async)] fn push_force(repo: String) -> AppResult { ops::push_force(&open(&repo)?) } -#[tauri::command] +#[tauri::command(async)] fn pull(repo: String, mode: String) -> AppResult { ops::pull(&open(&repo)?, &mode) } -#[tauri::command] +#[tauri::command(async)] fn fetch(repo: String) -> AppResult { ops::fetch(&open(&repo)?) } diff --git a/src/components/RepoView.tsx b/src/components/RepoView.tsx index 9ffba57..7501ba2 100644 --- a/src/components/RepoView.tsx +++ b/src/components/RepoView.tsx @@ -280,6 +280,12 @@ export default function RepoView({ path, isActive, onLoaded, onOpenPath }: Props const run = useCallback( async (fn: () => Promise, action?: string) => { + // Re-entrancy guard: one op at a time. busyRef is set synchronously (not + // via the `busy` state, which lags a render) so a rapid double-trigger - + // e.g. hitting the pull/push keyboard shortcut twice - can't launch two + // concurrent git ops racing on the same .git lock files. + if (busyRef.current) return; + busyRef.current = true; setBusy(true); setActiveAction(action ?? null); try { @@ -287,6 +293,7 @@ export default function RepoView({ path, isActive, onLoaded, onOpenPath }: Props } catch (e) { notify(String(e), true); } finally { + busyRef.current = false; setBusy(false); setActiveAction(null); } @@ -778,13 +785,10 @@ export default function RepoView({ path, isActive, onLoaded, onOpenPath }: Props return () => window.removeEventListener("keydown", onKey); }, [isActive, onPush, onPullAction]); - // Auto-fetch: mirror `busy` into a ref, re-read the setting on a prefs change, - // and run a background fetch on the active tab at the chosen interval. Skips a - // tick while an op is in flight or the window is hidden, and never toasts on - // failure (a background fetch offline shouldn't nag). - useEffect(() => { - busyRef.current = busy; - }, [busy]); + // Auto-fetch: re-read the setting on a prefs change, and run a background + // fetch on the active tab at the chosen interval. Skips a tick while an op is + // in flight (busyRef, owned by run()) or the window is hidden, and never + // toasts on failure (a background fetch offline shouldn't nag). useEffect(() => { const onPrefs = () => setAutoFetchTick((t) => t + 1); window.addEventListener("gitchef:prefs", onPrefs);