mod worktree_settings_tests;

use anyhow::Result;
use encoding_rs;
use fs::{FakeFs, Fs, PathEventKind, RealFs, RemoveOptions};
use git::{DOT_GIT, GITIGNORE, REPO_EXCLUDE};
use gpui::{AppContext as _, BackgroundExecutor, BorrowAppContext, Context, Task, TestAppContext};
use parking_lot::Mutex;
use postage::stream::Stream;
use pretty_assertions::assert_eq;
use rand::prelude::*;
use rpc::{AnyProtoClient, NoopProtoClient, proto};
use worktree::{Entry, EntryKind, Event, PathChange, Worktree, WorktreeModelHandle};

use serde_json::json;
use settings::{SettingsStore, WorktreeId};
use std::{
    cell::Cell,
    env,
    fmt::Write,
    mem,
    path::{Path, PathBuf},
    rc::Rc,
    sync::Arc,
};
use util::{
    ResultExt, path,
    paths::PathStyle,
    rel_path::{RelPath, rel_path},
    test::TempTree,
};

#[gpui::test]
async fn test_traversal(cx: &mut TestAppContext) {
    init_test(cx);
    let fs = FakeFs::new(cx.background_executor.clone());
    fs.insert_tree(
        "/root",
        json!({
           ".gitignore": "a/b\n",
           "a": {
               "b": "",
               "c": "",
           }
        }),
    )
    .await;

    let tree = Worktree::local(
        Path::new("/root"),
        true,
        fs,
        Default::default(),
        true,
        WorktreeId::from_proto(0),
        &mut cx.to_async(),
    )
    .await
    .unwrap();
    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
        .await;

    tree.read_with(cx, |tree, _| {
        assert_eq!(
            tree.entries(false, 0)
                .map(|entry| entry.path.as_ref())
                .collect::<Vec<_>>(),
            vec![
                rel_path(""),
                rel_path(".gitignore"),
                rel_path("a"),
                rel_path("a/c"),
            ]
        );
        assert_eq!(
            tree.entries(true, 0)
                .map(|entry| entry.path.as_ref())
                .collect::<Vec<_>>(),
            vec![
                rel_path(""),
                rel_path(".gitignore"),
                rel_path("a"),
                rel_path("a/b"),
                rel_path("a/c"),
            ]
        );
    })
}

#[gpui::test(iterations = 10)]
async fn test_circular_symlinks(cx: &mut TestAppContext) {
    init_test(cx);
    let fs = FakeFs::new(cx.background_executor.clone());
    fs.insert_tree(
        "/root",
        json!({
            "lib": {
                "a": {
                    "a.txt": ""
                },
                "b": {
                    "b.txt": ""
                }
            }
        }),
    )
    .await;
    fs.create_symlink("/root/lib/a/lib".as_ref(), "..".into())
        .await
        .unwrap();
    fs.create_symlink("/root/lib/b/lib".as_ref(), "..".into())
        .await
        .unwrap();

    let tree = Worktree::local(
        Path::new("/root"),
        true,
        fs.clone(),
        Default::default(),
        true,
        WorktreeId::from_proto(0),
        &mut cx.to_async(),
    )
    .await
    .unwrap();

    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
        .await;

    tree.read_with(cx, |tree, _| {
        assert_eq!(
            tree.entries(false, 0)
                .map(|entry| entry.path.as_ref())
                .collect::<Vec<_>>(),
            vec![
                rel_path(""),
                rel_path("lib"),
                rel_path("lib/a"),
                rel_path("lib/a/a.txt"),
                rel_path("lib/a/lib"),
                rel_path("lib/b"),
                rel_path("lib/b/b.txt"),
                rel_path("lib/b/lib"),
            ]
        );
    });

    fs.rename(
        Path::new("/root/lib/a/lib"),
        Path::new("/root/lib/a/lib-2"),
        Default::default(),
    )
    .await
    .unwrap();
    cx.executor().run_until_parked();
    tree.read_with(cx, |tree, _| {
        assert_eq!(
            tree.entries(false, 0)
                .map(|entry| entry.path.as_ref())
                .collect::<Vec<_>>(),
            vec![
                rel_path(""),
                rel_path("lib"),
                rel_path("lib/a"),
                rel_path("lib/a/a.txt"),
                rel_path("lib/a/lib-2"),
                rel_path("lib/b"),
                rel_path("lib/b/b.txt"),
                rel_path("lib/b/lib"),
            ]
        );
    });
}

#[gpui::test]
async fn test_symlinks_pointing_outside(cx: &mut TestAppContext) {
    init_test(cx);
    let fs = FakeFs::new(cx.background_executor.clone());
    fs.insert_tree(
        "/root",
        json!({
            "dir1": {
                "deps": {
                    // symlinks here
                },
                "src": {
                    "a.rs": "",
                    "b.rs": "",
                },
            },
            "dir2": {
                "src": {
                    "c.rs": "",
                    "d.rs": "",
                }
            },
            "dir3": {
                "deps": {},
                "src": {
                    "e.rs": "",
                    "f.rs": "",
                    "nested": {
                        "deep.rs": ""
                    }
                },
            }
        }),
    )
    .await;

    // These symlinks point to directories outside of the worktree's root, dir1.
    fs.create_symlink("/root/dir1/deps/dep-dir2".as_ref(), "../../dir2".into())
        .await
        .unwrap();
    fs.create_symlink("/root/dir1/deps/dep-dir3".as_ref(), "../../dir3".into())
        .await
        .unwrap();
    fs.create_symlink(
        "/root/dir1/deps/dep-dir3-alias".as_ref(),
        "../../dir3".into(),
    )
    .await
    .unwrap();
    fs.create_symlink(
        "/root/dir1/deps/dep-dir3-nested".as_ref(),
        "../../dir3/src/nested".into(),
    )
    .await
    .unwrap();

    let tree = Worktree::local(
        Path::new("/root/dir1"),
        true,
        fs.clone(),
        Default::default(),
        true,
        WorktreeId::from_proto(0),
        &mut cx.to_async(),
    )
    .await
    .unwrap();

    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
        .await;

    let tree_updates = Arc::new(Mutex::new(Vec::new()));
    tree.update(cx, |_, cx| {
        let tree_updates = tree_updates.clone();
        cx.subscribe(&tree, move |_, _, event, _| {
            if let Event::UpdatedEntries(update) = event {
                tree_updates.lock().extend(
                    update
                        .iter()
                        .map(|(path, _, change)| (path.clone(), *change)),
                );
            }
        })
        .detach();
    });

    // The symlinked directories are not scanned by default.
    tree.read_with(cx, |tree, _| {
        assert_eq!(
            tree.entries(true, 0)
                .map(|entry| (entry.path.as_ref(), entry.is_external))
                .collect::<Vec<_>>(),
            vec![
                (rel_path(""), false),
                (rel_path("deps"), false),
                (rel_path("deps/dep-dir2"), true),
                (rel_path("deps/dep-dir3"), true),
                (rel_path("deps/dep-dir3-alias"), true),
                (rel_path("deps/dep-dir3-nested"), true),
                (rel_path("src"), false),
                (rel_path("src/a.rs"), false),
                (rel_path("src/b.rs"), false),
            ]
        );

        assert_eq!(
            tree.entry_for_path(rel_path("deps/dep-dir2")).unwrap().kind,
            EntryKind::UnloadedDir
        );
    });

    // Expand one of the symlinked directories.
    tree.read_with(cx, |tree, _| {
        tree.as_local()
            .unwrap()
            .refresh_entries_for_paths(vec![rel_path("deps/dep-dir3").into()])
    })
    .recv()
    .await;

    // The expanded directory's contents are loaded. Subdirectories are
    // not scanned yet.
    tree.read_with(cx, |tree, _| {
        assert_eq!(
            tree.entries(true, 0)
                .map(|entry| (entry.path.as_ref(), entry.is_external))
                .collect::<Vec<_>>(),
            vec![
                (rel_path(""), false),
                (rel_path("deps"), false),
                (rel_path("deps/dep-dir2"), true),
                (rel_path("deps/dep-dir3"), true),
                (rel_path("deps/dep-dir3/deps"), true),
                (rel_path("deps/dep-dir3/src"), true),
                (rel_path("deps/dep-dir3-alias"), true),
                (rel_path("deps/dep-dir3-nested"), true),
                (rel_path("src"), false),
                (rel_path("src/a.rs"), false),
                (rel_path("src/b.rs"), false),
            ]
        );
    });
    assert_eq!(
        mem::take(&mut *tree_updates.lock()),
        &[
            (rel_path("deps/dep-dir3").into(), PathChange::Loaded),
            (rel_path("deps/dep-dir3/deps").into(), PathChange::Loaded),
            (rel_path("deps/dep-dir3/src").into(), PathChange::Loaded)
        ]
    );

    // Expand a subdirectory of one of the symlinked directories.
    tree.read_with(cx, |tree, _| {
        tree.as_local()
            .unwrap()
            .refresh_entries_for_paths(vec![rel_path("deps/dep-dir3/src").into()])
    })
    .recv()
    .await;

    // The expanded subdirectory's contents are loaded.
    tree.read_with(cx, |tree, _| {
        assert_eq!(
            tree.entries(true, 0)
                .map(|entry| (entry.path.as_ref(), entry.is_external))
                .collect::<Vec<_>>(),
            vec![
                (rel_path(""), false),
                (rel_path("deps"), false),
                (rel_path("deps/dep-dir2"), true),
                (rel_path("deps/dep-dir3"), true),
                (rel_path("deps/dep-dir3/deps"), true),
                (rel_path("deps/dep-dir3/src"), true),
                (rel_path("deps/dep-dir3/src/e.rs"), true),
                (rel_path("deps/dep-dir3/src/f.rs"), true),
                (rel_path("deps/dep-dir3/src/nested"), true),
                (rel_path("deps/dep-dir3-alias"), true),
                (rel_path("deps/dep-dir3-nested"), true),
                (rel_path("src"), false),
                (rel_path("src/a.rs"), false),
                (rel_path("src/b.rs"), false),
            ]
        );
    });

    assert_eq!(
        mem::take(&mut *tree_updates.lock()),
        &[
            (rel_path("deps/dep-dir3/src").into(), PathChange::Loaded),
            (
                rel_path("deps/dep-dir3/src/e.rs").into(),
                PathChange::Loaded
            ),
            (
                rel_path("deps/dep-dir3/src/f.rs").into(),
                PathChange::Loaded
            ),
            (
                rel_path("deps/dep-dir3/src/nested").into(),
                PathChange::Loaded
            )
        ]
    );

    // After an external symlink subtree is loaded, changes in the target should be reflected.
    fs.insert_file(Path::new("/root/dir3/src/new.rs"), b"".to_vec())
        .await;

    wait_for_condition(cx, |cx| {
        tree.read_with(cx, |tree, _| {
            tree.entry_for_path(rel_path("deps/dep-dir3/src/new.rs"))
                .is_some()
        })
    })
    .await;

    tree.read_with(cx, |tree, _| {
        assert!(
            tree.entry_for_path(rel_path("deps/dep-dir3/src/new.rs"))
                .is_some()
        );
    });

    tree.read_with(cx, |tree, _| {
        tree.as_local()
            .unwrap()
            .refresh_entries_for_paths(vec![rel_path("deps/dep-dir3-alias").into()])
    })
    .recv()
    .await;

    tree.read_with(cx, |tree, _| {
        tree.as_local()
            .unwrap()
            .refresh_entries_for_paths(vec![rel_path("deps/dep-dir3-alias/src").into()])
    })
    .recv()
    .await;

    tree.read_with(cx, |tree, _| {
        tree.as_local()
            .unwrap()
            .refresh_entries_for_paths(vec![rel_path("deps/dep-dir3-nested").into()])
    })
    .recv()
    .await;
    // Create a file in the shared target subtree. Because dep-dir3 and dep-dir3-alias both
    // point to the same target, both logical paths should observe the new file.
    fs.insert_file(Path::new("/root/dir3/src/shared-new.rs"), b"".to_vec())
        .await;

    wait_for_condition(cx, |cx| {
        tree.read_with(cx, |tree, _| {
            tree.entry_for_path(rel_path("deps/dep-dir3/src/shared-new.rs"))
                .is_some()
                && tree
                    .entry_for_path(rel_path("deps/dep-dir3-alias/src/shared-new.rs"))
                    .is_some()
        })
    })
    .await;

    tree.read_with(cx, |tree, _| {
        assert!(
            tree.entry_for_path(rel_path("deps/dep-dir3/src/shared-new.rs"))
                .is_some()
        );
        assert!(
            tree.entry_for_path(rel_path("deps/dep-dir3-alias/src/shared-new.rs"))
                .is_some()
        );
    });

    // Create a file under the more specific nested target. Longest-prefix matching means this should appear under dep-dir3-nested
    fs.insert_file(
        Path::new("/root/dir3/src/nested/longest-prefix.rs"),
        b"".to_vec(),
    )
    .await;

    wait_for_condition(cx, |cx| {
        tree.read_with(cx, |tree, _| {
            tree.entry_for_path(rel_path("deps/dep-dir3-nested/longest-prefix.rs"))
                .is_some()
        })
    })
    .await;

    tree.read_with(cx, |tree, _| {
        assert!(
            tree.entry_for_path(rel_path("deps/dep-dir3-nested/longest-prefix.rs"))
                .is_some()
        );
        assert!(
            tree.entry_for_path(rel_path("deps/dep-dir3/src/nested/longest-prefix.rs"))
                .is_none()
        );
        assert!(
            tree.entry_for_path(rel_path("deps/dep-dir3-alias/src/nested/longest-prefix.rs"))
                .is_none()
        );
    });
}

#[gpui::test]
async fn test_renaming_subdir_under_symlinked_root_keeps_children(cx: &mut TestAppContext) {
    init_test(cx);
    let fs = FakeFs::new(cx.background_executor.clone());
    fs.insert_tree(
        "/target",
        json!({
            "file1.txt": "",
            "file2.log": "",
            "subdir-a": {
                "config.ini": "",
            },
            "subdir-b": {
                "nested": {
                    "note.md": "",
                },
            },
        }),
    )
    .await;
    fs.create_symlink("/link".as_ref(), "/target".into())
        .await
        .unwrap();

    let tree = Worktree::local(
        Path::new("/link"),
        true,
        fs.clone(),
        Default::default(),
        true,
        WorktreeId::from_proto(0),
        &mut cx.to_async(),
    )
    .await
    .unwrap();

    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
        .await;

    fs.rename(
        Path::new("/link/subdir-a"),
        Path::new("/link/subdir-aa"),
        Default::default(),
    )
    .await
    .unwrap();

    wait_for_condition(cx, |cx| {
        tree.read_with(cx, |tree, _| {
            tree.entry_for_path(rel_path("subdir-a")).is_none()
                && tree
                    .entry_for_path(rel_path("subdir-aa/config.ini"))
                    .is_some()
        })
    })
    .await;

    tree.read_with(cx, |tree, _| {
        assert_eq!(
            tree.entries(true, 0)
                .map(|entry| entry.path.as_ref())
                .collect::<Vec<_>>(),
            vec![
                rel_path(""),
                rel_path("file1.txt"),
                rel_path("file2.log"),
                rel_path("subdir-aa"),
                rel_path("subdir-aa/config.ini"),
                rel_path("subdir-b"),
                rel_path("subdir-b/nested"),
                rel_path("subdir-b/nested/note.md"),
            ]
        );
    });
}

#[gpui::test]
async fn test_symlinked_dir_inside_project(cx: &mut TestAppContext) {
    init_test(cx);
    let fs = FakeFs::new(cx.background_executor.clone());

    fs.insert_tree(
        "/root",
        json!({
            "project": {
                "real-dir": {
                    "existing.rs": "",
                    "nested": {
                        "deep.rs": ""
                    }
                },
                "links": {}
            }
        }),
    )
    .await;

    fs.create_symlink(
        "/root/project/links/internal".as_ref(),
        "../real-dir".into(),
    )
    .await
    .unwrap();

    let tree = Worktree::local(
        Path::new("/root/project"),
        true,
        fs.clone(),
        Default::default(),
        true,
        WorktreeId::from_proto(0),
        &mut cx.to_async(),
    )
    .await
    .unwrap();

    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
        .await;

    tree.read_with(cx, |tree, _| {
        assert_eq!(
            tree.entries(true, 0)
                .map(|entry| (entry.path.as_ref(), entry.is_external))
                .collect::<Vec<_>>(),
            vec![
                (rel_path(""), false),
                (rel_path("links"), false),
                (rel_path("links/internal"), false),
                (rel_path("links/internal/existing.rs"), false),
                (rel_path("links/internal/nested"), false),
                (rel_path("links/internal/nested/deep.rs"), false),
                (rel_path("real-dir"), false),
                (rel_path("real-dir/existing.rs"), false),
                (rel_path("real-dir/nested"), false),
                (rel_path("real-dir/nested/deep.rs"), false),
            ]
        );

        assert_eq!(
            tree.entry_for_path(rel_path("links/internal"))
                .unwrap()
                .kind,
            EntryKind::Dir
        );
    });

    fs.insert_file(Path::new("/root/project/real-dir/new.txt"), b"".to_vec())
        .await;
    wait_for_condition(cx, |cx| {
        tree.read_with(cx, |tree, _| {
            tree.entry_for_path(rel_path("links/internal/new.txt"))
                .is_some()
        })
    })
    .await;

    tree.read_with(cx, |tree, _| {
        assert!(
            tree.entry_for_path(rel_path("links/internal/new.txt"))
                .is_some()
        );
    });

    fs.insert_file(
        Path::new("/root/project/real-dir/nested/inner.txt"),
        b"".to_vec(),
    )
    .await;
    wait_for_condition(cx, |cx| {
        tree.read_with(cx, |tree, _| {
            tree.entry_for_path(rel_path("links/internal/nested/inner.txt"))
                .is_some()
        })
    })
    .await;

    tree.read_with(cx, |tree, _| {
        assert!(
            tree.entry_for_path(rel_path("links/internal/nested/inner.txt"))
                .is_some()
        );
    });
}

#[gpui::test]
async fn test_scan_symlinks_always(cx: &mut TestAppContext) {
    init_test(cx);

    cx.update(|cx| {
        cx.update_global::<SettingsStore, _>(|store, cx| {
            store.update_user_settings(cx, |settings| {
                settings.project.worktree.scan_symlinks =
                    Some(settings::ScanSymlinksSetting::Always);
            });
        });
    });

    let fs = FakeFs::new(cx.background_executor.clone());
    fs.insert_tree(
        "/root",
        json!({
            "dir1": {
                "deps": {
                    // symlink target placed here by create_symlink below
                },
                "src": {
                    "a.rs": "",
                },
            },
            "dir2": {
                "src": {
                    "b.rs": "",
                }
            }
        }),
    )
    .await;

    fs.create_symlink("/root/dir1/deps/dep-dir2".as_ref(), "../../dir2".into())
        .await
        .unwrap();

    let tree = Worktree::local(
        Path::new("/root/dir1"),
        true,
        fs.clone(),
        Default::default(),
        true,
        WorktreeId::from_proto(0),
        &mut cx.to_async(),
    )
    .await
    .unwrap();

    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
        .await;

    // With scan_symlinks = Always, the symlinked directory's contents should be
    // fully visible on the first scan without any manual expansion.
    tree.read_with(cx, |tree, _| {
        assert_eq!(
            tree.entries(true, 0)
                .map(|entry| (entry.path.as_ref(), entry.is_external))
                .collect::<Vec<_>>(),
            vec![
                (rel_path(""), false),
                (rel_path("deps"), false),
                (rel_path("deps/dep-dir2"), true),
                (rel_path("deps/dep-dir2/src"), true),
                (rel_path("deps/dep-dir2/src/b.rs"), true),
                (rel_path("src"), false),
                (rel_path("src/a.rs"), false),
            ]
        );
    });
}

#[gpui::test]
async fn test_scan_symlinks_expanded(cx: &mut TestAppContext) {
    init_test(cx);

    // scan_symlinks defaults to Expanded — no settings change needed.

    let fs = FakeFs::new(cx.background_executor.clone());
    fs.insert_tree(
        "/root",
        json!({
            "dir1": {
                "deps": {
                    // symlink target placed here by create_symlink below
                },
                "src": {
                    "a.rs": "",
                },
            },
            "dir2": {
                "src": {
                    "b.rs": "",
                }
            }
        }),
    )
    .await;

    fs.create_symlink("/root/dir1/deps/dep-dir2".as_ref(), "../../dir2".into())
        .await
        .unwrap();

    let tree = Worktree::local(
        Path::new("/root/dir1"),
        true,
        fs.clone(),
        Default::default(),
        true,
        WorktreeId::from_proto(0),
        &mut cx.to_async(),
    )
    .await
    .unwrap();

    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
        .await;

    // With the default scan_symlinks = Expanded, the symlinked directory
    // should appear as an UnloadedDir entry with no children visible.
    tree.read_with(cx, |tree, _| {
        assert_eq!(
            tree.entries(true, 0)
                .map(|entry| (entry.path.as_ref(), entry.is_external))
                .collect::<Vec<_>>(),
            vec![
                (rel_path(""), false),
                (rel_path("deps"), false),
                (rel_path("deps/dep-dir2"), true),
                (rel_path("src"), false),
                (rel_path("src/a.rs"), false),
            ]
        );

        assert_eq!(
            tree.entry_for_path(rel_path("deps/dep-dir2")).unwrap().kind,
            EntryKind::UnloadedDir
        );
    });

    // Manually expand the symlinked directory.
    tree.read_with(cx, |tree, _| {
        tree.as_local()
            .unwrap()
            .refresh_entries_for_paths(vec![rel_path("deps/dep-dir2").into()])
    })
    .recv()
    .await;

    // After expansion, dep-dir2's immediate children are visible. Subdirectories
    // within it are present but not yet scanned.
    tree.read_with(cx, |tree, _| {
        assert_eq!(
            tree.entries(true, 0)
                .map(|entry| (entry.path.as_ref(), entry.is_external))
                .collect::<Vec<_>>(),
            vec![
                (rel_path(""), false),
                (rel_path("deps"), false),
                (rel_path("deps/dep-dir2"), true),
                (rel_path("deps/dep-dir2/src"), true),
                (rel_path("src"), false),
                (rel_path("src/a.rs"), false),
            ]
        );

        assert_eq!(
            tree.entry_for_path(rel_path("deps/dep-dir2/src"))
                .unwrap()
                .kind,
            EntryKind::UnloadedDir
        );
    });

    // Expand the subdirectory inside the symlinked directory.
    tree.read_with(cx, |tree, _| {
        tree.as_local()
            .unwrap()
            .refresh_entries_for_paths(vec![rel_path("deps/dep-dir2/src").into()])
    })
    .recv()
    .await;

    // After expanding the subdirectory, its files are visible.
    tree.read_with(cx, |tree, _| {
        assert_eq!(
            tree.entries(true, 0)
                .map(|entry| (entry.path.as_ref(), entry.is_external))
                .collect::<Vec<_>>(),
            vec![
                (rel_path(""), false),
                (rel_path("deps"), false),
                (rel_path("deps/dep-dir2"), true),
                (rel_path("deps/dep-dir2/src"), true),
                (rel_path("deps/dep-dir2/src/b.rs"), true),
                (rel_path("src"), false),
                (rel_path("src/a.rs"), false),
            ]
        );
    });
}

