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 12:46:27 -04:00 committed by GitHub
parent a66098b485
commit 8b6f3ec647
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 214 additions and 159 deletions

View file

@ -156,7 +156,7 @@ use project::{
},
session::{Session, SessionEvent},
},
git_store::{GitStoreEvent, RepositoryEvent},
git_store::GitStoreEvent,
lsp_store::{
CacheInlayHints, CompletionDocumentation, FormatTrigger, LspFormatTarget,
OpenLspBufferHandle,
@ -1978,14 +1978,7 @@ impl Editor {
let git_store = project.read(cx).git_store().clone();
let project = project.clone();
project_subscriptions.push(cx.subscribe(&git_store, move |this, _, event, cx| {
if let GitStoreEvent::RepositoryUpdated(
_,
RepositoryEvent::Updated {
new_instance: true, ..
},
_,
) = event
{
if let GitStoreEvent::RepositoryAdded = event {
this.load_diff_task = Some(
update_uncommitted_diff_for_buffer(
cx.entity(),

View file

@ -12615,18 +12615,6 @@ async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
)
.await;
cx.run_until_parked();
// Set up a buffer white some trailing whitespace and no trailing newline.
cx.set_state(
&[
"one ", //
"twoˇ", //
"three ", //
"four", //
]
.join("\n"),
);
// Record which buffer changes have been sent to the language server
let buffer_changes = Arc::new(Mutex::new(Vec::new()));
cx.lsp
@ -12641,8 +12629,6 @@ async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
);
}
});
cx.run_until_parked();
// Handle formatting requests to the language server.
cx.lsp
.set_request_handler::<lsp::request::Formatting, _, _>({
@ -12691,6 +12677,18 @@ async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
}
});
// Set up a buffer white some trailing whitespace and no trailing newline.
cx.set_state(
&[
"one ", //
"twoˇ", //
"three ", //
"four", //
]
.join("\n"),
);
cx.run_until_parked();
// Submit a format request.
let format = cx
.update_editor(|editor, window, cx| editor.format(&Format, window, cx))

View file

@ -16,7 +16,7 @@ use markdown::Markdown;
use multi_buffer::{MultiBuffer, RowInfo};
use project::{
Project, ProjectItem as _,
git_store::{GitStoreEvent, Repository, RepositoryEvent},
git_store::{GitStoreEvent, Repository},
};
use smallvec::SmallVec;
use std::{sync::Arc, time::Duration};
@ -235,8 +235,8 @@ impl GitBlame {
let git_store = project.read(cx).git_store().clone();
let git_store_subscription =
cx.subscribe(&git_store, move |this, _, event, cx| match event {
GitStoreEvent::RepositoryUpdated(_, RepositoryEvent::Updated { .. }, _)
| GitStoreEvent::RepositoryAdded(_)
GitStoreEvent::RepositoryUpdated(_, _, _)
| GitStoreEvent::RepositoryAdded
| GitStoreEvent::RepositoryRemoved(_) => {
log::debug!("Status of git repositories updated. Regenerating blame data...",);
this.generate(cx);

View file

@ -11,14 +11,20 @@ use git::{
},
status::{FileStatus, GitStatus, StatusCode, TrackedStatus, UnmergedStatus},
};
use gpui::{AsyncApp, BackgroundExecutor, SharedString, Task};
use gpui::{AsyncApp, BackgroundExecutor, SharedString, Task, TaskLabel};
use ignore::gitignore::GitignoreBuilder;
use parking_lot::Mutex;
use rope::Rope;
use smol::future::FutureExt as _;
use std::{path::PathBuf, sync::Arc};
use std::{
path::PathBuf,
sync::{Arc, LazyLock},
};
use util::{paths::PathStyle, rel_path::RelPath};
pub static LOAD_INDEX_TEXT_TASK: LazyLock<TaskLabel> = LazyLock::new(TaskLabel::new);
pub static LOAD_HEAD_TEXT_TASK: LazyLock<TaskLabel> = LazyLock::new(TaskLabel::new);
#[derive(Clone)]
pub struct FakeGitRepository {
pub(crate) fs: Arc<FakeFs>,
@ -79,33 +85,29 @@ impl GitRepository for FakeGitRepository {
fn reload_index(&self) {}
fn load_index_text(&self, path: RepoPath) -> BoxFuture<'_, Option<String>> {
async {
self.with_state_async(false, move |state| {
state
.index_contents
.get(&path)
.context("not present in index")
.cloned()
})
.await
.ok()
}
.boxed()
let fut = self.with_state_async(false, move |state| {
state
.index_contents
.get(&path)
.context("not present in index")
.cloned()
});
self.executor
.spawn_labeled(*LOAD_INDEX_TEXT_TASK, async move { fut.await.ok() })
.boxed()
}
fn load_committed_text(&self, path: RepoPath) -> BoxFuture<'_, Option<String>> {
async {
self.with_state_async(false, move |state| {
state
.head_contents
.get(&path)
.context("not present in HEAD")
.cloned()
})
.await
.ok()
}
.boxed()
let fut = self.with_state_async(false, move |state| {
state
.head_contents
.get(&path)
.context("not present in HEAD")
.cloned()
});
self.executor
.spawn_labeled(*LOAD_HEAD_TEXT_TASK, async move { fut.await.ok() })
.boxed()
}
fn load_commit(

View file

@ -58,6 +58,9 @@ use smol::io::AsyncReadExt;
#[cfg(any(test, feature = "test-support"))]
use std::ffi::OsStr;
#[cfg(any(test, feature = "test-support"))]
pub use fake_git_repo::{LOAD_HEAD_TEXT_TASK, LOAD_INDEX_TEXT_TASK};
pub trait Watcher: Send + Sync {
fn add(&self, path: &Path) -> Result<()>;
fn remove(&self, path: &Path) -> Result<()>;

View file

@ -425,13 +425,20 @@ impl GitPanel {
}
GitStoreEvent::RepositoryUpdated(
_,
RepositoryEvent::Updated { full_scan, .. },
RepositoryEvent::StatusesChanged { full_scan: true }
| RepositoryEvent::BranchChanged
| RepositoryEvent::MergeHeadsChanged,
true,
) => {
this.schedule_update(*full_scan, window, cx);
this.schedule_update(true, window, cx);
}
GitStoreEvent::RepositoryAdded(_) | GitStoreEvent::RepositoryRemoved(_) => {
GitStoreEvent::RepositoryUpdated(
_,
RepositoryEvent::StatusesChanged { full_scan: false },
true,
)
| GitStoreEvent::RepositoryAdded
| GitStoreEvent::RepositoryRemoved(_) => {
this.schedule_update(false, window, cx);
}
GitStoreEvent::IndexWriteError(error) => {

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");
}
}

View file

@ -72,7 +72,7 @@ impl StashList {
if let Some(repo) = repository.clone() {
_subscriptions.push(
cx.subscribe_in(&repo, window, |this, _, event, window, cx| {
if matches!(event, RepositoryEvent::Updated { .. }) {
if matches!(event, RepositoryEvent::StashEntriesChanged) {
let stash_entries = this.picker.read_with(cx, |picker, cx| {
picker
.delegate

View file

@ -166,8 +166,8 @@ impl MultiBufferDiffHunk {
#[derive(PartialEq, Eq, Ord, PartialOrd, Clone, Hash, Debug)]
pub struct PathKey {
// Used by the derived PartialOrd & Ord
sort_prefix: Option<u64>,
path: Arc<RelPath>,
pub sort_prefix: Option<u64>,
pub path: Arc<RelPath>,
}
impl PathKey {
@ -190,11 +190,6 @@ impl PathKey {
}
}
}
#[cfg(any(test, feature = "test-support"))]
pub fn path(&self) -> &Arc<RelPath> {
&self.path
}
}
pub type MultiBufferPoint = Point;

View file

@ -301,9 +301,13 @@ pub enum RepositoryState {
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum RepositoryEvent {
Updated { full_scan: bool, new_instance: bool },
StatusesChanged {
// TODO could report which statuses changed here
full_scan: bool,
},
MergeHeadsChanged,
PathsChanged,
BranchChanged,
StashEntriesChanged,
}
#[derive(Clone, Debug)]
@ -313,7 +317,7 @@ pub struct JobsUpdated;
pub enum GitStoreEvent {
ActiveRepositoryChanged(Option<RepositoryId>),
RepositoryUpdated(RepositoryId, RepositoryEvent, bool),
RepositoryAdded(RepositoryId),
RepositoryAdded,
RepositoryRemoved(RepositoryId),
IndexWriteError(anyhow::Error),
JobsUpdated,
@ -1218,7 +1222,7 @@ impl GitStore {
self._subscriptions
.push(cx.subscribe(&repo, Self::on_jobs_updated));
self.repositories.insert(id, repo);
cx.emit(GitStoreEvent::RepositoryAdded(id));
cx.emit(GitStoreEvent::RepositoryAdded);
self.active_repo_id.get_or_insert_with(|| {
cx.emit(GitStoreEvent::ActiveRepositoryChanged(Some(id)));
id
@ -1485,11 +1489,10 @@ impl GitStore {
let id = RepositoryId::from_proto(update.id);
let client = this.upstream_client().context("no upstream client")?;
let mut is_new = false;
let mut repo_subscription = None;
let repo = this.repositories.entry(id).or_insert_with(|| {
is_new = true;
let git_store = cx.weak_entity();
cx.new(|cx| {
let repo = cx.new(|cx| {
Repository::remote(
id,
Path::new(&update.abs_path).into(),
@ -1499,16 +1502,16 @@ impl GitStore {
git_store,
cx,
)
})
});
repo_subscription = Some(cx.subscribe(&repo, Self::on_repository_event));
cx.emit(GitStoreEvent::RepositoryAdded);
repo
});
if is_new {
this._subscriptions
.push(cx.subscribe(repo, Self::on_repository_event))
}
this._subscriptions.extend(repo_subscription);
repo.update(cx, {
let update = update.clone();
|repo, cx| repo.apply_remote_update(update, is_new, cx)
|repo, cx| repo.apply_remote_update(update, cx)
})?;
this.active_repo_id.get_or_insert_with(|| {
@ -3877,18 +3880,15 @@ impl Repository {
environment,
..
} => {
// TODO would be nice to not have to do this manually
let result = backend.stash_drop(index, environment).await;
if result.is_ok()
&& let Ok(stash_entries) = backend.stash_entries().await
{
let snapshot = this.update(&mut cx, |this, cx| {
this.snapshot.stash_entries = stash_entries;
let snapshot = this.snapshot.clone();
cx.emit(RepositoryEvent::Updated {
full_scan: false,
new_instance: false,
});
snapshot
cx.emit(RepositoryEvent::StashEntriesChanged);
this.snapshot.clone()
})?;
if let Some(updates_tx) = updates_tx {
updates_tx
@ -4048,18 +4048,15 @@ impl Repository {
cx.clone(),
)
.await;
// TODO would be nice to not have to do this manually
if result.is_ok() {
let branches = backend.branches().await?;
let branch = branches.into_iter().find(|branch| branch.is_head);
log::info!("head branch after scan is {branch:?}");
let snapshot = this.update(&mut cx, |this, cx| {
this.snapshot.branch = branch;
let snapshot = this.snapshot.clone();
cx.emit(RepositoryEvent::Updated {
full_scan: false,
new_instance: false,
});
snapshot
cx.emit(RepositoryEvent::BranchChanged);
this.snapshot.clone()
})?;
if let Some(updates_tx) = updates_tx {
updates_tx
@ -4458,7 +4455,6 @@ impl Repository {
pub(crate) fn apply_remote_update(
&mut self,
update: proto::UpdateRepository,
is_new: bool,
cx: &mut Context<Self>,
) -> Result<()> {
let conflicted_paths = TreeSet::from_ordered_entries(
@ -4467,21 +4463,30 @@ impl Repository {
.into_iter()
.filter_map(|path| RepoPath::from_proto(&path).log_err()),
);
self.snapshot.branch = update.branch_summary.as_ref().map(proto_to_branch);
self.snapshot.head_commit = update
let new_branch = update.branch_summary.as_ref().map(proto_to_branch);
let new_head_commit = update
.head_commit_details
.as_ref()
.map(proto_to_commit_details);
if self.snapshot.branch != new_branch || self.snapshot.head_commit != new_head_commit {
cx.emit(RepositoryEvent::BranchChanged)
}
self.snapshot.branch = new_branch;
self.snapshot.head_commit = new_head_commit;
self.snapshot.merge.conflicted_paths = conflicted_paths;
self.snapshot.merge.message = update.merge_message.map(SharedString::from);
self.snapshot.stash_entries = GitStash {
let new_stash_entries = GitStash {
entries: update
.stash_entries
.iter()
.filter_map(|entry| proto_to_stash(entry).ok())
.collect(),
};
if self.snapshot.stash_entries != new_stash_entries {
cx.emit(RepositoryEvent::StashEntriesChanged)
}
self.snapshot.stash_entries = new_stash_entries;
let edits = update
.removed_statuses
@ -4500,14 +4505,13 @@ impl Repository {
}),
)
.collect::<Vec<_>>();
if !edits.is_empty() {
cx.emit(RepositoryEvent::StatusesChanged { full_scan: true });
}
self.snapshot.statuses_by_path.edit(edits, ());
if update.is_last_update {
self.snapshot.scan_id = update.scan_id;
}
cx.emit(RepositoryEvent::Updated {
full_scan: true,
new_instance: is_new,
});
Ok(())
}
@ -4830,23 +4834,19 @@ impl Repository {
.await;
this.update(&mut cx, |this, cx| {
let needs_update = !changed_path_statuses.is_empty()
|| this.snapshot.stash_entries != stash_entries;
this.snapshot.stash_entries = stash_entries;
if this.snapshot.stash_entries != stash_entries {
cx.emit(RepositoryEvent::StashEntriesChanged);
this.snapshot.stash_entries = stash_entries;
}
if !changed_path_statuses.is_empty() {
cx.emit(RepositoryEvent::StatusesChanged { full_scan: false });
this.snapshot
.statuses_by_path
.edit(changed_path_statuses, ());
this.snapshot.scan_id += 1;
}
if needs_update {
cx.emit(RepositoryEvent::Updated {
full_scan: false,
new_instance: false,
});
}
if let Some(updates_tx) = updates_tx {
updates_tx
.unbounded_send(DownstreamUpdate::UpdateRepository(
@ -4854,7 +4854,6 @@ impl Repository {
))
.ok();
}
cx.emit(RepositoryEvent::PathsChanged);
})
},
);
@ -5117,28 +5116,24 @@ async fn compute_snapshot(
MergeDetails::load(&backend, &statuses_by_path, &prev_snapshot).await?;
log::debug!("new merge details (changed={merge_heads_changed:?}): {merge_details:?}");
if merge_heads_changed
|| branch != prev_snapshot.branch
|| statuses_by_path != prev_snapshot.statuses_by_path
{
events.push(RepositoryEvent::Updated {
full_scan: true,
new_instance: false,
});
}
// Cache merge conflict paths so they don't change from staging/unstaging,
// until the merge heads change (at commit time, etc.).
if merge_heads_changed {
events.push(RepositoryEvent::MergeHeadsChanged);
}
if statuses_by_path != prev_snapshot.statuses_by_path {
events.push(RepositoryEvent::StatusesChanged { full_scan: true })
}
// 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,
};
if branch != prev_snapshot.branch || head_commit != prev_snapshot.head_commit {
events.push(RepositoryEvent::BranchChanged);
}
// Used by edit prediction data collection
let remote_origin_url = backend.remote_url("origin");
let remote_upstream_url = backend.remote_url("upstream");

View file

@ -8936,10 +8936,7 @@ async fn test_ignored_dirs_events(cx: &mut gpui::TestAppContext) {
assert_eq!(
repository_updates.lock().drain(..).collect::<Vec<_>>(),
vec![
RepositoryEvent::Updated {
full_scan: true,
new_instance: false,
},
RepositoryEvent::StatusesChanged { full_scan: true },
RepositoryEvent::MergeHeadsChanged,
],
"Initial worktree scan should produce a repo update event"
@ -9000,7 +8997,6 @@ async fn test_ignored_dirs_events(cx: &mut gpui::TestAppContext) {
repository_updates
.lock()
.iter()
.filter(|update| !matches!(update, RepositoryEvent::PathsChanged))
.cloned()
.collect::<Vec<_>>(),
Vec::new(),
@ -9104,17 +9100,10 @@ async fn test_odd_events_for_ignored_dirs(
});
assert_eq!(
repository_updates
.lock()
.drain(..)
.filter(|update| !matches!(update, RepositoryEvent::PathsChanged))
.collect::<Vec<_>>(),
repository_updates.lock().drain(..).collect::<Vec<_>>(),
vec![
RepositoryEvent::Updated {
full_scan: true,
new_instance: false,
},
RepositoryEvent::MergeHeadsChanged,
RepositoryEvent::BranchChanged
],
"Initial worktree scan should produce a repo update event"
);
@ -9142,7 +9131,6 @@ async fn test_odd_events_for_ignored_dirs(
repository_updates
.lock()
.iter()
.filter(|update| !matches!(update, RepositoryEvent::PathsChanged))
.cloned()
.collect::<Vec<_>>(),
Vec::new(),

View file

@ -496,8 +496,12 @@ impl ProjectPanel {
&git_store,
window,
|this, _, event, window, cx| match event {
GitStoreEvent::RepositoryUpdated(_, RepositoryEvent::Updated { .. }, _)
| GitStoreEvent::RepositoryAdded(_)
GitStoreEvent::RepositoryUpdated(
_,
RepositoryEvent::StatusesChanged { full_scan: _ },
_,
)
| GitStoreEvent::RepositoryAdded
| GitStoreEvent::RepositoryRemoved(_) => {
this.update_visible_entries(None, false, false, window, cx);
cx.notify();

View file

@ -30,10 +30,7 @@ use gpui::{
Subscription, WeakEntity, Window, actions, div,
};
use onboarding_banner::OnboardingBanner;
use project::{
Project, WorktreeSettings,
git_store::{GitStoreEvent, RepositoryEvent},
};
use project::{Project, WorktreeSettings, git_store::GitStoreEvent};
use remote::RemoteConnectionOptions;
use settings::{Settings, SettingsLocation};
use std::sync::Arc;
@ -287,9 +284,7 @@ impl TitleBar {
subscriptions.push(
cx.subscribe(&git_store, move |_, _, event, cx| match event {
GitStoreEvent::ActiveRepositoryChanged(_)
| GitStoreEvent::RepositoryUpdated(_, RepositoryEvent::Updated { .. }, _)
| GitStoreEvent::RepositoryAdded(_)
| GitStoreEvent::RepositoryRemoved(_) => {
| GitStoreEvent::RepositoryUpdated(_, _, true) => {
cx.notify();
}
_ => {}