worktree_tests.rs

   1use crate::{
   2    Entry, EntryKind, Event, PathChange, WorkDirectory, Worktree, WorktreeModelHandle,
   3    worktree_settings::WorktreeSettings,
   4};
   5use anyhow::Result;
   6use fs::{FakeFs, Fs, RealFs, RemoveOptions};
   7use git::GITIGNORE;
   8use gpui::{AppContext as _, BackgroundExecutor, BorrowAppContext, Context, Task, TestAppContext};
   9use parking_lot::Mutex;
  10use postage::stream::Stream;
  11use pretty_assertions::assert_eq;
  12use rand::prelude::*;
  13
  14use serde_json::json;
  15use settings::{Settings, SettingsStore};
  16use std::{
  17    env,
  18    fmt::Write,
  19    mem,
  20    path::{Path, PathBuf},
  21    sync::Arc,
  22};
  23use util::{ResultExt, path, test::TempTree};
  24
  25#[gpui::test]
  26async fn test_traversal(cx: &mut TestAppContext) {
  27    init_test(cx);
  28    let fs = FakeFs::new(cx.background_executor.clone());
  29    fs.insert_tree(
  30        "/root",
  31        json!({
  32           ".gitignore": "a/b\n",
  33           "a": {
  34               "b": "",
  35               "c": "",
  36           }
  37        }),
  38    )
  39    .await;
  40
  41    let tree = Worktree::local(
  42        Path::new("/root"),
  43        true,
  44        fs,
  45        Default::default(),
  46        &mut cx.to_async(),
  47    )
  48    .await
  49    .unwrap();
  50    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
  51        .await;
  52
  53    tree.read_with(cx, |tree, _| {
  54        assert_eq!(
  55            tree.entries(false, 0)
  56                .map(|entry| entry.path.as_ref())
  57                .collect::<Vec<_>>(),
  58            vec![
  59                Path::new(""),
  60                Path::new(".gitignore"),
  61                Path::new("a"),
  62                Path::new("a/c"),
  63            ]
  64        );
  65        assert_eq!(
  66            tree.entries(true, 0)
  67                .map(|entry| entry.path.as_ref())
  68                .collect::<Vec<_>>(),
  69            vec![
  70                Path::new(""),
  71                Path::new(".gitignore"),
  72                Path::new("a"),
  73                Path::new("a/b"),
  74                Path::new("a/c"),
  75            ]
  76        );
  77    })
  78}
  79
  80#[gpui::test(iterations = 10)]
  81async fn test_circular_symlinks(cx: &mut TestAppContext) {
  82    init_test(cx);
  83    let fs = FakeFs::new(cx.background_executor.clone());
  84    fs.insert_tree(
  85        "/root",
  86        json!({
  87            "lib": {
  88                "a": {
  89                    "a.txt": ""
  90                },
  91                "b": {
  92                    "b.txt": ""
  93                }
  94            }
  95        }),
  96    )
  97    .await;
  98    fs.create_symlink("/root/lib/a/lib".as_ref(), "..".into())
  99        .await
 100        .unwrap();
 101    fs.create_symlink("/root/lib/b/lib".as_ref(), "..".into())
 102        .await
 103        .unwrap();
 104
 105    let tree = Worktree::local(
 106        Path::new("/root"),
 107        true,
 108        fs.clone(),
 109        Default::default(),
 110        &mut cx.to_async(),
 111    )
 112    .await
 113    .unwrap();
 114
 115    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
 116        .await;
 117
 118    tree.read_with(cx, |tree, _| {
 119        assert_eq!(
 120            tree.entries(false, 0)
 121                .map(|entry| entry.path.as_ref())
 122                .collect::<Vec<_>>(),
 123            vec![
 124                Path::new(""),
 125                Path::new("lib"),
 126                Path::new("lib/a"),
 127                Path::new("lib/a/a.txt"),
 128                Path::new("lib/a/lib"),
 129                Path::new("lib/b"),
 130                Path::new("lib/b/b.txt"),
 131                Path::new("lib/b/lib"),
 132            ]
 133        );
 134    });
 135
 136    fs.rename(
 137        Path::new("/root/lib/a/lib"),
 138        Path::new("/root/lib/a/lib-2"),
 139        Default::default(),
 140    )
 141    .await
 142    .unwrap();
 143    cx.executor().run_until_parked();
 144    tree.read_with(cx, |tree, _| {
 145        assert_eq!(
 146            tree.entries(false, 0)
 147                .map(|entry| entry.path.as_ref())
 148                .collect::<Vec<_>>(),
 149            vec![
 150                Path::new(""),
 151                Path::new("lib"),
 152                Path::new("lib/a"),
 153                Path::new("lib/a/a.txt"),
 154                Path::new("lib/a/lib-2"),
 155                Path::new("lib/b"),
 156                Path::new("lib/b/b.txt"),
 157                Path::new("lib/b/lib"),
 158            ]
 159        );
 160    });
 161}
 162
 163#[gpui::test]
 164async fn test_symlinks_pointing_outside(cx: &mut TestAppContext) {
 165    init_test(cx);
 166    let fs = FakeFs::new(cx.background_executor.clone());
 167    fs.insert_tree(
 168        "/root",
 169        json!({
 170            "dir1": {
 171                "deps": {
 172                    // symlinks here
 173                },
 174                "src": {
 175                    "a.rs": "",
 176                    "b.rs": "",
 177                },
 178            },
 179            "dir2": {
 180                "src": {
 181                    "c.rs": "",
 182                    "d.rs": "",
 183                }
 184            },
 185            "dir3": {
 186                "deps": {},
 187                "src": {
 188                    "e.rs": "",
 189                    "f.rs": "",
 190                },
 191            }
 192        }),
 193    )
 194    .await;
 195
 196    // These symlinks point to directories outside of the worktree's root, dir1.
 197    fs.create_symlink("/root/dir1/deps/dep-dir2".as_ref(), "../../dir2".into())
 198        .await
 199        .unwrap();
 200    fs.create_symlink("/root/dir1/deps/dep-dir3".as_ref(), "../../dir3".into())
 201        .await
 202        .unwrap();
 203
 204    let tree = Worktree::local(
 205        Path::new("/root/dir1"),
 206        true,
 207        fs.clone(),
 208        Default::default(),
 209        &mut cx.to_async(),
 210    )
 211    .await
 212    .unwrap();
 213
 214    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
 215        .await;
 216
 217    let tree_updates = Arc::new(Mutex::new(Vec::new()));
 218    tree.update(cx, |_, cx| {
 219        let tree_updates = tree_updates.clone();
 220        cx.subscribe(&tree, move |_, _, event, _| {
 221            if let Event::UpdatedEntries(update) = event {
 222                tree_updates.lock().extend(
 223                    update
 224                        .iter()
 225                        .map(|(path, _, change)| (path.clone(), *change)),
 226                );
 227            }
 228        })
 229        .detach();
 230    });
 231
 232    // The symlinked directories are not scanned by default.
 233    tree.read_with(cx, |tree, _| {
 234        assert_eq!(
 235            tree.entries(true, 0)
 236                .map(|entry| (entry.path.as_ref(), entry.is_external))
 237                .collect::<Vec<_>>(),
 238            vec![
 239                (Path::new(""), false),
 240                (Path::new("deps"), false),
 241                (Path::new("deps/dep-dir2"), true),
 242                (Path::new("deps/dep-dir3"), true),
 243                (Path::new("src"), false),
 244                (Path::new("src/a.rs"), false),
 245                (Path::new("src/b.rs"), false),
 246            ]
 247        );
 248
 249        assert_eq!(
 250            tree.entry_for_path("deps/dep-dir2").unwrap().kind,
 251            EntryKind::UnloadedDir
 252        );
 253    });
 254
 255    // Expand one of the symlinked directories.
 256    tree.read_with(cx, |tree, _| {
 257        tree.as_local()
 258            .unwrap()
 259            .refresh_entries_for_paths(vec![Path::new("deps/dep-dir3").into()])
 260    })
 261    .recv()
 262    .await;
 263
 264    // The expanded directory's contents are loaded. Subdirectories are
 265    // not scanned yet.
 266    tree.read_with(cx, |tree, _| {
 267        assert_eq!(
 268            tree.entries(true, 0)
 269                .map(|entry| (entry.path.as_ref(), entry.is_external))
 270                .collect::<Vec<_>>(),
 271            vec![
 272                (Path::new(""), false),
 273                (Path::new("deps"), false),
 274                (Path::new("deps/dep-dir2"), true),
 275                (Path::new("deps/dep-dir3"), true),
 276                (Path::new("deps/dep-dir3/deps"), true),
 277                (Path::new("deps/dep-dir3/src"), true),
 278                (Path::new("src"), false),
 279                (Path::new("src/a.rs"), false),
 280                (Path::new("src/b.rs"), false),
 281            ]
 282        );
 283    });
 284    assert_eq!(
 285        mem::take(&mut *tree_updates.lock()),
 286        &[
 287            (Path::new("deps/dep-dir3").into(), PathChange::Loaded),
 288            (Path::new("deps/dep-dir3/deps").into(), PathChange::Loaded),
 289            (Path::new("deps/dep-dir3/src").into(), PathChange::Loaded)
 290        ]
 291    );
 292
 293    // Expand a subdirectory of one of the symlinked directories.
 294    tree.read_with(cx, |tree, _| {
 295        tree.as_local()
 296            .unwrap()
 297            .refresh_entries_for_paths(vec![Path::new("deps/dep-dir3/src").into()])
 298    })
 299    .recv()
 300    .await;
 301
 302    // The expanded subdirectory's contents are loaded.
 303    tree.read_with(cx, |tree, _| {
 304        assert_eq!(
 305            tree.entries(true, 0)
 306                .map(|entry| (entry.path.as_ref(), entry.is_external))
 307                .collect::<Vec<_>>(),
 308            vec![
 309                (Path::new(""), false),
 310                (Path::new("deps"), false),
 311                (Path::new("deps/dep-dir2"), true),
 312                (Path::new("deps/dep-dir3"), true),
 313                (Path::new("deps/dep-dir3/deps"), true),
 314                (Path::new("deps/dep-dir3/src"), true),
 315                (Path::new("deps/dep-dir3/src/e.rs"), true),
 316                (Path::new("deps/dep-dir3/src/f.rs"), true),
 317                (Path::new("src"), false),
 318                (Path::new("src/a.rs"), false),
 319                (Path::new("src/b.rs"), false),
 320            ]
 321        );
 322    });
 323
 324    assert_eq!(
 325        mem::take(&mut *tree_updates.lock()),
 326        &[
 327            (Path::new("deps/dep-dir3/src").into(), PathChange::Loaded),
 328            (
 329                Path::new("deps/dep-dir3/src/e.rs").into(),
 330                PathChange::Loaded
 331            ),
 332            (
 333                Path::new("deps/dep-dir3/src/f.rs").into(),
 334                PathChange::Loaded
 335            )
 336        ]
 337    );
 338}
 339
 340#[cfg(target_os = "macos")]
 341#[gpui::test]
 342async fn test_renaming_case_only(cx: &mut TestAppContext) {
 343    cx.executor().allow_parking();
 344    init_test(cx);
 345
 346    const OLD_NAME: &str = "aaa.rs";
 347    const NEW_NAME: &str = "AAA.rs";
 348
 349    let fs = Arc::new(RealFs::new(None, cx.executor()));
 350    let temp_root = TempTree::new(json!({
 351        OLD_NAME: "",
 352    }));
 353
 354    let tree = Worktree::local(
 355        temp_root.path(),
 356        true,
 357        fs.clone(),
 358        Default::default(),
 359        &mut cx.to_async(),
 360    )
 361    .await
 362    .unwrap();
 363
 364    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
 365        .await;
 366    tree.read_with(cx, |tree, _| {
 367        assert_eq!(
 368            tree.entries(true, 0)
 369                .map(|entry| entry.path.as_ref())
 370                .collect::<Vec<_>>(),
 371            vec![Path::new(""), Path::new(OLD_NAME)]
 372        );
 373    });
 374
 375    fs.rename(
 376        &temp_root.path().join(OLD_NAME),
 377        &temp_root.path().join(NEW_NAME),
 378        fs::RenameOptions {
 379            overwrite: true,
 380            ignore_if_exists: true,
 381        },
 382    )
 383    .await
 384    .unwrap();
 385
 386    tree.flush_fs_events(cx).await;
 387
 388    tree.read_with(cx, |tree, _| {
 389        assert_eq!(
 390            tree.entries(true, 0)
 391                .map(|entry| entry.path.as_ref())
 392                .collect::<Vec<_>>(),
 393            vec![Path::new(""), Path::new(NEW_NAME)]
 394        );
 395    });
 396}
 397
 398#[gpui::test]
 399async fn test_open_gitignored_files(cx: &mut TestAppContext) {
 400    init_test(cx);
 401    let fs = FakeFs::new(cx.background_executor.clone());
 402    fs.insert_tree(
 403        "/root",
 404        json!({
 405            ".gitignore": "node_modules\n",
 406            "one": {
 407                "node_modules": {
 408                    "a": {
 409                        "a1.js": "a1",
 410                        "a2.js": "a2",
 411                    },
 412                    "b": {
 413                        "b1.js": "b1",
 414                        "b2.js": "b2",
 415                    },
 416                    "c": {
 417                        "c1.js": "c1",
 418                        "c2.js": "c2",
 419                    }
 420                },
 421            },
 422            "two": {
 423                "x.js": "",
 424                "y.js": "",
 425            },
 426        }),
 427    )
 428    .await;
 429
 430    let tree = Worktree::local(
 431        Path::new("/root"),
 432        true,
 433        fs.clone(),
 434        Default::default(),
 435        &mut cx.to_async(),
 436    )
 437    .await
 438    .unwrap();
 439
 440    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
 441        .await;
 442
 443    tree.read_with(cx, |tree, _| {
 444        assert_eq!(
 445            tree.entries(true, 0)
 446                .map(|entry| (entry.path.as_ref(), entry.is_ignored))
 447                .collect::<Vec<_>>(),
 448            vec![
 449                (Path::new(""), false),
 450                (Path::new(".gitignore"), false),
 451                (Path::new("one"), false),
 452                (Path::new("one/node_modules"), true),
 453                (Path::new("two"), false),
 454                (Path::new("two/x.js"), false),
 455                (Path::new("two/y.js"), false),
 456            ]
 457        );
 458    });
 459
 460    // Open a file that is nested inside of a gitignored directory that
 461    // has not yet been expanded.
 462    let prev_read_dir_count = fs.read_dir_call_count();
 463    let loaded = tree
 464        .update(cx, |tree, cx| {
 465            tree.load_file("one/node_modules/b/b1.js".as_ref(), cx)
 466        })
 467        .await
 468        .unwrap();
 469
 470    tree.read_with(cx, |tree, _| {
 471        assert_eq!(
 472            tree.entries(true, 0)
 473                .map(|entry| (entry.path.as_ref(), entry.is_ignored))
 474                .collect::<Vec<_>>(),
 475            vec![
 476                (Path::new(""), false),
 477                (Path::new(".gitignore"), false),
 478                (Path::new("one"), false),
 479                (Path::new("one/node_modules"), true),
 480                (Path::new("one/node_modules/a"), true),
 481                (Path::new("one/node_modules/b"), true),
 482                (Path::new("one/node_modules/b/b1.js"), true),
 483                (Path::new("one/node_modules/b/b2.js"), true),
 484                (Path::new("one/node_modules/c"), true),
 485                (Path::new("two"), false),
 486                (Path::new("two/x.js"), false),
 487                (Path::new("two/y.js"), false),
 488            ]
 489        );
 490
 491        assert_eq!(
 492            loaded.file.path.as_ref(),
 493            Path::new("one/node_modules/b/b1.js")
 494        );
 495
 496        // Only the newly-expanded directories are scanned.
 497        assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 2);
 498    });
 499
 500    // Open another file in a different subdirectory of the same
 501    // gitignored directory.
 502    let prev_read_dir_count = fs.read_dir_call_count();
 503    let loaded = tree
 504        .update(cx, |tree, cx| {
 505            tree.load_file("one/node_modules/a/a2.js".as_ref(), cx)
 506        })
 507        .await
 508        .unwrap();
 509
 510    tree.read_with(cx, |tree, _| {
 511        assert_eq!(
 512            tree.entries(true, 0)
 513                .map(|entry| (entry.path.as_ref(), entry.is_ignored))
 514                .collect::<Vec<_>>(),
 515            vec![
 516                (Path::new(""), false),
 517                (Path::new(".gitignore"), false),
 518                (Path::new("one"), false),
 519                (Path::new("one/node_modules"), true),
 520                (Path::new("one/node_modules/a"), true),
 521                (Path::new("one/node_modules/a/a1.js"), true),
 522                (Path::new("one/node_modules/a/a2.js"), true),
 523                (Path::new("one/node_modules/b"), true),
 524                (Path::new("one/node_modules/b/b1.js"), true),
 525                (Path::new("one/node_modules/b/b2.js"), true),
 526                (Path::new("one/node_modules/c"), true),
 527                (Path::new("two"), false),
 528                (Path::new("two/x.js"), false),
 529                (Path::new("two/y.js"), false),
 530            ]
 531        );
 532
 533        assert_eq!(
 534            loaded.file.path.as_ref(),
 535            Path::new("one/node_modules/a/a2.js")
 536        );
 537
 538        // Only the newly-expanded directory is scanned.
 539        assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 1);
 540    });
 541
 542    let path = PathBuf::from("/root/one/node_modules/c/lib");
 543
 544    // No work happens when files and directories change within an unloaded directory.
 545    let prev_fs_call_count = fs.read_dir_call_count() + fs.metadata_call_count();
 546    // When we open a directory, we check each ancestor whether it's a git
 547    // repository. That means we have an fs.metadata call per ancestor that we
 548    // need to subtract here.
 549    let ancestors = path.ancestors().count();
 550
 551    fs.create_dir(path.as_ref()).await.unwrap();
 552    cx.executor().run_until_parked();
 553
 554    assert_eq!(
 555        fs.read_dir_call_count() + fs.metadata_call_count() - prev_fs_call_count - ancestors,
 556        0
 557    );
 558}
 559
 560#[gpui::test]
 561async fn test_dirs_no_longer_ignored(cx: &mut TestAppContext) {
 562    init_test(cx);
 563    let fs = FakeFs::new(cx.background_executor.clone());
 564    fs.insert_tree(
 565        "/root",
 566        json!({
 567            ".gitignore": "node_modules\n",
 568            "a": {
 569                "a.js": "",
 570            },
 571            "b": {
 572                "b.js": "",
 573            },
 574            "node_modules": {
 575                "c": {
 576                    "c.js": "",
 577                },
 578                "d": {
 579                    "d.js": "",
 580                    "e": {
 581                        "e1.js": "",
 582                        "e2.js": "",
 583                    },
 584                    "f": {
 585                        "f1.js": "",
 586                        "f2.js": "",
 587                    }
 588                },
 589            },
 590        }),
 591    )
 592    .await;
 593
 594    let tree = Worktree::local(
 595        Path::new("/root"),
 596        true,
 597        fs.clone(),
 598        Default::default(),
 599        &mut cx.to_async(),
 600    )
 601    .await
 602    .unwrap();
 603
 604    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
 605        .await;
 606
 607    // Open a file within the gitignored directory, forcing some of its
 608    // subdirectories to be read, but not all.
 609    let read_dir_count_1 = fs.read_dir_call_count();
 610    tree.read_with(cx, |tree, _| {
 611        tree.as_local()
 612            .unwrap()
 613            .refresh_entries_for_paths(vec![Path::new("node_modules/d/d.js").into()])
 614    })
 615    .recv()
 616    .await;
 617
 618    // Those subdirectories are now loaded.
 619    tree.read_with(cx, |tree, _| {
 620        assert_eq!(
 621            tree.entries(true, 0)
 622                .map(|e| (e.path.as_ref(), e.is_ignored))
 623                .collect::<Vec<_>>(),
 624            &[
 625                (Path::new(""), false),
 626                (Path::new(".gitignore"), false),
 627                (Path::new("a"), false),
 628                (Path::new("a/a.js"), false),
 629                (Path::new("b"), false),
 630                (Path::new("b/b.js"), false),
 631                (Path::new("node_modules"), true),
 632                (Path::new("node_modules/c"), true),
 633                (Path::new("node_modules/d"), true),
 634                (Path::new("node_modules/d/d.js"), true),
 635                (Path::new("node_modules/d/e"), true),
 636                (Path::new("node_modules/d/f"), true),
 637            ]
 638        );
 639    });
 640    let read_dir_count_2 = fs.read_dir_call_count();
 641    assert_eq!(read_dir_count_2 - read_dir_count_1, 2);
 642
 643    // Update the gitignore so that node_modules is no longer ignored,
 644    // but a subdirectory is ignored
 645    fs.save("/root/.gitignore".as_ref(), &"e".into(), Default::default())
 646        .await
 647        .unwrap();
 648    cx.executor().run_until_parked();
 649
 650    // All of the directories that are no longer ignored are now loaded.
 651    tree.read_with(cx, |tree, _| {
 652        assert_eq!(
 653            tree.entries(true, 0)
 654                .map(|e| (e.path.as_ref(), e.is_ignored))
 655                .collect::<Vec<_>>(),
 656            &[
 657                (Path::new(""), false),
 658                (Path::new(".gitignore"), false),
 659                (Path::new("a"), false),
 660                (Path::new("a/a.js"), false),
 661                (Path::new("b"), false),
 662                (Path::new("b/b.js"), false),
 663                // This directory is no longer ignored
 664                (Path::new("node_modules"), false),
 665                (Path::new("node_modules/c"), false),
 666                (Path::new("node_modules/c/c.js"), false),
 667                (Path::new("node_modules/d"), false),
 668                (Path::new("node_modules/d/d.js"), false),
 669                // This subdirectory is now ignored
 670                (Path::new("node_modules/d/e"), true),
 671                (Path::new("node_modules/d/f"), false),
 672                (Path::new("node_modules/d/f/f1.js"), false),
 673                (Path::new("node_modules/d/f/f2.js"), false),
 674            ]
 675        );
 676    });
 677
 678    // Each of the newly-loaded directories is scanned only once.
 679    let read_dir_count_3 = fs.read_dir_call_count();
 680    assert_eq!(read_dir_count_3 - read_dir_count_2, 2);
 681}
 682
 683#[gpui::test]
 684async fn test_write_file(cx: &mut TestAppContext) {
 685    init_test(cx);
 686    cx.executor().allow_parking();
 687    let dir = TempTree::new(json!({
 688        ".git": {},
 689        ".gitignore": "ignored-dir\n",
 690        "tracked-dir": {},
 691        "ignored-dir": {}
 692    }));
 693
 694    let worktree = Worktree::local(
 695        dir.path(),
 696        true,
 697        Arc::new(RealFs::new(None, cx.executor())),
 698        Default::default(),
 699        &mut cx.to_async(),
 700    )
 701    .await
 702    .unwrap();
 703
 704    #[cfg(not(target_os = "macos"))]
 705    fs::fs_watcher::global(|_| {}).unwrap();
 706
 707    cx.read(|cx| worktree.read(cx).as_local().unwrap().scan_complete())
 708        .await;
 709    worktree.flush_fs_events(cx).await;
 710
 711    worktree
 712        .update(cx, |tree, cx| {
 713            tree.write_file(
 714                Path::new("tracked-dir/file.txt"),
 715                "hello".into(),
 716                Default::default(),
 717                cx,
 718            )
 719        })
 720        .await
 721        .unwrap();
 722    worktree
 723        .update(cx, |tree, cx| {
 724            tree.write_file(
 725                Path::new("ignored-dir/file.txt"),
 726                "world".into(),
 727                Default::default(),
 728                cx,
 729            )
 730        })
 731        .await
 732        .unwrap();
 733
 734    worktree.read_with(cx, |tree, _| {
 735        let tracked = tree.entry_for_path("tracked-dir/file.txt").unwrap();
 736        let ignored = tree.entry_for_path("ignored-dir/file.txt").unwrap();
 737        assert!(!tracked.is_ignored);
 738        assert!(ignored.is_ignored);
 739    });
 740}
 741
 742#[gpui::test]
 743async fn test_file_scan_inclusions(cx: &mut TestAppContext) {
 744    init_test(cx);
 745    cx.executor().allow_parking();
 746    let dir = TempTree::new(json!({
 747        ".gitignore": "**/target\n/node_modules\ntop_level.txt\n",
 748        "target": {
 749            "index": "blah2"
 750        },
 751        "node_modules": {
 752            ".DS_Store": "",
 753            "prettier": {
 754                "package.json": "{}",
 755            },
 756        },
 757        "src": {
 758            ".DS_Store": "",
 759            "foo": {
 760                "foo.rs": "mod another;\n",
 761                "another.rs": "// another",
 762            },
 763            "bar": {
 764                "bar.rs": "// bar",
 765            },
 766            "lib.rs": "mod foo;\nmod bar;\n",
 767        },
 768        "top_level.txt": "top level file",
 769        ".DS_Store": "",
 770    }));
 771    cx.update(|cx| {
 772        cx.update_global::<SettingsStore, _>(|store, cx| {
 773            store.update_user_settings(cx, |settings| {
 774                settings.project.worktree.file_scan_exclusions = Some(vec![]);
 775                settings.project.worktree.file_scan_inclusions = Some(vec![
 776                    "node_modules/**/package.json".to_string(),
 777                    "**/.DS_Store".to_string(),
 778                ]);
 779            });
 780        });
 781    });
 782
 783    let tree = Worktree::local(
 784        dir.path(),
 785        true,
 786        Arc::new(RealFs::new(None, cx.executor())),
 787        Default::default(),
 788        &mut cx.to_async(),
 789    )
 790    .await
 791    .unwrap();
 792    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
 793        .await;
 794    tree.flush_fs_events(cx).await;
 795    tree.read_with(cx, |tree, _| {
 796        // Assert that file_scan_inclusions overrides  file_scan_exclusions.
 797        check_worktree_entries(
 798            tree,
 799            &[],
 800            &["target", "node_modules"],
 801            &["src/lib.rs", "src/bar/bar.rs", ".gitignore"],
 802            &[
 803                "node_modules/prettier/package.json",
 804                ".DS_Store",
 805                "node_modules/.DS_Store",
 806                "src/.DS_Store",
 807            ],
 808        )
 809    });
 810}
 811
 812#[gpui::test]
 813async fn test_file_scan_exclusions_overrules_inclusions(cx: &mut TestAppContext) {
 814    init_test(cx);
 815    cx.executor().allow_parking();
 816    let dir = TempTree::new(json!({
 817        ".gitignore": "**/target\n/node_modules\n",
 818        "target": {
 819            "index": "blah2"
 820        },
 821        "node_modules": {
 822            ".DS_Store": "",
 823            "prettier": {
 824                "package.json": "{}",
 825            },
 826        },
 827        "src": {
 828            ".DS_Store": "",
 829            "foo": {
 830                "foo.rs": "mod another;\n",
 831                "another.rs": "// another",
 832            },
 833        },
 834        ".DS_Store": "",
 835    }));
 836
 837    cx.update(|cx| {
 838        cx.update_global::<SettingsStore, _>(|store, cx| {
 839            store.update_user_settings(cx, |settings| {
 840                settings.project.worktree.file_scan_exclusions =
 841                    Some(vec!["**/.DS_Store".to_string()]);
 842                settings.project.worktree.file_scan_inclusions =
 843                    Some(vec!["**/.DS_Store".to_string()]);
 844            });
 845        });
 846    });
 847
 848    let tree = Worktree::local(
 849        dir.path(),
 850        true,
 851        Arc::new(RealFs::new(None, cx.executor())),
 852        Default::default(),
 853        &mut cx.to_async(),
 854    )
 855    .await
 856    .unwrap();
 857    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
 858        .await;
 859    tree.flush_fs_events(cx).await;
 860    tree.read_with(cx, |tree, _| {
 861        // Assert that file_scan_inclusions overrides  file_scan_exclusions.
 862        check_worktree_entries(
 863            tree,
 864            &[".DS_Store, src/.DS_Store"],
 865            &["target", "node_modules"],
 866            &["src/foo/another.rs", "src/foo/foo.rs", ".gitignore"],
 867            &[],
 868        )
 869    });
 870}
 871
 872#[gpui::test]
 873async fn test_file_scan_inclusions_reindexes_on_setting_change(cx: &mut TestAppContext) {
 874    init_test(cx);
 875    cx.executor().allow_parking();
 876    let dir = TempTree::new(json!({
 877        ".gitignore": "**/target\n/node_modules/\n",
 878        "target": {
 879            "index": "blah2"
 880        },
 881        "node_modules": {
 882            ".DS_Store": "",
 883            "prettier": {
 884                "package.json": "{}",
 885            },
 886        },
 887        "src": {
 888            ".DS_Store": "",
 889            "foo": {
 890                "foo.rs": "mod another;\n",
 891                "another.rs": "// another",
 892            },
 893        },
 894        ".DS_Store": "",
 895    }));
 896
 897    cx.update(|cx| {
 898        cx.update_global::<SettingsStore, _>(|store, cx| {
 899            store.update_user_settings(cx, |settings| {
 900                settings.project.worktree.file_scan_exclusions = Some(vec![]);
 901                settings.project.worktree.file_scan_inclusions =
 902                    Some(vec!["node_modules/**".to_string()]);
 903            });
 904        });
 905    });
 906    let tree = Worktree::local(
 907        dir.path(),
 908        true,
 909        Arc::new(RealFs::new(None, cx.executor())),
 910        Default::default(),
 911        &mut cx.to_async(),
 912    )
 913    .await
 914    .unwrap();
 915    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
 916        .await;
 917    tree.flush_fs_events(cx).await;
 918
 919    tree.read_with(cx, |tree, _| {
 920        assert!(
 921            tree.entry_for_path("node_modules")
 922                .is_some_and(|f| f.is_always_included)
 923        );
 924        assert!(
 925            tree.entry_for_path("node_modules/prettier/package.json")
 926                .is_some_and(|f| f.is_always_included)
 927        );
 928    });
 929
 930    cx.update(|cx| {
 931        cx.update_global::<SettingsStore, _>(|store, cx| {
 932            store.update_user_settings(cx, |settings| {
 933                settings.project.worktree.file_scan_exclusions = Some(vec![]);
 934                settings.project.worktree.file_scan_inclusions = Some(vec![]);
 935            });
 936        });
 937    });
 938    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
 939        .await;
 940    tree.flush_fs_events(cx).await;
 941
 942    tree.read_with(cx, |tree, _| {
 943        assert!(
 944            tree.entry_for_path("node_modules")
 945                .is_some_and(|f| !f.is_always_included)
 946        );
 947        assert!(
 948            tree.entry_for_path("node_modules/prettier/package.json")
 949                .is_some_and(|f| !f.is_always_included)
 950        );
 951    });
 952}
 953
 954#[gpui::test]
 955async fn test_file_scan_exclusions(cx: &mut TestAppContext) {
 956    init_test(cx);
 957    cx.executor().allow_parking();
 958    let dir = TempTree::new(json!({
 959        ".gitignore": "**/target\n/node_modules\n",
 960        "target": {
 961            "index": "blah2"
 962        },
 963        "node_modules": {
 964            ".DS_Store": "",
 965            "prettier": {
 966                "package.json": "{}",
 967            },
 968        },
 969        "src": {
 970            ".DS_Store": "",
 971            "foo": {
 972                "foo.rs": "mod another;\n",
 973                "another.rs": "// another",
 974            },
 975            "bar": {
 976                "bar.rs": "// bar",
 977            },
 978            "lib.rs": "mod foo;\nmod bar;\n",
 979        },
 980        ".DS_Store": "",
 981    }));
 982    cx.update(|cx| {
 983        cx.update_global::<SettingsStore, _>(|store, cx| {
 984            store.update_user_settings(cx, |settings| {
 985                settings.project.worktree.file_scan_exclusions =
 986                    Some(vec!["**/foo/**".to_string(), "**/.DS_Store".to_string()]);
 987            });
 988        });
 989    });
 990
 991    let tree = Worktree::local(
 992        dir.path(),
 993        true,
 994        Arc::new(RealFs::new(None, cx.executor())),
 995        Default::default(),
 996        &mut cx.to_async(),
 997    )
 998    .await
 999    .unwrap();
1000    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1001        .await;
1002    tree.flush_fs_events(cx).await;
1003    tree.read_with(cx, |tree, _| {
1004        check_worktree_entries(
1005            tree,
1006            &[
1007                "src/foo/foo.rs",
1008                "src/foo/another.rs",
1009                "node_modules/.DS_Store",
1010                "src/.DS_Store",
1011                ".DS_Store",
1012            ],
1013            &["target", "node_modules"],
1014            &["src/lib.rs", "src/bar/bar.rs", ".gitignore"],
1015            &[],
1016        )
1017    });
1018
1019    cx.update(|cx| {
1020        cx.update_global::<SettingsStore, _>(|store, cx| {
1021            store.update_user_settings(cx, |settings| {
1022                settings.project.worktree.file_scan_exclusions =
1023                    Some(vec!["**/node_modules/**".to_string()]);
1024            });
1025        });
1026    });
1027    tree.flush_fs_events(cx).await;
1028    cx.executor().run_until_parked();
1029    tree.read_with(cx, |tree, _| {
1030        check_worktree_entries(
1031            tree,
1032            &[
1033                "node_modules/prettier/package.json",
1034                "node_modules/.DS_Store",
1035                "node_modules",
1036            ],
1037            &["target"],
1038            &[
1039                ".gitignore",
1040                "src/lib.rs",
1041                "src/bar/bar.rs",
1042                "src/foo/foo.rs",
1043                "src/foo/another.rs",
1044                "src/.DS_Store",
1045                ".DS_Store",
1046            ],
1047            &[],
1048        )
1049    });
1050}
1051
1052#[gpui::test]
1053async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) {
1054    init_test(cx);
1055    cx.executor().allow_parking();
1056    let dir = TempTree::new(json!({
1057        ".git": {
1058            "HEAD": "ref: refs/heads/main\n",
1059            "foo": "bar",
1060        },
1061        ".gitignore": "**/target\n/node_modules\ntest_output\n",
1062        "target": {
1063            "index": "blah2"
1064        },
1065        "node_modules": {
1066            ".DS_Store": "",
1067            "prettier": {
1068                "package.json": "{}",
1069            },
1070        },
1071        "src": {
1072            ".DS_Store": "",
1073            "foo": {
1074                "foo.rs": "mod another;\n",
1075                "another.rs": "// another",
1076            },
1077            "bar": {
1078                "bar.rs": "// bar",
1079            },
1080            "lib.rs": "mod foo;\nmod bar;\n",
1081        },
1082        ".DS_Store": "",
1083    }));
1084    cx.update(|cx| {
1085        cx.update_global::<SettingsStore, _>(|store, cx| {
1086            store.update_user_settings(cx, |settings| {
1087                settings.project.worktree.file_scan_exclusions = Some(vec![
1088                    "**/.git".to_string(),
1089                    "node_modules/".to_string(),
1090                    "build_output".to_string(),
1091                ]);
1092            });
1093        });
1094    });
1095
1096    let tree = Worktree::local(
1097        dir.path(),
1098        true,
1099        Arc::new(RealFs::new(None, cx.executor())),
1100        Default::default(),
1101        &mut cx.to_async(),
1102    )
1103    .await
1104    .unwrap();
1105    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1106        .await;
1107    tree.flush_fs_events(cx).await;
1108    tree.read_with(cx, |tree, _| {
1109        check_worktree_entries(
1110            tree,
1111            &[
1112                ".git/HEAD",
1113                ".git/foo",
1114                "node_modules",
1115                "node_modules/.DS_Store",
1116                "node_modules/prettier",
1117                "node_modules/prettier/package.json",
1118            ],
1119            &["target"],
1120            &[
1121                ".DS_Store",
1122                "src/.DS_Store",
1123                "src/lib.rs",
1124                "src/foo/foo.rs",
1125                "src/foo/another.rs",
1126                "src/bar/bar.rs",
1127                ".gitignore",
1128            ],
1129            &[],
1130        )
1131    });
1132
1133    let new_excluded_dir = dir.path().join("build_output");
1134    let new_ignored_dir = dir.path().join("test_output");
1135    std::fs::create_dir_all(&new_excluded_dir)
1136        .unwrap_or_else(|e| panic!("Failed to create a {new_excluded_dir:?} directory: {e}"));
1137    std::fs::create_dir_all(&new_ignored_dir)
1138        .unwrap_or_else(|e| panic!("Failed to create a {new_ignored_dir:?} directory: {e}"));
1139    let node_modules_dir = dir.path().join("node_modules");
1140    let dot_git_dir = dir.path().join(".git");
1141    let src_dir = dir.path().join("src");
1142    for existing_dir in [&node_modules_dir, &dot_git_dir, &src_dir] {
1143        assert!(
1144            existing_dir.is_dir(),
1145            "Expect {existing_dir:?} to be present in the FS already"
1146        );
1147    }
1148
1149    for directory_for_new_file in [
1150        new_excluded_dir,
1151        new_ignored_dir,
1152        node_modules_dir,
1153        dot_git_dir,
1154        src_dir,
1155    ] {
1156        std::fs::write(directory_for_new_file.join("new_file"), "new file contents")
1157            .unwrap_or_else(|e| {
1158                panic!("Failed to create in {directory_for_new_file:?} a new file: {e}")
1159            });
1160    }
1161    tree.flush_fs_events(cx).await;
1162
1163    tree.read_with(cx, |tree, _| {
1164        check_worktree_entries(
1165            tree,
1166            &[
1167                ".git/HEAD",
1168                ".git/foo",
1169                ".git/new_file",
1170                "node_modules",
1171                "node_modules/.DS_Store",
1172                "node_modules/prettier",
1173                "node_modules/prettier/package.json",
1174                "node_modules/new_file",
1175                "build_output",
1176                "build_output/new_file",
1177                "test_output/new_file",
1178            ],
1179            &["target", "test_output"],
1180            &[
1181                ".DS_Store",
1182                "src/.DS_Store",
1183                "src/lib.rs",
1184                "src/foo/foo.rs",
1185                "src/foo/another.rs",
1186                "src/bar/bar.rs",
1187                "src/new_file",
1188                ".gitignore",
1189            ],
1190            &[],
1191        )
1192    });
1193}
1194
1195#[gpui::test]
1196async fn test_fs_events_in_dot_git_worktree(cx: &mut TestAppContext) {
1197    init_test(cx);
1198    cx.executor().allow_parking();
1199    let dir = TempTree::new(json!({
1200        ".git": {
1201            "HEAD": "ref: refs/heads/main\n",
1202            "foo": "foo contents",
1203        },
1204    }));
1205    let dot_git_worktree_dir = dir.path().join(".git");
1206
1207    let tree = Worktree::local(
1208        dot_git_worktree_dir.clone(),
1209        true,
1210        Arc::new(RealFs::new(None, cx.executor())),
1211        Default::default(),
1212        &mut cx.to_async(),
1213    )
1214    .await
1215    .unwrap();
1216    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1217        .await;
1218    tree.flush_fs_events(cx).await;
1219    tree.read_with(cx, |tree, _| {
1220        check_worktree_entries(tree, &[], &["HEAD", "foo"], &[], &[])
1221    });
1222
1223    std::fs::write(dot_git_worktree_dir.join("new_file"), "new file contents")
1224        .unwrap_or_else(|e| panic!("Failed to create in {dot_git_worktree_dir:?} a new file: {e}"));
1225    tree.flush_fs_events(cx).await;
1226    tree.read_with(cx, |tree, _| {
1227        check_worktree_entries(tree, &[], &["HEAD", "foo", "new_file"], &[], &[])
1228    });
1229}
1230
1231#[gpui::test(iterations = 30)]
1232async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
1233    init_test(cx);
1234    let fs = FakeFs::new(cx.background_executor.clone());
1235    fs.insert_tree(
1236        "/root",
1237        json!({
1238            "b": {},
1239            "c": {},
1240            "d": {},
1241        }),
1242    )
1243    .await;
1244
1245    let tree = Worktree::local(
1246        "/root".as_ref(),
1247        true,
1248        fs,
1249        Default::default(),
1250        &mut cx.to_async(),
1251    )
1252    .await
1253    .unwrap();
1254
1255    let snapshot1 = tree.update(cx, |tree, cx| {
1256        let tree = tree.as_local_mut().unwrap();
1257        let snapshot = Arc::new(Mutex::new(tree.snapshot()));
1258        tree.observe_updates(0, cx, {
1259            let snapshot = snapshot.clone();
1260            let settings = tree.settings();
1261            move |update| {
1262                snapshot
1263                    .lock()
1264                    .apply_remote_update(update, &settings.file_scan_inclusions)
1265                    .unwrap();
1266                async { true }
1267            }
1268        });
1269        snapshot
1270    });
1271
1272    let entry = tree
1273        .update(cx, |tree, cx| {
1274            tree.as_local_mut()
1275                .unwrap()
1276                .create_entry("a/e".as_ref(), true, None, cx)
1277        })
1278        .await
1279        .unwrap()
1280        .into_included()
1281        .unwrap();
1282    assert!(entry.is_dir());
1283
1284    cx.executor().run_until_parked();
1285    tree.read_with(cx, |tree, _| {
1286        assert_eq!(tree.entry_for_path("a/e").unwrap().kind, EntryKind::Dir);
1287    });
1288
1289    let snapshot2 = tree.update(cx, |tree, _| tree.as_local().unwrap().snapshot());
1290    assert_eq!(
1291        snapshot1.lock().entries(true, 0).collect::<Vec<_>>(),
1292        snapshot2.entries(true, 0).collect::<Vec<_>>()
1293    );
1294}
1295
1296#[gpui::test]
1297async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
1298    init_test(cx);
1299    cx.executor().allow_parking();
1300
1301    let fs_fake = FakeFs::new(cx.background_executor.clone());
1302    fs_fake
1303        .insert_tree(
1304            "/root",
1305            json!({
1306                "a": {},
1307            }),
1308        )
1309        .await;
1310
1311    let tree_fake = Worktree::local(
1312        "/root".as_ref(),
1313        true,
1314        fs_fake,
1315        Default::default(),
1316        &mut cx.to_async(),
1317    )
1318    .await
1319    .unwrap();
1320
1321    let entry = tree_fake
1322        .update(cx, |tree, cx| {
1323            tree.as_local_mut()
1324                .unwrap()
1325                .create_entry("a/b/c/d.txt".as_ref(), false, None, cx)
1326        })
1327        .await
1328        .unwrap()
1329        .into_included()
1330        .unwrap();
1331    assert!(entry.is_file());
1332
1333    cx.executor().run_until_parked();
1334    tree_fake.read_with(cx, |tree, _| {
1335        assert!(tree.entry_for_path("a/b/c/d.txt").unwrap().is_file());
1336        assert!(tree.entry_for_path("a/b/c/").unwrap().is_dir());
1337        assert!(tree.entry_for_path("a/b/").unwrap().is_dir());
1338    });
1339
1340    let fs_real = Arc::new(RealFs::new(None, cx.executor()));
1341    let temp_root = TempTree::new(json!({
1342        "a": {}
1343    }));
1344
1345    let tree_real = Worktree::local(
1346        temp_root.path(),
1347        true,
1348        fs_real,
1349        Default::default(),
1350        &mut cx.to_async(),
1351    )
1352    .await
1353    .unwrap();
1354
1355    let entry = tree_real
1356        .update(cx, |tree, cx| {
1357            tree.as_local_mut()
1358                .unwrap()
1359                .create_entry("a/b/c/d.txt".as_ref(), false, None, cx)
1360        })
1361        .await
1362        .unwrap()
1363        .into_included()
1364        .unwrap();
1365    assert!(entry.is_file());
1366
1367    cx.executor().run_until_parked();
1368    tree_real.read_with(cx, |tree, _| {
1369        assert!(tree.entry_for_path("a/b/c/d.txt").unwrap().is_file());
1370        assert!(tree.entry_for_path("a/b/c/").unwrap().is_dir());
1371        assert!(tree.entry_for_path("a/b/").unwrap().is_dir());
1372    });
1373
1374    // Test smallest change
1375    let entry = tree_real
1376        .update(cx, |tree, cx| {
1377            tree.as_local_mut()
1378                .unwrap()
1379                .create_entry("a/b/c/e.txt".as_ref(), false, None, cx)
1380        })
1381        .await
1382        .unwrap()
1383        .into_included()
1384        .unwrap();
1385    assert!(entry.is_file());
1386
1387    cx.executor().run_until_parked();
1388    tree_real.read_with(cx, |tree, _| {
1389        assert!(tree.entry_for_path("a/b/c/e.txt").unwrap().is_file());
1390    });
1391
1392    // Test largest change
1393    let entry = tree_real
1394        .update(cx, |tree, cx| {
1395            tree.as_local_mut()
1396                .unwrap()
1397                .create_entry("d/e/f/g.txt".as_ref(), false, None, cx)
1398        })
1399        .await
1400        .unwrap()
1401        .into_included()
1402        .unwrap();
1403    assert!(entry.is_file());
1404
1405    cx.executor().run_until_parked();
1406    tree_real.read_with(cx, |tree, _| {
1407        assert!(tree.entry_for_path("d/e/f/g.txt").unwrap().is_file());
1408        assert!(tree.entry_for_path("d/e/f").unwrap().is_dir());
1409        assert!(tree.entry_for_path("d/e/").unwrap().is_dir());
1410        assert!(tree.entry_for_path("d/").unwrap().is_dir());
1411    });
1412}
1413
1414#[gpui::test(iterations = 100)]
1415async fn test_random_worktree_operations_during_initial_scan(
1416    cx: &mut TestAppContext,
1417    mut rng: StdRng,
1418) {
1419    init_test(cx);
1420    let operations = env::var("OPERATIONS")
1421        .map(|o| o.parse().unwrap())
1422        .unwrap_or(5);
1423    let initial_entries = env::var("INITIAL_ENTRIES")
1424        .map(|o| o.parse().unwrap())
1425        .unwrap_or(20);
1426
1427    let root_dir = Path::new(path!("/test"));
1428    let fs = FakeFs::new(cx.background_executor.clone()) as Arc<dyn Fs>;
1429    fs.as_fake().insert_tree(root_dir, json!({})).await;
1430    for _ in 0..initial_entries {
1431        randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
1432    }
1433    log::info!("generated initial tree");
1434
1435    let worktree = Worktree::local(
1436        root_dir,
1437        true,
1438        fs.clone(),
1439        Default::default(),
1440        &mut cx.to_async(),
1441    )
1442    .await
1443    .unwrap();
1444
1445    let mut snapshots = vec![worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot())];
1446    let updates = Arc::new(Mutex::new(Vec::new()));
1447    worktree.update(cx, |tree, cx| {
1448        check_worktree_change_events(tree, cx);
1449
1450        tree.as_local_mut().unwrap().observe_updates(0, cx, {
1451            let updates = updates.clone();
1452            move |update| {
1453                updates.lock().push(update);
1454                async { true }
1455            }
1456        });
1457    });
1458
1459    for _ in 0..operations {
1460        worktree
1461            .update(cx, |worktree, cx| {
1462                randomly_mutate_worktree(worktree, &mut rng, cx)
1463            })
1464            .await
1465            .log_err();
1466        worktree.read_with(cx, |tree, _| {
1467            tree.as_local().unwrap().snapshot().check_invariants(true)
1468        });
1469
1470        if rng.random_bool(0.6) {
1471            snapshots.push(worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot()));
1472        }
1473    }
1474
1475    worktree
1476        .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
1477        .await;
1478
1479    cx.executor().run_until_parked();
1480
1481    let final_snapshot = worktree.read_with(cx, |tree, _| {
1482        let tree = tree.as_local().unwrap();
1483        let snapshot = tree.snapshot();
1484        snapshot.check_invariants(true);
1485        snapshot
1486    });
1487
1488    let settings = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().settings());
1489
1490    for (i, snapshot) in snapshots.into_iter().enumerate().rev() {
1491        let mut updated_snapshot = snapshot.clone();
1492        for update in updates.lock().iter() {
1493            if update.scan_id >= updated_snapshot.scan_id() as u64 {
1494                updated_snapshot
1495                    .apply_remote_update(update.clone(), &settings.file_scan_inclusions)
1496                    .unwrap();
1497            }
1498        }
1499
1500        assert_eq!(
1501            updated_snapshot.entries(true, 0).collect::<Vec<_>>(),
1502            final_snapshot.entries(true, 0).collect::<Vec<_>>(),
1503            "wrong updates after snapshot {i}: {snapshot:#?} {updates:#?}",
1504        );
1505    }
1506}
1507
1508#[gpui::test(iterations = 100)]
1509async fn test_random_worktree_changes(cx: &mut TestAppContext, mut rng: StdRng) {
1510    init_test(cx);
1511    let operations = env::var("OPERATIONS")
1512        .map(|o| o.parse().unwrap())
1513        .unwrap_or(40);
1514    let initial_entries = env::var("INITIAL_ENTRIES")
1515        .map(|o| o.parse().unwrap())
1516        .unwrap_or(20);
1517
1518    let root_dir = Path::new(path!("/test"));
1519    let fs = FakeFs::new(cx.background_executor.clone()) as Arc<dyn Fs>;
1520    fs.as_fake().insert_tree(root_dir, json!({})).await;
1521    for _ in 0..initial_entries {
1522        randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
1523    }
1524    log::info!("generated initial tree");
1525
1526    let worktree = Worktree::local(
1527        root_dir,
1528        true,
1529        fs.clone(),
1530        Default::default(),
1531        &mut cx.to_async(),
1532    )
1533    .await
1534    .unwrap();
1535
1536    let updates = Arc::new(Mutex::new(Vec::new()));
1537    worktree.update(cx, |tree, cx| {
1538        check_worktree_change_events(tree, cx);
1539
1540        tree.as_local_mut().unwrap().observe_updates(0, cx, {
1541            let updates = updates.clone();
1542            move |update| {
1543                updates.lock().push(update);
1544                async { true }
1545            }
1546        });
1547    });
1548
1549    worktree
1550        .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
1551        .await;
1552
1553    fs.as_fake().pause_events();
1554    let mut snapshots = Vec::new();
1555    let mut mutations_len = operations;
1556    while mutations_len > 1 {
1557        if rng.random_bool(0.2) {
1558            worktree
1559                .update(cx, |worktree, cx| {
1560                    randomly_mutate_worktree(worktree, &mut rng, cx)
1561                })
1562                .await
1563                .log_err();
1564        } else {
1565            randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
1566        }
1567
1568        let buffered_event_count = fs.as_fake().buffered_event_count();
1569        if buffered_event_count > 0 && rng.random_bool(0.3) {
1570            let len = rng.random_range(0..=buffered_event_count);
1571            log::info!("flushing {} events", len);
1572            fs.as_fake().flush_events(len);
1573        } else {
1574            randomly_mutate_fs(&fs, root_dir, 0.6, &mut rng).await;
1575            mutations_len -= 1;
1576        }
1577
1578        cx.executor().run_until_parked();
1579        if rng.random_bool(0.2) {
1580            log::info!("storing snapshot {}", snapshots.len());
1581            let snapshot = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
1582            snapshots.push(snapshot);
1583        }
1584    }
1585
1586    log::info!("quiescing");
1587    fs.as_fake().flush_events(usize::MAX);
1588    cx.executor().run_until_parked();
1589
1590    let snapshot = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
1591    snapshot.check_invariants(true);
1592    let expanded_paths = snapshot
1593        .expanded_entries()
1594        .map(|e| e.path.clone())
1595        .collect::<Vec<_>>();
1596
1597    {
1598        let new_worktree = Worktree::local(
1599            root_dir,
1600            true,
1601            fs.clone(),
1602            Default::default(),
1603            &mut cx.to_async(),
1604        )
1605        .await
1606        .unwrap();
1607        new_worktree
1608            .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
1609            .await;
1610        new_worktree
1611            .update(cx, |tree, _| {
1612                tree.as_local_mut()
1613                    .unwrap()
1614                    .refresh_entries_for_paths(expanded_paths)
1615            })
1616            .recv()
1617            .await;
1618        let new_snapshot =
1619            new_worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
1620        assert_eq!(
1621            snapshot.entries_without_ids(true),
1622            new_snapshot.entries_without_ids(true)
1623        );
1624    }
1625
1626    let settings = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().settings());
1627
1628    for (i, mut prev_snapshot) in snapshots.into_iter().enumerate().rev() {
1629        for update in updates.lock().iter() {
1630            if update.scan_id >= prev_snapshot.scan_id() as u64 {
1631                prev_snapshot
1632                    .apply_remote_update(update.clone(), &settings.file_scan_inclusions)
1633                    .unwrap();
1634            }
1635        }
1636
1637        assert_eq!(
1638            prev_snapshot
1639                .entries(true, 0)
1640                .map(ignore_pending_dir)
1641                .collect::<Vec<_>>(),
1642            snapshot
1643                .entries(true, 0)
1644                .map(ignore_pending_dir)
1645                .collect::<Vec<_>>(),
1646            "wrong updates after snapshot {i}: {updates:#?}",
1647        );
1648    }
1649
1650    fn ignore_pending_dir(entry: &Entry) -> Entry {
1651        let mut entry = entry.clone();
1652        if entry.kind.is_dir() {
1653            entry.kind = EntryKind::Dir
1654        }
1655        entry
1656    }
1657}
1658
1659// The worktree's `UpdatedEntries` event can be used to follow along with
1660// all changes to the worktree's snapshot.
1661fn check_worktree_change_events(tree: &mut Worktree, cx: &mut Context<Worktree>) {
1662    let mut entries = tree.entries(true, 0).cloned().collect::<Vec<_>>();
1663    cx.subscribe(&cx.entity(), move |tree, _, event, _| {
1664        if let Event::UpdatedEntries(changes) = event {
1665            for (path, _, change_type) in changes.iter() {
1666                let entry = tree.entry_for_path(path).cloned();
1667                let ix = match entries.binary_search_by_key(&path, |e| &e.path) {
1668                    Ok(ix) | Err(ix) => ix,
1669                };
1670                match change_type {
1671                    PathChange::Added => entries.insert(ix, entry.unwrap()),
1672                    PathChange::Removed => drop(entries.remove(ix)),
1673                    PathChange::Updated => {
1674                        let entry = entry.unwrap();
1675                        let existing_entry = entries.get_mut(ix).unwrap();
1676                        assert_eq!(existing_entry.path, entry.path);
1677                        *existing_entry = entry;
1678                    }
1679                    PathChange::AddedOrUpdated | PathChange::Loaded => {
1680                        let entry = entry.unwrap();
1681                        if entries.get(ix).map(|e| &e.path) == Some(&entry.path) {
1682                            *entries.get_mut(ix).unwrap() = entry;
1683                        } else {
1684                            entries.insert(ix, entry);
1685                        }
1686                    }
1687                }
1688            }
1689
1690            let new_entries = tree.entries(true, 0).cloned().collect::<Vec<_>>();
1691            assert_eq!(entries, new_entries, "incorrect changes: {:?}", changes);
1692        }
1693    })
1694    .detach();
1695}
1696
1697fn randomly_mutate_worktree(
1698    worktree: &mut Worktree,
1699    rng: &mut impl Rng,
1700    cx: &mut Context<Worktree>,
1701) -> Task<Result<()>> {
1702    log::info!("mutating worktree");
1703    let worktree = worktree.as_local_mut().unwrap();
1704    let snapshot = worktree.snapshot();
1705    let entry = snapshot.entries(false, 0).choose(rng).unwrap();
1706
1707    match rng.random_range(0_u32..100) {
1708        0..=33 if entry.path.as_ref() != Path::new("") => {
1709            log::info!("deleting entry {:?} ({})", entry.path, entry.id.0);
1710            worktree.delete_entry(entry.id, false, cx).unwrap()
1711        }
1712        ..=66 if entry.path.as_ref() != Path::new("") => {
1713            let other_entry = snapshot.entries(false, 0).choose(rng).unwrap();
1714            let new_parent_path = if other_entry.is_dir() {
1715                other_entry.path.clone()
1716            } else {
1717                other_entry.path.parent().unwrap().into()
1718            };
1719            let mut new_path = new_parent_path.join(random_filename(rng));
1720            if new_path.starts_with(&entry.path) {
1721                new_path = random_filename(rng).into();
1722            }
1723
1724            log::info!(
1725                "renaming entry {:?} ({}) to {:?}",
1726                entry.path,
1727                entry.id.0,
1728                new_path
1729            );
1730            let task = worktree.rename_entry(entry.id, new_path, cx);
1731            cx.background_spawn(async move {
1732                task.await?.into_included().unwrap();
1733                Ok(())
1734            })
1735        }
1736        _ => {
1737            if entry.is_dir() {
1738                let child_path = entry.path.join(random_filename(rng));
1739                let is_dir = rng.random_bool(0.3);
1740                log::info!(
1741                    "creating {} at {:?}",
1742                    if is_dir { "dir" } else { "file" },
1743                    child_path,
1744                );
1745                let task = worktree.create_entry(child_path, is_dir, None, cx);
1746                cx.background_spawn(async move {
1747                    task.await?;
1748                    Ok(())
1749                })
1750            } else {
1751                log::info!("overwriting file {:?} ({})", entry.path, entry.id.0);
1752                let task =
1753                    worktree.write_file(entry.path.clone(), "".into(), Default::default(), cx);
1754                cx.background_spawn(async move {
1755                    task.await?;
1756                    Ok(())
1757                })
1758            }
1759        }
1760    }
1761}
1762
1763async fn randomly_mutate_fs(
1764    fs: &Arc<dyn Fs>,
1765    root_path: &Path,
1766    insertion_probability: f64,
1767    rng: &mut impl Rng,
1768) {
1769    log::info!("mutating fs");
1770    let mut files = Vec::new();
1771    let mut dirs = Vec::new();
1772    for path in fs.as_fake().paths(false) {
1773        if path.starts_with(root_path) {
1774            if fs.is_file(&path).await {
1775                files.push(path);
1776            } else {
1777                dirs.push(path);
1778            }
1779        }
1780    }
1781
1782    if (files.is_empty() && dirs.len() == 1) || rng.random_bool(insertion_probability) {
1783        let path = dirs.choose(rng).unwrap();
1784        let new_path = path.join(random_filename(rng));
1785
1786        if rng.random() {
1787            log::info!(
1788                "creating dir {:?}",
1789                new_path.strip_prefix(root_path).unwrap()
1790            );
1791            fs.create_dir(&new_path).await.unwrap();
1792        } else {
1793            log::info!(
1794                "creating file {:?}",
1795                new_path.strip_prefix(root_path).unwrap()
1796            );
1797            fs.create_file(&new_path, Default::default()).await.unwrap();
1798        }
1799    } else if rng.random_bool(0.05) {
1800        let ignore_dir_path = dirs.choose(rng).unwrap();
1801        let ignore_path = ignore_dir_path.join(*GITIGNORE);
1802
1803        let subdirs = dirs
1804            .iter()
1805            .filter(|d| d.starts_with(ignore_dir_path))
1806            .cloned()
1807            .collect::<Vec<_>>();
1808        let subfiles = files
1809            .iter()
1810            .filter(|d| d.starts_with(ignore_dir_path))
1811            .cloned()
1812            .collect::<Vec<_>>();
1813        let files_to_ignore = {
1814            let len = rng.random_range(0..=subfiles.len());
1815            subfiles.choose_multiple(rng, len)
1816        };
1817        let dirs_to_ignore = {
1818            let len = rng.random_range(0..subdirs.len());
1819            subdirs.choose_multiple(rng, len)
1820        };
1821
1822        let mut ignore_contents = String::new();
1823        for path_to_ignore in files_to_ignore.chain(dirs_to_ignore) {
1824            writeln!(
1825                ignore_contents,
1826                "{}",
1827                path_to_ignore
1828                    .strip_prefix(ignore_dir_path)
1829                    .unwrap()
1830                    .to_str()
1831                    .unwrap()
1832            )
1833            .unwrap();
1834        }
1835        log::info!(
1836            "creating gitignore {:?} with contents:\n{}",
1837            ignore_path.strip_prefix(root_path).unwrap(),
1838            ignore_contents
1839        );
1840        fs.save(
1841            &ignore_path,
1842            &ignore_contents.as_str().into(),
1843            Default::default(),
1844        )
1845        .await
1846        .unwrap();
1847    } else {
1848        let old_path = {
1849            let file_path = files.choose(rng);
1850            let dir_path = dirs[1..].choose(rng);
1851            file_path.into_iter().chain(dir_path).choose(rng).unwrap()
1852        };
1853
1854        let is_rename = rng.random();
1855        if is_rename {
1856            let new_path_parent = dirs
1857                .iter()
1858                .filter(|d| !d.starts_with(old_path))
1859                .choose(rng)
1860                .unwrap();
1861
1862            let overwrite_existing_dir =
1863                !old_path.starts_with(new_path_parent) && rng.random_bool(0.3);
1864            let new_path = if overwrite_existing_dir {
1865                fs.remove_dir(
1866                    new_path_parent,
1867                    RemoveOptions {
1868                        recursive: true,
1869                        ignore_if_not_exists: true,
1870                    },
1871                )
1872                .await
1873                .unwrap();
1874                new_path_parent.to_path_buf()
1875            } else {
1876                new_path_parent.join(random_filename(rng))
1877            };
1878
1879            log::info!(
1880                "renaming {:?} to {}{:?}",
1881                old_path.strip_prefix(root_path).unwrap(),
1882                if overwrite_existing_dir {
1883                    "overwrite "
1884                } else {
1885                    ""
1886                },
1887                new_path.strip_prefix(root_path).unwrap()
1888            );
1889            fs.rename(
1890                old_path,
1891                &new_path,
1892                fs::RenameOptions {
1893                    overwrite: true,
1894                    ignore_if_exists: true,
1895                },
1896            )
1897            .await
1898            .unwrap();
1899        } else if fs.is_file(old_path).await {
1900            log::info!(
1901                "deleting file {:?}",
1902                old_path.strip_prefix(root_path).unwrap()
1903            );
1904            fs.remove_file(old_path, Default::default()).await.unwrap();
1905        } else {
1906            log::info!(
1907                "deleting dir {:?}",
1908                old_path.strip_prefix(root_path).unwrap()
1909            );
1910            fs.remove_dir(
1911                old_path,
1912                RemoveOptions {
1913                    recursive: true,
1914                    ignore_if_not_exists: true,
1915                },
1916            )
1917            .await
1918            .unwrap();
1919        }
1920    }
1921}
1922
1923fn random_filename(rng: &mut impl Rng) -> String {
1924    (0..6)
1925        .map(|_| rng.sample(rand::distr::Alphanumeric))
1926        .map(char::from)
1927        .collect()
1928}
1929
1930#[gpui::test]
1931async fn test_rename_file_to_new_directory(cx: &mut TestAppContext) {
1932    init_test(cx);
1933    let fs = FakeFs::new(cx.background_executor.clone());
1934    let expected_contents = "content";
1935    fs.as_fake()
1936        .insert_tree(
1937            "/root",
1938            json!({
1939                "test.txt": expected_contents
1940            }),
1941        )
1942        .await;
1943    let worktree = Worktree::local(
1944        Path::new("/root"),
1945        true,
1946        fs.clone(),
1947        Arc::default(),
1948        &mut cx.to_async(),
1949    )
1950    .await
1951    .unwrap();
1952    cx.read(|cx| worktree.read(cx).as_local().unwrap().scan_complete())
1953        .await;
1954
1955    let entry_id = worktree.read_with(cx, |worktree, _| {
1956        worktree.entry_for_path("test.txt").unwrap().id
1957    });
1958    let _result = worktree
1959        .update(cx, |worktree, cx| {
1960            worktree.rename_entry(entry_id, Path::new("dir1/dir2/dir3/test.txt"), cx)
1961        })
1962        .await
1963        .unwrap();
1964    worktree.read_with(cx, |worktree, _| {
1965        assert!(
1966            worktree.entry_for_path("test.txt").is_none(),
1967            "Old file should have been removed"
1968        );
1969        assert!(
1970            worktree.entry_for_path("dir1/dir2/dir3/test.txt").is_some(),
1971            "Whole directory hierarchy and the new file should have been created"
1972        );
1973    });
1974    assert_eq!(
1975        worktree
1976            .update(cx, |worktree, cx| {
1977                worktree.load_file("dir1/dir2/dir3/test.txt".as_ref(), cx)
1978            })
1979            .await
1980            .unwrap()
1981            .text,
1982        expected_contents,
1983        "Moved file's contents should be preserved"
1984    );
1985
1986    let entry_id = worktree.read_with(cx, |worktree, _| {
1987        worktree
1988            .entry_for_path("dir1/dir2/dir3/test.txt")
1989            .unwrap()
1990            .id
1991    });
1992    let _result = worktree
1993        .update(cx, |worktree, cx| {
1994            worktree.rename_entry(entry_id, Path::new("dir1/dir2/test.txt"), cx)
1995        })
1996        .await
1997        .unwrap();
1998    worktree.read_with(cx, |worktree, _| {
1999        assert!(
2000            worktree.entry_for_path("test.txt").is_none(),
2001            "First file should not reappear"
2002        );
2003        assert!(
2004            worktree.entry_for_path("dir1/dir2/dir3/test.txt").is_none(),
2005            "Old file should have been removed"
2006        );
2007        assert!(
2008            worktree.entry_for_path("dir1/dir2/test.txt").is_some(),
2009            "No error should have occurred after moving into existing directory"
2010        );
2011    });
2012    assert_eq!(
2013        worktree
2014            .update(cx, |worktree, cx| {
2015                worktree.load_file("dir1/dir2/test.txt".as_ref(), cx)
2016            })
2017            .await
2018            .unwrap()
2019            .text,
2020        expected_contents,
2021        "Moved file's contents should be preserved"
2022    );
2023}
2024
2025#[gpui::test]
2026async fn test_private_single_file_worktree(cx: &mut TestAppContext) {
2027    init_test(cx);
2028    let fs = FakeFs::new(cx.background_executor.clone());
2029    fs.insert_tree("/", json!({".env": "PRIVATE=secret\n"}))
2030        .await;
2031    let tree = Worktree::local(
2032        Path::new("/.env"),
2033        true,
2034        fs.clone(),
2035        Default::default(),
2036        &mut cx.to_async(),
2037    )
2038    .await
2039    .unwrap();
2040    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2041        .await;
2042    tree.read_with(cx, |tree, _| {
2043        let entry = tree.entry_for_path("").unwrap();
2044        assert!(entry.is_private);
2045    });
2046}
2047
2048#[gpui::test]
2049fn test_unrelativize() {
2050    let work_directory = WorkDirectory::in_project("");
2051    pretty_assertions::assert_eq!(
2052        work_directory.try_unrelativize(&"crates/gpui/gpui.rs".into()),
2053        Some(Path::new("crates/gpui/gpui.rs").into())
2054    );
2055
2056    let work_directory = WorkDirectory::in_project("vendor/some-submodule");
2057    pretty_assertions::assert_eq!(
2058        work_directory.try_unrelativize(&"src/thing.c".into()),
2059        Some(Path::new("vendor/some-submodule/src/thing.c").into())
2060    );
2061
2062    let work_directory = WorkDirectory::AboveProject {
2063        absolute_path: Path::new("/projects/zed").into(),
2064        location_in_repo: Path::new("crates/gpui").into(),
2065    };
2066
2067    pretty_assertions::assert_eq!(
2068        work_directory.try_unrelativize(&"crates/util/util.rs".into()),
2069        None,
2070    );
2071
2072    pretty_assertions::assert_eq!(
2073        work_directory.unrelativize(&"crates/util/util.rs".into()),
2074        Path::new("../util/util.rs").into()
2075    );
2076
2077    pretty_assertions::assert_eq!(work_directory.try_unrelativize(&"README.md".into()), None,);
2078
2079    pretty_assertions::assert_eq!(
2080        work_directory.unrelativize(&"README.md".into()),
2081        Path::new("../../README.md").into()
2082    );
2083}
2084
2085#[gpui::test]
2086async fn test_repository_above_root(executor: BackgroundExecutor, cx: &mut TestAppContext) {
2087    init_test(cx);
2088
2089    let fs = FakeFs::new(executor);
2090    fs.insert_tree(
2091        path!("/root"),
2092        json!({
2093            ".git": {},
2094            "subproject": {
2095                "a.txt": "A"
2096            }
2097        }),
2098    )
2099    .await;
2100    let worktree = Worktree::local(
2101        path!("/root/subproject").as_ref(),
2102        true,
2103        fs.clone(),
2104        Arc::default(),
2105        &mut cx.to_async(),
2106    )
2107    .await
2108    .unwrap();
2109    worktree
2110        .update(cx, |worktree, _| {
2111            worktree.as_local().unwrap().scan_complete()
2112        })
2113        .await;
2114    cx.run_until_parked();
2115    let repos = worktree.update(cx, |worktree, _| {
2116        worktree
2117            .as_local()
2118            .unwrap()
2119            .git_repositories
2120            .values()
2121            .map(|entry| entry.work_directory_abs_path.clone())
2122            .collect::<Vec<_>>()
2123    });
2124    pretty_assertions::assert_eq!(repos, [Path::new(path!("/root")).into()]);
2125
2126    fs.touch_path(path!("/root/subproject")).await;
2127    worktree
2128        .update(cx, |worktree, _| {
2129            worktree.as_local().unwrap().scan_complete()
2130        })
2131        .await;
2132    cx.run_until_parked();
2133
2134    let repos = worktree.update(cx, |worktree, _| {
2135        worktree
2136            .as_local()
2137            .unwrap()
2138            .git_repositories
2139            .values()
2140            .map(|entry| entry.work_directory_abs_path.clone())
2141            .collect::<Vec<_>>()
2142    });
2143    pretty_assertions::assert_eq!(repos, [Path::new(path!("/root")).into()]);
2144}
2145
2146#[gpui::test]
2147async fn test_global_gitignore(executor: BackgroundExecutor, cx: &mut TestAppContext) {
2148    init_test(cx);
2149
2150    let home = paths::home_dir();
2151    let fs = FakeFs::new(executor);
2152    fs.insert_tree(
2153        home,
2154        json!({
2155            ".config": {
2156                "git": {
2157                    "ignore": "foo\n/bar\nbaz\n"
2158                }
2159            },
2160            "project": {
2161                ".git": {},
2162                ".gitignore": "!baz",
2163                "foo": "",
2164                "bar": "",
2165                "sub": {
2166                    "bar": "",
2167                },
2168                "subrepo": {
2169                    ".git": {},
2170                    "bar": ""
2171                },
2172                "baz": ""
2173            }
2174        }),
2175    )
2176    .await;
2177    let worktree = Worktree::local(
2178        home.join("project"),
2179        true,
2180        fs.clone(),
2181        Arc::default(),
2182        &mut cx.to_async(),
2183    )
2184    .await
2185    .unwrap();
2186    worktree
2187        .update(cx, |worktree, _| {
2188            worktree.as_local().unwrap().scan_complete()
2189        })
2190        .await;
2191    cx.run_until_parked();
2192
2193    // .gitignore overrides excludesFile, and anchored paths in excludesFile are resolved
2194    // relative to the nearest containing repository
2195    worktree.update(cx, |worktree, _cx| {
2196        check_worktree_entries(
2197            worktree,
2198            &[],
2199            &["foo", "bar", "subrepo/bar"],
2200            &["sub/bar", "baz"],
2201            &[],
2202        );
2203    });
2204
2205    // Ignore statuses are updated when excludesFile changes
2206    fs.write(
2207        &home.join(".config").join("git").join("ignore"),
2208        "/bar\nbaz\n".as_bytes(),
2209    )
2210    .await
2211    .unwrap();
2212    worktree
2213        .update(cx, |worktree, _| {
2214            worktree.as_local().unwrap().scan_complete()
2215        })
2216        .await;
2217    cx.run_until_parked();
2218
2219    worktree.update(cx, |worktree, _cx| {
2220        check_worktree_entries(
2221            worktree,
2222            &[],
2223            &["bar", "subrepo/bar"],
2224            &["foo", "sub/bar", "baz"],
2225            &[],
2226        );
2227    });
2228
2229    // Statuses are updated when .git added/removed
2230    fs.remove_dir(
2231        &home.join("project").join("subrepo").join(".git"),
2232        RemoveOptions {
2233            recursive: true,
2234            ..Default::default()
2235        },
2236    )
2237    .await
2238    .unwrap();
2239    worktree
2240        .update(cx, |worktree, _| {
2241            worktree.as_local().unwrap().scan_complete()
2242        })
2243        .await;
2244    cx.run_until_parked();
2245
2246    worktree.update(cx, |worktree, _cx| {
2247        check_worktree_entries(
2248            worktree,
2249            &[],
2250            &["bar"],
2251            &["foo", "sub/bar", "baz", "subrepo/bar"],
2252            &[],
2253        );
2254    });
2255}
2256
2257#[track_caller]
2258fn check_worktree_entries(
2259    tree: &Worktree,
2260    expected_excluded_paths: &[&str],
2261    expected_ignored_paths: &[&str],
2262    expected_tracked_paths: &[&str],
2263    expected_included_paths: &[&str],
2264) {
2265    for path in expected_excluded_paths {
2266        let entry = tree.entry_for_path(path);
2267        assert!(
2268            entry.is_none(),
2269            "expected path '{path}' to be excluded, but got entry: {entry:?}",
2270        );
2271    }
2272    for path in expected_ignored_paths {
2273        let entry = tree
2274            .entry_for_path(path)
2275            .unwrap_or_else(|| panic!("Missing entry for expected ignored path '{path}'"));
2276        assert!(
2277            entry.is_ignored,
2278            "expected path '{path}' to be ignored, but got entry: {entry:?}",
2279        );
2280    }
2281    for path in expected_tracked_paths {
2282        let entry = tree
2283            .entry_for_path(path)
2284            .unwrap_or_else(|| panic!("Missing entry for expected tracked path '{path}'"));
2285        assert!(
2286            !entry.is_ignored || entry.is_always_included,
2287            "expected path '{path}' to be tracked, but got entry: {entry:?}",
2288        );
2289    }
2290    for path in expected_included_paths {
2291        let entry = tree
2292            .entry_for_path(path)
2293            .unwrap_or_else(|| panic!("Missing entry for expected included path '{path}'"));
2294        assert!(
2295            entry.is_always_included,
2296            "expected path '{path}' to always be included, but got entry: {entry:?}",
2297        );
2298    }
2299}
2300
2301fn init_test(cx: &mut gpui::TestAppContext) {
2302    zlog::init_test();
2303
2304    cx.update(|cx| {
2305        let settings_store = SettingsStore::test(cx);
2306        cx.set_global(settings_store);
2307        WorktreeSettings::register(cx);
2308    });
2309}