#[gpui::test(iterations = 10)]
async fn test_circular_symlinks_always(cx: &mut TestAppContext) {
    init_test(cx);

    cx.update(|cx| {
        cx.update_global::<SettingsStore, _>(|store, cx| {
            store.update_user_settings(cx, |settings| {
                settings.project.worktree.scan_symlinks =
                    Some(settings::ScanSymlinksSetting::Always);
            });
        });
    });

    let fs = FakeFs::new(cx.background_executor.clone());
    fs.insert_tree(
        "/root",
        json!({
            "project": {
                "lib": {
                    "a": {
                        "a.txt": ""
                    }
                },
                "deps": {}
            },
            "outside": {
                "data.txt": ""
            }
        }),
    )
    .await;

    fs.create_symlink("/root/project/deps/ext".as_ref(), "../../outside".into())
        .await
        .unwrap();
    fs.create_symlink("/root/outside/back".as_ref(), "../../project".into())
        .await
        .unwrap();

    let tree = Worktree::local(
        Path::new("/root/project"),
        true,
        fs.clone(),
        Default::default(),
        true,
        WorktreeId::from_proto(0),
        &mut cx.to_async(),
    )
    .await
    .unwrap();

    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
        .await;

    tree.read_with(cx, |tree, _| {
        let entries: Vec<_> = tree
            .entries(true, 0)
            .map(|entry| (entry.path.as_ref(), entry.is_external))
            .collect();

        assert_eq!(
            entries,
            vec![
                (rel_path(""), false),
                (rel_path("deps"), false),
                (rel_path("deps/ext"), true),
                (rel_path("deps/ext/data.txt"), true),
                (rel_path("lib"), false),
                (rel_path("lib/a"), false),
                (rel_path("lib/a/a.txt"), false),
            ]
        );
    });
}

#[gpui::test]
async fn test_scan_symlinks_always_respects_gitignore(cx: &mut TestAppContext) {
    init_test(cx);

    cx.update(|cx| {
        cx.update_global::<SettingsStore, _>(|store, cx| {
            store.update_user_settings(cx, |settings| {
                settings.project.worktree.scan_symlinks =
                    Some(settings::ScanSymlinksSetting::Always);
            });
        });
    });

    let fs = FakeFs::new(cx.background_executor.clone());
    fs.insert_tree(
        "/root",
        json!({
            "project": {
                ".gitignore": "ignored-dep\n",
                "deps": {}
            },
            "external-included": {
                "src": {
                    "included.rs": ""
                }
            },
            "external-ignored": {
                "src": {
                    "ignored.rs": ""
                }
            }
        }),
    )
    .await;

    fs.create_symlink(
        "/root/project/deps/included-dep".as_ref(),
        "../../external-included".into(),
    )
    .await
    .unwrap();
    fs.create_symlink(
        "/root/project/deps/ignored-dep".as_ref(),
        "../../external-ignored".into(),
    )
    .await
    .unwrap();

    let tree = Worktree::local(
        Path::new("/root/project"),
        true,
        fs.clone(),
        Default::default(),
        true,
        WorktreeId::from_proto(0),
        &mut cx.to_async(),
    )
    .await
    .unwrap();

    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
        .await;

    tree.read_with(cx, |tree, _| {
        assert_eq!(
            tree.entries(true, 0)
                .map(|entry| (entry.path.as_ref(), entry.is_external, entry.is_ignored))
                .collect::<Vec<_>>(),
            vec![
                (rel_path(""), false, false),
                (rel_path(".gitignore"), false, false),
                (rel_path("deps"), false, false),
                (rel_path("deps/ignored-dep"), true, true),
                (rel_path("deps/included-dep"), true, false),
                (rel_path("deps/included-dep/src"), true, false),
                (rel_path("deps/included-dep/src/included.rs"), true, false),
            ]
        );

        assert_eq!(
            tree.entry_for_path(rel_path("deps/ignored-dep"))
                .unwrap()
                .kind,
            EntryKind::UnloadedDir
        );
    });
}

// Real-fs counterparts to the FakeFs scan_symlinks tests above. FakeFs does not
// model `fs::canonicalize` against a real filesystem, so platform-specific
// canonicalization or readdir behavior is not covered by the FakeFs tests.
// These tests use a real temp directory and a real symlink to exercise the
// production path on the host platform.
#[cfg(unix)]
#[gpui::test]
async fn test_real_fs_scan_symlinks_always(cx: &mut TestAppContext) {
    cx.executor().allow_parking();
    init_test(cx);

    cx.update(|cx| {
        cx.update_global::<SettingsStore, _>(|store, cx| {
            store.update_user_settings(cx, |settings| {
                settings.project.worktree.scan_symlinks =
                    Some(settings::ScanSymlinksSetting::Always);
            });
        });
    });

    let temp_root = TempTree::new(json!({
        "project": {
            "deps": {},
            "src": {
                "a.rs": "",
            },
        },
        "external": {
            "src": {
                "b.rs": "",
            },
        },
    }));

    // Relative symlink: from temp_root/project/deps/, `../../external` resolves
    // to temp_root/external — outside the worktree root at temp_root/project.
    std::os::unix::fs::symlink(
        "../../external",
        temp_root.path().join("project/deps/dep-external"),
    )
    .unwrap();

    let project_root = temp_root.path().join("project");
    let tree = Worktree::local(
        project_root.as_path(),
        true,
        Arc::new(RealFs::new(None, cx.executor())),
        Default::default(),
        true,
        WorktreeId::from_proto(0),
        &mut cx.to_async(),
    )
    .await
    .unwrap();

    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
        .await;

    tree.read_with(cx, |tree, _| {
        assert_eq!(
            tree.entries(true, 0)
                .map(|entry| (entry.path.as_ref(), entry.is_external))
                .collect::<Vec<_>>(),
            vec![
                (rel_path(""), false),
                (rel_path("deps"), false),
                (rel_path("deps/dep-external"), true),
                (rel_path("deps/dep-external/src"), true),
                (rel_path("deps/dep-external/src/b.rs"), true),
                (rel_path("src"), false),
                (rel_path("src/a.rs"), false),
            ]
        );
    });
}

#[cfg(unix)]
#[gpui::test]
async fn test_real_fs_scan_symlinks_expanded(cx: &mut TestAppContext) {
    cx.executor().allow_parking();
    init_test(cx);

    // scan_symlinks defaults to Expanded — no settings change needed.

    let temp_root = TempTree::new(json!({
        "project": {
            "deps": {},
            "src": {
                "a.rs": "",
            },
        },
        "external": {
            "src": {
                "b.rs": "",
            },
        },
    }));

    std::os::unix::fs::symlink(
        "../../external",
        temp_root.path().join("project/deps/dep-external"),
    )
    .unwrap();

    let project_root = temp_root.path().join("project");
    let tree = Worktree::local(
        project_root.as_path(),
        true,
        Arc::new(RealFs::new(None, cx.executor())),
        Default::default(),
        true,
        WorktreeId::from_proto(0),
        &mut cx.to_async(),
    )
    .await
    .unwrap();

    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
        .await;

    // Before expansion, the symlinked directory should appear as an UnloadedDir
    // with no children visible.
    tree.read_with(cx, |tree, _| {
        assert_eq!(
            tree.entries(true, 0)
                .map(|entry| (entry.path.as_ref(), entry.is_external))
                .collect::<Vec<_>>(),
            vec![
                (rel_path(""), false),
                (rel_path("deps"), false),
                (rel_path("deps/dep-external"), true),
                (rel_path("src"), false),
                (rel_path("src/a.rs"), false),
            ]
        );

        assert_eq!(
            tree.entry_for_path(rel_path("deps/dep-external"))
                .unwrap()
                .kind,
            EntryKind::UnloadedDir
        );
    });

    // Manually expand the symlinked directory. This is the case #51382 was
    // added to fix; if this assertion fails it's a regression of that fix on
    // real filesystems.
    tree.read_with(cx, |tree, _| {
        tree.as_local()
            .unwrap()
            .refresh_entries_for_paths(vec![rel_path("deps/dep-external").into()])
    })
    .recv()
    .await;

    tree.read_with(cx, |tree, _| {
        assert_eq!(
            tree.entries(true, 0)
                .map(|entry| (entry.path.as_ref(), entry.is_external))
                .collect::<Vec<_>>(),
            vec![
                (rel_path(""), false),
                (rel_path("deps"), false),
                (rel_path("deps/dep-external"), true),
                (rel_path("deps/dep-external/src"), true),
                (rel_path("src"), false),
                (rel_path("src/a.rs"), false),
            ]
        );
    });
}

#[gpui::test]
async fn test_internal_symlink_updates_preserve_entry_ids(cx: &mut TestAppContext) {
    init_test(cx);
    let fs = FakeFs::new(cx.background_executor.clone());

    fs.insert_tree(
        "/root",
        json!({
            "project": {
                "real-dir": {
                    "existing.rs": "old",
                },
                "links": {}
            }
        }),
    )
    .await;

    fs.create_symlink(
        "/root/project/links/internal".as_ref(),
        "../real-dir".into(),
    )
    .await
    .unwrap();

    let tree = Worktree::local(
        Path::new("/root/project"),
        true,
        fs.clone(),
        Default::default(),
        true,
        WorktreeId::from_proto(0),
        &mut cx.to_async(),
    )
    .await
    .unwrap();

    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
        .await;

    let (real_entry_id, symlink_entry_id, old_mtime) = tree.read_with(cx, |tree, _| {
        let real_entry = tree
            .entry_for_path(rel_path("real-dir/existing.rs"))
            .unwrap();
        let symlink_entry = tree
            .entry_for_path(rel_path("links/internal/existing.rs"))
            .unwrap();
        assert_eq!(real_entry.inode, symlink_entry.inode);
        assert_eq!(real_entry.mtime, symlink_entry.mtime);
        assert_ne!(real_entry.id, symlink_entry.id);
        (real_entry.id, symlink_entry.id, real_entry.mtime)
    });

    fs.write(Path::new("/root/project/real-dir/existing.rs"), b"new")
        .await
        .unwrap();

    wait_for_condition(cx, |cx| {
        tree.read_with(cx, |tree, _| {
            let real_entry = tree
                .entry_for_path(rel_path("real-dir/existing.rs"))
                .unwrap();
            let symlink_entry = tree
                .entry_for_path(rel_path("links/internal/existing.rs"))
                .unwrap();
            real_entry.mtime != old_mtime && symlink_entry.mtime != old_mtime
        })
    })
    .await;

    tree.read_with(cx, |tree, _| {
        let real_entry = tree
            .entry_for_path(rel_path("real-dir/existing.rs"))
            .unwrap();
        let symlink_entry = tree
            .entry_for_path(rel_path("links/internal/existing.rs"))
            .unwrap();

        assert_eq!(real_entry.inode, symlink_entry.inode);
        assert_eq!(real_entry.id, real_entry_id);
        assert_eq!(
            tree.entry_for_id(real_entry_id).unwrap().path.as_ref(),
            rel_path("real-dir/existing.rs")
        );
        assert_eq!(symlink_entry.id, symlink_entry_id);
        assert_eq!(
            tree.entry_for_id(symlink_entry_id).unwrap().path.as_ref(),
            rel_path("links/internal/existing.rs")
        );
    });
}

#[cfg(target_os = "macos")]
#[gpui::test]
async fn test_renaming_case_only(cx: &mut TestAppContext) {
    cx.executor().allow_parking();
    init_test(cx);

    const OLD_NAME: &str = "aaa.rs";
    const NEW_NAME: &str = "AAA.rs";

    let fs = Arc::new(RealFs::new(None, cx.executor()));
    let temp_root = TempTree::new(json!({
        OLD_NAME: "",
    }));

    let tree = Worktree::local(
        temp_root.path(),
        true,
        fs.clone(),
        Default::default(),
        true,
        WorktreeId::from_proto(0),
        &mut cx.to_async(),
    )
    .await
    .unwrap();

    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
        .await;
    tree.read_with(cx, |tree, _| {
        assert_eq!(
            tree.entries(true, 0)
                .map(|entry| entry.path.as_ref())
                .collect::<Vec<_>>(),
            vec![rel_path(""), rel_path(OLD_NAME)]
        );
    });

    fs.rename(
        &temp_root.path().join(OLD_NAME),
        &temp_root.path().join(NEW_NAME),
        fs::RenameOptions {
            overwrite: true,
            ignore_if_exists: true,
            create_parents: false,
        },
    )
    .await
    .unwrap();

    tree.flush_fs_events(cx).await;

    tree.read_with(cx, |tree, _| {
        assert_eq!(
            tree.entries(true, 0)
                .map(|entry| entry.path.as_ref())
                .collect::<Vec<_>>(),
            vec![rel_path(""), rel_path(NEW_NAME)]
        );
    });
}

#[gpui::test]
async fn test_root_rescan_reconciles_stale_state(cx: &mut TestAppContext) {
    init_test(cx);
    let fs = FakeFs::new(cx.background_executor.clone());
    fs.insert_tree(
        "/root",
        json!({
            "old.txt": "",
        }),
    )
    .await;

    let tree = Worktree::local(
        Path::new("/root"),
        true,
        fs.clone(),
        Default::default(),
        true,
        WorktreeId::from_proto(0),
        &mut cx.to_async(),
    )
    .await
    .unwrap();

    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
        .await;

    tree.read_with(cx, |tree, _| {
        assert_eq!(
            tree.entries(true, 0)
                .map(|entry| entry.path.as_ref())
                .collect::<Vec<_>>(),
            vec![rel_path(""), rel_path("old.txt")]
        );
    });

    fs.pause_events();
    fs.remove_file(Path::new("/root/old.txt"), RemoveOptions::default())
        .await
        .unwrap();
    fs.insert_file(Path::new("/root/new.txt"), Vec::new()).await;
    assert_eq!(fs.buffered_event_count(), 2);
    fs.clear_buffered_events();

    tree.read_with(cx, |tree, _| {
        assert!(tree.entry_for_path(rel_path("old.txt")).is_some());
        assert!(tree.entry_for_path(rel_path("new.txt")).is_none());
    });

    fs.emit_fs_event("/root", Some(fs::PathEventKind::Rescan));
    fs.unpause_events_and_flush();
    tree.flush_fs_events(cx).await;

    tree.read_with(cx, |tree, _| {
        assert!(tree.entry_for_path(rel_path("old.txt")).is_none());
        assert!(tree.entry_for_path(rel_path("new.txt")).is_some());
        assert_eq!(
            tree.entries(true, 0)
                .map(|entry| entry.path.as_ref())
                .collect::<Vec<_>>(),
            vec![rel_path(""), rel_path("new.txt")]
        );
    });
}

#[gpui::test]
async fn test_root_rescan_does_not_miss_event_before_readding_root_watcher(
    cx: &mut TestAppContext,
) {
    init_test(cx);
    let fs = FakeFs::new(cx.background_executor.clone());
    fs.insert_tree("/root", json!({})).await;

    let tree = Worktree::local(
        Path::new("/root"),
        true,
        fs.clone(),
        Default::default(),
        true,
        WorktreeId::from_proto(0),
        &mut cx.to_async(),
    )
    .await
    .unwrap();

    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
        .await;

    fs.create_file_before_next_watch_add("/root", "/root/created-before-root-readd.txt");
    fs.emit_fs_event("/root", Some(PathEventKind::Rescan));

    wait_for_condition(cx, |cx| {
        tree.read_with(cx, |tree, _| {
            tree.entry_for_path(rel_path("created-before-root-readd.txt"))
                .is_some()
        })
    })
    .await;
}

#[gpui::test]
async fn test_subtree_rescan_reports_unchanged_descendants_as_updated(cx: &mut TestAppContext) {
    init_test(cx);
    let fs = FakeFs::new(cx.background_executor.clone());
    fs.insert_tree(
        "/root",
        json!({
            "dir": {
                "child.txt": "",
                "nested": {
                    "grandchild.txt": "",
                },
                "remove": {
                    "removed.txt": "",
                }
            },
            "other.txt": "",
        }),
    )
    .await;

    let tree = Worktree::local(
        Path::new("/root"),
        true,
        fs.clone(),
        Default::default(),
        true,
        WorktreeId::from_proto(0),
        &mut cx.to_async(),
    )
    .await
    .unwrap();

    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
        .await;

    let tree_updates = Arc::new(Mutex::new(Vec::new()));
    tree.update(cx, |_, cx| {
        let tree_updates = tree_updates.clone();
        cx.subscribe(&tree, move |_, _, event, _| {
            if let Event::UpdatedEntries(update) = event {
                tree_updates.lock().extend(
                    update
                        .iter()
                        .filter(|(path, _, _)| path.as_ref() != rel_path("fs-event-sentinel"))
                        .map(|(path, _, change)| (path.clone(), *change)),
                );
            }
        })
        .detach();
    });
    fs.pause_events();
    fs.insert_file("/root/dir/new.txt", b"new content".to_vec())
        .await;
    fs.remove_dir(
        "/root/dir/remove".as_ref(),
        RemoveOptions {
            recursive: true,
            ignore_if_not_exists: false,
        },
    )
    .await
    .unwrap();
    fs.clear_buffered_events();
    fs.unpause_events_and_flush();

    fs.emit_fs_event("/root/dir", Some(fs::PathEventKind::Rescan));
    tree.flush_fs_events(cx).await;

    assert_eq!(
        mem::take(&mut *tree_updates.lock()),
        &[
            (rel_path("dir").into(), PathChange::Updated),
            (rel_path("dir/child.txt").into(), PathChange::Updated),
            (rel_path("dir/nested").into(), PathChange::Updated),
            (
                rel_path("dir/nested/grandchild.txt").into(),
                PathChange::Updated
            ),
            (rel_path("dir/new.txt").into(), PathChange::Added),
            (rel_path("dir/remove").into(), PathChange::Removed),
            (
                rel_path("dir/remove/removed.txt").into(),
                PathChange::Removed
            ),
        ]
    );

    tree.read_with(cx, |tree, _| {
        assert!(tree.entry_for_path(rel_path("other.txt")).is_some());
    });
}

#[gpui::test]
async fn test_open_gitignored_files(cx: &mut TestAppContext) {
    init_test(cx);
    let fs = FakeFs::new(cx.background_executor.clone());
    fs.insert_tree(
        "/root",
        json!({
            ".gitignore": "node_modules\n",
            "one": {
                "node_modules": {
                    "a": {
                        "a1.js": "a1",
                        "a2.js": "a2",
                    },
                    "b": {
                        "b1.js": "b1",
                        "b2.js": "b2",
                    },
                    "c": {
                        "c1.js": "c1",
                        "c2.js": "c2",
                    }
                },
            },
            "two": {
                "x.js": "",
                "y.js": "",
            },
        }),
    )
    .await;

    let tree = Worktree::local(
        Path::new("/root"),
        true,
        fs.clone(),
        Default::default(),
        true,
        WorktreeId::from_proto(0),
        &mut cx.to_async(),
    )
    .await
    .unwrap();

    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
        .await;

    tree.read_with(cx, |tree, _| {
        assert_eq!(
            tree.entries(true, 0)
                .map(|entry| (entry.path.as_ref(), entry.is_ignored))
                .collect::<Vec<_>>(),
            vec![
                (rel_path(""), false),
                (rel_path(".gitignore"), false),
                (rel_path("one"), false),
                (rel_path("one/node_modules"), true),
                (rel_path("two"), false),
                (rel_path("two/x.js"), false),
                (rel_path("two/y.js"), false),
            ]
        );
    });

    // Open a file that is nested inside of a gitignored directory that
    // has not yet been expanded.
    let prev_read_dir_count = fs.read_dir_call_count();
    let loaded = tree
        .update(cx, |tree, cx| {
            tree.load_file(rel_path("one/node_modules/b/b1.js"), cx)
        })
        .await
        .unwrap();

    tree.read_with(cx, |tree, _| {
        assert_eq!(
            tree.entries(true, 0)
                .map(|entry| (entry.path.as_ref(), entry.is_ignored))
                .collect::<Vec<_>>(),
            vec![
                (rel_path(""), false),
                (rel_path(".gitignore"), false),
                (rel_path("one"), false),
                (rel_path("one/node_modules"), true),
                (rel_path("one/node_modules/a"), true),
                (rel_path("one/node_modules/b"), true),
                (rel_path("one/node_modules/b/b1.js"), true),
                (rel_path("one/node_modules/b/b2.js"), true),
                (rel_path("one/node_modules/c"), true),
                (rel_path("two"), false),
                (rel_path("two/x.js"), false),
                (rel_path("two/y.js"), false),
            ]
        );

        assert_eq!(
            loaded.file.path.as_ref(),
            rel_path("one/node_modules/b/b1.js")
        );

        // Only the newly-expanded directories are scanned.
        assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 2);
    });

    // Open another file in a different subdirectory of the same
    // gitignored directory.
    let prev_read_dir_count = fs.read_dir_call_count();
    let loaded = tree
        .update(cx, |tree, cx| {
            tree.load_file(rel_path("one/node_modules/a/a2.js"), cx)
        })
        .await
        .unwrap();

    tree.read_with(cx, |tree, _| {
        assert_eq!(
            tree.entries(true, 0)
                .map(|entry| (entry.path.as_ref(), entry.is_ignored))
                .collect::<Vec<_>>(),
            vec![
                (rel_path(""), false),
                (rel_path(".gitignore"), false),
                (rel_path("one"), false),
                (rel_path("one/node_modules"), true),
                (rel_path("one/node_modules/a"), true),
                (rel_path("one/node_modules/a/a1.js"), true),
                (rel_path("one/node_modules/a/a2.js"), true),
                (rel_path("one/node_modules/b"), true),
                (rel_path("one/node_modules/b/b1.js"), true),
                (rel_path("one/node_modules/b/b2.js"), true),
                (rel_path("one/node_modules/c"), true),
                (rel_path("two"), false),
                (rel_path("two/x.js"), false),
                (rel_path("two/y.js"), false),
            ]
        );

        assert_eq!(
            loaded.file.path.as_ref(),
            rel_path("one/node_modules/a/a2.js")
        );

        // Only the newly-expanded directory is scanned.
        assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 1);
    });

    let path = PathBuf::from("/root/one/node_modules/c/lib");

    // No work happens when files and directories change within an unloaded directory.
    let prev_fs_call_count = fs.read_dir_call_count() + fs.metadata_call_count();
    // When we open a directory, we check each ancestor whether it's a git
    // repository. That means we have an fs.metadata call per ancestor that we
    // need to subtract here.
    let ancestors = path.ancestors().count();

    fs.create_dir(path.as_ref()).await.unwrap();
    cx.executor().run_until_parked();

    assert_eq!(
        fs.read_dir_call_count() + fs.metadata_call_count() - prev_fs_call_count - ancestors,
        0
    );
}

