Clear pending staged/unstaged diff hunks hunks when writing to the git index fails (#26173)

Release Notes:

- Git Beta: Fixed a bug where discarding a hunk in the project diff view
performed two concurrent saves of the buffer.
- Git Beta: Fixed an issue where diff hunks appeared in the wrong state
after failing to write to the git index.
This commit is contained in:
Max Brunsfeld 2025-03-05 18:45:09 -08:00 committed by GitHub
parent d3c68650c0
commit 314ad5dd5f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 534 additions and 143 deletions

View file

@ -1,5 +1,7 @@
use crate::{task_inventory::TaskContexts, Event, *};
use buffer_diff::{assert_hunks, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
use buffer_diff::{
assert_hunks, BufferDiffEvent, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind,
};
use fs::FakeFs;
use futures::{future, StreamExt};
use gpui::{App, SemanticVersion, UpdateGlobal};
@ -5786,7 +5788,7 @@ async fn test_unstaged_diff_for_buffer(cx: &mut gpui::TestAppContext) {
unstaged_diff.update(cx, |unstaged_diff, cx| {
let snapshot = buffer.read(cx).snapshot();
assert_hunks(
unstaged_diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx),
unstaged_diff.hunks(&snapshot, cx),
&snapshot,
&unstaged_diff.base_text_string().unwrap(),
&[
@ -6008,6 +6010,271 @@ async fn test_uncommitted_diff_for_buffer(cx: &mut gpui::TestAppContext) {
});
}
#[gpui::test]
async fn test_staging_hunks(cx: &mut gpui::TestAppContext) {
use DiffHunkSecondaryStatus::*;
init_test(cx);
let committed_contents = r#"
zero
one
two
three
four
five
"#
.unindent();
let file_contents = r#"
one
TWO
three
FOUR
five
"#
.unindent();
let fs = FakeFs::new(cx.background_executor.clone());
fs.insert_tree(
"/dir",
json!({
".git": {},
"file.txt": file_contents.clone()
}),
)
.await;
fs.set_head_for_repo(
"/dir/.git".as_ref(),
&[("file.txt".into(), committed_contents.clone())],
);
fs.set_index_for_repo(
"/dir/.git".as_ref(),
&[("file.txt".into(), committed_contents.clone())],
);
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
let buffer = project
.update(cx, |project, cx| {
project.open_local_buffer("/dir/file.txt", cx)
})
.await
.unwrap();
let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
let uncommitted_diff = project
.update(cx, |project, cx| {
project.open_uncommitted_diff(buffer.clone(), cx)
})
.await
.unwrap();
let mut diff_events = cx.events(&uncommitted_diff);
// The hunks are initially unstaged.
uncommitted_diff.read_with(cx, |diff, cx| {
assert_hunks(
diff.hunks(&snapshot, cx),
&snapshot,
&diff.base_text_string().unwrap(),
&[
(
0..0,
"zero\n",
"",
DiffHunkStatus::deleted(HasSecondaryHunk),
),
(
1..2,
"two\n",
"TWO\n",
DiffHunkStatus::modified(HasSecondaryHunk),
),
(
3..4,
"four\n",
"FOUR\n",
DiffHunkStatus::modified(HasSecondaryHunk),
),
],
);
});
// Stage a hunk. It appears as optimistically staged.
uncommitted_diff.update(cx, |diff, cx| {
let range =
snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_before(Point::new(2, 0));
let hunks = diff
.hunks_intersecting_range(range, &snapshot, cx)
.collect::<Vec<_>>();
diff.stage_or_unstage_hunks(true, &hunks, &snapshot, true, cx);
assert_hunks(
diff.hunks(&snapshot, cx),
&snapshot,
&diff.base_text_string().unwrap(),
&[
(
0..0,
"zero\n",
"",
DiffHunkStatus::deleted(HasSecondaryHunk),
),
(
1..2,
"two\n",
"TWO\n",
DiffHunkStatus::modified(SecondaryHunkRemovalPending),
),
(
3..4,
"four\n",
"FOUR\n",
DiffHunkStatus::modified(HasSecondaryHunk),
),
],
);
});
// The diff emits a change event for the range of the staged hunk.
assert!(matches!(
diff_events.next().await.unwrap(),
BufferDiffEvent::HunksStagedOrUnstaged(_)
));
let event = diff_events.next().await.unwrap();
if let BufferDiffEvent::DiffChanged {
changed_range: Some(changed_range),
} = event
{
let changed_range = changed_range.to_point(&snapshot);
assert_eq!(changed_range, Point::new(1, 0)..Point::new(2, 0));
} else {
panic!("Unexpected event {event:?}");
}
// When the write to the index completes, it appears as staged.
cx.run_until_parked();
uncommitted_diff.update(cx, |diff, cx| {
assert_hunks(
diff.hunks(&snapshot, cx),
&snapshot,
&diff.base_text_string().unwrap(),
&[
(
0..0,
"zero\n",
"",
DiffHunkStatus::deleted(HasSecondaryHunk),
),
(1..2, "two\n", "TWO\n", DiffHunkStatus::modified(None)),
(
3..4,
"four\n",
"FOUR\n",
DiffHunkStatus::modified(HasSecondaryHunk),
),
],
);
});
// The diff emits a change event for the changed index text.
let event = diff_events.next().await.unwrap();
if let BufferDiffEvent::DiffChanged {
changed_range: Some(changed_range),
} = event
{
let changed_range = changed_range.to_point(&snapshot);
assert_eq!(changed_range, Point::new(0, 0)..Point::new(5, 0));
} else {
panic!("Unexpected event {event:?}");
}
// Simulate a problem writing to the git index.
fs.set_error_message_for_index_write(
"/dir/.git".as_ref(),
Some("failed to write git index".into()),
);
// Stage another hunk.
uncommitted_diff.update(cx, |diff, cx| {
let range =
snapshot.anchor_before(Point::new(3, 0))..snapshot.anchor_before(Point::new(4, 0));
let hunks = diff
.hunks_intersecting_range(range, &snapshot, cx)
.collect::<Vec<_>>();
diff.stage_or_unstage_hunks(true, &hunks, &snapshot, true, cx);
assert_hunks(
diff.hunks(&snapshot, cx),
&snapshot,
&diff.base_text_string().unwrap(),
&[
(
0..0,
"zero\n",
"",
DiffHunkStatus::deleted(HasSecondaryHunk),
),
(1..2, "two\n", "TWO\n", DiffHunkStatus::modified(None)),
(
3..4,
"four\n",
"FOUR\n",
DiffHunkStatus::modified(SecondaryHunkRemovalPending),
),
],
);
});
assert!(matches!(
diff_events.next().await.unwrap(),
BufferDiffEvent::HunksStagedOrUnstaged(_)
));
let event = diff_events.next().await.unwrap();
if let BufferDiffEvent::DiffChanged {
changed_range: Some(changed_range),
} = event
{
let changed_range = changed_range.to_point(&snapshot);
assert_eq!(changed_range, Point::new(3, 0)..Point::new(4, 0));
} else {
panic!("Unexpected event {event:?}");
}
// When the write fails, the hunk returns to being unstaged.
cx.run_until_parked();
uncommitted_diff.update(cx, |diff, cx| {
assert_hunks(
diff.hunks(&snapshot, cx),
&snapshot,
&diff.base_text_string().unwrap(),
&[
(
0..0,
"zero\n",
"",
DiffHunkStatus::deleted(HasSecondaryHunk),
),
(1..2, "two\n", "TWO\n", DiffHunkStatus::modified(None)),
(
3..4,
"four\n",
"FOUR\n",
DiffHunkStatus::modified(HasSecondaryHunk),
),
],
);
});
let event = diff_events.next().await.unwrap();
if let BufferDiffEvent::DiffChanged {
changed_range: Some(changed_range),
} = event
{
let changed_range = changed_range.to_point(&snapshot);
assert_eq!(changed_range, Point::new(0, 0)..Point::new(5, 0));
} else {
panic!("Unexpected event {event:?}");
}
}
#[gpui::test]
async fn test_single_file_diffs(cx: &mut gpui::TestAppContext) {
init_test(cx);
@ -6065,7 +6332,7 @@ async fn test_single_file_diffs(cx: &mut gpui::TestAppContext) {
uncommitted_diff.update(cx, |uncommitted_diff, cx| {
let snapshot = buffer.read(cx).snapshot();
assert_hunks(
uncommitted_diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx),
uncommitted_diff.hunks(&snapshot, cx),
&snapshot,
&uncommitted_diff.base_text_string().unwrap(),
&[(