Fix race conditions in updating buffer diffs on git changes (#26409)

Release Notes:

- N/A

---------

Co-authored-by: Cole Miller <m@cole-miller.net>
This commit is contained in:
Max Brunsfeld 2025-03-10 16:52:18 -07:00 committed by GitHub
parent c747cccde3
commit 1a3597d726
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 116 additions and 100 deletions

View file

@ -837,8 +837,8 @@ impl BufferDiff {
language: Option<Arc<Language>>,
language_registry: Option<Arc<LanguageRegistry>>,
cx: &mut AsyncApp,
) -> anyhow::Result<Option<Range<Anchor>>> {
let snapshot = if base_text_changed || language_changed {
) -> anyhow::Result<BufferDiffSnapshot> {
let inner = if base_text_changed || language_changed {
cx.update(|cx| {
Self::build(
buffer.clone(),
@ -860,18 +860,45 @@ impl BufferDiff {
})?
.await
};
this.update(cx, |this, _| this.set_state(snapshot, &buffer))
Ok(BufferDiffSnapshot {
inner,
secondary_diff: None,
})
}
pub fn update_diff_from(
pub fn set_snapshot(
&mut self,
buffer: &text::BufferSnapshot,
other: &Entity<Self>,
new_snapshot: BufferDiffSnapshot,
language_changed: bool,
secondary_changed_range: Option<Range<Anchor>>,
cx: &mut Context<Self>,
) -> Option<Range<Anchor>> {
let other = other.read(cx).inner.clone();
self.set_state(other, buffer)
let changed_range = self.set_state(new_snapshot.inner, buffer);
if language_changed {
cx.emit(BufferDiffEvent::LanguageChanged);
}
let changed_range = match (secondary_changed_range, changed_range) {
(None, None) => None,
(Some(unstaged_range), None) => self.range_to_hunk_range(unstaged_range, &buffer, cx),
(None, Some(uncommitted_range)) => Some(uncommitted_range),
(Some(unstaged_range), Some(uncommitted_range)) => {
let mut start = uncommitted_range.start;
let mut end = uncommitted_range.end;
if let Some(unstaged_range) = self.range_to_hunk_range(unstaged_range, &buffer, cx)
{
start = unstaged_range.start.min(&uncommitted_range.start, &buffer);
end = unstaged_range.end.max(&uncommitted_range.end, &buffer);
}
Some(start..end)
}
};
cx.emit(BufferDiffEvent::DiffChanged {
changed_range: changed_range.clone(),
});
changed_range
}
fn set_state(

View file

@ -6,7 +6,7 @@ use crate::{
};
use ::git::{parse_git_remote_url, BuildPermalinkParams, GitHostingProviderRegistry};
use anyhow::{anyhow, bail, Context as _, Result};
use buffer_diff::{BufferDiff, BufferDiffEvent};
use buffer_diff::BufferDiff;
use client::Client;
use collections::{hash_map, HashMap, HashSet};
use fs::Fs;
@ -217,39 +217,29 @@ impl BufferDiffState {
_ => false,
};
self.recalculate_diff_task = Some(cx.spawn(|this, mut cx| async move {
let mut unstaged_changed_range = None;
let mut new_unstaged_diff = None;
if let Some(unstaged_diff) = &unstaged_diff {
unstaged_changed_range = BufferDiff::update_diff(
unstaged_diff.clone(),
buffer.clone(),
index,
index_changed,
language_changed,
language.clone(),
language_registry.clone(),
&mut cx,
)
.await?;
unstaged_diff.update(&mut cx, |_, cx| {
if language_changed {
cx.emit(BufferDiffEvent::LanguageChanged);
}
if let Some(changed_range) = unstaged_changed_range.clone() {
cx.emit(BufferDiffEvent::DiffChanged {
changed_range: Some(changed_range),
})
}
})?;
new_unstaged_diff = Some(
BufferDiff::update_diff(
unstaged_diff.clone(),
buffer.clone(),
index,
index_changed,
language_changed,
language.clone(),
language_registry.clone(),
&mut cx,
)
.await?,
);
}
let mut new_uncommitted_diff = None;
if let Some(uncommitted_diff) = &uncommitted_diff {
let uncommitted_changed_range =
if let (Some(unstaged_diff), true) = (&unstaged_diff, index_matches_head) {
uncommitted_diff.update(&mut cx, |uncommitted_diff, cx| {
uncommitted_diff.update_diff_from(&buffer, unstaged_diff, cx)
})?
} else {
new_uncommitted_diff = if index_matches_head {
new_unstaged_diff.clone()
} else {
Some(
BufferDiff::update_diff(
uncommitted_diff.clone(),
buffer.clone(),
@ -260,32 +250,32 @@ impl BufferDiffState {
language_registry.clone(),
&mut cx,
)
.await?
};
.await?,
)
}
}
let unstaged_changed_range = if let Some((unstaged_diff, new_unstaged_diff)) =
unstaged_diff.as_ref().zip(new_unstaged_diff.clone())
{
unstaged_diff.update(&mut cx, |diff, cx| {
diff.set_snapshot(&buffer, new_unstaged_diff, language_changed, None, cx)
})?
} else {
None
};
if let Some((uncommitted_diff, new_uncommitted_diff)) =
uncommitted_diff.as_ref().zip(new_uncommitted_diff.clone())
{
uncommitted_diff.update(&mut cx, |uncommitted_diff, cx| {
if language_changed {
cx.emit(BufferDiffEvent::LanguageChanged);
}
let changed_range = match (unstaged_changed_range, uncommitted_changed_range) {
(None, None) => None,
(Some(unstaged_range), None) => {
uncommitted_diff.range_to_hunk_range(unstaged_range, &buffer, cx)
}
(None, Some(uncommitted_range)) => Some(uncommitted_range),
(Some(unstaged_range), Some(uncommitted_range)) => {
let mut start = uncommitted_range.start;
let mut end = uncommitted_range.end;
if let Some(unstaged_range) =
uncommitted_diff.range_to_hunk_range(unstaged_range, &buffer, cx)
{
start = unstaged_range.start.min(&uncommitted_range.start, &buffer);
end = unstaged_range.end.max(&uncommitted_range.end, &buffer);
}
Some(start..end)
}
};
cx.emit(BufferDiffEvent::DiffChanged { changed_range });
uncommitted_diff.set_snapshot(
&buffer,
new_uncommitted_diff,
language_changed,
unstaged_changed_range,
cx,
);
})?;
}
@ -813,8 +803,7 @@ impl LocalBufferStore {
let Some(buffer) = buffer.upgrade() else {
continue;
};
let buffer = buffer.read(cx);
let Some(file) = File::from_dyn(buffer.file()) else {
let Some(file) = File::from_dyn(buffer.read(cx).file()) else {
continue;
};
if file.worktree != worktree_handle {
@ -825,7 +814,6 @@ impl LocalBufferStore {
.iter()
.any(|(work_dir, _)| file.path.starts_with(work_dir))
{
let snapshot = buffer.text_snapshot();
let has_unstaged_diff = diff_state
.unstaged_diff
.as_ref()
@ -835,7 +823,7 @@ impl LocalBufferStore {
.as_ref()
.is_some_and(|set| set.is_upgradable());
diff_state_updates.push((
snapshot.clone(),
buffer,
file.path.clone(),
has_unstaged_diff.then(|| diff_state.index_text.clone()),
has_uncommitted_diff.then(|| diff_state.head_text.clone()),
@ -854,36 +842,33 @@ impl LocalBufferStore {
.background_spawn(async move {
diff_state_updates
.into_iter()
.filter_map(
|(buffer_snapshot, path, current_index_text, current_head_text)| {
let local_repo = snapshot.local_repo_for_path(&path)?;
let relative_path = local_repo.relativize(&path).ok()?;
let index_text = if current_index_text.is_some() {
local_repo.repo().load_index_text(&relative_path)
} else {
None
};
let head_text = if current_head_text.is_some() {
local_repo.repo().load_committed_text(&relative_path)
} else {
None
};
.filter_map(|(buffer, path, current_index_text, current_head_text)| {
let local_repo = snapshot.local_repo_for_path(&path)?;
let relative_path = local_repo.relativize(&path).ok()?;
let index_text = if current_index_text.is_some() {
local_repo.repo().load_index_text(&relative_path)
} else {
None
};
let head_text = if current_head_text.is_some() {
local_repo.repo().load_committed_text(&relative_path)
} else {
None
};
// Avoid triggering a diff update if the base text has not changed.
if let Some((current_index, current_head)) =
current_index_text.as_ref().zip(current_head_text.as_ref())
// Avoid triggering a diff update if the base text has not changed.
if let Some((current_index, current_head)) =
current_index_text.as_ref().zip(current_head_text.as_ref())
{
if current_index.as_deref() == index_text.as_ref()
&& current_head.as_deref() == head_text.as_ref()
{
if current_index.as_deref() == index_text.as_ref()
&& current_head.as_deref() == head_text.as_ref()
{
return None;
}
return None;
}
}
let diff_bases_change = match (
current_index_text.is_some(),
current_head_text.is_some(),
) {
let diff_bases_change =
match (current_index_text.is_some(), current_head_text.is_some()) {
(true, true) => Some(if index_text == head_text {
DiffBasesChange::SetBoth(head_text)
} else {
@ -896,17 +881,17 @@ impl LocalBufferStore {
(false, true) => Some(DiffBasesChange::SetHead(head_text)),
(false, false) => None,
};
Some((buffer_snapshot, diff_bases_change))
},
)
Some((buffer, diff_bases_change))
})
.collect::<Vec<_>>()
})
.await;
this.update(&mut cx, |this, cx| {
for (buffer_snapshot, diff_bases_change) in diff_bases_changes_by_buffer {
for (buffer, diff_bases_change) in diff_bases_changes_by_buffer {
let Some(OpenBuffer::Complete { diff_state, .. }) =
this.opened_buffers.get_mut(&buffer_snapshot.remote_id())
this.opened_buffers.get_mut(&buffer.read(cx).remote_id())
else {
continue;
};
@ -917,8 +902,9 @@ impl LocalBufferStore {
diff_state.update(cx, |diff_state, cx| {
use proto::update_diff_bases::Mode;
let buffer = buffer.read(cx);
if let Some((client, project_id)) = this.downstream_client.as_ref() {
let buffer_id = buffer_snapshot.remote_id().to_proto();
let buffer_id = buffer.remote_id().to_proto();
let (staged_text, committed_text, mode) = match diff_bases_change
.clone()
{
@ -942,8 +928,11 @@ impl LocalBufferStore {
client.send(message).log_err();
}
let _ =
diff_state.diff_bases_changed(buffer_snapshot, diff_bases_change, cx);
let _ = diff_state.diff_bases_changed(
buffer.text_snapshot(),
diff_bases_change,
cx,
);
});
}
})