#[gpui::test]
async fn test_dirs_no_longer_ignored(cx: &mut TestAppContext) {
    init_test(cx);
    let fs = FakeFs::new(cx.background_executor.clone());
    fs.insert_tree(
        "/root",
        json!({
            ".gitignore": "node_modules\n",
            "a": {
                "a.js": "",
            },
            "b": {
                "b.js": "",
            },
            "node_modules": {
                "c": {
                    "c.js": "",
                },
                "d": {
                    "d.js": "",
                    "e": {
                        "e1.js": "",
                        "e2.js": "",
                    },
                    "f": {
                        "f1.js": "",
                        "f2.js": "",
                    }
                },
            },
        }),
    )
    .await;

    let tree = Worktree::local(
        Path::new("/root"),
        true,
        fs.clone(),
        Default::default(),
        true,
        WorktreeId::from_proto(0),
        &mut cx.to_async(),
    )
    .await
    .unwrap();

    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
        .await;

    // Open a file within the gitignored directory, forcing some of its
    // subdirectories to be read, but not all.
    let read_dir_count_1 = fs.read_dir_call_count();
    tree.read_with(cx, |tree, _| {
        tree.as_local()
            .unwrap()
            .refresh_entries_for_paths(vec![rel_path("node_modules/d/d.js").into()])
    })
    .recv()
    .await;

    // Those subdirectories are now loaded.
    tree.read_with(cx, |tree, _| {
        assert_eq!(
            tree.entries(true, 0)
                .map(|e| (e.path.as_ref(), e.is_ignored))
                .collect::<Vec<_>>(),
            &[
                (rel_path(""), false),
                (rel_path(".gitignore"), false),
                (rel_path("a"), false),
                (rel_path("a/a.js"), false),
                (rel_path("b"), false),
                (rel_path("b/b.js"), false),
                (rel_path("node_modules"), true),
                (rel_path("node_modules/c"), true),
                (rel_path("node_modules/d"), true),
                (rel_path("node_modules/d/d.js"), true),
                (rel_path("node_modules/d/e"), true),
                (rel_path("node_modules/d/f"), true),
            ]
        );
    });
    let read_dir_count_2 = fs.read_dir_call_count();
    assert_eq!(read_dir_count_2 - read_dir_count_1, 2);

    // Update the gitignore so that node_modules is no longer ignored,
    // but a subdirectory is ignored
    fs.save("/root/.gitignore".as_ref(), &"e".into(), Default::default())
        .await
        .unwrap();
    cx.executor().run_until_parked();

    // All of the directories that are no longer ignored are now loaded.
    tree.read_with(cx, |tree, _| {
        assert_eq!(
            tree.entries(true, 0)
                .map(|e| (e.path.as_ref(), e.is_ignored))
                .collect::<Vec<_>>(),
            &[
                (rel_path(""), false),
                (rel_path(".gitignore"), false),
                (rel_path("a"), false),
                (rel_path("a/a.js"), false),
                (rel_path("b"), false),
                (rel_path("b/b.js"), false),
                // This directory is no longer ignored
                (rel_path("node_modules"), false),
                (rel_path("node_modules/c"), false),
                (rel_path("node_modules/c/c.js"), false),
                (rel_path("node_modules/d"), false),
                (rel_path("node_modules/d/d.js"), false),
                // This subdirectory is now ignored
                (rel_path("node_modules/d/e"), true),
                (rel_path("node_modules/d/f"), false),
                (rel_path("node_modules/d/f/f1.js"), false),
                (rel_path("node_modules/d/f/f2.js"), false),
            ]
        );
    });

    // Each of the newly-loaded directories is scanned only once.
    let read_dir_count_3 = fs.read_dir_call_count();
    assert_eq!(read_dir_count_3 - read_dir_count_2, 2);
}

#[gpui::test]
async fn test_write_file(cx: &mut TestAppContext) {
    init_test(cx);
    cx.executor().allow_parking();
    let dir = TempTree::new(json!({
        ".git": {},
        ".gitignore": "ignored-dir\n",
        "tracked-dir": {},
        "ignored-dir": {}
    }));

    let worktree = Worktree::local(
        dir.path(),
        true,
        Arc::new(RealFs::new(None, cx.executor())),
        Default::default(),
        true,
        WorktreeId::from_proto(0),
        &mut cx.to_async(),
    )
    .await
    .unwrap();

    #[cfg(not(target_os = "macos"))]
    fs::fs_watcher::global(|_| {}).unwrap();

    cx.read(|cx| worktree.read(cx).as_local().unwrap().scan_complete())
        .await;
    worktree.flush_fs_events(cx).await;

    worktree
        .update(cx, |tree, cx| {
            tree.write_file(
                rel_path("tracked-dir/file.txt").into(),
                "hello".into(),
                Default::default(),
                encoding_rs::UTF_8,
                false,
                cx,
            )
        })
        .await
        .unwrap();
    worktree
        .update(cx, |tree, cx| {
            tree.write_file(
                rel_path("ignored-dir/file.txt").into(),
                "world".into(),
                Default::default(),
                encoding_rs::UTF_8,
                false,
                cx,
            )
        })
        .await
        .unwrap();
    worktree.read_with(cx, |tree, _| {
        let tracked = tree
            .entry_for_path(rel_path("tracked-dir/file.txt"))
            .unwrap();
        let ignored = tree
            .entry_for_path(rel_path("ignored-dir/file.txt"))
            .unwrap();
        assert!(!tracked.is_ignored);
        assert!(ignored.is_ignored);
    });
}

#[gpui::test]
async fn test_file_scan_inclusions(cx: &mut TestAppContext) {
    init_test(cx);
    cx.executor().allow_parking();
    let dir = TempTree::new(json!({
        ".gitignore": "**/target\n/node_modules\ntop_level.txt\n",
        "target": {
            "index": "blah2"
        },
        "node_modules": {
            ".DS_Store": "",
            "prettier": {
                "package.json": "{}",
            },
            "package.json": "//package.json"
        },
        "src": {
            ".DS_Store": "",
            "foo": {
                "foo.rs": "mod another;\n",
                "another.rs": "// another",
            },
            "bar": {
                "bar.rs": "// bar",
            },
            "lib.rs": "mod foo;\nmod bar;\n",
        },
        "top_level.txt": "top level file",
        ".DS_Store": "",
    }));
    cx.update(|cx| {
        cx.update_global::<SettingsStore, _>(|store, cx| {
            store.update_user_settings(cx, |settings| {
                settings.project.worktree.file_scan_exclusions = Some(vec![]);
                settings.project.worktree.file_scan_inclusions = Some(vec![
                    "node_modules/**/package.json".to_string(),
                    "**/.DS_Store".to_string(),
                ]);
            });
        });
    });

    let tree = Worktree::local(
        dir.path(),
        true,
        Arc::new(RealFs::new(None, cx.executor())),
        Default::default(),
        true,
        WorktreeId::from_proto(0),
        &mut cx.to_async(),
    )
    .await
    .unwrap();
    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
        .await;
    tree.flush_fs_events(cx).await;
    tree.read_with(cx, |tree, _| {
        // Assert that file_scan_inclusions overrides  file_scan_exclusions.
        check_worktree_entries(
            tree,
            WorktreeExpectations {
                excluded_paths: &[],
                ignored_paths: &["target", "node_modules"],
                tracked_paths: &["src/lib.rs", "src/bar/bar.rs", ".gitignore"],
                included_paths: &[
                    "node_modules/prettier/package.json",
                    ".DS_Store",
                    "node_modules/.DS_Store",
                    "src/.DS_Store",
                ],
            },
        )
    });
}

#[gpui::test]
async fn test_file_scan_exclusions_overrules_inclusions(cx: &mut TestAppContext) {
    init_test(cx);
    cx.executor().allow_parking();
    let dir = TempTree::new(json!({
        ".gitignore": "**/target\n/node_modules\n",
        "target": {
            "index": "blah2"
        },
        "node_modules": {
            ".DS_Store": "",
            "prettier": {
                "package.json": "{}",
            },
        },
        "src": {
            ".DS_Store": "",
            "foo": {
                "foo.rs": "mod another;\n",
                "another.rs": "// another",
            },
        },
        ".DS_Store": "",
    }));

    cx.update(|cx| {
        cx.update_global::<SettingsStore, _>(|store, cx| {
            store.update_user_settings(cx, |settings| {
                settings.project.worktree.file_scan_exclusions =
                    Some(vec!["**/.DS_Store".to_string()]);
                settings.project.worktree.file_scan_inclusions =
                    Some(vec!["**/.DS_Store".to_string()]);
            });
        });
    });

    let tree = Worktree::local(
        dir.path(),
        true,
        Arc::new(RealFs::new(None, cx.executor())),
        Default::default(),
        true,
        WorktreeId::from_proto(0),
        &mut cx.to_async(),
    )
    .await
    .unwrap();
    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
        .await;
    tree.flush_fs_events(cx).await;
    tree.read_with(cx, |tree, _| {
        // Assert that file_scan_inclusions overrides  file_scan_exclusions.
        check_worktree_entries(
            tree,
            WorktreeExpectations {
                excluded_paths: &[".DS_Store, src/.DS_Store"],
                ignored_paths: &["target", "node_modules"],
                tracked_paths: &["src/foo/another.rs", "src/foo/foo.rs", ".gitignore"],
                included_paths: &[],
            },
        )
    });
}

#[gpui::test]
async fn test_file_scan_inclusions_reindexes_on_setting_change(cx: &mut TestAppContext) {
    init_test(cx);
    cx.executor().allow_parking();
    let dir = TempTree::new(json!({
        ".gitignore": "**/target\n/node_modules/\n",
        "target": {
            "index": "blah2"
        },
        "node_modules": {
            ".DS_Store": "",
            "prettier": {
                "package.json": "{}",
            },
        },
        "src": {
            ".DS_Store": "",
            "foo": {
                "foo.rs": "mod another;\n",
                "another.rs": "// another",
            },
        },
        ".DS_Store": "",
    }));

    cx.update(|cx| {
        cx.update_global::<SettingsStore, _>(|store, cx| {
            store.update_user_settings(cx, |settings| {
                settings.project.worktree.file_scan_exclusions = Some(vec![]);
                settings.project.worktree.file_scan_inclusions =
                    Some(vec!["node_modules/**".to_string()]);
            });
        });
    });
    let tree = Worktree::local(
        dir.path(),
        true,
        Arc::new(RealFs::new(None, cx.executor())),
        Default::default(),
        true,
        WorktreeId::from_proto(0),
        &mut cx.to_async(),
    )
    .await
    .unwrap();
    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
        .await;
    tree.flush_fs_events(cx).await;

    tree.read_with(cx, |tree, _| {
        assert!(
            tree.entry_for_path(rel_path("node_modules"))
                .is_some_and(|f| f.is_always_included)
        );
        assert!(
            tree.entry_for_path(rel_path("node_modules/prettier/package.json"))
                .is_some_and(|f| f.is_always_included)
        );
    });

    cx.update(|cx| {
        cx.update_global::<SettingsStore, _>(|store, cx| {
            store.update_user_settings(cx, |settings| {
                settings.project.worktree.file_scan_exclusions = Some(vec![]);
                settings.project.worktree.file_scan_inclusions = Some(vec![]);
            });
        });
    });
    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
        .await;
    tree.flush_fs_events(cx).await;

    tree.read_with(cx, |tree, _| {
        assert!(
            tree.entry_for_path(rel_path("node_modules"))
                .is_some_and(|f| !f.is_always_included)
        );
        assert!(
            tree.entry_for_path(rel_path("node_modules/prettier/package.json"))
                .is_some_and(|f| !f.is_always_included)
        );
    });
}

#[gpui::test]
async fn test_file_scan_exclusions(cx: &mut TestAppContext) {
    init_test(cx);
    cx.executor().allow_parking();
    let dir = TempTree::new(json!({
        ".gitignore": "**/target\n/node_modules\n",
        "target": {
            "index": "blah2"
        },
        "node_modules": {
            ".DS_Store": "",
            "prettier": {
                "package.json": "{}",
            },
        },
        "src": {
            ".DS_Store": "",
            "foo": {
                "foo.rs": "mod another;\n",
                "another.rs": "// another",
            },
            "bar": {
                "bar.rs": "// bar",
            },
            "lib.rs": "mod foo;\nmod bar;\n",
        },
        ".DS_Store": "",
    }));
    cx.update(|cx| {
        cx.update_global::<SettingsStore, _>(|store, cx| {
            store.update_user_settings(cx, |settings| {
                settings.project.worktree.file_scan_exclusions =
                    Some(vec!["**/foo/**".to_string(), "**/.DS_Store".to_string()]);
            });
        });
    });

    let tree = Worktree::local(
        dir.path(),
        true,
        Arc::new(RealFs::new(None, cx.executor())),
        Default::default(),
        true,
        WorktreeId::from_proto(0),
        &mut cx.to_async(),
    )
    .await
    .unwrap();
    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
        .await;
    tree.flush_fs_events(cx).await;
    tree.read_with(cx, |tree, _| {
        check_worktree_entries(
            tree,
            WorktreeExpectations {
                excluded_paths: &[
                    "src/foo/foo.rs",
                    "src/foo/another.rs",
                    "node_modules/.DS_Store",
                    "src/.DS_Store",
                    ".DS_Store",
                ],
                ignored_paths: &["target", "node_modules"],
                tracked_paths: &["src/lib.rs", "src/bar/bar.rs", ".gitignore"],
                included_paths: &[],
            },
        )
    });

    cx.update(|cx| {
        cx.update_global::<SettingsStore, _>(|store, cx| {
            store.update_user_settings(cx, |settings| {
                settings.project.worktree.file_scan_exclusions =
                    Some(vec!["**/node_modules/**".to_string()]);
            });
        });
    });
    tree.flush_fs_events(cx).await;
    cx.executor().run_until_parked();
    tree.read_with(cx, |tree, _| {
        check_worktree_entries(
            tree,
            WorktreeExpectations {
                excluded_paths: &[
                    "node_modules/prettier/package.json",
                    "node_modules/.DS_Store",
                    "node_modules",
                ],
                ignored_paths: &["target"],
                tracked_paths: &[
                    ".gitignore",
                    "src/lib.rs",
                    "src/bar/bar.rs",
                    "src/foo/foo.rs",
                    "src/foo/another.rs",
                    "src/.DS_Store",
                    ".DS_Store",
                ],
                included_paths: &[],
            },
        )
    });
}

#[gpui::test]
async fn test_hidden_files(cx: &mut TestAppContext) {
    init_test(cx);
    cx.executor().allow_parking();
    let dir = TempTree::new(json!({
        ".gitignore": "**/target\n",
        ".hidden_file": "content",
        ".hidden_dir": {
            "nested.rs": "code",
        },
        "src": {
            "visible.rs": "code",
        },
        "logs": {
            "app.log": "logs",
            "debug.log": "logs",
        },
        "visible.txt": "content",
    }));

    let tree = Worktree::local(
        dir.path(),
        true,
        Arc::new(RealFs::new(None, cx.executor())),
        Default::default(),
        true,
        WorktreeId::from_proto(0),
        &mut cx.to_async(),
    )
    .await
    .unwrap();
    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
        .await;
    tree.flush_fs_events(cx).await;

    tree.read_with(cx, |tree, _| {
        assert_eq!(
            tree.entries(true, 0)
                .map(|entry| (entry.path.as_ref(), entry.is_hidden))
                .collect::<Vec<_>>(),
            vec![
                (rel_path(""), false),
                (rel_path(".gitignore"), true),
                (rel_path(".hidden_dir"), true),
                (rel_path(".hidden_dir/nested.rs"), true),
                (rel_path(".hidden_file"), true),
                (rel_path("logs"), false),
                (rel_path("logs/app.log"), false),
                (rel_path("logs/debug.log"), false),
                (rel_path("src"), false),
                (rel_path("src/visible.rs"), false),
                (rel_path("visible.txt"), false),
            ]
        );
    });

    cx.update(|cx| {
        cx.update_global::<SettingsStore, _>(|store, cx| {
            store.update_user_settings(cx, |settings| {
                settings.project.worktree.hidden_files = Some(vec!["**/*.log".to_string()]);
            });
        });
    });
    tree.flush_fs_events(cx).await;
    cx.executor().run_until_parked();

    tree.read_with(cx, |tree, _| {
        assert_eq!(
            tree.entries(true, 0)
                .map(|entry| (entry.path.as_ref(), entry.is_hidden))
                .collect::<Vec<_>>(),
            vec![
                (rel_path(""), false),
                (rel_path(".gitignore"), false),
                (rel_path(".hidden_dir"), false),
                (rel_path(".hidden_dir/nested.rs"), false),
                (rel_path(".hidden_file"), false),
                (rel_path("logs"), false),
                (rel_path("logs/app.log"), true),
                (rel_path("logs/debug.log"), true),
                (rel_path("src"), false),
                (rel_path("src/visible.rs"), false),
                (rel_path("visible.txt"), false),
            ]
        );
    });
}

#[gpui::test]
async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) {
    init_test(cx);
    cx.executor().allow_parking();
    let dir = TempTree::new(json!({
        ".git": {
            "HEAD": "ref: refs/heads/main\n",
            "foo": "bar",
        },
        ".gitignore": "**/target\n/node_modules\ntest_output\n",
        "target": {
            "index": "blah2"
        },
        "node_modules": {
            ".DS_Store": "",
            "prettier": {
                "package.json": "{}",
            },
        },
        "src": {
            ".DS_Store": "",
            "foo": {
                "foo.rs": "mod another;\n",
                "another.rs": "// another",
            },
            "bar": {
                "bar.rs": "// bar",
            },
            "lib.rs": "mod foo;\nmod bar;\n",
        },
        ".DS_Store": "",
    }));
    cx.update(|cx| {
        cx.update_global::<SettingsStore, _>(|store, cx| {
            store.update_user_settings(cx, |settings| {
                settings.project.worktree.file_scan_exclusions = Some(vec![
                    "**/.git".to_string(),
                    "node_modules/".to_string(),
                    "build_output".to_string(),
                ]);
            });
        });
    });

    let tree = Worktree::local(
        dir.path(),
        true,
        Arc::new(RealFs::new(None, cx.executor())),
        Default::default(),
        true,
        WorktreeId::from_proto(0),
        &mut cx.to_async(),
    )
    .await
    .unwrap();
    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
        .await;
    tree.flush_fs_events(cx).await;
    tree.read_with(cx, |tree, _| {
        check_worktree_entries(
            tree,
            WorktreeExpectations {
                excluded_paths: &[
                    ".git/HEAD",
                    ".git/foo",
                    "node_modules",
                    "node_modules/.DS_Store",
                    "node_modules/prettier",
                    "node_modules/prettier/package.json",
                ],
                ignored_paths: &["target"],
                tracked_paths: &[
                    ".DS_Store",
                    "src/.DS_Store",
                    "src/lib.rs",
                    "src/foo/foo.rs",
                    "src/foo/another.rs",
                    "src/bar/bar.rs",
                    ".gitignore",
                ],
                included_paths: &[],
            },
        )
    });

    let new_excluded_dir = dir.path().join("build_output");
    let new_ignored_dir = dir.path().join("test_output");
    std::fs::create_dir_all(&new_excluded_dir)
        .unwrap_or_else(|e| panic!("Failed to create a {new_excluded_dir:?} directory: {e}"));
    std::fs::create_dir_all(&new_ignored_dir)
        .unwrap_or_else(|e| panic!("Failed to create a {new_ignored_dir:?} directory: {e}"));
    let node_modules_dir = dir.path().join("node_modules");
    let dot_git_dir = dir.path().join(".git");
    let src_dir = dir.path().join("src");
    for existing_dir in [&node_modules_dir, &dot_git_dir, &src_dir] {
        assert!(
            existing_dir.is_dir(),
            "Expect {existing_dir:?} to be present in the FS already"
        );
    }

    for directory_for_new_file in [
        new_excluded_dir,
        new_ignored_dir,
        node_modules_dir,
        dot_git_dir,
        src_dir,
    ] {
        std::fs::write(directory_for_new_file.join("new_file"), "new file contents")
            .unwrap_or_else(|e| {
                panic!("Failed to create in {directory_for_new_file:?} a new file: {e}")
            });
    }
    tree.flush_fs_events(cx).await;

    tree.read_with(cx, |tree, _| {
        check_worktree_entries(
            tree,
            WorktreeExpectations {
                excluded_paths: &[
                    ".git/HEAD",
                    ".git/foo",
                    ".git/new_file",
                    "node_modules",
                    "node_modules/.DS_Store",
                    "node_modules/prettier",
                    "node_modules/prettier/package.json",
                    "node_modules/new_file",
                    "build_output",
                    "build_output/new_file",
                    "test_output/new_file",
                ],
                ignored_paths: &["target", "test_output"],
                tracked_paths: &[
                    ".DS_Store",
                    "src/.DS_Store",
                    "src/lib.rs",
                    "src/foo/foo.rs",
                    "src/foo/another.rs",
                    "src/bar/bar.rs",
                    "src/new_file",
                    ".gitignore",
                ],
                included_paths: &[],
            },
        )
    });
}

