git: Add support for opening git worktrees (#20164)

This adds support for [git
worktrees](https://matklad.github.io/2024/07/25/git-worktrees.html). It
fixes the errors that show up (git blame not working) and actually adds
support for detecting git changes in a `.git` folder that's outside of
our path (and not even in the ancestor chain of our root path).

(While working on this we discovered that our `.gitignore` handling is
not 100% correct. For example: we do stop processing `.gitignore` files
once we found a `.git` repository and don't go further up the ancestors,
which is correct, but then we also don't take into account the
`excludesFile` that a user might have configured, see:
https://git-scm.com/docs/gitignore)


Closes https://github.com/zed-industries/zed/issues/19842
Closes https://github.com/zed-industries/zed/issues/4670

Release Notes:

- Added support for git worktrees. Zed can now open git worktrees and
the git status in them is correctly handled.

---------

Co-authored-by: Antonio <antonio@zed.dev>
Co-authored-by: Bennet <bennet@zed.dev>
This commit is contained in:
Thorsten Ball 2024-11-06 09:43:39 +01:00 committed by GitHub
parent 3f777f0c68
commit bd03dea296
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 337 additions and 205 deletions

View file

@ -720,7 +720,7 @@ async fn test_rescan_with_gitignore(cx: &mut TestAppContext) {
cx.read(|cx| {
let tree = tree.read(cx);
assert_entry_git_state(tree, "tracked-dir/tracked-file1", None, false);
assert_entry_git_state(tree, "tracked-dir/ancestor-ignored-file1", None, true);
assert_entry_git_state(tree, "tracked-dir/ancestor-ignored-file1", None, false);
assert_entry_git_state(tree, "ignored-dir/ignored-file1", None, true);
});
@ -757,7 +757,7 @@ async fn test_rescan_with_gitignore(cx: &mut TestAppContext) {
Some(GitFileStatus::Added),
false,
);
assert_entry_git_state(tree, "tracked-dir/ancestor-ignored-file2", None, true);
assert_entry_git_state(tree, "tracked-dir/ancestor-ignored-file2", None, false);
assert_entry_git_state(tree, "ignored-dir/ignored-file2", None, true);
assert!(tree.entry_for_path(".git").unwrap().is_ignored);
});
@ -843,7 +843,7 @@ async fn test_write_file(cx: &mut TestAppContext) {
.unwrap();
#[cfg(target_os = "linux")]
fs::watcher::global(|_| {}).unwrap();
fs::linux_watcher::global(|_| {}).unwrap();
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
.await;
@ -2635,6 +2635,12 @@ fn assert_entry_git_state(
is_ignored: bool,
) {
let entry = tree.entry_for_path(path).expect("entry {path} not found");
assert_eq!(entry.git_status, git_status);
assert_eq!(entry.is_ignored, is_ignored);
assert_eq!(
entry.git_status, git_status,
"expected {path} to have git status: {git_status:?}"
);
assert_eq!(
entry.is_ignored, is_ignored,
"expected {path} to have is_ignored: {is_ignored}"
);
}