diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index 5fcf28aff3554149ece954074f312e0fe37a9208..e75bafa2d2bdc3b8854e71d7e1e7c543c131d2ee 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -1399,7 +1399,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(); + } + } _ => {} } } diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index ad2c339d22fdd49d6565ff5be491749cfcac7830..c07ca96cd80a42500768a42a696b871f8c54bf04 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -10045,6 +10045,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( + path!("/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!("/dir/.git").as_ref(), + &[ + ("src/file_1.rs", file_1_committed.clone()), + ("src/file_2.rs", file_2_committed.clone()), + ], + "deadbeef", + ); + fs.set_index_for_repo( + 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(), [path!("/dir").as_ref()], cx).await; + + let buffer = project + .update(cx, |project, cx| { + project.open_local_buffer(path!("/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,