#[gpui::test]
async fn test_fs_events_in_dot_git_worktree(cx: &mut TestAppContext) {
    init_test(cx);
    cx.executor().allow_parking();
    let dir = TempTree::new(json!({
        ".git": {
            "HEAD": "ref: refs/heads/main\n",
            "foo": "foo contents",
        },
    }));
    let dot_git_worktree_dir = dir.path().join(".git");

    let tree = Worktree::local(
        dot_git_worktree_dir.clone(),
        true,
        Arc::new(RealFs::new(None, cx.executor())),
        Default::default(),
        true,
        WorktreeId::from_proto(0),
        &mut cx.to_async(),
    )
    .await
    .unwrap();
    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
        .await;
    tree.flush_fs_events(cx).await;
    tree.read_with(cx, |tree, _| {
        check_worktree_entries(
            tree,
            WorktreeExpectations {
                ignored_paths: &["HEAD", "foo"],
                ..Default::default()
            },
        )
    });

    std::fs::write(dot_git_worktree_dir.join("new_file"), "new file contents")
        .unwrap_or_else(|e| panic!("Failed to create in {dot_git_worktree_dir:?} a new file: {e}"));
    tree.flush_fs_events(cx).await;
    tree.read_with(cx, |tree, _| {
        check_worktree_entries(
            tree,
            WorktreeExpectations {
                ignored_paths: &["HEAD", "foo", "new_file"],
                ..Default::default()
            },
        )
    });
}

#[gpui::test(iterations = 30)]
async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
    init_test(cx);
    let fs = FakeFs::new(cx.background_executor.clone());
    fs.insert_tree(
        "/root",
        json!({
            "b": {},
            "c": {},
            "d": {},
        }),
    )
    .await;

    let tree = Worktree::local(
        "/root".as_ref(),
        true,
        fs,
        Default::default(),
        true,
        WorktreeId::from_proto(0),
        &mut cx.to_async(),
    )
    .await
    .unwrap();

    let snapshot1 = tree.update(cx, |tree, cx| {
        let tree = tree.as_local_mut().unwrap();
        let snapshot = Arc::new(Mutex::new(tree.snapshot()));
        tree.observe_updates(0, cx, {
            let snapshot = snapshot.clone();
            let settings = tree.settings();
            move |update| {
                snapshot
                    .lock()
                    .apply_remote_update(update, &settings.file_scan_inclusions);
                async { true }
            }
        });
        snapshot
    });

    let entry = tree
        .update(cx, |tree, cx| {
            tree.as_local_mut()
                .unwrap()
                .create_entry(rel_path("a/e").into(), true, None, cx)
        })
        .await
        .unwrap()
        .into_included()
        .unwrap();
    assert!(entry.is_dir());

    cx.executor().run_until_parked();
    tree.read_with(cx, |tree, _| {
        assert_eq!(
            tree.entry_for_path(rel_path("a/e")).unwrap().kind,
            EntryKind::Dir
        );
    });

    let snapshot2 = tree.update(cx, |tree, _| tree.as_local().unwrap().snapshot());
    assert_eq!(
        snapshot1.lock().entries(true, 0).collect::<Vec<_>>(),
        snapshot2.entries(true, 0).collect::<Vec<_>>()
    );
}

#[gpui::test]
async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
    init_test(cx);
    cx.executor().allow_parking();

    let fs_fake = FakeFs::new(cx.background_executor.clone());
    fs_fake
        .insert_tree(
            "/root",
            json!({
                "a": {},
            }),
        )
        .await;

    let tree_fake = Worktree::local(
        "/root".as_ref(),
        true,
        fs_fake,
        Default::default(),
        true,
        WorktreeId::from_proto(0),
        &mut cx.to_async(),
    )
    .await
    .unwrap();

    let entry = tree_fake
        .update(cx, |tree, cx| {
            tree.as_local_mut().unwrap().create_entry(
                rel_path("a/b/c/d.txt").into(),
                false,
                None,
                cx,
            )
        })
        .await
        .unwrap()
        .into_included()
        .unwrap();
    assert!(entry.is_file());

    cx.executor().run_until_parked();
    tree_fake.read_with(cx, |tree, _| {
        assert!(
            tree.entry_for_path(rel_path("a/b/c/d.txt"))
                .unwrap()
                .is_file()
        );
        assert!(tree.entry_for_path(rel_path("a/b/c")).unwrap().is_dir());
        assert!(tree.entry_for_path(rel_path("a/b")).unwrap().is_dir());
    });

    let fs_real = Arc::new(RealFs::new(None, cx.executor()));
    let temp_root = TempTree::new(json!({
        "a": {}
    }));

    let tree_real = Worktree::local(
        temp_root.path(),
        true,
        fs_real,
        Default::default(),
        true,
        WorktreeId::from_proto(0),
        &mut cx.to_async(),
    )
    .await
    .unwrap();

    let entry = tree_real
        .update(cx, |tree, cx| {
            tree.as_local_mut().unwrap().create_entry(
                rel_path("a/b/c/d.txt").into(),
                false,
                None,
                cx,
            )
        })
        .await
        .unwrap()
        .into_included()
        .unwrap();
    assert!(entry.is_file());

    cx.executor().run_until_parked();
    tree_real.read_with(cx, |tree, _| {
        assert!(
            tree.entry_for_path(rel_path("a/b/c/d.txt"))
                .unwrap()
                .is_file()
        );
        assert!(tree.entry_for_path(rel_path("a/b/c")).unwrap().is_dir());
        assert!(tree.entry_for_path(rel_path("a/b")).unwrap().is_dir());
    });

    // Test smallest change
    let entry = tree_real
        .update(cx, |tree, cx| {
            tree.as_local_mut().unwrap().create_entry(
                rel_path("a/b/c/e.txt").into(),
                false,
                None,
                cx,
            )
        })
        .await
        .unwrap()
        .into_included()
        .unwrap();
    assert!(entry.is_file());

    cx.executor().run_until_parked();
    tree_real.read_with(cx, |tree, _| {
        assert!(
            tree.entry_for_path(rel_path("a/b/c/e.txt"))
                .unwrap()
                .is_file()
        );
    });

    // Test largest change
    let entry = tree_real
        .update(cx, |tree, cx| {
            tree.as_local_mut().unwrap().create_entry(
                rel_path("d/e/f/g.txt").into(),
                false,
                None,
                cx,
            )
        })
        .await
        .unwrap()
        .into_included()
        .unwrap();
    assert!(entry.is_file());

    cx.executor().run_until_parked();
    tree_real.read_with(cx, |tree, _| {
        assert!(
            tree.entry_for_path(rel_path("d/e/f/g.txt"))
                .unwrap()
                .is_file()
        );
        assert!(tree.entry_for_path(rel_path("d/e/f")).unwrap().is_dir());
        assert!(tree.entry_for_path(rel_path("d/e")).unwrap().is_dir());
        assert!(tree.entry_for_path(rel_path("d")).unwrap().is_dir());
    });
}

#[gpui::test]
async fn test_create_file_in_expanded_gitignored_dir(cx: &mut TestAppContext) {
    // Tests the behavior of our worktree refresh when a file in a gitignored directory
    // is created.
    init_test(cx);
    let fs = FakeFs::new(cx.background_executor.clone());
    fs.insert_tree(
        "/root",
        json!({
            ".gitignore": "ignored_dir\n",
            "ignored_dir": {
                "existing_file.txt": "existing content",
                "another_file.txt": "another content",
            },
        }),
    )
    .await;

    let tree = Worktree::local(
        Path::new("/root"),
        true,
        fs.clone(),
        Default::default(),
        true,
        WorktreeId::from_proto(0),
        &mut cx.to_async(),
    )
    .await
    .unwrap();

    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
        .await;

    tree.read_with(cx, |tree, _| {
        let ignored_dir = tree.entry_for_path(rel_path("ignored_dir")).unwrap();
        assert!(ignored_dir.is_ignored);
        assert_eq!(ignored_dir.kind, EntryKind::UnloadedDir);
    });

    tree.update(cx, |tree, cx| {
        tree.load_file(rel_path("ignored_dir/existing_file.txt"), cx)
    })
    .await
    .unwrap();

    tree.read_with(cx, |tree, _| {
        let ignored_dir = tree.entry_for_path(rel_path("ignored_dir")).unwrap();
        assert!(ignored_dir.is_ignored);
        assert_eq!(ignored_dir.kind, EntryKind::Dir);

        assert!(
            tree.entry_for_path(rel_path("ignored_dir/existing_file.txt"))
                .is_some()
        );
        assert!(
            tree.entry_for_path(rel_path("ignored_dir/another_file.txt"))
                .is_some()
        );
    });

    let entry = tree
        .update(cx, |tree, cx| {
            tree.create_entry(rel_path("ignored_dir/new_file.txt").into(), false, None, cx)
        })
        .await
        .unwrap();
    assert!(entry.into_included().is_some());

    cx.executor().run_until_parked();

    tree.read_with(cx, |tree, _| {
        let ignored_dir = tree.entry_for_path(rel_path("ignored_dir")).unwrap();
        assert!(ignored_dir.is_ignored);
        assert_eq!(
            ignored_dir.kind,
            EntryKind::Dir,
            "ignored_dir should still be loaded, not UnloadedDir"
        );

        assert!(
            tree.entry_for_path(rel_path("ignored_dir/existing_file.txt"))
                .is_some(),
            "existing_file.txt should still be visible"
        );
        assert!(
            tree.entry_for_path(rel_path("ignored_dir/another_file.txt"))
                .is_some(),
            "another_file.txt should still be visible"
        );
        assert!(
            tree.entry_for_path(rel_path("ignored_dir/new_file.txt"))
                .is_some(),
            "new_file.txt should be visible"
        );
    });
}

#[gpui::test]
async fn test_fs_event_for_gitignored_dir_does_not_lose_contents(cx: &mut TestAppContext) {
    // Tests the behavior of our worktree refresh when a directory modification for a gitignored directory
    // is triggered.
    init_test(cx);
    let fs = FakeFs::new(cx.background_executor.clone());
    fs.insert_tree(
        "/root",
        json!({
            ".gitignore": "ignored_dir\n",
            "ignored_dir": {
                "file1.txt": "content1",
                "file2.txt": "content2",
            },
        }),
    )
    .await;

    let tree = Worktree::local(
        Path::new("/root"),
        true,
        fs.clone(),
        Default::default(),
        true,
        WorktreeId::from_proto(0),
        &mut cx.to_async(),
    )
    .await
    .unwrap();

    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
        .await;

    // Load a file to expand the ignored directory
    tree.update(cx, |tree, cx| {
        tree.load_file(rel_path("ignored_dir/file1.txt"), cx)
    })
    .await
    .unwrap();

    tree.read_with(cx, |tree, _| {
        let ignored_dir = tree.entry_for_path(rel_path("ignored_dir")).unwrap();
        assert_eq!(ignored_dir.kind, EntryKind::Dir);
        assert!(
            tree.entry_for_path(rel_path("ignored_dir/file1.txt"))
                .is_some()
        );
        assert!(
            tree.entry_for_path(rel_path("ignored_dir/file2.txt"))
                .is_some()
        );
    });

    fs.emit_fs_event("/root/ignored_dir", Some(fs::PathEventKind::Changed));
    tree.flush_fs_events(cx).await;

    tree.read_with(cx, |tree, _| {
        let ignored_dir = tree.entry_for_path(rel_path("ignored_dir")).unwrap();
        assert_eq!(
            ignored_dir.kind,
            EntryKind::Dir,
            "ignored_dir should still be loaded (Dir), not UnloadedDir"
        );
        assert!(
            tree.entry_for_path(rel_path("ignored_dir/file1.txt"))
                .is_some(),
            "file1.txt should still be visible after directory fs event"
        );
        assert!(
            tree.entry_for_path(rel_path("ignored_dir/file2.txt"))
                .is_some(),
            "file2.txt should still be visible after directory fs event"
        );
    });
}

#[gpui::test(iterations = 100)]
async fn test_random_worktree_operations_during_initial_scan(
    cx: &mut TestAppContext,
    mut rng: StdRng,
) {
    init_test(cx);
    let operations = env::var("OPERATIONS")
        .map(|o| o.parse().unwrap())
        .unwrap_or(5);
    let initial_entries = env::var("INITIAL_ENTRIES")
        .map(|o| o.parse().unwrap())
        .unwrap_or(20);

    let root_dir = Path::new(path!("/test"));
    let fs = FakeFs::new(cx.background_executor.clone()) as Arc<dyn Fs>;
    fs.as_fake().insert_tree(root_dir, json!({})).await;
    for _ in 0..initial_entries {
        randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
    }
    log::info!("generated initial tree");

    let worktree = Worktree::local(
        root_dir,
        true,
        fs.clone(),
        Default::default(),
        true,
        WorktreeId::from_proto(0),
        &mut cx.to_async(),
    )
    .await
    .unwrap();

    let mut snapshots = vec![worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot())];
    let updates = Arc::new(Mutex::new(Vec::new()));
    worktree.update(cx, |tree, cx| {
        check_worktree_change_events(tree, cx);

        tree.as_local_mut().unwrap().observe_updates(0, cx, {
            let updates = updates.clone();
            move |update| {
                updates.lock().push(update);
                async { true }
            }
        });
    });

    for _ in 0..operations {
        worktree
            .update(cx, |worktree, cx| {
                randomly_mutate_worktree(worktree, &mut rng, cx)
            })
            .await
            .log_err();
        worktree.read_with(cx, |tree, _| {
            tree.as_local().unwrap().snapshot().check_invariants(true)
        });

        if rng.random_bool(0.6) {
            snapshots.push(worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot()));
        }
    }

    worktree
        .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
        .await;

    cx.executor().run_until_parked();

    let final_snapshot = worktree.read_with(cx, |tree, _| {
        let tree = tree.as_local().unwrap();
        let snapshot = tree.snapshot();
        snapshot.check_invariants(true);
        snapshot
    });

    let settings = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().settings());

    for (i, snapshot) in snapshots.into_iter().enumerate().rev() {
        let mut updated_snapshot = snapshot.clone();
        for update in updates.lock().iter() {
            if update.scan_id >= updated_snapshot.scan_id() as u64 {
                updated_snapshot
                    .apply_remote_update(update.clone(), &settings.file_scan_inclusions);
            }
        }

        assert_eq!(
            updated_snapshot.entries(true, 0).collect::<Vec<_>>(),
            final_snapshot.entries(true, 0).collect::<Vec<_>>(),
            "wrong updates after snapshot {i}: {updates:#?}",
        );
    }
}

#[gpui::test(iterations = 100)]
async fn test_random_worktree_changes(cx: &mut TestAppContext, mut rng: StdRng) {
    init_test(cx);
    let operations = env::var("OPERATIONS")
        .map(|o| o.parse().unwrap())
        .unwrap_or(40);
    let initial_entries = env::var("INITIAL_ENTRIES")
        .map(|o| o.parse().unwrap())
        .unwrap_or(20);

    let root_dir = Path::new(path!("/test"));
    let fs = FakeFs::new(cx.background_executor.clone()) as Arc<dyn Fs>;
    fs.as_fake().insert_tree(root_dir, json!({})).await;
    for _ in 0..initial_entries {
        randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
    }
    log::info!("generated initial tree");

    let worktree = Worktree::local(
        root_dir,
        true,
        fs.clone(),
        Default::default(),
        true,
        WorktreeId::from_proto(0),
        &mut cx.to_async(),
    )
    .await
    .unwrap();

    let updates = Arc::new(Mutex::new(Vec::new()));
    worktree.update(cx, |tree, cx| {
        check_worktree_change_events(tree, cx);

        tree.as_local_mut().unwrap().observe_updates(0, cx, {
            let updates = updates.clone();
            move |update| {
                updates.lock().push(update);
                async { true }
            }
        });
    });

    worktree
        .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
        .await;

    fs.as_fake().pause_events();
    let mut snapshots = Vec::new();
    let mut mutations_len = operations;
    while mutations_len > 1 {
        if rng.random_bool(0.2) {
            worktree
                .update(cx, |worktree, cx| {
                    randomly_mutate_worktree(worktree, &mut rng, cx)
                })
                .await
                .log_err();
        } else {
            randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
        }

        let buffered_event_count = fs.as_fake().buffered_event_count();
        if buffered_event_count > 0 && rng.random_bool(0.3) {
            if rng.random_bool(0.2) {
                log::info!(
                    "simulating watcher overflow, losing {} events",
                    buffered_event_count
                );
                fs.as_fake().simulate_watcher_overflow(root_dir);
            } else {
                let len = rng.random_range(0..=buffered_event_count);
                log::info!("flushing {} events", len);
                fs.as_fake().flush_events(len);
            }
        } else {
            randomly_mutate_fs(&fs, root_dir, 0.6, &mut rng).await;
            mutations_len -= 1;
        }

        cx.executor().run_until_parked();
        if rng.random_bool(0.2) {
            log::info!("storing snapshot {}", snapshots.len());
            let snapshot = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
            snapshots.push(snapshot);
        }
    }

    log::info!("quiescing");
    fs.as_fake().flush_events(usize::MAX);
    cx.executor().run_until_parked();

    let snapshot = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
    snapshot.check_invariants(true);
    let expanded_paths = snapshot
        .expanded_entries()
        .map(|e| e.path.clone())
        .collect::<Vec<_>>();

    {
        let new_worktree = Worktree::local(
            root_dir,
            true,
            fs.clone(),
            Default::default(),
            true,
            WorktreeId::from_proto(0),
            &mut cx.to_async(),
        )
        .await
        .unwrap();
        new_worktree
            .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
            .await;
        new_worktree
            .update(cx, |tree, _| {
                tree.as_local_mut()
                    .unwrap()
                    .refresh_entries_for_paths(expanded_paths)
            })
            .recv()
            .await;
        let new_snapshot =
            new_worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
        assert_eq!(
            snapshot.entries_without_ids(true),
            new_snapshot.entries_without_ids(true)
        );
    }

    let settings = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().settings());

    for (i, mut prev_snapshot) in snapshots.into_iter().enumerate().rev() {
        for update in updates.lock().iter() {
            if update.scan_id >= prev_snapshot.scan_id() as u64 {
                prev_snapshot.apply_remote_update(update.clone(), &settings.file_scan_inclusions);
            }
        }

        assert_eq!(
            prev_snapshot
                .entries(true, 0)
                .map(ignore_pending_dir)
                .collect::<Vec<_>>(),
            snapshot
                .entries(true, 0)
                .map(ignore_pending_dir)
                .collect::<Vec<_>>(),
            "wrong updates after snapshot {i}: {updates:#?}",
        );
    }

    fn ignore_pending_dir(entry: &Entry) -> Entry {
        let mut entry = entry.clone();
        if entry.kind.is_dir() {
            entry.kind = EntryKind::Dir
        }
        entry
    }
}

#[gpui::test(iterations = 100)]
async fn test_random_git_updates_with_watcher_overflows(cx: &mut TestAppContext, mut rng: StdRng) {
    // Property: every git state change is eventually signaled via
    // `UpdatedGitRepositories`, no matter how the events reporting it are
    // batched, delayed, or lost to watcher overflows.
    init_test(cx);
    let operations = env::var("OPERATIONS")
        .map(|o| o.parse().unwrap())
        .unwrap_or(40);

    let root_dir = Path::new(path!("/test"));
    let dot_git = root_dir.join(".git");
    // Random fs mutations are confined to this subdirectory so that they
    // cannot rename or delete `.git` itself.
    let src_dir = root_dir.join("src");
    let fs = FakeFs::new(cx.background_executor.clone()) as Arc<dyn Fs>;
    fs.as_fake()
        .insert_tree(
            root_dir,
            json!({
                ".git": {},
                "src": {
                    "main.rs": "fn main() {}",
                },
            }),
        )
        .await;

    let worktree = Worktree::local(
        root_dir,
        true,
        fs.clone(),
        Default::default(),
        true,
        WorktreeId::from_proto(0),
        &mut cx.to_async(),
    )
    .await
    .unwrap();
    worktree
        .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
        .await;
    cx.executor().run_until_parked();

    // Set when the fake repository's state is mutated, cleared when the
    // worktree signals a git update. A signal observed after a mutation
    // prompts downstream consumers (the GitStore) to re-read the repository,
    // at which point they see that mutation's state, so clearing on any
    // subsequent signal is sound.
    let pending_git_update: Rc<Cell<bool>> = Rc::new(Cell::new(false));
    worktree.update(cx, {
        let pending_git_update = pending_git_update.clone();
        |_, cx| {
            cx.subscribe(&cx.entity(), move |_, _, event, _| {
                if matches!(event, Event::UpdatedGitRepositories(_)) {
                    pending_git_update.set(false);
                }
            })
            .detach();
        }
    });

    fs.as_fake().pause_events();
    let mut commit_count = 0;
    for _ in 0..operations {
        match rng.random_range(0_u32..100) {
            // Change the repository's git state, e.g. a commit moving HEAD.
            0..25 => {
                commit_count += 1;
                log::info!("setting HEAD to commit {commit_count}");
                fs.as_fake().set_head_for_repo(
                    &dot_git,
                    &[("src/main.rs", format!("fn main() {{}} // {commit_count}"))],
                    format!("sha-{commit_count}"),
                );
                pending_git_update.set(true);
            }
            // The watch queue overflows: all undelivered events are lost and
            // only a rescan for the root is reported.
            25..40 => {
                log::info!(
                    "simulating watcher overflow, losing {} events",
                    fs.as_fake().buffered_event_count()
                );
                fs.as_fake().simulate_watcher_overflow(root_dir);
            }
            // Deliver a prefix of the queued events.
            40..65 => {
                let buffered_event_count = fs.as_fake().buffered_event_count();
                let len = rng.random_range(0..=buffered_event_count);
                log::info!("flushing {len} of {buffered_event_count} events");
                fs.as_fake().flush_events(len);
            }
            // Unrelated churn in the working tree.
            _ => {
                randomly_mutate_fs(&fs, &src_dir, 1.0, &mut rng).await;
            }
        }
        cx.executor().run_until_parked();
    }

    log::info!("quiescing");
    fs.as_fake().unpause_events_and_flush();
    cx.executor().run_until_parked();

    worktree.read_with(cx, |tree, _| {
        tree.as_local().unwrap().snapshot().check_invariants(true)
    });
    assert!(
        !pending_git_update.get(),
        "a git state change was never signaled via UpdatedGitRepositories"
    );
}

