Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 122 additions & 13 deletions crates/project/src/git_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -860,6 +860,17 @@ impl GitStore {
.map(|id| self.repositories[id].clone())
}

fn file_is_symlink(file: &File, cx: &App) -> bool {
file.worktree
.read(cx)
.entry_for_path(&file.path)
.is_some_and(|entry| entry.canonical_path.is_some())
}

fn buffer_is_symlink(buffer: &Entity<Buffer>, cx: &App) -> bool {
File::from_dyn(buffer.read(cx).file()).is_some_and(|file| Self::file_is_symlink(file, cx))
}

pub fn open_unstaged_diff(
&mut self,
buffer: Entity<Buffer>,
Expand Down Expand Up @@ -890,13 +901,18 @@ impl GitStore {
return Task::ready(Err(anyhow!("failed to find git repository for buffer")));
};

let is_symlink = Self::buffer_is_symlink(&buffer, cx);
let task = self
.loading_diffs
.entry((buffer_id, DiffKind::Unstaged))
.or_insert_with(|| {
let staged_text = repo.update(cx, |repo, cx| {
repo.load_staged_text(buffer_id, repo_path, cx)
});
let staged_text = if is_symlink {
Task::ready(Ok(None))
} else {
repo.update(cx, |repo, cx| {
repo.load_staged_text(buffer_id, repo_path, cx)
})
};
cx.spawn(async move |this, cx| {
Self::open_diff_internal(
this,
Expand Down Expand Up @@ -1048,13 +1064,18 @@ impl GitStore {
return Task::ready(Err(anyhow!("failed to find git repository for buffer")));
};

let is_symlink = Self::buffer_is_symlink(&buffer, cx);
let task = self
.loading_diffs
.entry((buffer_id, DiffKind::Uncommitted))
.or_insert_with(|| {
let changes = repo.update(cx, |repo, cx| {
repo.load_committed_text(buffer_id, repo_path, cx)
});
let changes = if is_symlink {
Task::ready(Ok(DiffBasesChange::SetBoth(None)))
} else {
repo.update(cx, |repo, cx| {
repo.load_committed_text(buffer_id, repo_path, cx)
})
};

// todo(lw): hot foreground spawn
cx.spawn(async move |this, cx| {
Expand Down Expand Up @@ -1857,14 +1878,18 @@ impl GitStore {
{
let buffer = buffer.clone();
let diff_state = diff_state.clone();
let is_symlink = Self::buffer_is_symlink(&buffer, cx);

cx.spawn(async move |_git_store, cx| {
async {
let diff_bases_change = repo
.update(cx, |repo, cx| {
let diff_bases_change = if is_symlink {
DiffBasesChange::SetBoth(None)
} else {
repo.update(cx, |repo, cx| {
repo.load_committed_text(buffer_id, repo_path, cx)
})
.await?;
.await?
};

diff_state.update(cx, |diff_state, cx| {
let buffer_snapshot = buffer.read(cx).text_snapshot();
Expand Down Expand Up @@ -4770,6 +4795,7 @@ impl Repository {
let file = File::from_dyn(buffer.read(cx).file())?;
let abs_path = file.worktree.read(cx).absolutize(&file.path);
let repo_path = this.abs_path_to_repo_path(&abs_path)?;
let is_symlink = GitStore::file_is_symlink(file, cx);
log::debug!(
"start reload diff bases for repo path {}",
repo_path.as_unix_str()
Expand All @@ -4787,6 +4813,7 @@ impl Repository {
Some((
buffer,
repo_path,
is_symlink,
has_unstaged_diff.then(|| diff_state.index_text.clone()),
has_uncommitted_diff.then(|| diff_state.head_text.clone()),
))
Expand All @@ -4799,15 +4826,20 @@ impl Repository {
let buffer_diff_base_changes = cx
.background_spawn(async move {
let mut changes = Vec::new();
for (buffer, repo_path, current_index_text, current_head_text) in
&repo_diff_state_updates
for (
buffer,
repo_path,
is_symlink,
current_index_text,
current_head_text,
) in &repo_diff_state_updates
{
let index_text = if current_index_text.is_some() {
let index_text = if current_index_text.is_some() && !*is_symlink {
backend.load_index_text(repo_path.clone())
} else {
future::ready(None).boxed()
};
let head_text = if current_head_text.is_some() {
let head_text = if current_head_text.is_some() && !*is_symlink {
backend.load_committed_text(repo_path.clone())
} else {
future::ready(None).boxed()
Expand Down Expand Up @@ -8975,6 +9007,83 @@ mod tests {
});
}

#[gpui::test]
async fn test_open_uncommitted_diff_skips_symlinks(cx: &mut TestAppContext) {
use util::rel_path::rel_path;

init_test(cx);

let fs = FakeFs::new(cx.executor());
fs.insert_tree(
Path::new("/project"),
json!({
".git": {},
"target.txt": "rule one\nrule two\n",
}),
)
.await;
fs.insert_symlink("/project/agents.md", PathBuf::from("target.txt"))
.await;

fs.set_head_and_index_for_repo(
Path::new("/project/.git"),
&[
// git stores the symlink's target path as the blob for `agents.md`
("agents.md", "target.txt".into()),
("target.txt", "rule one\n".into()),
],
);

let project = Project::test(fs.clone(), [Path::new("/project")], cx).await;
project
.update(cx, |project, cx| project.git_scans_complete(cx))
.await;

let worktree_id = project.read_with(cx, |project, cx| {
project.worktrees(cx).next().unwrap().read(cx).id()
});

// symlink file should not produce a base diff
let symlink_buffer = project
.update(cx, |project, cx| {
project.open_buffer((worktree_id, rel_path("agents.md")), cx)
})
.await
.unwrap();
let symlink_diff = project
.update(cx, |project, cx| {
project.open_uncommitted_diff(symlink_buffer, cx)
})
.await
.unwrap();
symlink_diff.read_with(cx, |diff, _| {
assert!(
!diff.base_text_exists(),
"symlinked buffer should not have a git diff base"
);
});

// regular file should still produce a base diff
let regular_buffer = project
.update(cx, |project, cx| {
project.open_buffer((worktree_id, rel_path("target.txt")), cx)
})
.await
.unwrap();
let regular_diff = project
.update(cx, |project, cx| {
project.open_uncommitted_diff(regular_buffer, cx)
})
.await
.unwrap();
regular_diff.read_with(cx, |diff, _| {
assert!(
diff.base_text_exists(),
"regular file should have a git diff base"
);
});
}

#[test]
fn test_new_worktree_path_uses_posix_style_for_remote_paths() {
let work_dir = Path::new("/home/user/dev/lsp-tests");
Expand Down
Loading