From f9f192045264af4cb9d953fde7b43b8e81e52dae Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Mon, 23 Mar 2026 13:57:38 +0100 Subject: [PATCH] git: Split up compute snapshot to improve repository responsiveness (#51882) ## Context This should allow the UI to update faster with stateful git operations like branch changes, commits, etc. This will also allow Zed to recognized when a new project belongs to a branch sooner than before. ## Self-Review Checklist - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Release Notes: - git: Speed up UI responsiveness to branch/worktree changes --- crates/project/src/git_store.rs | 207 ++++++++++++++++++++------------ 1 file changed, 132 insertions(+), 75 deletions(-) diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index 85278dae6eecf09e70343976823c2285e1d24f39..3a4653345d7a84e702e657e80a360eeae00385ab 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -6345,22 +6345,9 @@ impl Repository { let RepositoryState::Local(LocalRepositoryState { backend, .. }) = state else { bail!("not a local repository") }; - let compute_snapshot = this.update(&mut cx, |this, _| { - this.paths_needing_status_update.clear(); - compute_snapshot( - this.id, - this.work_directory_abs_path.clone(), - this.snapshot.clone(), - backend.clone(), - ) - }); - let (snapshot, events) = cx.background_spawn(compute_snapshot).await?; + let snapshot = compute_snapshot(this.clone(), backend.clone(), &mut cx).await?; this.update(&mut cx, |this, cx| { - this.snapshot = snapshot.clone(); this.clear_pending_ops(cx); - for event in events { - cx.emit(event); - } }); if let Some(updates_tx) = updates_tx { updates_tx @@ -7087,47 +7074,124 @@ fn proto_to_commit_details(proto: &proto::GitCommitDetails) -> CommitDetails { } } +/// This snapshot computes the repository state on the foreground thread while +/// running the git commands on the background thread. We update branch, head, +/// remotes, and worktrees first so the UI can react sooner, then compute file +/// state and emit those events immediately after. async fn compute_snapshot( - id: RepositoryId, - work_directory_abs_path: Arc, - prev_snapshot: RepositorySnapshot, + this: Entity, backend: Arc, -) -> Result<(RepositorySnapshot, Vec)> { - let mut events = Vec::new(); - let branches = backend.branches().await?; - let branch = branches.into_iter().find(|branch| branch.is_head); - - // Useful when branch is None in detached head state - let head_commit = match backend.head_sha().await { - Some(head_sha) => backend.show(head_sha).await.log_err(), - None => None, - }; + cx: &mut AsyncApp, +) -> Result { + let (id, work_directory_abs_path, prev_snapshot) = this.update(cx, |this, _| { + this.paths_needing_status_update.clear(); + ( + this.id, + this.work_directory_abs_path.clone(), + this.snapshot.clone(), + ) + }); - let diff_stat_future: BoxFuture<'_, Result> = if head_commit.is_some() { - backend.diff_stat(&[]) - } else { - future::ready(Ok(status::GitDiffStat { - entries: Arc::default(), - })) - .boxed() + let head_commit_future = { + let backend = backend.clone(); + async move { + Ok(match backend.head_sha().await { + Some(head_sha) => backend.show(head_sha).await.log_err(), + None => None, + }) + } }; - let (statuses, diff_stats, all_worktrees) = futures::future::try_join3( - backend.status(&[RepoPath::from_rel_path( - &RelPath::new(".".as_ref(), PathStyle::local()).unwrap(), - )]), - diff_stat_future, - backend.worktrees(), - ) - .await?; + let (branches, head_commit, all_worktrees) = cx + .background_spawn({ + let backend = backend.clone(); + async move { + futures::future::try_join3( + backend.branches(), + head_commit_future, + backend.worktrees(), + ) + .await + } + }) + .await?; + let branch = branches.into_iter().find(|branch| branch.is_head); let linked_worktrees: Arc<[GitWorktree]> = all_worktrees .into_iter() .filter(|wt| wt.path != *work_directory_abs_path) .collect(); + let (remote_origin_url, remote_upstream_url) = cx + .background_spawn({ + let backend = backend.clone(); + async move { + Ok::<_, anyhow::Error>( + futures::future::join( + backend.remote_url("origin"), + backend.remote_url("upstream"), + ) + .await, + ) + } + }) + .await?; + + let snapshot = this.update(cx, |this, cx| { + let branch_changed = + branch != this.snapshot.branch || head_commit != this.snapshot.head_commit; + let worktrees_changed = *linked_worktrees != *this.snapshot.linked_worktrees; + + this.snapshot = RepositorySnapshot { + id, + work_directory_abs_path, + branch, + head_commit, + remote_origin_url, + remote_upstream_url, + linked_worktrees, + scan_id: prev_snapshot.scan_id + 1, + ..prev_snapshot + }; + + if branch_changed { + cx.emit(RepositoryEvent::BranchChanged); + } + + if worktrees_changed { + cx.emit(RepositoryEvent::GitWorktreeListChanged); + } + + this.snapshot.clone() + }); + + let (statuses, diff_stats, stash_entries) = cx + .background_spawn({ + let backend = backend.clone(); + let snapshot = snapshot.clone(); + async move { + let diff_stat_future: BoxFuture<'_, Result> = + if snapshot.head_commit.is_some() { + backend.diff_stat(&[]) + } else { + future::ready(Ok(status::GitDiffStat { + entries: Arc::default(), + })) + .boxed() + }; + futures::future::try_join3( + backend.status(&[RepoPath::from_rel_path( + &RelPath::new(".".as_ref(), PathStyle::local()).unwrap(), + )]), + diff_stat_future, + backend.stash_entries(), + ) + .await + } + }) + .await?; + let diff_stat_map: HashMap<&RepoPath, DiffStat> = diff_stats.entries.iter().map(|(p, s)| (p, *s)).collect(); - let stash_entries = backend.stash_entries().await?; let mut conflicted_paths = Vec::new(); let statuses_by_path = SumTree::from_iter( statuses.entries.iter().map(|(repo_path, status)| { @@ -7142,42 +7206,35 @@ async fn compute_snapshot( }), (), ); - let mut merge_details = prev_snapshot.merge; - let conflicts_changed = merge_details.update(&backend, conflicted_paths).await?; - log::debug!("new merge details: {merge_details:?}"); - - if conflicts_changed || statuses_by_path != prev_snapshot.statuses_by_path { - events.push(RepositoryEvent::StatusesChanged) - } - if branch != prev_snapshot.branch || head_commit != prev_snapshot.head_commit { - events.push(RepositoryEvent::BranchChanged); - } - - if *linked_worktrees != *prev_snapshot.linked_worktrees { - events.push(RepositoryEvent::GitWorktreeListChanged); - } + let merge_details = cx + .background_spawn({ + let backend = backend.clone(); + let mut merge_details = snapshot.merge.clone(); + async move { + let conflicts_changed = merge_details.update(&backend, conflicted_paths).await?; + Ok::<_, anyhow::Error>((merge_details, conflicts_changed)) + } + }) + .await?; + let (merge_details, conflicts_changed) = merge_details; + log::debug!("new merge details: {merge_details:?}"); - let remote_origin_url = backend.remote_url("origin").await; - let remote_upstream_url = backend.remote_url("upstream").await; + Ok(this.update(cx, |this, cx| { + if conflicts_changed || statuses_by_path != this.snapshot.statuses_by_path { + cx.emit(RepositoryEvent::StatusesChanged); + } + if stash_entries != this.snapshot.stash_entries { + cx.emit(RepositoryEvent::StashEntriesChanged); + } - let snapshot = RepositorySnapshot { - id, - statuses_by_path, - work_directory_abs_path, - original_repo_abs_path: prev_snapshot.original_repo_abs_path, - path_style: prev_snapshot.path_style, - scan_id: prev_snapshot.scan_id + 1, - branch, - head_commit, - merge: merge_details, - remote_origin_url, - remote_upstream_url, - stash_entries, - linked_worktrees, - }; + this.snapshot.scan_id += 1; + this.snapshot.merge = merge_details; + this.snapshot.statuses_by_path = statuses_by_path; + this.snapshot.stash_entries = stash_entries; - Ok((snapshot, events)) + this.snapshot.clone() + })) } fn status_from_proto(