// The worktree's `UpdatedEntries` event can be used to follow along with
// all changes to the worktree's snapshot.
fn check_worktree_change_events(tree: &mut Worktree, cx: &mut Context<Worktree>) {
    let mut entries = tree.entries(true, 0).cloned().collect::<Vec<_>>();
    cx.subscribe(&cx.entity(), move |tree, _, event, _| {
        if let Event::UpdatedEntries(changes) = event {
            for (path, _, change_type) in changes.iter() {
                let entry = tree.entry_for_path(path).cloned();
                let ix = match entries.binary_search_by_key(&path, |e| &e.path) {
                    Ok(ix) | Err(ix) => ix,
                };
                match change_type {
                    PathChange::Added => entries.insert(ix, entry.unwrap()),
                    PathChange::Removed => drop(entries.remove(ix)),
                    PathChange::Updated => {
                        let entry = entry.unwrap();
                        let existing_entry = entries.get_mut(ix).unwrap();
                        assert_eq!(existing_entry.path, entry.path);
                        *existing_entry = entry;
                    }
                    PathChange::AddedOrUpdated | PathChange::Loaded => {
                        let entry = entry.unwrap();
                        if entries.get(ix).map(|e| &e.path) == Some(&entry.path) {
                            *entries.get_mut(ix).unwrap() = entry;
                        } else {
                            entries.insert(ix, entry);
                        }
                    }
                }
            }

            let new_entries = tree.entries(true, 0).cloned().collect::<Vec<_>>();
            assert_eq!(entries, new_entries, "incorrect changes: {:?}", changes);
        }
    })
    .detach();
}

fn randomly_mutate_worktree(
    worktree: &mut Worktree,
    rng: &mut impl Rng,
    cx: &mut Context<Worktree>,
) -> Task<Result<()>> {
    log::info!("mutating worktree");
    let worktree = worktree.as_local_mut().unwrap();
    let snapshot = worktree.snapshot();
    let entry = snapshot.entries(false, 0).choose(rng).unwrap();

    match rng.random_range(0_u32..100) {
        0..=33 if entry.path.as_ref() != RelPath::empty() => {
            log::info!("deleting entry {:?} ({})", entry.path, entry.id.to_usize());
            let task = worktree.delete_entry(entry.clone(), cx);

            cx.background_spawn(async move {
                task.await?;
                Ok(())
            })
        }
        _ => {
            if entry.is_dir() {
                let child_path = entry.path.join(rel_path(&random_filename(rng)));
                let is_dir = rng.random_bool(0.3);
                log::info!(
                    "creating {} at {:?}",
                    if is_dir { "dir" } else { "file" },
                    child_path,
                );
                let task = worktree.create_entry(child_path.into(), is_dir, None, cx);
                cx.background_spawn(async move {
                    task.await?;
                    Ok(())
                })
            } else {
                log::info!(
                    "overwriting file {:?} ({})",
                    entry.path,
                    entry.id.to_usize()
                );
                let task = worktree.write_file(
                    entry.path.clone(),
                    "".into(),
                    Default::default(),
                    encoding_rs::UTF_8,
                    false,
                    cx,
                );
                cx.background_spawn(async move {
                    task.await?;
                    Ok(())
                })
            }
        }
    }
}

async fn randomly_mutate_fs(
    fs: &Arc<dyn Fs>,
    root_path: &Path,
    insertion_probability: f64,
    rng: &mut impl Rng,
) {
    log::info!("mutating fs");
    let mut files = Vec::new();
    let mut dirs = Vec::new();
    for path in fs.as_fake().paths(false) {
        if path.starts_with(root_path) {
            if fs.is_file(&path).await {
                files.push(path);
            } else {
                dirs.push(path);
            }
        }
    }

    if (files.is_empty() && dirs.len() == 1) || rng.random_bool(insertion_probability) {
        let path = dirs.choose(rng).unwrap();
        let new_path = path.join(random_filename(rng));

        if rng.random() {
            log::info!(
                "creating dir {:?}",
                new_path.strip_prefix(root_path).unwrap()
            );
            fs.create_dir(&new_path).await.unwrap();
        } else {
            log::info!(
                "creating file {:?}",
                new_path.strip_prefix(root_path).unwrap()
            );
            fs.create_file(&new_path, Default::default()).await.unwrap();
        }
    } else if rng.random_bool(0.05) {
        let ignore_dir_path = dirs.choose(rng).unwrap();
        let ignore_path = ignore_dir_path.join(GITIGNORE);

        let subdirs = dirs
            .iter()
            .filter(|d| d.starts_with(ignore_dir_path))
            .cloned()
            .collect::<Vec<_>>();
        let subfiles = files
            .iter()
            .filter(|d| d.starts_with(ignore_dir_path))
            .cloned()
            .collect::<Vec<_>>();
        let files_to_ignore = {
            let len = rng.random_range(0..=subfiles.len());
            subfiles.choose_multiple(rng, len)
        };
        let dirs_to_ignore = {
            let len = rng.random_range(0..subdirs.len());
            subdirs.choose_multiple(rng, len)
        };

        let mut ignore_contents = String::new();
        for path_to_ignore in files_to_ignore.chain(dirs_to_ignore) {
            writeln!(
                ignore_contents,
                "{}",
                path_to_ignore
                    .strip_prefix(ignore_dir_path)
                    .unwrap()
                    .to_str()
                    .unwrap()
            )
            .unwrap();
        }
        log::info!(
            "creating gitignore {:?} with contents:\n{}",
            ignore_path.strip_prefix(root_path).unwrap(),
            ignore_contents
        );
        fs.save(
            &ignore_path,
            &ignore_contents.as_str().into(),
            Default::default(),
        )
        .await
        .unwrap();
    } else {
        let old_path = {
            let file_path = files.choose(rng);
            let dir_path = dirs[1..].choose(rng);
            file_path.into_iter().chain(dir_path).choose(rng).unwrap()
        };

        let is_rename = rng.random();
        if is_rename {
            let new_path_parent = dirs
                .iter()
                .filter(|d| !d.starts_with(old_path))
                .choose(rng)
                .unwrap();

            let overwrite_existing_dir =
                !old_path.starts_with(new_path_parent) && rng.random_bool(0.3);
            let new_path = if overwrite_existing_dir {
                fs.remove_dir(
                    new_path_parent,
                    RemoveOptions {
                        recursive: true,
                        ignore_if_not_exists: true,
                    },
                )
                .await
                .unwrap();
                new_path_parent.to_path_buf()
            } else {
                new_path_parent.join(random_filename(rng))
            };

            log::info!(
                "renaming {:?} to {}{:?}",
                old_path.strip_prefix(root_path).unwrap(),
                if overwrite_existing_dir {
                    "overwrite "
                } else {
                    ""
                },
                new_path.strip_prefix(root_path).unwrap()
            );
            fs.rename(
                old_path,
                &new_path,
                fs::RenameOptions {
                    overwrite: true,
                    ignore_if_exists: true,
                    create_parents: false,
                },
            )
            .await
            .unwrap();
        } else if fs.is_file(old_path).await {
            log::info!(
                "deleting file {:?}",
                old_path.strip_prefix(root_path).unwrap()
            );
            fs.remove_file(old_path, Default::default()).await.unwrap();
        } else {
            log::info!(
                "deleting dir {:?}",
                old_path.strip_prefix(root_path).unwrap()
            );
            fs.remove_dir(
                old_path,
                RemoveOptions {
                    recursive: true,
                    ignore_if_not_exists: true,
                },
            )
            .await
            .unwrap();
        }
    }
}

fn random_filename(rng: &mut impl Rng) -> String {
    (0..6)
        .map(|_| rng.sample(rand::distr::Alphanumeric))
        .map(char::from)
        .collect()
}

#[gpui::test]
async fn test_private_single_file_worktree(cx: &mut TestAppContext) {
    init_test(cx);
    let fs = FakeFs::new(cx.background_executor.clone());
    fs.insert_tree("/", json!({".env": "PRIVATE=secret\n"}))
        .await;
    let tree = Worktree::local(
        Path::new("/.env"),
        true,
        fs.clone(),
        Default::default(),
        true,
        WorktreeId::from_proto(0),
        &mut cx.to_async(),
    )
    .await
    .unwrap();
    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
        .await;
    tree.read_with(cx, |tree, _| {
        let entry = tree.entry_for_path(rel_path("")).unwrap();
        assert!(entry.is_private);
    });
}

#[gpui::test]
async fn test_repository_above_root(executor: BackgroundExecutor, cx: &mut TestAppContext) {
    init_test(cx);

    let fs = FakeFs::new(executor);
    fs.insert_tree(
        path!("/root"),
        json!({
            ".git": {},
            "subproject": {
                "a.txt": "A"
            }
        }),
    )
    .await;
    let worktree = Worktree::local(
        path!("/root/subproject").as_ref(),
        true,
        fs.clone(),
        Arc::default(),
        true,
        WorktreeId::from_proto(0),
        &mut cx.to_async(),
    )
    .await
    .unwrap();
    worktree
        .update(cx, |worktree, _| {
            worktree.as_local().unwrap().scan_complete()
        })
        .await;
    cx.run_until_parked();
    let repos = worktree.update(cx, |worktree, _| {
        worktree.as_local().unwrap().repositories()
    });
    pretty_assertions::assert_eq!(repos, [Path::new(path!("/root")).into()]);

    fs.touch_path(path!("/root/subproject")).await;
    worktree
        .update(cx, |worktree, _| {
            worktree.as_local().unwrap().scan_complete()
        })
        .await;
    cx.run_until_parked();

    let repos = worktree.update(cx, |worktree, _| {
        worktree.as_local().unwrap().repositories()
    });
    pretty_assertions::assert_eq!(repos, [Path::new(path!("/root")).into()]);
}

#[gpui::test]
async fn test_global_gitignore(executor: BackgroundExecutor, cx: &mut TestAppContext) {
    init_test(cx);

    let home = paths::home_dir();
    let project_path = home.join("example.com").join("project");
    let fs = FakeFs::new(executor);
    fs.insert_tree(
        home,
        json!({
            ".config": {
                "git": {
                    "ignore": "foo\n/bar\nbaz\n*.com\n"
                }
            },
            "example.com": {
                "project": {
                    ".git": {},
                    ".gitignore": "!baz",
                    "foo": "",
                    "bar": "",
                    "sub": {
                        "bar": "",
                    },
                    "subrepo": {
                        ".git": {},
                        "bar": ""
                    },
                    "baz": ""
                }
            }
        }),
    )
    .await;
    let worktree = Worktree::local(
        project_path.clone(),
        true,
        fs.clone(),
        Arc::default(),
        true,
        WorktreeId::from_proto(0),
        &mut cx.to_async(),
    )
    .await
    .unwrap();
    worktree
        .update(cx, |worktree, _| {
            worktree.as_local().unwrap().scan_complete()
        })
        .await;
    cx.run_until_parked();

    // .gitignore overrides excludesFile, and anchored paths in excludesFile are resolved
    // relative to the nearest containing repository
    worktree.update(cx, |worktree, _cx| {
        check_worktree_entries(
            worktree,
            WorktreeExpectations {
                ignored_paths: &["foo", "bar", "subrepo/bar"],
                tracked_paths: &["sub/bar", "baz"],
                ..Default::default()
            },
        );
    });

    // Ignore statuses are updated when excludesFile changes
    fs.write(
        &home.join(".config").join("git").join("ignore"),
        "/bar\nbaz\n*.com\n".as_bytes(),
    )
    .await
    .unwrap();
    worktree
        .update(cx, |worktree, _| {
            worktree.as_local().unwrap().scan_complete()
        })
        .await;
    cx.run_until_parked();

    worktree.update(cx, |worktree, _cx| {
        check_worktree_entries(
            worktree,
            WorktreeExpectations {
                ignored_paths: &["bar", "subrepo/bar"],
                tracked_paths: &["foo", "sub/bar", "baz"],
                ..Default::default()
            },
        );
    });

    // Statuses are updated when .git added/removed
    fs.remove_dir(
        &project_path.join("subrepo").join(".git"),
        RemoveOptions {
            recursive: true,
            ..Default::default()
        },
    )
    .await
    .unwrap();
    worktree
        .update(cx, |worktree, _| {
            worktree.as_local().unwrap().scan_complete()
        })
        .await;
    cx.run_until_parked();

    worktree.update(cx, |worktree, _cx| {
        check_worktree_entries(
            worktree,
            WorktreeExpectations {
                ignored_paths: &["bar"],
                tracked_paths: &["foo", "sub/bar", "baz", "subrepo/bar"],
                ..Default::default()
            },
        );
    });
}

#[gpui::test]
async fn test_repo_exclude_in_worktree(executor: BackgroundExecutor, cx: &mut TestAppContext) {
    init_test(cx);

    let fs = FakeFs::new(executor);

    fs.insert_tree(
        path!("/repo"),
        json!({
            ".git": {
                "info": {
                    "exclude": ".env.*"
                },
                "worktrees": {
                    "my-worktree": {
                        "commondir": "../.."
                    }
                }
            }
        }),
    )
    .await;

    fs.insert_tree(
        path!("/worktree"),
        json!({
            // .git is pointing to the repo
            ".git": "gitdir: /repo/.git/worktrees/my-worktree",
            ".env.local": "secret=1234",
            "not-ignored.txt": "",
        }),
    )
    .await;

    let worktree = Worktree::local(
        path!("/worktree").as_ref(),
        true,
        fs.clone(),
        Default::default(),
        true,
        WorktreeId::from_proto(0),
        &mut cx.to_async(),
    )
    .await
    .unwrap();

    worktree
        .update(cx, |worktree, _| {
            worktree.as_local().unwrap().scan_complete()
        })
        .await;
    cx.run_until_parked();

    // .env.local should be ignored via info/exclude from the repo's exclude
    worktree.update(cx, |worktree, _cx| {
        check_worktree_entries(
            worktree,
            WorktreeExpectations {
                ignored_paths: &[".env.local"],
                tracked_paths: &["not-ignored.txt"],
                ..Default::default()
            },
        );
    });
}

#[gpui::test]
async fn test_repo_exclude_naming_a_worktree_ancestor(
    executor: BackgroundExecutor,
    cx: &mut TestAppContext,
) {
    init_test(cx);

    let fs = FakeFs::new(executor);

    fs.insert_tree(
        path!("/scratch/proj"),
        json!({
            ".git": {
                "info": { "exclude": "scratch" }
            },
            "src": {
                "main.rs": "fn main() {}",
            }
        }),
    )
    .await;

    let worktree = Worktree::local(
        path!("/scratch/proj").as_ref(),
        true,
        fs.clone(),
        Default::default(),
        true,
        WorktreeId::from_proto(0),
        &mut cx.to_async(),
    )
    .await
    .unwrap();

    worktree
        .update(cx, |worktree, _| {
            worktree.as_local().unwrap().scan_complete()
        })
        .await;
    cx.run_until_parked();

    worktree.update(cx, |worktree, _cx| {
        assert!(
            !worktree.root_entry().unwrap().is_ignored,
            "an exclude pattern matching an ancestor must not ignore the worktree"
        );
        check_worktree_entries(
            worktree,
            WorktreeExpectations {
                tracked_paths: &["src/main.rs"],
                ..Default::default()
            },
        );
    });
}

#[gpui::test]
async fn test_repo_exclude(executor: BackgroundExecutor, cx: &mut TestAppContext) {
    init_test(cx);

    let fs = FakeFs::new(executor);
    let project_dir = Path::new(path!("/project"));
    fs.insert_tree(
        project_dir,
        json!({
            ".git": {
                "info": {
                    "exclude": ".env.*"
                }
            },
            ".env.example": "secret=xxxx",
            ".env.local": "secret=1234",
            ".gitignore": "!.env.example",
            "README.md": "# Repo Exclude",
            "src": {
                "main.rs": "fn main() {}",
            },
        }),
    )
    .await;

    let worktree = Worktree::local(
        project_dir,
        true,
        fs.clone(),
        Default::default(),
        true,
        WorktreeId::from_proto(0),
        &mut cx.to_async(),
    )
    .await
    .unwrap();
    worktree
        .update(cx, |worktree, _| {
            worktree.as_local().unwrap().scan_complete()
        })
        .await;
    cx.run_until_parked();

    // .gitignore overrides .git/info/exclude
    worktree.update(cx, |worktree, _cx| {
        check_worktree_entries(
            worktree,
            WorktreeExpectations {
                ignored_paths: &[".env.local"],
                tracked_paths: &[".env.example", "README.md", "src/main.rs"],
                ..Default::default()
            },
        );
    });

    // Ignore statuses are updated when .git/info/exclude file changes
    fs.write(
        &project_dir.join(DOT_GIT).join(REPO_EXCLUDE),
        ".env.example".as_bytes(),
    )
    .await
    .unwrap();
    worktree
        .update(cx, |worktree, _| {
            worktree.as_local().unwrap().scan_complete()
        })
        .await;
    cx.run_until_parked();

    worktree.update(cx, |worktree, _cx| {
        check_worktree_entries(
            worktree,
            WorktreeExpectations {
                tracked_paths: &[".env.example", ".env.local", "README.md", "src/main.rs"],
                ..Default::default()
            },
        );
    });
}

#[gpui::test]
async fn test_repo_exclude_anchored_pattern(executor: BackgroundExecutor, cx: &mut TestAppContext) {
    init_test(cx);

    let fs = FakeFs::new(executor);
    let project_dir = Path::new(path!("/project"));
    fs.insert_tree(
        project_dir,
        json!({
            ".git": {
                "info": {
                    "exclude": "vendor/cache"
                }
            },
            "vendor": {
                "cache": {
                    "blob.bin": "",
                },
                "keep.txt": "",
            },
            "elsewhere": {
                "vendor": {
                    "cache": {
                        "blob.bin": "",
                    },
                },
            },
        }),
    )
    .await;

    let worktree = Worktree::local(
        project_dir,
        true,
        fs.clone(),
        Default::default(),
        true,
        WorktreeId::from_proto(0),
        &mut cx.to_async(),
    )
    .await
    .unwrap();
    worktree
        .update(cx, |worktree, _| {
            worktree.as_local().unwrap().scan_complete()
        })
        .await;
    cx.run_until_parked();

    // An anchored pattern (containing a `/`) is matched relative to the work
    // tree root, so only the top-level `vendor/cache` is ignored.
    worktree.update(cx, |worktree, _cx| {
        check_worktree_entries(
            worktree,
            WorktreeExpectations {
                ignored_paths: &["vendor/cache"],
                tracked_paths: &["vendor/keep.txt", "elsewhere/vendor/cache"],
                ..Default::default()
            },
        );
    });
}

#[gpui::test]
async fn test_repo_exclude_applies_within_nested_repos(
    executor: BackgroundExecutor,
    cx: &mut TestAppContext,
) {
    init_test(cx);

    let fs = FakeFs::new(executor);
    let project_dir = Path::new(path!("/project"));

    // Mirrors the layout used by tools that keep working copies of the
    // repository inside the repository itself: a bare clone in
    // `.scratch/clones` and a linked worktree of that clone in
    // `.scratch/worktrees`, both hidden via the outer repository's
    // `.git/info/exclude` rather than a `.gitignore`.
    fs.insert_tree(
        project_dir,
        json!({
            ".git": {
                "info": {
                    "exclude": "/.scratch/worktrees/\n/.scratch/clones/\n"
                }
            },
            "src": {
                "main.rs": "fn main() {}",
            },
            ".scratch": {
                "clones": {
                    "abc": {
                        "project.git": {
                            "HEAD": "ref: refs/heads/main",
                            "worktrees": {
                                "project": {
                                    "HEAD": "ref: refs/heads/feature",
                                    "commondir": "../..",
                                }
                            }
                        }
                    }
                },
                "worktrees": {
                    "abc": {
                        "project": {
                            ".git": "gitdir: ../../../clones/abc/project.git/worktrees/project",
                            "src": {
                                "main.rs": "fn main() {}",
                            }
                        }
                    }
                }
            }
        }),
    )
    .await;

    let worktree = Worktree::local(
        project_dir,
        true,
        fs.clone(),
        Default::default(),
        true,
        WorktreeId::from_proto(0),
        &mut cx.to_async(),
    )
    .await
    .unwrap();
    worktree
        .update(cx, |worktree, _| {
            worktree.as_local().unwrap().scan_complete()
        })
        .await;
    cx.run_until_parked();

    // After the initial scan, both excluded directories are ignored.
    worktree.update(cx, |worktree, _cx| {
        check_worktree_entries(
            worktree,
            WorktreeExpectations {
                ignored_paths: &[".scratch/clones", ".scratch/worktrees"],
                tracked_paths: &["src/main.rs"],
                ..Default::default()
            },
        );
    });

    // Load a file within the excluded nested repository, as happens when a
    // search that includes ignored files runs or when the file is opened.
    worktree
        .update(cx, |worktree, _| {
            worktree.as_local().unwrap().refresh_entries_for_paths(vec![
                rel_path(".scratch/worktrees/abc/project/src/main.rs").into(),
            ])
        })
        .recv()
        .await;
    cx.run_until_parked();

    // The nested repository's own `.git` must not cause the outer
    // repository's `info/exclude` rules to be dropped.
    worktree.update(cx, |worktree, _cx| {
        check_worktree_entries(
            worktree,
            WorktreeExpectations {
                ignored_paths: &[
                    ".scratch/worktrees/abc",
                    ".scratch/worktrees/abc/project",
                    ".scratch/worktrees/abc/project/src",
                    ".scratch/worktrees/abc/project/src/main.rs",
                ],
                tracked_paths: &["src/main.rs"],
                ..Default::default()
            },
        );
    });

    // A file written inside the loaded nested repository (e.g. by a tool
    // working in the clone) must also be ignored.
    fs.save(
        path!("/project/.scratch/worktrees/abc/project/src/generated.rs").as_ref(),
        &"fn generated() {}".into(),
        Default::default(),
    )
    .await
    .unwrap();
    cx.run_until_parked();

    worktree.update(cx, |worktree, _cx| {
        check_worktree_entries(
            worktree,
            WorktreeExpectations {
                ignored_paths: &[".scratch/worktrees/abc/project/src/generated.rs"],
                ..Default::default()
            },
        );
    });

    // Nothing under the excluded directories is visible to a traversal that
    // skips ignored entries, which is what project search uses.
    worktree.update(cx, |worktree, _cx| {
        let unignored_entries = worktree
            .entries(false, 0)
            .filter(|entry| {
                entry.path.starts_with(rel_path(".scratch"))
                    && entry.path.as_ref() != rel_path(".scratch")
            })
            .map(|entry| entry.path.clone())
            .collect::<Vec<_>>();
        assert_eq!(
            unignored_entries,
            Vec::<Arc<RelPath>>::new(),
            "entries under the excluded .scratch directories leaked into the unignored traversal",
        );
    });
}

