Fix the project diff sometimes missing updates (#40662)

This PR does two related things:

- First, it gets rid of the undifferentiated `RepositoryEvent::Updated`
in favor of three new events that have clearer definitions:
`BranchChanged`, `StashEntriesChanged`, and `StatusesChanged`. An
implication of this is that we no longer emit a `RepositoryEvent` unless
some git state changed; previously we would emit `RepositoryUpdated`
after doing a git status scan even if no statuses changed.
- Second, it changes the subscription strategy of the project diff to
make it update more robustly. Previously, the project diff only
subscribed to the `GitStore`, so it relied on getting a `GitStoreEvent`
when some buffer's diff hunks changed, even if the git status of the
buffer's file didn't change (e.g. a second hunk in a file that was
already modified). After this PR, it also subscribes to the individual
`BufferDiff` entities for buffers that have a git status, so the
`GitStore` is freed from that responsibility. This also fixes some real
cases where the previous strategy was not effective in keeping the
project diff up to date (captured in a test).

Release Notes:

- Fixed some cases where the project diff would fail to update in
response to git events.
This commit is contained in:
Cole Miller
2025-10-23 16:46:27 +00:00
committed by GitHub
parent a66098b485
commit 8b6f3ec647
13 changed files with 214 additions and 159 deletions
+90 -15
View File
@@ -6,7 +6,7 @@ use crate::{
};
use anyhow::Result;
use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus};
use collections::HashSet;
use collections::{HashMap, HashSet};
use editor::{
Editor, EditorEvent, SelectionEffects,
actions::{GoToHunk, GoToPreviousHunk},
@@ -27,7 +27,7 @@ use language::{Anchor, Buffer, Capability, OffsetRangeExt};
use multi_buffer::{MultiBuffer, PathKey};
use project::{
Project, ProjectPath,
git_store::{GitStore, GitStoreEvent, Repository},
git_store::{GitStore, GitStoreEvent, Repository, RepositoryEvent},
};
use settings::{Settings, SettingsStore};
use std::any::{Any, TypeId};
@@ -57,12 +57,13 @@ pub struct ProjectDiff {
multibuffer: Entity<MultiBuffer>,
editor: Entity<Editor>,
git_store: Entity<GitStore>,
buffer_diff_subscriptions: HashMap<RepoPath, (Entity<BufferDiff>, Subscription)>,
workspace: WeakEntity<Workspace>,
focus_handle: FocusHandle,
update_needed: postage::watch::Sender<()>,
pending_scroll: Option<PathKey>,
_task: Task<Result<()>>,
_subscription: Subscription,
_git_store_subscription: Subscription,
}
#[derive(Debug)]
@@ -177,7 +178,11 @@ impl ProjectDiff {
window,
move |this, _git_store, event, _window, _cx| match event {
GitStoreEvent::ActiveRepositoryChanged(_)
| GitStoreEvent::RepositoryUpdated(_, _, true)
| GitStoreEvent::RepositoryUpdated(
_,
RepositoryEvent::StatusesChanged { full_scan: _ },
true,
)
| GitStoreEvent::ConflictsUpdated => {
*this.update_needed.borrow_mut() = ();
}
@@ -217,10 +222,11 @@ impl ProjectDiff {
focus_handle,
editor,
multibuffer,
buffer_diff_subscriptions: Default::default(),
pending_scroll: None,
update_needed: send,
_task: worker,
_subscription: git_store_subscription,
_git_store_subscription: git_store_subscription,
}
}
@@ -365,6 +371,7 @@ impl ProjectDiff {
self.multibuffer.update(cx, |multibuffer, cx| {
multibuffer.clear(cx);
});
self.buffer_diff_subscriptions.clear();
return vec![];
};
@@ -407,6 +414,8 @@ impl ProjectDiff {
});
self.multibuffer.update(cx, |multibuffer, cx| {
for path in previous_paths {
self.buffer_diff_subscriptions
.remove(&path.path.clone().into());
multibuffer.remove_excerpts_for_path(path, cx);
}
});
@@ -419,9 +428,15 @@ impl ProjectDiff {
window: &mut Window,
cx: &mut Context<Self>,
) {
let path_key = diff_buffer.path_key;
let buffer = diff_buffer.buffer;
let diff = diff_buffer.diff;
let path_key = diff_buffer.path_key.clone();
let buffer = diff_buffer.buffer.clone();
let diff = diff_buffer.diff.clone();
let subscription = cx.subscribe(&diff, move |this, _, _, _| {
*this.update_needed.borrow_mut() = ();
});
self.buffer_diff_subscriptions
.insert(path_key.path.clone().into(), (diff.clone(), subscription));
let conflict_addon = self
.editor
@@ -440,9 +455,10 @@ impl ProjectDiff {
.unwrap_or_default();
let conflicts = conflicts.iter().map(|conflict| conflict.range.clone());
let excerpt_ranges = merge_anchor_ranges(diff_hunk_ranges, conflicts, &snapshot)
.map(|range| range.to_point(&snapshot))
.collect::<Vec<_>>();
let excerpt_ranges =
merge_anchor_ranges(diff_hunk_ranges.into_iter(), conflicts, &snapshot)
.map(|range| range.to_point(&snapshot))
.collect::<Vec<_>>();
let (was_empty, is_excerpt_newly_added) = self.multibuffer.update(cx, |multibuffer, cx| {
let was_empty = multibuffer.is_empty();
@@ -519,8 +535,7 @@ impl ProjectDiff {
self.multibuffer
.read(cx)
.excerpt_paths()
.map(|key| key.path())
.cloned()
.map(|key| key.path.clone())
.collect()
}
}
@@ -1621,8 +1636,8 @@ mod tests {
cx,
&"
- original
+ different
ˇ"
+ ˇdifferent
"
.unindent(),
);
}
@@ -1950,6 +1965,7 @@ mod tests {
.unindent(),
);
// The project diff updates its excerpts when a new hunk appears in a buffer that already has a diff.
let buffer = project
.update(cx, |project, cx| {
project.open_local_buffer(path!("/project/foo.txt"), cx)
@@ -2002,4 +2018,63 @@ mod tests {
.unindent(),
);
}
#[gpui::test]
async fn test_update_on_uncommit(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
path!("/project"),
json!({
".git": {},
"README.md": "# My cool project\n".to_owned()
}),
)
.await;
fs.set_head_and_index_for_repo(
Path::new(path!("/project/.git")),
&[("README.md", "# My cool project\n".to_owned())],
);
let project = Project::test(fs.clone(), [Path::new(path!("/project"))], cx).await;
let worktree_id = project.read_with(cx, |project, cx| {
project.worktrees(cx).next().unwrap().read(cx).id()
});
let (workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
cx.run_until_parked();
let _editor = workspace
.update_in(cx, |workspace, window, cx| {
workspace.open_path((worktree_id, rel_path("README.md")), None, true, window, cx)
})
.await
.unwrap()
.downcast::<Editor>()
.unwrap();
cx.focus(&workspace);
cx.update(|window, cx| {
window.dispatch_action(project_diff::Diff.boxed_clone(), cx);
});
cx.run_until_parked();
let item = workspace.update(cx, |workspace, cx| {
workspace.active_item_as::<ProjectDiff>(cx).unwrap()
});
cx.focus(&item);
let editor = item.read_with(cx, |item, _| item.editor.clone());
fs.set_head_and_index_for_repo(
Path::new(path!("/project/.git")),
&[(
"README.md",
"# My cool project\nDetails to come.\n".to_owned(),
)],
);
cx.run_until_parked();
let mut cx = EditorTestContext::for_editor_in(editor, cx).await;
cx.assert_excerpts_with_selections("[EXCERPT]\nˇ# My cool project\nDetails to come.\n");
}
}