git: Compute and synchronize diffs from HEAD (#23626)

This PR builds on #21258 to make it possible to use HEAD as a diff base.
The buffer store is extended to support holding multiple change sets,
and collab gains support for synchronizing the committed text of files
when any collaborator requires it.

Not implemented in this PR:

- Exposing the diff from HEAD to the user
- Decorating the diff from HEAD with information about which hunks are
staged

`test_random_multibuffer` now fails first at `SEED=13277`, similar to
the previous high-water mark, but with various bugs in the multibuffer
logic now shaken out.

Release Notes:

- N/A

---------

Co-authored-by: Max <max@zed.dev>
Co-authored-by: Ben <ben@zed.dev>
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
Co-authored-by: Conrad <conrad@zed.dev>
This commit is contained in:
Cole Miller 2025-02-04 15:29:10 -05:00 committed by GitHub
parent 871f98bc4d
commit 5704b50fb1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 2799 additions and 603 deletions

File diff suppressed because it is too large Load diff

View file

@ -1970,6 +1970,20 @@ impl Project {
})
}
pub fn open_uncommitted_changes(
&mut self,
buffer: Entity<Buffer>,
cx: &mut Context<Self>,
) -> Task<Result<Entity<BufferChangeSet>>> {
if self.is_disconnected(cx) {
return Task::ready(Err(anyhow!(ErrorCode::Disconnected)));
}
self.buffer_store.update(cx, |buffer_store, cx| {
buffer_store.open_uncommitted_changes(buffer, cx)
})
}
pub fn open_buffer_by_id(
&mut self,
id: BufferId,

View file

@ -5624,7 +5624,7 @@ async fn test_unstaged_changes_for_buffer(cx: &mut gpui::TestAppContext) {
fs.set_index_for_repo(
Path::new("/dir/.git"),
&[(Path::new("src/main.rs"), staged_contents)],
&[("src/main.rs".into(), staged_contents)],
);
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
@ -5669,7 +5669,7 @@ async fn test_unstaged_changes_for_buffer(cx: &mut gpui::TestAppContext) {
fs.set_index_for_repo(
Path::new("/dir/.git"),
&[(Path::new("src/main.rs"), staged_contents)],
&[("src/main.rs".into(), staged_contents)],
);
cx.run_until_parked();
@ -5684,6 +5684,108 @@ async fn test_unstaged_changes_for_buffer(cx: &mut gpui::TestAppContext) {
});
}
#[gpui::test]
async fn test_uncommitted_changes_for_buffer(cx: &mut gpui::TestAppContext) {
init_test(cx);
let committed_contents = r#"
fn main() {
println!("hello world");
}
"#
.unindent();
let staged_contents = r#"
fn main() {
println!("goodbye world");
}
"#
.unindent();
let file_contents = r#"
// print goodbye
fn main() {
println!("goodbye world");
}
"#
.unindent();
let fs = FakeFs::new(cx.background_executor.clone());
fs.insert_tree(
"/dir",
json!({
".git": {},
"src": {
"main.rs": file_contents,
}
}),
)
.await;
fs.set_index_for_repo(
Path::new("/dir/.git"),
&[("src/main.rs".into(), staged_contents)],
);
fs.set_head_for_repo(
Path::new("/dir/.git"),
&[("src/main.rs".into(), committed_contents)],
);
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
let buffer = project
.update(cx, |project, cx| {
project.open_local_buffer("/dir/src/main.rs", cx)
})
.await
.unwrap();
let uncommitted_changes = project
.update(cx, |project, cx| {
project.open_uncommitted_changes(buffer.clone(), cx)
})
.await
.unwrap();
cx.run_until_parked();
uncommitted_changes.update(cx, |uncommitted_changes, cx| {
let snapshot = buffer.read(cx).snapshot();
assert_hunks(
uncommitted_changes.diff_hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot),
&snapshot,
&uncommitted_changes.base_text.as_ref().unwrap().text(),
&[
(0..1, "", "// print goodbye\n"),
(
2..3,
" println!(\"hello world\");\n",
" println!(\"goodbye world\");\n",
),
],
);
});
let committed_contents = r#"
// print goodbye
fn main() {
}
"#
.unindent();
fs.set_head_for_repo(
Path::new("/dir/.git"),
&[("src/main.rs".into(), committed_contents)],
);
cx.run_until_parked();
uncommitted_changes.update(cx, |uncommitted_changes, cx| {
let snapshot = buffer.read(cx).snapshot();
assert_hunks(
uncommitted_changes.diff_hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot),
&snapshot,
&uncommitted_changes.base_text.as_ref().unwrap().text(),
&[(2..3, "", " println!(\"goodbye world\");\n")],
);
});
}
async fn search(
project: &Entity<Project>,
query: SearchQuery,