#[derive(Default)]
struct WorktreeExpectations {
    excluded_paths: &'static [&'static str],
    ignored_paths: &'static [&'static str],
    tracked_paths: &'static [&'static str],
    included_paths: &'static [&'static str],
}

#[track_caller]
fn check_worktree_entries(tree: &Worktree, expectations: WorktreeExpectations) {
    for path in expectations.excluded_paths {
        let entry = tree.entry_for_path(rel_path(path));
        assert!(
            entry.is_none(),
            "expected path '{path}' to be excluded, but got entry: {entry:?}",
        );
    }
    for path in expectations.ignored_paths {
        let entry = tree
            .entry_for_path(rel_path(path))
            .unwrap_or_else(|| panic!("Missing entry for expected ignored path '{path}'"));
        assert!(
            entry.is_ignored,
            "expected path '{path}' to be ignored, but got entry: {entry:?}",
        );
    }
    for path in expectations.tracked_paths {
        let entry = tree
            .entry_for_path(rel_path(path))
            .unwrap_or_else(|| panic!("Missing entry for expected tracked path '{path}'"));
        assert!(
            !entry.is_ignored || entry.is_always_included,
            "expected path '{path}' to be tracked, but got entry: {entry:?}",
        );
    }
    for path in expectations.included_paths {
        let entry = tree
            .entry_for_path(rel_path(path))
            .unwrap_or_else(|| panic!("Missing entry for expected included path '{path}'"));
        assert!(
            entry.is_always_included,
            "expected path '{path}' to always be included, but got entry: {entry:?}",
        );
    }
}

#[gpui::test]
async fn test_root_repo_common_dir_for_relative_gitdir(
    executor: BackgroundExecutor,
    cx: &mut TestAppContext,
) {
    init_test(cx);

    let fs = FakeFs::new(executor);
    fs.insert_tree(
        path!("/repo"),
        json!({
            ".git": {
                "HEAD": "ref: refs/heads/main",
                "config": "[core]\n\tbare = false\n",
                "info": {
                    "exclude": "ignored.txt\n",
                },
                "worktrees": {
                    "feature-a": {
                        "HEAD": "ref: refs/heads/feature-a",
                        "commondir": "../..",
                        "gitdir": "/repo/feature-a/.git",
                    },
                },
            },
            "feature-a": {
                ".git": "gitdir: ../.git/worktrees/feature-a",
                "file.txt": "content",
                "ignored.txt": "ignored",
                "subdir": {
                    "file.txt": "content",
                    "ignored.txt": "ignored",
                },
            },
        }),
    )
    .await;

    let feature_tree = Worktree::local(
        path!("/repo/feature-a").as_ref(),
        true,
        fs.clone(),
        Arc::default(),
        true,
        WorktreeId::from_proto(0),
        &mut cx.to_async(),
    )
    .await
    .unwrap();
    feature_tree
        .update(cx, |tree, _| tree.as_local().unwrap().scan_complete())
        .await;
    cx.run_until_parked();

    feature_tree.read_with(cx, |tree, _| {
        assert_eq!(
            tree.snapshot()
                .root_repo_common_dir()
                .map(|path| path.as_ref()),
            Some(Path::new(path!("/repo/.git"))),
        );
        check_worktree_entries(
            tree,
            WorktreeExpectations {
                ignored_paths: &["ignored.txt"],
                tracked_paths: &["file.txt"],
                ..Default::default()
            },
        );
    });

    let nested_tree = Worktree::local(
        path!("/repo/feature-a/subdir").as_ref(),
        true,
        fs.clone(),
        Arc::default(),
        true,
        WorktreeId::from_proto(1),
        &mut cx.to_async(),
    )
    .await
    .unwrap();
    nested_tree
        .update(cx, |tree, _| tree.as_local().unwrap().scan_complete())
        .await;
    cx.run_until_parked();

    nested_tree.read_with(cx, |tree, _| {
        check_worktree_entries(
            tree,
            WorktreeExpectations {
                ignored_paths: &["ignored.txt"],
                tracked_paths: &["file.txt"],
                ..Default::default()
            },
        );
    });

    fs.write(
        Path::new(path!("/repo/.git")).join(REPO_EXCLUDE).as_ref(),
        "file.txt\n".as_bytes(),
    )
    .await
    .unwrap();
    cx.run_until_parked();

    feature_tree.read_with(cx, |tree, _| {
        check_worktree_entries(
            tree,
            WorktreeExpectations {
                ignored_paths: &["file.txt", "subdir/file.txt"],
                tracked_paths: &["ignored.txt", "subdir/ignored.txt"],
                ..Default::default()
            },
        );
    });
    nested_tree.read_with(cx, |tree, _| {
        check_worktree_entries(
            tree,
            WorktreeExpectations {
                ignored_paths: &["file.txt"],
                tracked_paths: &["ignored.txt"],
                ..Default::default()
            },
        );
    });
}

#[gpui::test]
async fn test_root_repo_common_dir(executor: BackgroundExecutor, cx: &mut TestAppContext) {
    init_test(cx);

    use git::repository::Worktree as GitWorktree;

    let fs = FakeFs::new(executor);

    // Set up a main repo and a linked worktree pointing back to it.
    fs.insert_tree(
        path!("/main_repo"),
        json!({
            ".git": {},
            "file.txt": "content",
        }),
    )
    .await;
    fs.add_linked_worktree_for_repo(
        Path::new(path!("/main_repo/.git")),
        false,
        GitWorktree {
            path: PathBuf::from(path!("/linked_worktree")),
            ref_name: Some("refs/heads/feature".into()),
            sha: "abc123".into(),
            is_main: false,
            is_bare: false,
        },
    )
    .await;
    fs.write(
        path!("/linked_worktree/file.txt").as_ref(),
        "content".as_bytes(),
    )
    .await
    .unwrap();

    let tree = Worktree::local(
        path!("/linked_worktree").as_ref(),
        true,
        fs.clone(),
        Arc::default(),
        true,
        WorktreeId::from_proto(0),
        &mut cx.to_async(),
    )
    .await
    .unwrap();
    tree.update(cx, |tree, _| tree.as_local().unwrap().scan_complete())
        .await;
    cx.run_until_parked();

    // For a linked worktree, root_repo_common_dir should point to the
    // main repo's .git, not the worktree-specific git directory.
    tree.read_with(cx, |tree, _| {
        assert_eq!(
            tree.snapshot().root_repo_common_dir().map(|p| p.as_ref()),
            Some(Path::new(path!("/main_repo/.git"))),
        );
    });

    let event_count: Rc<Cell<usize>> = Rc::new(Cell::new(0));
    tree.update(cx, {
        let event_count = event_count.clone();
        |_, cx| {
            cx.subscribe(&cx.entity(), move |_, _, event, _| {
                if matches!(event, Event::UpdatedRootRepoCommonDir { .. }) {
                    event_count.set(event_count.get() + 1);
                }
            })
            .detach();
        }
    });

    // Remove .git — root_repo_common_dir should become None.
    fs.remove_file(
        &PathBuf::from(path!("/linked_worktree/.git")),
        Default::default(),
    )
    .await
    .unwrap();
    tree.flush_fs_events(cx).await;

    tree.read_with(cx, |tree, _| {
        assert_eq!(tree.snapshot().root_repo_common_dir(), None);
    });
    assert_eq!(
        event_count.get(),
        1,
        "should have emitted UpdatedRootRepoCommonDir on removal"
    );
}

#[gpui::test]
async fn test_invisible_worktree_does_not_track_ancestor_git_repository(
    executor: BackgroundExecutor,
    cx: &mut TestAppContext,
) {
    init_test(cx);

    let fs = FakeFs::new(executor);
    fs.insert_tree(
        path!("/repo"),
        json!({
            ".git": {},
            "project": {
                "file.txt": "content",
            },
        }),
    )
    .await;

    let worktree = Worktree::local(
        path!("/repo/project").as_ref(),
        false,
        fs.clone(),
        Arc::default(),
        true,
        WorktreeId::from_proto(0),
        &mut cx.to_async(),
    )
    .await
    .unwrap();
    worktree
        .update(cx, |worktree, _| {
            worktree.as_local().unwrap().scan_complete()
        })
        .await;
    cx.run_until_parked();

    worktree.read_with(cx, |worktree, _| {
        let local_worktree = worktree.as_local().unwrap();
        assert!(local_worktree.repositories().is_empty());
        assert_eq!(local_worktree.root_repo_common_dir(), None);
    });
}

#[gpui::test]
async fn test_linked_worktree_gitfile_event_preserves_repo(
    executor: BackgroundExecutor,
    cx: &mut TestAppContext,
) {
    // Regression test: in a linked worktree, `.git` is a file (containing
    // "gitdir: ..."), not a directory. When the background scanner receives
    // a filesystem event for a path inside the main repo's `.git` directory
    // (which it watches via the commondir), the ancestor-walking code in
    // `process_events` calls `is_git_dir` on each ancestor. If `is_git_dir`
    // treats `.git` files the same as `.git` directories, it incorrectly
    // identifies the gitfile as a git dir, adds it to `dot_git_abs_paths`,
    // and `update_git_repositories` panics because the path is outside the
    // worktree root.
    init_test(cx);
    use git::repository::Worktree as GitWorktree;

    let fs = FakeFs::new(executor);
    fs.insert_tree(path!("/main_repo"), json!({ ".git": {}, "file.txt": "" }))
        .await;
    fs.add_linked_worktree_for_repo(
        Path::new(path!("/main_repo/.git")),
        false,
        GitWorktree {
            path: PathBuf::from(path!("/linked_worktree")),
            ref_name: Some("refs/heads/feature".into()),
            sha: "abc123".into(),
            is_main: false,
            is_bare: false,
        },
    )
    .await;
    fs.write(path!("/linked_worktree/file.txt").as_ref(), b"content")
        .await
        .unwrap();

    let tree = Worktree::local(
        path!("/linked_worktree").as_ref(),
        true,
        fs.clone(),
        Arc::default(),
        true,
        WorktreeId::from_proto(0),
        &mut cx.to_async(),
    )
    .await
    .unwrap();
    tree.update(cx, |tree, _| tree.as_local().unwrap().scan_complete())
        .await;
    cx.run_until_parked();

    // Overwrite the .git gitfile with garbage to trigger an event for the
    // gitfile path itself, which only matches `dot_git_abs_path`.
    fs.write(path!("/linked_worktree/.git").as_ref(), b"garbage")
        .await
        .unwrap();
    tree.flush_fs_events(cx).await;

    // The worktree should still be intact.
    tree.read_with(cx, |tree, _| {
        assert_eq!(
            tree.snapshot().root_repo_common_dir().map(|p| p.as_ref()),
            Some(Path::new(path!("/main_repo/.git"))),
            "linked worktree repo should survive a gitfile change event"
        );
    });
}

#[gpui::test]
async fn test_shared_common_dir_event_updates_all_repositories(
    executor: BackgroundExecutor,
    cx: &mut TestAppContext,
) {
    // A main checkout and one of its linked worktrees can both live inside the
    // same project worktree, sharing a common git directory. An event in that
    // common directory (e.g. a ref update) must refresh every repository that
    // reads from it, not just the first match.
    init_test(cx);

    use git::repository::Worktree as GitWorktree;

    let fs = FakeFs::new(executor.clone());
    fs.insert_tree(
        path!("/project"),
        json!({
            "main_repo": {
                ".git": {},
                "file.txt": "content",
            },
        }),
    )
    .await;
    fs.add_linked_worktree_for_repo(
        Path::new(path!("/project/main_repo/.git")),
        false,
        GitWorktree {
            path: PathBuf::from(path!("/project/linked")),
            ref_name: Some("refs/heads/feature".into()),
            sha: "abc123".into(),
            is_main: false,
            is_bare: false,
        },
    )
    .await;

    let tree = Worktree::local(
        path!("/project").as_ref(),
        true,
        fs.clone(),
        Arc::default(),
        true,
        WorktreeId::from_proto(0),
        &mut cx.to_async(),
    )
    .await
    .unwrap();
    tree.update(cx, |tree, _| tree.as_local().unwrap().scan_complete())
        .await;
    cx.run_until_parked();

    let mut events = cx.events(&tree);
    fs.emit_fs_event(
        path!("/project/main_repo/.git/refs/heads/main"),
        Some(PathEventKind::Changed),
    );
    executor.run_until_parked();

    let mut updated_work_dirs = Vec::new();
    while let Ok(event) = events.try_recv() {
        if let Event::UpdatedGitRepositories(updates) = event {
            updated_work_dirs.extend(
                updates
                    .iter()
                    .filter_map(|update| update.new_work_directory_abs_path.clone()),
            );
        }
    }
    updated_work_dirs.sort();
    assert_eq!(
        updated_work_dirs,
        [
            Arc::from(Path::new(path!("/project/linked"))),
            Arc::from(Path::new(path!("/project/main_repo"))),
        ],
        "a ref update in the shared common dir should refresh both repositories"
    );
}

#[gpui::test]
async fn test_noisy_dot_git_events_do_not_emit_git_repo_update(
    executor: BackgroundExecutor,
    cx: &mut TestAppContext,
) {
    // Events for object database writes, hook files, lock files, and the
    // reflogs of HEAD/branches/remote-tracking branches carry no git state
    // changes that Zed cares about beyond what the accompanying ref or index
    // events already convey, so they must not trigger a git metadata rescan.
    // The stash reflog and ref updates themselves must still trigger one.
    //
    init_test(cx);

    use git::repository::Worktree as GitWorktree;

    let fs = FakeFs::new(executor);

    fs.insert_tree(
        path!("/main_repo"),
        json!({
            ".git": {},
            "file.txt": "content",
        }),
    )
    .await;
    fs.add_linked_worktree_for_repo(
        Path::new(path!("/main_repo/.git")),
        false,
        GitWorktree {
            path: PathBuf::from(path!("/linked_worktree")),
            ref_name: Some("refs/heads/feature".into()),
            sha: "abc123".into(),
            is_main: false,
            is_bare: false,
        },
    )
    .await;
    fs.write(
        path!("/linked_worktree/file.txt").as_ref(),
        "content".as_bytes(),
    )
    .await
    .unwrap();

    let tree = Worktree::local(
        path!("/linked_worktree").as_ref(),
        true,
        fs.clone(),
        Arc::default(),
        true,
        WorktreeId::from_proto(0),
        &mut cx.to_async(),
    )
    .await
    .unwrap();
    tree.update(cx, |tree, _| tree.as_local().unwrap().scan_complete())
        .await;
    cx.run_until_parked();

    let repo_update_count: Rc<Cell<usize>> = Rc::new(Cell::new(0));
    tree.update(cx, {
        let repo_update_count = repo_update_count.clone();
        |_, cx| {
            cx.subscribe(&cx.entity(), move |_, _, event, _| {
                if matches!(event, Event::UpdatedGitRepositories(_)) {
                    repo_update_count.set(repo_update_count.get() + 1);
                }
            })
            .detach();
        }
    });

    let skipped_paths = [
        // Standard common git dir skipped paths
        path!("/main_repo/.git/objects/aa/bbccddee"),
        path!("/main_repo/.git/objects/pack/pack-1234.pack"),
        path!("/main_repo/.git/hooks/pre-commit"),
        path!("/main_repo/.git/logs/HEAD"),
        path!("/main_repo/.git/logs/refs/heads/main"),
        path!("/main_repo/.git/logs/refs/remotes/origin/main"),
        path!("/main_repo/.git/logs/refs/tags/v1.0"),
        path!("/main_repo/.git/rebase-merge/done"),
        path!("/main_repo/.git/rebase-apply/onto"),
        path!("/main_repo/.git/sequencer/todo"),
        path!("/main_repo/.git/index.lock"),
        path!("/main_repo/.git/refs/heads/main.lock"),
        path!("/main_repo/.git/COMMIT_EDITMSG"),
        path!("/main_repo/.git/packed-refs.new"),
        path!("/main_repo/.git/config.new"),
        path!("/main_repo/.git/index.new"),
        path!("/main_repo/.git/index-abc123.tmp"),
        path!("/main_repo/.git/FETCH_HEAD"),
        path!("/main_repo/.git/ORIG_HEAD"),
        path!("/main_repo/.git/BISECT_LOG"),
        path!("/main_repo/.git/info/refs"),
        path!("/main_repo/.git/info/refs_lzOf51"),
        path!("/main_repo/.git/gc.pid"),
        // Linked-worktree specific skipped paths
        path!("/main_repo/.git/worktrees/feature/index.lock"),
    ];
    for path in skipped_paths {
        fs.emit_fs_event(path, Some(PathEventKind::Changed));
        cx.run_until_parked();
        assert_eq!(
            repo_update_count.get(),
            0,
            "event for {path} should not emit UpdatedGitRepositories"
        );
    }

    let rescan_paths = [
        // Standard common git dir rescan paths
        path!("/main_repo/.git/logs/refs/stash"),
        path!("/main_repo/.git/refs/heads/main"),
        path!("/main_repo/.git/info/exclude"),
        path!("/main_repo/.git/refs/heads/branch.new"),
        path!("/main_repo/.git/refs/heads/branch.tmp"),
        // Linked-worktree worktree-specific rescan paths
        path!("/main_repo/.git/worktrees/feature/index"),
        path!("/main_repo/.git/worktrees/feature/HEAD"),
    ];
    for path in rescan_paths {
        let count_before = repo_update_count.get();
        fs.emit_fs_event(path, Some(PathEventKind::Changed));
        cx.run_until_parked();
        assert!(
            repo_update_count.get() > count_before,
            "event for {path} should emit UpdatedGitRepositories"
        );
    }
}

#[gpui::test]
async fn test_watcher_overflow_rescan_reloads_git_state(cx: &mut TestAppContext) {
    // When the OS watch queue overflows, pending events are dropped and the
    // watcher reports only a `Rescan` event for the worktree root. The dropped
    // events may have included changes inside `.git`, so the rescan must
    // trigger a git state reload even though no `.git` event is ever seen.
    init_test(cx);
    let fs = FakeFs::new(cx.background_executor.clone());
    fs.insert_tree(
        path!("/root"),
        json!({
            ".git": {},
            "file.txt": "content",
        }),
    )
    .await;

    let tree = Worktree::local(
        path!("/root").as_ref(),
        true,
        fs.clone(),
        Default::default(),
        true,
        WorktreeId::from_proto(0),
        &mut cx.to_async(),
    )
    .await
    .unwrap();
    tree.update(cx, |tree, _| tree.as_local().unwrap().scan_complete())
        .await;
    cx.run_until_parked();

    let repo_update_count: Rc<Cell<usize>> = Rc::new(Cell::new(0));
    tree.update(cx, {
        let repo_update_count = repo_update_count.clone();
        |_, cx| {
            cx.subscribe(&cx.entity(), move |_, _, event, _| {
                if matches!(event, Event::UpdatedGitRepositories(_)) {
                    repo_update_count.set(repo_update_count.get() + 1);
                }
            })
            .detach();
        }
    });

    // A git state change occurs while events are queued but undelivered, and
    // is then lost to a watcher overflow: the only event the worktree ever
    // receives is the root rescan.
    fs.pause_events();
    fs.set_head_for_repo(
        path!("/root/.git").as_ref(),
        &[("file.txt", "content".into())],
        "sha-after-overflow",
    );
    fs.simulate_watcher_overflow(path!("/root"));
    fs.unpause_events_and_flush();
    cx.run_until_parked();

    assert!(
        repo_update_count.get() > 0,
        "a watcher overflow rescan should reload git state, since the dropped \
         events may have included .git changes"
    );
}

#[gpui::test]
async fn test_git_update_in_same_batch_as_rescan_is_not_lost(cx: &mut TestAppContext) {
    // When a `.git` event and a watcher rescan arrive in the same batch,
    // `update_git_repositories` stamps the repository's `git_dir_scan_id`, but
    // the rescan then re-inserts the repository entry. If the re-insertion
    // resets `git_dir_scan_id`, the stamp is lost before the snapshot diff can
    // observe it. The git update must still be signaled via
    // `UpdatedGitRepositories`.
    init_test(cx);
    let fs = FakeFs::new(cx.background_executor.clone());
    fs.insert_tree(
        path!("/root"),
        json!({
            ".git": {},
            "file.txt": "content",
        }),
    )
    .await;

    let tree = Worktree::local(
        path!("/root").as_ref(),
        true,
        fs.clone(),
        Default::default(),
        true,
        WorktreeId::from_proto(0),
        &mut cx.to_async(),
    )
    .await
    .unwrap();
    tree.update(cx, |tree, _| tree.as_local().unwrap().scan_complete())
        .await;
    cx.run_until_parked();

    let repo_update_count: Rc<Cell<usize>> = Rc::new(Cell::new(0));
    tree.update(cx, {
        let repo_update_count = repo_update_count.clone();
        |_, cx| {
            cx.subscribe(&cx.entity(), move |_, _, event, _| {
                if matches!(event, Event::UpdatedGitRepositories(_)) {
                    repo_update_count.set(repo_update_count.get() + 1);
                }
            })
            .detach();
        }
    });

    // Deliver the git change and the rescan in a single batch, as happens when
    // a rescan arrives while other events are still queued.
    fs.pause_events();
    fs.set_head_for_repo(
        path!("/root/.git").as_ref(),
        &[("file.txt", "content".into())],
        "sha-with-rescan",
    );
    fs.emit_fs_event(path!("/root"), Some(PathEventKind::Rescan));
    fs.unpause_events_and_flush();
    cx.run_until_parked();

    assert!(
        repo_update_count.get() > 0,
        "a git update processed in the same batch as a rescan should still be \
         signaled via UpdatedGitRepositories"
    );
}

