Fix missing diff hunks in single-file worktrees (#24377)

Cole Miller created

Release Notes:

- Fixed diff hunks not appearing when opening a single file within a
larger repository

Change summary

crates/project/src/project_tests.rs | 65 +++++++++++++++++++++++++++++++
crates/worktree/src/worktree.rs     | 20 +++++---
2 files changed, 77 insertions(+), 8 deletions(-)

Detailed changes

crates/project/src/project_tests.rs 🔗

@@ -5832,6 +5832,71 @@ async fn test_uncommitted_changes_for_buffer(cx: &mut gpui::TestAppContext) {
     });
 }
 
+#[gpui::test]
+async fn test_single_file_diffs(cx: &mut gpui::TestAppContext) {
+    init_test(cx);
+
+    let committed_contents = r#"
+            fn main() {
+                println!("hello from HEAD");
+            }
+        "#
+    .unindent();
+    let file_contents = r#"
+            fn main() {
+                println!("hello from the working copy");
+            }
+        "#
+    .unindent();
+
+    let fs = FakeFs::new(cx.background_executor.clone());
+    fs.insert_tree(
+        "/dir",
+        json!({
+            ".git": {},
+           "src": {
+               "main.rs": file_contents,
+           }
+        }),
+    )
+    .await;
+
+    fs.set_head_for_repo(
+        Path::new("/dir/.git"),
+        &[("src/main.rs".into(), committed_contents)],
+    );
+
+    let project = Project::test(fs.clone(), ["/dir/src/main.rs".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(),
+            &[(
+                1..2,
+                "    println!(\"hello from HEAD\");\n",
+                "    println!(\"hello from the working copy\");\n",
+            )],
+        );
+    });
+}
+
 async fn search(
     project: &Entity<Project>,
     query: SearchQuery,

crates/worktree/src/worktree.rs 🔗

@@ -372,15 +372,19 @@ impl WorkDirectory {
     /// of the project root folder, then the returned RepoPath is relative to the root
     /// of the repository and not a valid path inside the project.
     pub fn relativize(&self, path: &Path) -> Result<RepoPath> {
-        if let Some(location_in_repo) = &self.location_in_repo {
-            Ok(location_in_repo.join(path).into())
+        let repo_path = if let Some(location_in_repo) = &self.location_in_repo {
+            // Avoid joining a `/` to location_in_repo in the case of a single-file worktree.
+            if path == Path::new("") {
+                RepoPath(location_in_repo.clone())
+            } else {
+                location_in_repo.join(path).into()
+            }
         } else {
-            let relativized_path = path
-                .strip_prefix(&self.path)
-                .map_err(|_| anyhow!("could not relativize {:?} against {:?}", path, self.path))?;
-
-            Ok(relativized_path.into())
-        }
+            path.strip_prefix(&self.path)
+                .map_err(|_| anyhow!("could not relativize {:?} against {:?}", path, self.path))?
+                .into()
+        };
+        Ok(repo_path)
     }
 
     /// This is the opposite operation to `relativize` above