From 0c9b8fdba801baef3e0ed8f25b75958b33d545dc Mon Sep 17 00:00:00 2001 From: Marco Mihai Condrache <52580954+marcocondrache@users.noreply.github.com> Date: Wed, 18 Mar 2026 07:11:43 +0100 Subject: [PATCH] git_ui: Avoid blocking main thread during stage/unstage (#47806) With a large number of files in the git status, pressing stage/unstage could cause a noticeable freeze. The async performance profiler showed that the task spawned inside `change_all_files_stage` blocked the main thread for 300ms+, resulting in a clear and visible UI stall. The main cause was the need to traverse all entries to determine what to stage or unstage, which becomes expensive for large change sets. Same repro as: #47800 Release Notes: - Improved latency of stage/unstage operations --- crates/project/src/git_store.rs | 78 ++++++++++++++++++++++----------- 1 file changed, 52 insertions(+), 26 deletions(-) diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index e9330014c3f066705ac3ea1e54f5e498c5d22348..f1a777711d5300f3aae479ce8bf30b6c64425532 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -5022,43 +5022,69 @@ impl Repository { } pub fn stage_all(&mut self, cx: &mut Context) -> Task> { - let to_stage = self - .cached_status() - .filter_map(|entry| { - if let Some(ops) = self.pending_ops_for_path(&entry.repo_path) { - if ops.staging() || ops.staged() { + let snapshot = self.snapshot.clone(); + let pending_ops = self.pending_ops.clone(); + let to_stage = cx.background_spawn(async move { + snapshot + .status() + .filter_map(|entry| { + if let Some(ops) = + pending_ops.get(&PathKey(entry.repo_path.as_ref().clone()), ()) + { + if ops.staging() || ops.staged() { + None + } else { + Some(entry.repo_path) + } + } else if entry.status.staging().is_fully_staged() { None } else { Some(entry.repo_path) } - } else if entry.status.staging().is_fully_staged() { - None - } else { - Some(entry.repo_path) - } - }) - .collect(); - self.stage_or_unstage_entries(true, to_stage, cx) + }) + .collect() + }); + + cx.spawn(async move |this, cx| { + let to_stage = to_stage.await; + this.update(cx, |this, cx| { + this.stage_or_unstage_entries(true, to_stage, cx) + })? + .await + }) } pub fn unstage_all(&mut self, cx: &mut Context) -> Task> { - let to_unstage = self - .cached_status() - .filter_map(|entry| { - if let Some(ops) = self.pending_ops_for_path(&entry.repo_path) { - if !ops.staging() && !ops.staged() { + let snapshot = self.snapshot.clone(); + let pending_ops = self.pending_ops.clone(); + let to_unstage = cx.background_spawn(async move { + snapshot + .status() + .filter_map(|entry| { + if let Some(ops) = + pending_ops.get(&PathKey(entry.repo_path.as_ref().clone()), ()) + { + if !ops.staging() && !ops.staged() { + None + } else { + Some(entry.repo_path) + } + } else if entry.status.staging().is_fully_unstaged() { None } else { Some(entry.repo_path) } - } else if entry.status.staging().is_fully_unstaged() { - None - } else { - Some(entry.repo_path) - } - }) - .collect(); - self.stage_or_unstage_entries(false, to_unstage, cx) + }) + .collect() + }); + + cx.spawn(async move |this, cx| { + let to_unstage = to_unstage.await; + this.update(cx, |this, cx| { + this.stage_or_unstage_entries(false, to_unstage, cx) + })? + .await + }) } pub fn stash_all(&mut self, cx: &mut Context) -> Task> {