#[gpui::test]
async fn test_linked_worktree_event_in_unregistered_common_git_dir_does_not_panic(
    executor: BackgroundExecutor,
    cx: &mut TestAppContext,
) {
    // Regression test: a rescan event on a linked worktree's commondir
    // must not panic when the worktree's repository has already been
    // unregistered from `git_repositories`.
    init_test(cx);

    use git::repository::Worktree as GitWorktree;

    let fs = FakeFs::new(executor);

    fs.insert_tree(
        path!("/main_repo"),
        json!({
            ".git": {},
            "file.txt": "content",
        }),
    )
    .await;
    fs.add_linked_worktree_for_repo(
        Path::new(path!("/main_repo/.git")),
        false,
        GitWorktree {
            path: PathBuf::from(path!("/linked_worktree")),
            ref_name: Some("refs/heads/feature".into()),
            sha: "abc123".into(),
            is_main: false,
            is_bare: false,
        },
    )
    .await;
    fs.write(
        path!("/linked_worktree/file.txt").as_ref(),
        "content".as_bytes(),
    )
    .await
    .unwrap();

    let tree = Worktree::local(
        path!("/linked_worktree").as_ref(),
        true,
        fs.clone(),
        Arc::default(),
        true,
        WorktreeId::from_proto(0),
        &mut cx.to_async(),
    )
    .await
    .unwrap();
    tree.update(cx, |tree, _| tree.as_local().unwrap().scan_complete())
        .await;
    cx.run_until_parked();

    // Unregister the linked worktree's repository by removing its gitfile.
    fs.remove_file(
        Path::new(path!("/linked_worktree/.git")),
        Default::default(),
    )
    .await
    .unwrap();
    tree.flush_fs_events(cx).await;

    // Deliver the kind of Rescan event `FsWatcher` emits when the kernel
    // signals `need_rescan` for the commondir.
    fs.emit_fs_event(path!("/main_repo/.git"), Some(fs::PathEventKind::Rescan));
    cx.run_until_parked();
    tree.flush_fs_events(cx).await;
}

#[gpui::test]
async fn test_dot_git_dir_event_does_not_suppress_children(
    executor: BackgroundExecutor,
    cx: &mut TestAppContext,
) {
    // On Windows, modifying a file inside .git causes ReadDirectoryChangesW to also emit
    // a Modify event for the .git directory itself (because its last-write timestamp changes).
    // When these events arrive in the same batch, a naive ancestor-based dedup would collapse
    // all child events into the .git directory event, losing the information about which
    // specific files changed. This test verifies that the git-related event processing happens
    // before the dedup, so that meaningful .git child events still trigger UpdatedGitRepositories.
    init_test(cx);

    let fs = FakeFs::new(executor.clone());
    let project_dir = Path::new(path!("/project"));
    fs.insert_tree(
        project_dir,
        json!({
            ".git": {},
            "src": {
                "main.rs": "fn main() {}",
            },
        }),
    )
    .await;

    let worktree = Worktree::local(
        project_dir,
        true,
        fs.clone(),
        Default::default(),
        true,
        WorktreeId::from_proto(0),
        &mut cx.to_async(),
    )
    .await
    .unwrap();
    worktree
        .update(cx, |worktree, _| {
            worktree.as_local().unwrap().scan_complete()
        })
        .await;
    cx.run_until_parked();

    let dot_git = project_dir.join(DOT_GIT);

    // Case 1: Event for .git/index.lock only should NOT emit UpdatedGitRepositories
    // (index.lock is in the skipped files list)
    {
        let mut events = cx.events(&worktree);
        fs.pause_events();
        fs.emit_fs_event(dot_git.join("index.lock"), Some(PathEventKind::Created));
        fs.unpause_events_and_flush();
        executor.run_until_parked();

        let got_git_update = drain_git_repo_updates(&mut events);
        assert!(
            !got_git_update,
            "should NOT emit UpdatedGitRepositories when .git batch only contains index.lock"
        );
    }

    // Case 2: Event for just .git (bare directory event) should emit UpdatedGitRepositories
    {
        let mut events = cx.events(&worktree);
        fs.pause_events();
        fs.emit_fs_event(dot_git.clone(), Some(PathEventKind::Changed));
        fs.unpause_events_and_flush();
        executor.run_until_parked();

        let got_git_update = drain_git_repo_updates(&mut events);
        assert!(
            got_git_update,
            "should emit UpdatedGitRepositories for a bare .git directory event"
        );
    }

    // Case 3: Events for .git AND .git/index should emit UpdatedGitRepositories
    {
        let mut events = cx.events(&worktree);
        fs.pause_events();
        fs.emit_fs_event(dot_git.clone(), Some(PathEventKind::Changed));
        fs.emit_fs_event(dot_git.join("index"), Some(PathEventKind::Changed));
        fs.unpause_events_and_flush();
        executor.run_until_parked();

        let got_git_update = drain_git_repo_updates(&mut events);
        assert!(
            got_git_update,
            "should emit UpdatedGitRepositories when .git batch contains index"
        );
    }

    // Case 4: Event for .git/index only should emit UpdatedGitRepositories
    {
        let mut events = cx.events(&worktree);
        fs.pause_events();
        fs.emit_fs_event(dot_git.join("index"), Some(PathEventKind::Changed));
        fs.unpause_events_and_flush();
        executor.run_until_parked();

        let got_git_update = drain_git_repo_updates(&mut events);
        assert!(
            got_git_update,
            "should emit UpdatedGitRepositories for a .git/index event"
        );
    }

    {
        let mut events = cx.events(&worktree);
        fs.pause_events();
        fs.emit_fs_event(dot_git, Some(PathEventKind::Rescan));
        fs.unpause_events_and_flush();
        executor.run_until_parked();

        let got_git_update = drain_git_repo_updates(&mut events);
        assert!(
            got_git_update,
            "should emit UpdatedGitRepositories for a .git rescan event"
        );
    }

    {
        let mut events = cx.events(&worktree);
        fs.pause_events();
        fs.emit_fs_event(project_dir, Some(PathEventKind::Rescan));
        fs.unpause_events_and_flush();
        executor.run_until_parked();

        let got_git_update = drain_git_repo_updates(&mut events);
        assert!(
            got_git_update,
            "should emit UpdatedGitRepositories for a .git rescan event"
        );
    }
}

#[gpui::test]
async fn test_dot_git_event_explained_by_filtered_sibling_does_not_emit_git_repo_update(
    executor: BackgroundExecutor,
    cx: &mut TestAppContext,
) {
    // On Windows, creating or deleting a file directly inside .git (such as
    // git's transient index.lock) updates the directory's last-write time, so
    // ReadDirectoryChangesW reports a Changed event for the .git directory
    // itself alongside the event for the file. Bare .git events schedule a git
    // rescan (to cope with coalesced events on macOS), but when the same batch
    // contains a filtered-out event that explains the directory change, acting
    // on the bare event turns every ignored lock file into a rescan. Since
    // Zed's own rescans take .git/index.lock via `git diff`, that feeds back
    // into an infinite loop of git scans.
    init_test(cx);

    let fs = FakeFs::new(executor.clone());
    let project_dir = Path::new(path!("/project"));
    fs.insert_tree(
        project_dir,
        json!({
            ".git": {},
            "src": {
                "main.rs": "fn main() {}",
            },
        }),
    )
    .await;

    let worktree = Worktree::local(
        project_dir,
        true,
        fs.clone(),
        Default::default(),
        true,
        WorktreeId::from_proto(0),
        &mut cx.to_async(),
    )
    .await
    .unwrap();
    worktree
        .update(cx, |worktree, _| {
            worktree.as_local().unwrap().scan_complete()
        })
        .await;
    cx.run_until_parked();

    let dot_git = project_dir.join(DOT_GIT);

    // The exact batch Windows delivers when git creates .git/index.lock.
    {
        let mut events = cx.events(&worktree);
        fs.pause_events();
        fs.emit_fs_event(dot_git.clone(), Some(PathEventKind::Changed));
        fs.emit_fs_event(dot_git.join("index.lock"), Some(PathEventKind::Created));
        fs.unpause_events_and_flush();
        executor.run_until_parked();

        let got_git_update = drain_git_repo_updates(&mut events);
        assert!(
            !got_git_update,
            "a bare .git event accompanied only by a filtered index.lock event \
             should NOT emit UpdatedGitRepositories"
        );
    }

    // Same batch with the events in the opposite order.
    {
        let mut events = cx.events(&worktree);
        fs.pause_events();
        fs.emit_fs_event(dot_git.join("index.lock"), Some(PathEventKind::Removed));
        fs.emit_fs_event(dot_git.clone(), Some(PathEventKind::Changed));
        fs.unpause_events_and_flush();
        executor.run_until_parked();

        let got_git_update = drain_git_repo_updates(&mut events);
        assert!(
            !got_git_update,
            "event order within the batch should not matter for suppressing \
             the bare .git event"
        );
    }

    // A meaningful change in the same batch must still trigger a rescan.
    {
        let mut events = cx.events(&worktree);
        fs.pause_events();
        fs.emit_fs_event(dot_git.clone(), Some(PathEventKind::Changed));
        fs.emit_fs_event(dot_git.join("index.lock"), Some(PathEventKind::Created));
        fs.emit_fs_event(dot_git.join("HEAD"), Some(PathEventKind::Changed));
        fs.unpause_events_and_flush();
        executor.run_until_parked();

        let got_git_update = drain_git_repo_updates(&mut events);
        assert!(
            got_git_update,
            "a meaningful .git change in the same batch as a filtered event \
             should still emit UpdatedGitRepositories"
        );
    }

    // A standalone bare .git event (macOS event coalescing) must still
    // trigger a rescan.
    {
        let mut events = cx.events(&worktree);
        fs.pause_events();
        fs.emit_fs_event(dot_git, Some(PathEventKind::Changed));
        fs.unpause_events_and_flush();
        executor.run_until_parked();

        let got_git_update = drain_git_repo_updates(&mut events);
        assert!(
            got_git_update,
            "a standalone bare .git event should still emit UpdatedGitRepositories"
        );
    }
}

#[gpui::test]
async fn test_ref_updates_in_dot_git_subdirectories_are_detected(cx: &mut TestAppContext) {
    // On Linux and FreeBSD the native file watcher is non-recursive: watching `.git`
    // does not deliver events for files nested below it, like the loose refs that git
    // updates on commit, fetch, and branch operations. The worktree must watch the
    // `refs` tree explicitly, including directories created after the initial scan.
    init_test(cx);
    cx.executor().allow_parking();

    let dir = TempTree::new(json!({
        ".git": {},
        "a.txt": "a-contents",
    }));
    std::fs::write(
        dir.path().join(".git/refs/heads/main"),
        "0000000000000000000000000000000000000000\n",
    )
    .unwrap();

    let tree = Worktree::local(
        dir.path(),
        true,
        Arc::new(RealFs::new(None, cx.executor())),
        Default::default(),
        true,
        WorktreeId::from_proto(0),
        &mut cx.to_async(),
    )
    .await
    .unwrap();
    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
        .await;
    tree.flush_fs_events(cx).await;

    let mut events = cx.events(&tree);
    std::fs::write(
        dir.path().join(".git/refs/heads/main"),
        "1111111111111111111111111111111111111111\n",
    )
    .unwrap();
    expect_git_repo_update(&mut events, cx, "updating a loose ref").await;

    std::fs::create_dir_all(dir.path().join(".git/refs/remotes/origin")).unwrap();
    expect_git_repo_update(&mut events, cx, "creating a directory under refs").await;
    tree.flush_fs_events(cx).await;
    drain_git_repo_updates(&mut events);

    std::fs::write(
        dir.path().join(".git/refs/remotes/origin/main"),
        "2222222222222222222222222222222222222222\n",
    )
    .unwrap();
    expect_git_repo_update(
        &mut events,
        cx,
        "updating a ref in a directory created after the initial scan",
    )
    .await;
}

async fn expect_git_repo_update(
    events: &mut futures::channel::mpsc::UnboundedReceiver<Event>,
    cx: &mut TestAppContext,
    description: &str,
) {
    let mut elapsed = std::time::Duration::ZERO;
    let timeout = std::time::Duration::from_secs(10);
    let poll_interval = std::time::Duration::from_millis(50);
    loop {
        match events.try_recv() {
            Ok(Event::UpdatedGitRepositories(_)) => return,
            Ok(_) => continue,
            Err(_) => {}
        }
        assert!(
            elapsed < timeout,
            "timed out waiting for UpdatedGitRepositories after {description}"
        );
        cx.background_executor.timer(poll_interval).await;
        elapsed += poll_interval;
    }
}

fn drain_git_repo_updates(events: &mut futures::channel::mpsc::UnboundedReceiver<Event>) -> bool {
    let mut found = false;
    while let Ok(event) = events.try_recv() {
        if matches!(event, Event::UpdatedGitRepositories(_)) {
            found = true;
        }
    }
    found
}

fn init_test(cx: &mut gpui::TestAppContext) {
    zlog::init_test();

    cx.update(|cx| {
        let settings_store = SettingsStore::test(cx);
        cx.set_global(settings_store);
    });
}

async fn wait_for_condition(
    cx: &mut TestAppContext,
    mut condition: impl FnMut(&mut TestAppContext) -> bool,
) {
    for _ in 0..50 {
        if condition(cx) {
            return;
        }
        cx.executor().run_until_parked();
        cx.background_executor
            .timer(std::time::Duration::from_millis(10))
            .await;
    }
    panic!("timed out waiting for test condition");
}

#[gpui::test]
async fn test_load_file_encoding(cx: &mut TestAppContext) {
    init_test(cx);

    struct TestCase {
        name: &'static str,
        bytes: Vec<u8>,
        expected_text: &'static str,
    }

    // --- Success Cases ---
    let success_cases = vec![
        TestCase {
            name: "utf8.txt",
            bytes: "こんにちは".as_bytes().to_vec(),
            expected_text: "こんにちは",
        },
        TestCase {
            name: "sjis.txt",
            bytes: vec![0x82, 0xb1, 0x82, 0xf1, 0x82, 0xc9, 0x82, 0xbf, 0x82, 0xcd],
            expected_text: "こんにちは",
        },
        TestCase {
            name: "eucjp.txt",
            bytes: vec![0xa4, 0xb3, 0xa4, 0xf3, 0xa4, 0xcb, 0xa4, 0xc1, 0xa4, 0xcf],
            expected_text: "こんにちは",
        },
        TestCase {
            name: "iso2022jp.txt",
            bytes: vec![
                0x1b, 0x24, 0x42, 0x24, 0x33, 0x24, 0x73, 0x24, 0x4b, 0x24, 0x41, 0x24, 0x4f, 0x1b,
                0x28, 0x42,
            ],
            expected_text: "こんにちは",
        },
        TestCase {
            name: "win1252.txt",
            bytes: vec![0x43, 0x61, 0x66, 0xe9],
            expected_text: "Café",
        },
        TestCase {
            name: "gbk.txt",
            bytes: vec![
                0xbd, 0xf1, 0xcc, 0xec, 0xcc, 0xec, 0xc6, 0xf8, 0xb2, 0xbb, 0xb4, 0xed,
            ],
            expected_text: "今天天气不错",
        },
        // UTF-16LE with BOM
        TestCase {
            name: "utf16le_bom.txt",
            bytes: vec![
                0xFF, 0xFE, // BOM
                0x53, 0x30, 0x93, 0x30, 0x6B, 0x30, 0x61, 0x30, 0x6F, 0x30,
            ],
            expected_text: "こんにちは",
        },
        // UTF-16BE with BOM
        TestCase {
            name: "utf16be_bom.txt",
            bytes: vec![
                0xFE, 0xFF, // BOM
                0x30, 0x53, 0x30, 0x93, 0x30, 0x6B, 0x30, 0x61, 0x30, 0x6F,
            ],
            expected_text: "こんにちは",
        },
        // UTF-16LE without BOM (ASCII only)
        // This relies on the "null byte heuristic" we implemented.
        // "ABC" -> 41 00 42 00 43 00
        TestCase {
            name: "utf16le_ascii_no_bom.txt",
            bytes: vec![0x41, 0x00, 0x42, 0x00, 0x43, 0x00],
            expected_text: "ABC",
        },
    ];

    // --- Failure Cases ---
    let failure_cases = vec![
        // Binary File (Should be detected by heuristic and return Error)
        // Contains random bytes and mixed nulls that don't match UTF-16 patterns
        TestCase {
            name: "binary.bin",
            bytes: vec![0x00, 0xFF, 0x12, 0x00, 0x99, 0x88, 0x77, 0x66, 0x00],
            expected_text: "", // Not used
        },
    ];

    let root_path = if cfg!(windows) {
        Path::new("C:\\root")
    } else {
        Path::new("/root")
    };

    let fs = FakeFs::new(cx.background_executor.clone());
    fs.create_dir(root_path).await.unwrap();

    for case in success_cases.iter().chain(failure_cases.iter()) {
        let path = root_path.join(case.name);
        fs.write(&path, &case.bytes).await.unwrap();
    }

    let tree = Worktree::local(
        root_path,
        true,
        fs,
        Default::default(),
        true,
        WorktreeId::from_proto(0),
        &mut cx.to_async(),
    )
    .await
    .unwrap();

    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
        .await;

    let rel_path = |name: &str| {
        RelPath::new(&Path::new(name), PathStyle::local())
            .unwrap()
            .into_arc()
    };

    // Run Success Tests
    for case in success_cases {
        let loaded = tree
            .update(cx, |tree, cx| tree.load_file(&rel_path(case.name), cx))
            .await;
        if let Err(e) = &loaded {
            panic!("Failed to load success case '{}': {:?}", case.name, e);
        }
        let loaded = loaded.unwrap();
        assert_eq!(
            loaded.text, case.expected_text,
            "Encoding mismatch for file: {}",
            case.name
        );
    }

    // Run Failure Tests
    for case in failure_cases {
        let loaded = tree
            .update(cx, |tree, cx| tree.load_file(&rel_path(case.name), cx))
            .await;
        assert!(
            loaded.is_err(),
            "Failure case '{}' unexpectedly succeeded! It should have been detected as binary.",
            case.name
        );
        let err_msg = loaded.unwrap_err().to_string();
        println!("Got expected error for {}: {}", case.name, err_msg);
    }
}

#[gpui::test]
async fn test_write_file_encoding(cx: &mut gpui::TestAppContext) {
    init_test(cx);
    let fs = FakeFs::new(cx.executor());

    let root_path = if cfg!(windows) {
        Path::new("C:\\root")
    } else {
        Path::new("/root")
    };
    fs.create_dir(root_path).await.unwrap();

    let worktree = Worktree::local(
        root_path,
        true,
        fs.clone(),
        Default::default(),
        true,
        WorktreeId::from_proto(0),
        &mut cx.to_async(),
    )
    .await
    .unwrap();

    // Define test case structure
    struct TestCase {
        name: &'static str,
        text: &'static str,
        encoding: &'static encoding_rs::Encoding,
        has_bom: bool,
        expected_bytes: Vec<u8>,
    }

    let cases = vec![
        // Shift_JIS with Japanese
        TestCase {
            name: "Shift_JIS with Japanese",
            text: "こんにちは",
            encoding: encoding_rs::SHIFT_JIS,
            has_bom: false,
            expected_bytes: vec![0x82, 0xb1, 0x82, 0xf1, 0x82, 0xc9, 0x82, 0xbf, 0x82, 0xcd],
        },
        // UTF-8 No BOM
        TestCase {
            name: "UTF-8 No BOM",
            text: "AB",
            encoding: encoding_rs::UTF_8,
            has_bom: false,
            expected_bytes: vec![0x41, 0x42],
        },
        // UTF-8 with BOM
        TestCase {
            name: "UTF-8 with BOM",
            text: "AB",
            encoding: encoding_rs::UTF_8,
            has_bom: true,
            expected_bytes: vec![0xEF, 0xBB, 0xBF, 0x41, 0x42],
        },
        // UTF-16LE No BOM with Japanese
        // NOTE: This passes thanks to the manual encoding fix implemented in `write_file`.
        TestCase {
            name: "UTF-16LE No BOM with Japanese",
            text: "こんにちは",
            encoding: encoding_rs::UTF_16LE,
            has_bom: false,
            expected_bytes: vec![0x53, 0x30, 0x93, 0x30, 0x6b, 0x30, 0x61, 0x30, 0x6f, 0x30],
        },
        // UTF-16LE with BOM
        TestCase {
            name: "UTF-16LE with BOM",
            text: "A",
            encoding: encoding_rs::UTF_16LE,
            has_bom: true,
            expected_bytes: vec![0xFF, 0xFE, 0x41, 0x00],
        },
        // UTF-16BE No BOM with Japanese
        // NOTE: This passes thanks to the manual encoding fix.
        TestCase {
            name: "UTF-16BE No BOM with Japanese",
            text: "こんにちは",
            encoding: encoding_rs::UTF_16BE,
            has_bom: false,
            expected_bytes: vec![0x30, 0x53, 0x30, 0x93, 0x30, 0x6b, 0x30, 0x61, 0x30, 0x6f],
        },
        // UTF-16BE with BOM
        TestCase {
            name: "UTF-16BE with BOM",
            text: "A",
            encoding: encoding_rs::UTF_16BE,
            has_bom: true,
            expected_bytes: vec![0xFE, 0xFF, 0x00, 0x41],
        },
    ];

    for (i, case) in cases.into_iter().enumerate() {
        let file_name = format!("test_{}.txt", i);
        let path: Arc<Path> = Path::new(&file_name).into();
        let file_path = root_path.join(&file_name);

        fs.insert_file(&file_path, "".into()).await;

        let rel_path = RelPath::new(&path, PathStyle::local()).unwrap().into_arc();
        let text = text::Rope::from(case.text);

        let task = worktree.update(cx, |wt, cx| {
            wt.write_file(
                rel_path,
                text,
                text::LineEnding::Unix,
                case.encoding,
                case.has_bom,
                cx,
            )
        });

        if let Err(e) = task.await {
            panic!("Unexpected error in case '{}': {:?}", case.name, e);
        }

        let bytes = fs.load_bytes(&file_path).await.unwrap();

        assert_eq!(
            bytes, case.expected_bytes,
            "case '{}' mismatch. Expected {:?}, but got {:?}",
            case.name, case.expected_bytes, bytes
        );
    }
}

