From bcc9232bc2405541f0a1807dabdc4ff7621e4cfe Mon Sep 17 00:00:00 2001 From: dino Date: Tue, 4 Nov 2025 21:55:38 +0000 Subject: [PATCH 1/3] chore(project): handle buffer file path changes in git store Update `GitStore.on_buffer_store_event` so that, when a `BufferStoreEvent::BufferChangedFilePath` event is received, we check if there's any diff state for the buffer and, if so, update it according to the new file path, in case the file exists in the repository. --- crates/project/src/git_store.rs | 37 +++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index f44a3cb5701096..8e6d92bd0d6812 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -1335,7 +1335,44 @@ impl GitStore { diffs.remove(buffer_id); } } + BufferStoreEvent::BufferChangedFilePath { buffer, .. } => { + // Whenever a buffer's file path changes, it's possible that the + // new path is actually a path that is being tracked by a git + // repository. In that case, we'll want to update the buffer's + // `BufferDiffState`, in case it already has one. + let buffer_id = buffer.read(cx).remote_id(); + let diff_state = self.diffs.get(&buffer_id); + let repo = self.repository_and_path_for_buffer_id(buffer_id, cx); + + if let Some(diff_state) = diff_state + && let Some((repo, repo_path)) = repo + { + let buffer = buffer.clone(); + let diff_state = diff_state.clone(); + + cx.spawn(async move |_git_store, cx| { + async { + let diff_bases_change = repo + .update(cx, |repo, cx| { + repo.load_committed_text(buffer_id, repo_path, cx) + })? + .await?; + diff_state.update(cx, |diff_state, cx| { + let buffer_snapshot = buffer.read(cx).text_snapshot(); + diff_state.diff_bases_changed( + buffer_snapshot, + Some(diff_bases_change), + cx, + ); + }) + } + .await + .log_err(); + }) + .detach(); + } + } _ => {} } } From 37fbc47dbc3fcd35aa7d0694c3614aa4247b5868 Mon Sep 17 00:00:00 2001 From: dino Date: Thu, 6 Nov 2025 19:52:58 +0000 Subject: [PATCH 2/3] test(project): add test for buffer file path changes with git --- crates/project/src/project_tests.rs | 114 ++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 3a824cb16eeaa0..baf16b0cacc7a7 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -9611,6 +9611,120 @@ async fn test_repository_deduplication(cx: &mut gpui::TestAppContext) { pretty_assertions::assert_eq!(repos, [Path::new(path!("/root/project")).into()]); } +#[gpui::test] +async fn test_buffer_changed_file_path_updates_git_diff(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let file_1_committed = String::from(r#"file_1_committed"#); + let file_1_staged = String::from(r#"file_1_staged"#); + let file_2_committed = String::from(r#"file_2_committed"#); + let file_2_staged = String::from(r#"file_2_staged"#); + let buffer_contents = String::from(r#"buffer"#); + + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree( + "/dir", + json!({ + ".git": {}, + "src": { + "file_1.rs": file_1_committed.clone(), + "file_2.rs": file_2_committed.clone(), + } + }), + ) + .await; + + fs.set_head_for_repo( + Path::new("/dir/.git"), + &[ + ("src/file_1.rs", file_1_committed.clone()), + ("src/file_2.rs", file_2_committed.clone()), + ], + "deadbeef", + ); + fs.set_index_for_repo( + Path::new("/dir/.git"), + &[ + ("src/file_1.rs", file_1_staged.clone()), + ("src/file_2.rs", file_2_staged.clone()), + ], + ); + + let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + + let buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/dir/src/file_1.rs", cx) + }) + .await + .unwrap(); + + buffer.update(cx, |buffer, cx| { + buffer.edit([(0..buffer.len(), buffer_contents.as_str())], None, cx); + }); + + let unstaged_diff = project + .update(cx, |project, cx| { + project.open_unstaged_diff(buffer.clone(), cx) + }) + .await + .unwrap(); + + cx.run_until_parked(); + + unstaged_diff.update(cx, |unstaged_diff, _cx| { + let base_text = unstaged_diff.base_text_string().unwrap(); + assert_eq!(base_text, file_1_staged, "Should start with file_1 staged"); + }); + + // Save the buffer as `file_2.rs`, which should trigger the + // `BufferChangedFilePath` event. + project + .update(cx, |project, cx| { + let worktree_id = project.worktrees(cx).next().unwrap().read(cx).id(); + let path = ProjectPath { + worktree_id, + path: rel_path("src/file_2.rs").into(), + }; + project.save_buffer_as(buffer.clone(), path, cx) + }) + .await + .unwrap(); + + cx.run_until_parked(); + + // Verify that the diff bases have been updated to file_2's contents due to + // the `BufferChangedFilePath` event being handled. + unstaged_diff.update(cx, |unstaged_diff, cx| { + let snapshot = buffer.read(cx).snapshot(); + let base_text = unstaged_diff.base_text_string().unwrap(); + assert_eq!( + base_text, file_2_staged, + "Diff bases should be automatically updated to file_2 staged content" + ); + + let hunks: Vec<_> = unstaged_diff.hunks(&snapshot, cx).collect(); + assert!(!hunks.is_empty(), "Should have diff hunks for file_2"); + }); + + let uncommitted_diff = project + .update(cx, |project, cx| { + project.open_uncommitted_diff(buffer.clone(), cx) + }) + .await + .unwrap(); + + cx.run_until_parked(); + + uncommitted_diff.update(cx, |uncommitted_diff, _cx| { + let base_text = uncommitted_diff.base_text_string().unwrap(); + assert_eq!( + base_text, file_2_committed, + "Uncommitted diff should compare against file_2 committed content" + ); + }); +} + async fn search( project: &Entity, query: SearchQuery, From cbb8ca4558a41c73b62221c570bc05db1f3060a4 Mon Sep 17 00:00:00 2001 From: dino Date: Thu, 6 Nov 2025 20:51:11 +0000 Subject: [PATCH 3/3] test: attempt to fix test in windows --- crates/project/src/project_tests.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index baf16b0cacc7a7..8e8a548d90f4b1 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -9623,7 +9623,7 @@ async fn test_buffer_changed_file_path_updates_git_diff(cx: &mut gpui::TestAppCo let fs = FakeFs::new(cx.background_executor.clone()); fs.insert_tree( - "/dir", + path!("/dir"), json!({ ".git": {}, "src": { @@ -9635,7 +9635,7 @@ async fn test_buffer_changed_file_path_updates_git_diff(cx: &mut gpui::TestAppCo .await; fs.set_head_for_repo( - Path::new("/dir/.git"), + path!("/dir/.git").as_ref(), &[ ("src/file_1.rs", file_1_committed.clone()), ("src/file_2.rs", file_2_committed.clone()), @@ -9643,18 +9643,18 @@ async fn test_buffer_changed_file_path_updates_git_diff(cx: &mut gpui::TestAppCo "deadbeef", ); fs.set_index_for_repo( - Path::new("/dir/.git"), + path!("/dir/.git").as_ref(), &[ ("src/file_1.rs", file_1_staged.clone()), ("src/file_2.rs", file_2_staged.clone()), ], ); - let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; let buffer = project .update(cx, |project, cx| { - project.open_local_buffer("/dir/src/file_1.rs", cx) + project.open_local_buffer(path!("/dir/src/file_1.rs"), cx) }) .await .unwrap();