#[gpui::test]
async fn test_refresh_entries_for_paths_creates_ancestors(cx: &mut TestAppContext) {
    init_test(cx);
    let fs = FakeFs::new(cx.background_executor.clone());
    fs.insert_tree(
        "/root",
        json!({
            "a": {
                "b": {
                    "c": {
                        "deep_file.txt": "content",
                        "sibling.txt": "content"
                    },
                    "d": {
                        "under_sibling_dir.txt": "content"
                    }
                }
            }
        }),
    )
    .await;

    let tree = Worktree::local(
        Path::new("/root"),
        true,
        fs.clone(),
        Default::default(),
        false, // Disable scanning so the initial scan doesn't discover any entries
        WorktreeId::from_proto(0),
        &mut cx.to_async(),
    )
    .await
    .unwrap();

    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
        .await;

    tree.read_with(cx, |tree, _| {
        assert_eq!(
            tree.entries(true, 0)
                .map(|e| e.path.as_ref())
                .collect::<Vec<_>>(),
            &[rel_path("")],
            "Only root entry should exist when scanning is disabled"
        );

        assert!(tree.entry_for_path(rel_path("a")).is_none());
        assert!(tree.entry_for_path(rel_path("a/b")).is_none());
        assert!(tree.entry_for_path(rel_path("a/b/c")).is_none());
        assert!(
            tree.entry_for_path(rel_path("a/b/c/deep_file.txt"))
                .is_none()
        );
    });

    tree.read_with(cx, |tree, _| {
        tree.as_local()
            .unwrap()
            .refresh_entries_for_paths(vec![rel_path("a/b/c/deep_file.txt").into()])
    })
    .recv()
    .await;

    tree.read_with(cx, |tree, _| {
        assert_eq!(
            tree.entries(true, 0)
                .map(|e| e.path.as_ref())
                .collect::<Vec<_>>(),
            &[
                rel_path(""),
                rel_path("a"),
                rel_path("a/b"),
                rel_path("a/b/c"),
                rel_path("a/b/c/deep_file.txt"),
                rel_path("a/b/c/sibling.txt"),
                rel_path("a/b/d"),
            ],
            "All ancestors should be created when refreshing a deeply nested path"
        );
    });
}

#[gpui::test]
async fn test_single_file_worktree_deleted(cx: &mut TestAppContext) {
    init_test(cx);
    let fs = FakeFs::new(cx.background_executor.clone());

    fs.insert_tree(
        "/root",
        json!({
            "test.txt": "content",
        }),
    )
    .await;

    let tree = Worktree::local(
        Path::new("/root/test.txt"),
        true,
        fs.clone(),
        Default::default(),
        true,
        WorktreeId::from_proto(0),
        &mut cx.to_async(),
    )
    .await
    .unwrap();

    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
        .await;

    tree.read_with(cx, |tree, _| {
        assert!(tree.is_single_file(), "Should be a single-file worktree");
        assert_eq!(tree.abs_path().as_ref(), Path::new("/root/test.txt"));
    });

    // Delete the file
    fs.remove_file(Path::new("/root/test.txt"), Default::default())
        .await
        .unwrap();

    // Subscribe to worktree events
    let deleted_event_received = Rc::new(Cell::new(false));
    let _subscription = cx.update({
        let deleted_event_received = deleted_event_received.clone();
        |cx| {
            cx.subscribe(&tree, move |_, event, _| {
                if matches!(event, Event::Deleted) {
                    deleted_event_received.set(true);
                }
            })
        }
    });

    // Trigger filesystem events - the scanner should detect the file is gone immediately
    // and emit a Deleted event
    cx.background_executor.run_until_parked();
    cx.background_executor
        .advance_clock(std::time::Duration::from_secs(1));
    cx.background_executor.run_until_parked();

    assert!(
        deleted_event_received.get(),
        "Should receive Deleted event when single-file worktree root is deleted"
    );
}

#[gpui::test]
async fn test_remote_worktree_without_git_emits_root_repo_event_after_first_update(
    cx: &mut TestAppContext,
) {
    cx.update(|cx| {
        let store = SettingsStore::test(cx);
        cx.set_global(store);
    });

    let client = AnyProtoClient::new(NoopProtoClient::new());

    let worktree = cx.update(|cx| {
        Worktree::remote(
            1,
            clock::ReplicaId::new(1),
            proto::WorktreeMetadata {
                id: 1,
                root_name: "project".to_string(),
                visible: true,
                abs_path: "/home/user/project".to_string(),
                root_repo_common_dir: None,
                root_repo_is_linked_worktree: false,
            },
            client,
            PathStyle::Unix,
            cx,
        )
    });

    let events: Arc<std::sync::Mutex<Vec<&'static str>>> =
        Arc::new(std::sync::Mutex::new(Vec::new()));
    let events_clone = events.clone();
    cx.update(|cx| {
        cx.subscribe(&worktree, move |_, event, _cx| {
            if matches!(event, Event::UpdatedRootRepoCommonDir { .. }) {
                events_clone
                    .lock()
                    .unwrap()
                    .push("UpdatedRootRepoCommonDir");
            }
            if matches!(event, Event::UpdatedEntries(_)) {
                events_clone.lock().unwrap().push("UpdatedEntries");
            }
        })
        .detach();
    });

    // Send an update with entries but no repo info (plain directory).
    worktree.update(cx, |worktree, _cx| {
        worktree
            .as_remote()
            .unwrap()
            .update_from_remote(proto::UpdateWorktree {
                project_id: 1,
                worktree_id: 1,
                abs_path: "/home/user/project".to_string(),
                root_name: "project".to_string(),
                updated_entries: vec![proto::Entry {
                    id: 1,
                    is_dir: true,
                    path: "".to_string(),
                    inode: 1,
                    mtime: Some(proto::Timestamp {
                        seconds: 0,
                        nanos: 0,
                    }),
                    is_ignored: false,
                    is_hidden: false,
                    is_external: false,
                    is_fifo: false,
                    size: None,
                    canonical_path: None,
                }],
                removed_entries: vec![],
                scan_id: 1,
                is_last_update: true,
                updated_repositories: vec![],
                removed_repositories: vec![],
                root_repo_common_dir: None,
                root_repo_is_linked_worktree: false,
            });
    });

    cx.run_until_parked();

    let fired = events.lock().unwrap();
    assert!(
        fired.contains(&"UpdatedEntries"),
        "UpdatedEntries should fire after remote update"
    );
    assert!(
        fired.contains(&"UpdatedRootRepoCommonDir"),
        "UpdatedRootRepoCommonDir should fire after first remote update even when \
         root_repo_common_dir is None, to signal that repo state is now known"
    );
}

#[gpui::test]
async fn test_remote_worktree_with_git_emits_root_repo_event_when_repo_info_arrives(
    cx: &mut TestAppContext,
) {
    cx.update(|cx| {
        let store = SettingsStore::test(cx);
        cx.set_global(store);
    });

    let client = AnyProtoClient::new(NoopProtoClient::new());

    let worktree = cx.update(|cx| {
        Worktree::remote(
            1,
            clock::ReplicaId::new(1),
            proto::WorktreeMetadata {
                id: 1,
                root_name: "project".to_string(),
                visible: true,
                abs_path: "/home/user/project".to_string(),
                root_repo_common_dir: None,
                root_repo_is_linked_worktree: false,
            },
            client,
            PathStyle::Unix,
            cx,
        )
    });

    let events: Arc<std::sync::Mutex<Vec<&'static str>>> =
        Arc::new(std::sync::Mutex::new(Vec::new()));
    let events_clone = events.clone();
    cx.update(|cx| {
        cx.subscribe(&worktree, move |_, event, _cx| {
            if matches!(event, Event::UpdatedRootRepoCommonDir { .. }) {
                events_clone
                    .lock()
                    .unwrap()
                    .push("UpdatedRootRepoCommonDir");
            }
        })
        .detach();
    });

    // Send an update where repo info arrives (None -> Some).
    worktree.update(cx, |worktree, _cx| {
        worktree
            .as_remote()
            .unwrap()
            .update_from_remote(proto::UpdateWorktree {
                project_id: 1,
                worktree_id: 1,
                abs_path: "/home/user/project".to_string(),
                root_name: "project".to_string(),
                updated_entries: vec![proto::Entry {
                    id: 1,
                    is_dir: true,
                    path: "".to_string(),
                    inode: 1,
                    mtime: Some(proto::Timestamp {
                        seconds: 0,
                        nanos: 0,
                    }),
                    is_ignored: false,
                    is_hidden: false,
                    is_external: false,
                    is_fifo: false,
                    size: None,
                    canonical_path: None,
                }],
                removed_entries: vec![],
                scan_id: 1,
                is_last_update: true,
                updated_repositories: vec![],
                removed_repositories: vec![],
                root_repo_common_dir: Some("/home/user/project/.git".to_string()),
                root_repo_is_linked_worktree: false,
            });
    });

    cx.run_until_parked();

    let fired = events.lock().unwrap();
    assert!(
        fired.contains(&"UpdatedRootRepoCommonDir"),
        "UpdatedRootRepoCommonDir should fire when repo info arrives (None -> Some)"
    );
    assert_eq!(
        fired
            .iter()
            .filter(|e| **e == "UpdatedRootRepoCommonDir")
            .count(),
        1,
        "should fire exactly once, not duplicate"
    );
}

#[gpui::test]
async fn test_remote_worktree_root_repo_metadata_cleared_only_by_completed_scan(
    cx: &mut TestAppContext,
) {
    cx.update(|cx| {
        let store = SettingsStore::test(cx);
        cx.set_global(store);
    });

    let client = AnyProtoClient::new(NoopProtoClient::new());

    // Metadata eagerly seeds the root repo info, as `AddWorktreeResponse` /
    // `WorktreeMetadata` do before the host's scan completes.
    let worktree = cx.update(|cx| {
        Worktree::remote(
            1,
            clock::ReplicaId::new(1),
            proto::WorktreeMetadata {
                id: 1,
                root_name: "feature-a".to_string(),
                visible: true,
                abs_path: "/home/user/monty/feature-a".to_string(),
                root_repo_common_dir: Some("/home/user/monty/.bare".to_string()),
                root_repo_is_linked_worktree: true,
            },
            client,
            PathStyle::Unix,
            cx,
        )
    });

    let root_repo_metadata = |cx: &mut TestAppContext| {
        worktree.read_with(cx, |worktree, _| {
            let snapshot = worktree.snapshot();
            (
                snapshot.root_repo_common_dir().cloned(),
                snapshot.root_repo_is_linked_worktree(),
            )
        })
    };

    let update = |scan_id: u64, is_last_update: bool| proto::UpdateWorktree {
        project_id: 1,
        worktree_id: 1,
        abs_path: "/home/user/monty/feature-a".to_string(),
        root_name: "feature-a".to_string(),
        updated_entries: vec![],
        removed_entries: vec![],
        scan_id,
        is_last_update,
        updated_repositories: vec![],
        removed_repositories: vec![],
        root_repo_common_dir: None,
        root_repo_is_linked_worktree: false,
    };

    // A mid-scan update without repo info must not clobber the seeded
    // metadata: the sender's scanner may not have registered the repo yet.
    worktree.update(cx, |worktree, _cx| {
        worktree
            .as_remote()
            .unwrap()
            .update_from_remote(update(2, false));
    });
    cx.run_until_parked();

    assert_eq!(
        root_repo_metadata(cx),
        (Some(Arc::from(Path::new("/home/user/monty/.bare"))), true,),
        "mid-scan update without repo info should not clear seeded metadata"
    );

    // A completed scan without repo info is authoritative: the repo is gone.
    worktree.update(cx, |worktree, _cx| {
        worktree
            .as_remote()
            .unwrap()
            .update_from_remote(update(3, true));
    });
    cx.run_until_parked();

    assert_eq!(
        root_repo_metadata(cx),
        (None, false),
        "completed scan without repo info should clear root repo metadata"
    );
}

// Regression test: a remote worktree used to emit `UpdatedEntries` with an
// empty changeset (`Arc::default()`), discarding the changed paths. Consumers
// that key off those paths - notably the agent's `.agents/skills` refresh -
// therefore never fired on remote projects, so skills pasted into an already
// open project were never picked up. The changeset must carry the real paths.
//
// This drives the real host -> remote pipeline: a `FakeFs`-backed local
// worktree scans the filesystem and produces `UpdateWorktree` messages via
// `observe_updates`, which we relay into a remote worktree exactly as the
// collab server does.
#[gpui::test]
async fn test_remote_worktree_update_entries_carry_changed_paths(cx: &mut TestAppContext) {
    init_test(cx);

    let fs = FakeFs::new(cx.background_executor.clone());
    fs.insert_tree(
        path!("/root"),
        json!({
            ".agents": {
                "skills": {}
            }
        }),
    )
    .await;

    // The host worktree scans the fake filesystem and broadcasts updates.
    let host = Worktree::local(
        path!("/root").as_ref(),
        true,
        fs.clone(),
        Default::default(),
        true,
        WorktreeId::from_proto(1),
        &mut cx.to_async(),
    )
    .await
    .unwrap();
    cx.read(|cx| host.read(cx).as_local().unwrap().scan_complete())
        .await;

    // The remote worktree receives those updates over a simulated connection.
    let remote = cx.update(|cx| {
        Worktree::remote(
            1,
            clock::ReplicaId::new(1),
            proto::WorktreeMetadata {
                id: 1,
                root_name: "root".to_string(),
                visible: true,
                abs_path: path!("/root").to_string(),
                root_repo_common_dir: None,
                root_repo_is_linked_worktree: false,
            },
            AnyProtoClient::new(NoopProtoClient::new()),
            PathStyle::local(),
            cx,
        )
    });

    // Relay every `UpdateWorktree` the host emits into the remote worktree,
    // mirroring how the collab server forwards them. The callback only buffers
    // the messages; we apply them on the foreground via `relay`.
    let pending: Arc<Mutex<Vec<proto::UpdateWorktree>>> = Arc::new(Mutex::new(Vec::new()));
    host.update(cx, |host, cx| {
        let pending = pending.clone();
        host.as_local_mut()
            .unwrap()
            .observe_updates(1, cx, move |update| {
                pending.lock().push(update);
                async { true }
            });
    });
    let relay = {
        let remote = remote.clone();
        move |cx: &mut TestAppContext| {
            let updates = std::mem::take(&mut *pending.lock());
            remote.update(cx, |remote, _| {
                let remote = remote.as_remote().unwrap();
                for update in updates {
                    remote.update_from_remote(update);
                }
            });
        }
    };

    // Record the (path, change) pairs from every `UpdatedEntries` event the
    // remote worktree emits.
    let changes: Arc<Mutex<Vec<(String, PathChange)>>> = Arc::new(Mutex::new(Vec::new()));
    cx.update(|cx| {
        let changes = changes.clone();
        cx.subscribe(&remote, move |_, event, _cx| {
            if let Event::UpdatedEntries(updated) = event {
                changes.lock().extend(
                    updated
                        .iter()
                        .map(|(path, _, change)| (path.as_unix_str().to_string(), *change)),
                );
            }
        })
        .detach();
    });

    // Flush the initial sync (root + existing dirs) and ignore those paths.
    cx.run_until_parked();
    relay(cx);
    cx.run_until_parked();
    changes.lock().clear();

    // Paste a skill folder into `.agents/skills` on the host.
    fs.insert_tree(
        path!("/root/.agents/skills/skill-1"),
        json!({ "SKILL.md": "skill" }),
    )
    .await;
    cx.run_until_parked();
    relay(cx);
    cx.run_until_parked();

    {
        let changes = changes.lock();
        assert!(
            changes
                .iter()
                .any(|(path, change)| path == ".agents/skills/skill-1/SKILL.md"
                    && *change == PathChange::AddedOrUpdated),
            "remote UpdatedEntries should carry the added skill path, got {:?}",
            changes
        );
    }
    changes.lock().clear();

    // Remove the skill folder. The wire format only carries entry ids for
    // removals, so the remote worktree must resolve their paths against the
    // previous snapshot before it is replaced.
    fs.remove_dir(
        path!("/root/.agents/skills/skill-1").as_ref(),
        RemoveOptions {
            recursive: true,
            ignore_if_not_exists: false,
        },
    )
    .await
    .unwrap();
    cx.run_until_parked();
    relay(cx);
    cx.run_until_parked();

    let changes = changes.lock();
    assert!(
        changes
            .iter()
            .any(|(path, change)| path == ".agents/skills/skill-1/SKILL.md"
                && *change == PathChange::Removed),
        "remote UpdatedEntries should carry removed paths resolved from the \
         previous snapshot, got {:?}",
        changes
    );
}

#[gpui::test]
async fn test_deferred_watch_repository_above_root(
    executor: BackgroundExecutor,
    cx: &mut TestAppContext,
) {
    init_test(cx);

    let fs = FakeFs::new(executor);
    fs.insert_tree(
        path!("/root"),
        json!({
            ".git": {},
            "subproject": {
                "a.txt": "A"
            }
        }),
    )
    .await;
    let worktree = Worktree::local(
        path!("/root/subproject").as_ref(),
        true,
        fs.clone(),
        Arc::default(),
        true,
        WorktreeId::from_proto(0),
        &mut cx.to_async(),
    )
    .await
    .unwrap();
    worktree
        .update(cx, |worktree, _| {
            worktree.as_local().unwrap().scan_complete()
        })
        .await;
    cx.run_until_parked();

    worktree.update(cx, |worktree, cx| {
        worktree.as_local_mut().unwrap().set_defer_watch(true, cx);
    });
    worktree
        .update(cx, |worktree, _| {
            worktree.as_local().unwrap().scan_complete()
        })
        .await;
    cx.run_until_parked();

    let repos = worktree.update(cx, |worktree, _| {
        worktree.as_local().unwrap().repositories()
    });
    pretty_assertions::assert_eq!(repos, [Path::new(path!("/root")).into()]);
}

#[gpui::test]
async fn test_deferred_watch_symlinks_pointing_outside(cx: &mut TestAppContext) {
    init_test(cx);
    let fs = FakeFs::new(cx.background_executor.clone());
    fs.insert_tree(
        "/root",
        json!({
            "dir1": {
                "deps": {},
                "src": {
                    "a.rs": "",
                },
            },
            "dir2": {
                "src": {
                    "c.rs": "",
                }
            },
        }),
    )
    .await;

    fs.create_symlink("/root/dir1/deps/dep-dir2".as_ref(), "../../dir2".into())
        .await
        .unwrap();

    let tree = Worktree::local(
        Path::new("/root/dir1"),
        true,
        fs.clone(),
        Default::default(),
        true,
        WorktreeId::from_proto(0),
        &mut cx.to_async(),
    )
    .await
    .unwrap();

    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
        .await;
    cx.run_until_parked();

    tree.update(cx, |tree, cx| {
        tree.as_local_mut().unwrap().set_defer_watch(true, cx);
    });
    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
        .await;
    cx.run_until_parked();

    tree.read_with(cx, |tree, _| {
        assert_eq!(
            tree.entries(true, 0)
                .map(|entry| (entry.path.as_ref(), entry.is_external))
                .collect::<Vec<_>>(),
            vec![
                (rel_path(""), false),
                (rel_path("deps"), false),
                (rel_path("deps/dep-dir2"), true),
                (rel_path("src"), false),
                (rel_path("src/a.rs"), false),
            ]
        );
    });

    tree.read_with(cx, |tree, _| {
        tree.as_local()
            .unwrap()
            .refresh_entries_for_paths(vec![rel_path("deps/dep-dir2").into()])
    })
    .recv()
    .await;

    tree.read_with(cx, |tree, _| {
        assert_eq!(
            tree.entries(true, 0)
                .map(|entry| (entry.path.as_ref(), entry.is_external))
                .collect::<Vec<_>>(),
            vec![
                (rel_path(""), false),
                (rel_path("deps"), false),
                (rel_path("deps/dep-dir2"), true),
                (rel_path("deps/dep-dir2/src"), true),
                (rel_path("src"), false),
                (rel_path("src/a.rs"), false),
            ]
        );
    });

    tree.read_with(cx, |tree, _| {
        tree.as_local()
            .unwrap()
            .refresh_entries_for_paths(vec![rel_path("deps/dep-dir2/src").into()])
    })
    .recv()
    .await;

    tree.read_with(cx, |tree, _| {
        assert!(
            tree.entry_for_path(rel_path("deps/dep-dir2/src/c.rs"))
                .is_some()
        );
    });

    fs.insert_file(Path::new("/root/dir2/src/new.rs"), b"".to_vec())
        .await;

    wait_for_condition(cx, |cx| {
        tree.read_with(cx, |tree, _| {
            tree.entry_for_path(rel_path("deps/dep-dir2/src/new.rs"))
                .is_some()
        })
    })
    .await;
}

#[test]
fn test_repo_exclude_does_not_match_outside_its_work_directory() {
    use ignore::gitignore::GitignoreBuilder;
    use worktree::{IgnoreKind, IgnoreStack};

    let mut builder = GitignoreBuilder::new("/repo/inner");
    builder.add_line(None, "build").unwrap();
    builder.add_line(None, "repo").unwrap();
    let exclude = Arc::new(builder.build().unwrap());

    let stack = IgnoreStack::none().append(IgnoreKind::RepoExclude, exclude);

    assert!(
        stack.is_abs_path_ignored(Path::new("/repo/inner/build"), true),
        "patterns must apply within the repository's work directory"
    );
    assert!(
        !stack.is_abs_path_ignored(Path::new("/repo"), true),
        "patterns must not apply outside the repository's work directory"
    );
}
