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                async { true }
1266            }
1267        });
1268        snapshot
1269    });
1270
1271    let entry = tree
1272        .update(cx, |tree, cx| {
1273            tree.as_local_mut()
1274                .unwrap()
1275                .create_entry("a/e".as_ref(), true, None, cx)
1276        })
1277        .await
1278        .unwrap()
1279        .into_included()
1280        .unwrap();
1281    assert!(entry.is_dir());
1282
1283    cx.executor().run_until_parked();
1284    tree.read_with(cx, |tree, _| {
1285        assert_eq!(tree.entry_for_path("a/e").unwrap().kind, EntryKind::Dir);
1286    });
1287
1288    let snapshot2 = tree.update(cx, |tree, _| tree.as_local().unwrap().snapshot());
1289    assert_eq!(
1290        snapshot1.lock().entries(true, 0).collect::<Vec<_>>(),
1291        snapshot2.entries(true, 0).collect::<Vec<_>>()
1292    );
1293}
1294
1295#[gpui::test]
1296async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
1297    init_test(cx);
1298    cx.executor().allow_parking();
1299
1300    let fs_fake = FakeFs::new(cx.background_executor.clone());
1301    fs_fake
1302        .insert_tree(
1303            "/root",
1304            json!({
1305                "a": {},
1306            }),
1307        )
1308        .await;
1309
1310    let tree_fake = Worktree::local(
1311        "/root".as_ref(),
1312        true,
1313        fs_fake,
1314        Default::default(),
1315        &mut cx.to_async(),
1316    )
1317    .await
1318    .unwrap();
1319
1320    let entry = tree_fake
1321        .update(cx, |tree, cx| {
1322            tree.as_local_mut()
1323                .unwrap()
1324                .create_entry("a/b/c/d.txt".as_ref(), false, None, cx)
1325        })
1326        .await
1327        .unwrap()
1328        .into_included()
1329        .unwrap();
1330    assert!(entry.is_file());
1331
1332    cx.executor().run_until_parked();
1333    tree_fake.read_with(cx, |tree, _| {
1334        assert!(tree.entry_for_path("a/b/c/d.txt").unwrap().is_file());
1335        assert!(tree.entry_for_path("a/b/c/").unwrap().is_dir());
1336        assert!(tree.entry_for_path("a/b/").unwrap().is_dir());
1337    });
1338
1339    let fs_real = Arc::new(RealFs::new(None, cx.executor()));
1340    let temp_root = TempTree::new(json!({
1341        "a": {}
1342    }));
1343
1344    let tree_real = Worktree::local(
1345        temp_root.path(),
1346        true,
1347        fs_real,
1348        Default::default(),
1349        &mut cx.to_async(),
1350    )
1351    .await
1352    .unwrap();
1353
1354    let entry = tree_real
1355        .update(cx, |tree, cx| {
1356            tree.as_local_mut()
1357                .unwrap()
1358                .create_entry("a/b/c/d.txt".as_ref(), false, None, cx)
1359        })
1360        .await
1361        .unwrap()
1362        .into_included()
1363        .unwrap();
1364    assert!(entry.is_file());
1365
1366    cx.executor().run_until_parked();
1367    tree_real.read_with(cx, |tree, _| {
1368        assert!(tree.entry_for_path("a/b/c/d.txt").unwrap().is_file());
1369        assert!(tree.entry_for_path("a/b/c/").unwrap().is_dir());
1370        assert!(tree.entry_for_path("a/b/").unwrap().is_dir());
1371    });
1372
1373    // Test smallest change
1374    let entry = tree_real
1375        .update(cx, |tree, cx| {
1376            tree.as_local_mut()
1377                .unwrap()
1378                .create_entry("a/b/c/e.txt".as_ref(), false, None, cx)
1379        })
1380        .await
1381        .unwrap()
1382        .into_included()
1383        .unwrap();
1384    assert!(entry.is_file());
1385
1386    cx.executor().run_until_parked();
1387    tree_real.read_with(cx, |tree, _| {
1388        assert!(tree.entry_for_path("a/b/c/e.txt").unwrap().is_file());
1389    });
1390
1391    // Test largest change
1392    let entry = tree_real
1393        .update(cx, |tree, cx| {
1394            tree.as_local_mut()
1395                .unwrap()
1396                .create_entry("d/e/f/g.txt".as_ref(), false, None, cx)
1397        })
1398        .await
1399        .unwrap()
1400        .into_included()
1401        .unwrap();
1402    assert!(entry.is_file());
1403
1404    cx.executor().run_until_parked();
1405    tree_real.read_with(cx, |tree, _| {
1406        assert!(tree.entry_for_path("d/e/f/g.txt").unwrap().is_file());
1407        assert!(tree.entry_for_path("d/e/f").unwrap().is_dir());
1408        assert!(tree.entry_for_path("d/e/").unwrap().is_dir());
1409        assert!(tree.entry_for_path("d/").unwrap().is_dir());
1410    });
1411}
1412
1413#[gpui::test(iterations = 100)]
1414async fn test_random_worktree_operations_during_initial_scan(
1415    cx: &mut TestAppContext,
1416    mut rng: StdRng,
1417) {
1418    init_test(cx);
1419    let operations = env::var("OPERATIONS")
1420        .map(|o| o.parse().unwrap())
1421        .unwrap_or(5);
1422    let initial_entries = env::var("INITIAL_ENTRIES")
1423        .map(|o| o.parse().unwrap())
1424        .unwrap_or(20);
1425
1426    let root_dir = Path::new(path!("/test"));
1427    let fs = FakeFs::new(cx.background_executor.clone()) as Arc<dyn Fs>;
1428    fs.as_fake().insert_tree(root_dir, json!({})).await;
1429    for _ in 0..initial_entries {
1430        randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
1431    }
1432    log::info!("generated initial tree");
1433
1434    let worktree = Worktree::local(
1435        root_dir,
1436        true,
1437        fs.clone(),
1438        Default::default(),
1439        &mut cx.to_async(),
1440    )
1441    .await
1442    .unwrap();
1443
1444    let mut snapshots = vec![worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot())];
1445    let updates = Arc::new(Mutex::new(Vec::new()));
1446    worktree.update(cx, |tree, cx| {
1447        check_worktree_change_events(tree, cx);
1448
1449        tree.as_local_mut().unwrap().observe_updates(0, cx, {
1450            let updates = updates.clone();
1451            move |update| {
1452                updates.lock().push(update);
1453                async { true }
1454            }
1455        });
1456    });
1457
1458    for _ in 0..operations {
1459        worktree
1460            .update(cx, |worktree, cx| {
1461                randomly_mutate_worktree(worktree, &mut rng, cx)
1462            })
1463            .await
1464            .log_err();
1465        worktree.read_with(cx, |tree, _| {
1466            tree.as_local().unwrap().snapshot().check_invariants(true)
1467        });
1468
1469        if rng.random_bool(0.6) {
1470            snapshots.push(worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot()));
1471        }
1472    }
1473
1474    worktree
1475        .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
1476        .await;
1477
1478    cx.executor().run_until_parked();
1479
1480    let final_snapshot = worktree.read_with(cx, |tree, _| {
1481        let tree = tree.as_local().unwrap();
1482        let snapshot = tree.snapshot();
1483        snapshot.check_invariants(true);
1484        snapshot
1485    });
1486
1487    let settings = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().settings());
1488
1489    for (i, snapshot) in snapshots.into_iter().enumerate().rev() {
1490        let mut updated_snapshot = snapshot.clone();
1491        for update in updates.lock().iter() {
1492            if update.scan_id >= updated_snapshot.scan_id() as u64 {
1493                updated_snapshot
1494                    .apply_remote_update(update.clone(), &settings.file_scan_inclusions);
1495            }
1496        }
1497
1498        assert_eq!(
1499            updated_snapshot.entries(true, 0).collect::<Vec<_>>(),
1500            final_snapshot.entries(true, 0).collect::<Vec<_>>(),
1501            "wrong updates after snapshot {i}: {snapshot:#?} {updates:#?}",
1502        );
1503    }
1504}
1505
1506#[gpui::test(iterations = 100)]
1507async fn test_random_worktree_changes(cx: &mut TestAppContext, mut rng: StdRng) {
1508    init_test(cx);
1509    let operations = env::var("OPERATIONS")
1510        .map(|o| o.parse().unwrap())
1511        .unwrap_or(40);
1512    let initial_entries = env::var("INITIAL_ENTRIES")
1513        .map(|o| o.parse().unwrap())
1514        .unwrap_or(20);
1515
1516    let root_dir = Path::new(path!("/test"));
1517    let fs = FakeFs::new(cx.background_executor.clone()) as Arc<dyn Fs>;
1518    fs.as_fake().insert_tree(root_dir, json!({})).await;
1519    for _ in 0..initial_entries {
1520        randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
1521    }
1522    log::info!("generated initial tree");
1523
1524    let worktree = Worktree::local(
1525        root_dir,
1526        true,
1527        fs.clone(),
1528        Default::default(),
1529        &mut cx.to_async(),
1530    )
1531    .await
1532    .unwrap();
1533
1534    let updates = Arc::new(Mutex::new(Vec::new()));
1535    worktree.update(cx, |tree, cx| {
1536        check_worktree_change_events(tree, cx);
1537
1538        tree.as_local_mut().unwrap().observe_updates(0, cx, {
1539            let updates = updates.clone();
1540            move |update| {
1541                updates.lock().push(update);
1542                async { true }
1543            }
1544        });
1545    });
1546
1547    worktree
1548        .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
1549        .await;
1550
1551    fs.as_fake().pause_events();
1552    let mut snapshots = Vec::new();
1553    let mut mutations_len = operations;
1554    while mutations_len > 1 {
1555        if rng.random_bool(0.2) {
1556            worktree
1557                .update(cx, |worktree, cx| {
1558                    randomly_mutate_worktree(worktree, &mut rng, cx)
1559                })
1560                .await
1561                .log_err();
1562        } else {
1563            randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
1564        }
1565
1566        let buffered_event_count = fs.as_fake().buffered_event_count();
1567        if buffered_event_count > 0 && rng.random_bool(0.3) {
1568            let len = rng.random_range(0..=buffered_event_count);
1569            log::info!("flushing {} events", len);
1570            fs.as_fake().flush_events(len);
1571        } else {
1572            randomly_mutate_fs(&fs, root_dir, 0.6, &mut rng).await;
1573            mutations_len -= 1;
1574        }
1575
1576        cx.executor().run_until_parked();
1577        if rng.random_bool(0.2) {
1578            log::info!("storing snapshot {}", snapshots.len());
1579            let snapshot = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
1580            snapshots.push(snapshot);
1581        }
1582    }
1583
1584    log::info!("quiescing");
1585    fs.as_fake().flush_events(usize::MAX);
1586    cx.executor().run_until_parked();
1587
1588    let snapshot = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
1589    snapshot.check_invariants(true);
1590    let expanded_paths = snapshot
1591        .expanded_entries()
1592        .map(|e| e.path.clone())
1593        .collect::<Vec<_>>();
1594
1595    {
1596        let new_worktree = Worktree::local(
1597            root_dir,
1598            true,
1599            fs.clone(),
1600            Default::default(),
1601            &mut cx.to_async(),
1602        )
1603        .await
1604        .unwrap();
1605        new_worktree
1606            .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
1607            .await;
1608        new_worktree
1609            .update(cx, |tree, _| {
1610                tree.as_local_mut()
1611                    .unwrap()
1612                    .refresh_entries_for_paths(expanded_paths)
1613            })
1614            .recv()
1615            .await;
1616        let new_snapshot =
1617            new_worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
1618        assert_eq!(
1619            snapshot.entries_without_ids(true),
1620            new_snapshot.entries_without_ids(true)
1621        );
1622    }
1623
1624    let settings = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().settings());
1625
1626    for (i, mut prev_snapshot) in snapshots.into_iter().enumerate().rev() {
1627        for update in updates.lock().iter() {
1628            if update.scan_id >= prev_snapshot.scan_id() as u64 {
1629                prev_snapshot.apply_remote_update(update.clone(), &settings.file_scan_inclusions);
1630            }
1631        }
1632
1633        assert_eq!(
1634            prev_snapshot
1635                .entries(true, 0)
1636                .map(ignore_pending_dir)
1637                .collect::<Vec<_>>(),
1638            snapshot
1639                .entries(true, 0)
1640                .map(ignore_pending_dir)
1641                .collect::<Vec<_>>(),
1642            "wrong updates after snapshot {i}: {updates:#?}",
1643        );
1644    }
1645
1646    fn ignore_pending_dir(entry: &Entry) -> Entry {
1647        let mut entry = entry.clone();
1648        if entry.kind.is_dir() {
1649            entry.kind = EntryKind::Dir
1650        }
1651        entry
1652    }
1653}
1654
1655// The worktree's `UpdatedEntries` event can be used to follow along with
1656// all changes to the worktree's snapshot.
1657fn check_worktree_change_events(tree: &mut Worktree, cx: &mut Context<Worktree>) {
1658    let mut entries = tree.entries(true, 0).cloned().collect::<Vec<_>>();
1659    cx.subscribe(&cx.entity(), move |tree, _, event, _| {
1660        if let Event::UpdatedEntries(changes) = event {
1661            for (path, _, change_type) in changes.iter() {
1662                let entry = tree.entry_for_path(path).cloned();
1663                let ix = match entries.binary_search_by_key(&path, |e| &e.path) {
1664                    Ok(ix) | Err(ix) => ix,
1665                };
1666                match change_type {
1667                    PathChange::Added => entries.insert(ix, entry.unwrap()),
1668                    PathChange::Removed => drop(entries.remove(ix)),
1669                    PathChange::Updated => {
1670                        let entry = entry.unwrap();
1671                        let existing_entry = entries.get_mut(ix).unwrap();
1672                        assert_eq!(existing_entry.path, entry.path);
1673                        *existing_entry = entry;
1674                    }
1675                    PathChange::AddedOrUpdated | PathChange::Loaded => {
1676                        let entry = entry.unwrap();
1677                        if entries.get(ix).map(|e| &e.path) == Some(&entry.path) {
1678                            *entries.get_mut(ix).unwrap() = entry;
1679                        } else {
1680                            entries.insert(ix, entry);
1681                        }
1682                    }
1683                }
1684            }
1685
1686            let new_entries = tree.entries(true, 0).cloned().collect::<Vec<_>>();
1687            assert_eq!(entries, new_entries, "incorrect changes: {:?}", changes);
1688        }
1689    })
1690    .detach();
1691}
1692
1693fn randomly_mutate_worktree(
1694    worktree: &mut Worktree,
1695    rng: &mut impl Rng,
1696    cx: &mut Context<Worktree>,
1697) -> Task<Result<()>> {
1698    log::info!("mutating worktree");
1699    let worktree = worktree.as_local_mut().unwrap();
1700    let snapshot = worktree.snapshot();
1701    let entry = snapshot.entries(false, 0).choose(rng).unwrap();
1702
1703    match rng.random_range(0_u32..100) {
1704        0..=33 if entry.path.as_ref() != Path::new("") => {
1705            log::info!("deleting entry {:?} ({})", entry.path, entry.id.0);
1706            worktree.delete_entry(entry.id, false, cx).unwrap()
1707        }
1708        ..=66 if entry.path.as_ref() != Path::new("") => {
1709            let other_entry = snapshot.entries(false, 0).choose(rng).unwrap();
1710            let new_parent_path = if other_entry.is_dir() {
1711                other_entry.path.clone()
1712            } else {
1713                other_entry.path.parent().unwrap().into()
1714            };
1715            let mut new_path = new_parent_path.join(random_filename(rng));
1716            if new_path.starts_with(&entry.path) {
1717                new_path = random_filename(rng).into();
1718            }
1719
1720            log::info!(
1721                "renaming entry {:?} ({}) to {:?}",
1722                entry.path,
1723                entry.id.0,
1724                new_path
1725            );
1726            let task = worktree.rename_entry(entry.id, new_path, cx);
1727            cx.background_spawn(async move {
1728                task.await?.into_included().unwrap();
1729                Ok(())
1730            })
1731        }
1732        _ => {
1733            if entry.is_dir() {
1734                let child_path = entry.path.join(random_filename(rng));
1735                let is_dir = rng.random_bool(0.3);
1736                log::info!(
1737                    "creating {} at {:?}",
1738                    if is_dir { "dir" } else { "file" },
1739                    child_path,
1740                );
1741                let task = worktree.create_entry(child_path, is_dir, None, cx);
1742                cx.background_spawn(async move {
1743                    task.await?;
1744                    Ok(())
1745                })
1746            } else {
1747                log::info!("overwriting file {:?} ({})", entry.path, entry.id.0);
1748                let task =
1749                    worktree.write_file(entry.path.clone(), "".into(), Default::default(), cx);
1750                cx.background_spawn(async move {
1751                    task.await?;
1752                    Ok(())
1753                })
1754            }
1755        }
1756    }
1757}
1758
1759async fn randomly_mutate_fs(
1760    fs: &Arc<dyn Fs>,
1761    root_path: &Path,
1762    insertion_probability: f64,
1763    rng: &mut impl Rng,
1764) {
1765    log::info!("mutating fs");
1766    let mut files = Vec::new();
1767    let mut dirs = Vec::new();
1768    for path in fs.as_fake().paths(false) {
1769        if path.starts_with(root_path) {
1770            if fs.is_file(&path).await {
1771                files.push(path);
1772            } else {
1773                dirs.push(path);
1774            }
1775        }
1776    }
1777
1778    if (files.is_empty() && dirs.len() == 1) || rng.random_bool(insertion_probability) {
1779        let path = dirs.choose(rng).unwrap();
1780        let new_path = path.join(random_filename(rng));
1781
1782        if rng.random() {
1783            log::info!(
1784                "creating dir {:?}",
1785                new_path.strip_prefix(root_path).unwrap()
1786            );
1787            fs.create_dir(&new_path).await.unwrap();
1788        } else {
1789            log::info!(
1790                "creating file {:?}",
1791                new_path.strip_prefix(root_path).unwrap()
1792            );
1793            fs.create_file(&new_path, Default::default()).await.unwrap();
1794        }
1795    } else if rng.random_bool(0.05) {
1796        let ignore_dir_path = dirs.choose(rng).unwrap();
1797        let ignore_path = ignore_dir_path.join(*GITIGNORE);
1798
1799        let subdirs = dirs
1800            .iter()
1801            .filter(|d| d.starts_with(ignore_dir_path))
1802            .cloned()
1803            .collect::<Vec<_>>();
1804        let subfiles = files
1805            .iter()
1806            .filter(|d| d.starts_with(ignore_dir_path))
1807            .cloned()
1808            .collect::<Vec<_>>();
1809        let files_to_ignore = {
1810            let len = rng.random_range(0..=subfiles.len());
1811            subfiles.choose_multiple(rng, len)
1812        };
1813        let dirs_to_ignore = {
1814            let len = rng.random_range(0..subdirs.len());
1815            subdirs.choose_multiple(rng, len)
1816        };
1817
1818        let mut ignore_contents = String::new();
1819        for path_to_ignore in files_to_ignore.chain(dirs_to_ignore) {
1820            writeln!(
1821                ignore_contents,
1822                "{}",
1823                path_to_ignore
1824                    .strip_prefix(ignore_dir_path)
1825                    .unwrap()
1826                    .to_str()
1827                    .unwrap()
1828            )
1829            .unwrap();
1830        }
1831        log::info!(
1832            "creating gitignore {:?} with contents:\n{}",
1833            ignore_path.strip_prefix(root_path).unwrap(),
1834            ignore_contents
1835        );
1836        fs.save(
1837            &ignore_path,
1838            &ignore_contents.as_str().into(),
1839            Default::default(),
1840        )
1841        .await
1842        .unwrap();
1843    } else {
1844        let old_path = {
1845            let file_path = files.choose(rng);
1846            let dir_path = dirs[1..].choose(rng);
1847            file_path.into_iter().chain(dir_path).choose(rng).unwrap()
1848        };
1849
1850        let is_rename = rng.random();
1851        if is_rename {
1852            let new_path_parent = dirs
1853                .iter()
1854                .filter(|d| !d.starts_with(old_path))
1855                .choose(rng)
1856                .unwrap();
1857
1858            let overwrite_existing_dir =
1859                !old_path.starts_with(new_path_parent) && rng.random_bool(0.3);
1860            let new_path = if overwrite_existing_dir {
1861                fs.remove_dir(
1862                    new_path_parent,
1863                    RemoveOptions {
1864                        recursive: true,
1865                        ignore_if_not_exists: true,
1866                    },
1867                )
1868                .await
1869                .unwrap();
1870                new_path_parent.to_path_buf()
1871            } else {
1872                new_path_parent.join(random_filename(rng))
1873            };
1874
1875            log::info!(
1876                "renaming {:?} to {}{:?}",
1877                old_path.strip_prefix(root_path).unwrap(),
1878                if overwrite_existing_dir {
1879                    "overwrite "
1880                } else {
1881                    ""
1882                },
1883                new_path.strip_prefix(root_path).unwrap()
1884            );
1885            fs.rename(
1886                old_path,
1887                &new_path,
1888                fs::RenameOptions {
1889                    overwrite: true,
1890                    ignore_if_exists: true,
1891                },
1892            )
1893            .await
1894            .unwrap();
1895        } else if fs.is_file(old_path).await {
1896            log::info!(
1897                "deleting file {:?}",
1898                old_path.strip_prefix(root_path).unwrap()
1899            );
1900            fs.remove_file(old_path, Default::default()).await.unwrap();
1901        } else {
1902            log::info!(
1903                "deleting dir {:?}",
1904                old_path.strip_prefix(root_path).unwrap()
1905            );
1906            fs.remove_dir(
1907                old_path,
1908                RemoveOptions {
1909                    recursive: true,
1910                    ignore_if_not_exists: true,
1911                },
1912            )
1913            .await
1914            .unwrap();
1915        }
1916    }
1917}
1918
1919fn random_filename(rng: &mut impl Rng) -> String {
1920    (0..6)
1921        .map(|_| rng.sample(rand::distr::Alphanumeric))
1922        .map(char::from)
1923        .collect()
1924}
1925
1926#[gpui::test]
1927async fn test_rename_file_to_new_directory(cx: &mut TestAppContext) {
1928    init_test(cx);
1929    let fs = FakeFs::new(cx.background_executor.clone());
1930    let expected_contents = "content";
1931    fs.as_fake()
1932        .insert_tree(
1933            "/root",
1934            json!({
1935                "test.txt": expected_contents
1936            }),
1937        )
1938        .await;
1939    let worktree = Worktree::local(
1940        Path::new("/root"),
1941        true,
1942        fs.clone(),
1943        Arc::default(),
1944        &mut cx.to_async(),
1945    )
1946    .await
1947    .unwrap();
1948    cx.read(|cx| worktree.read(cx).as_local().unwrap().scan_complete())
1949        .await;
1950
1951    let entry_id = worktree.read_with(cx, |worktree, _| {
1952        worktree.entry_for_path("test.txt").unwrap().id
1953    });
1954    let _result = worktree
1955        .update(cx, |worktree, cx| {
1956            worktree.rename_entry(entry_id, Path::new("dir1/dir2/dir3/test.txt"), cx)
1957        })
1958        .await
1959        .unwrap();
1960    worktree.read_with(cx, |worktree, _| {
1961        assert!(
1962            worktree.entry_for_path("test.txt").is_none(),
1963            "Old file should have been removed"
1964        );
1965        assert!(
1966            worktree.entry_for_path("dir1/dir2/dir3/test.txt").is_some(),
1967            "Whole directory hierarchy and the new file should have been created"
1968        );
1969    });
1970    assert_eq!(
1971        worktree
1972            .update(cx, |worktree, cx| {
1973                worktree.load_file("dir1/dir2/dir3/test.txt".as_ref(), cx)
1974            })
1975            .await
1976            .unwrap()
1977            .text,
1978        expected_contents,
1979        "Moved file's contents should be preserved"
1980    );
1981
1982    let entry_id = worktree.read_with(cx, |worktree, _| {
1983        worktree
1984            .entry_for_path("dir1/dir2/dir3/test.txt")
1985            .unwrap()
1986            .id
1987    });
1988    let _result = worktree
1989        .update(cx, |worktree, cx| {
1990            worktree.rename_entry(entry_id, Path::new("dir1/dir2/test.txt"), cx)
1991        })
1992        .await
1993        .unwrap();
1994    worktree.read_with(cx, |worktree, _| {
1995        assert!(
1996            worktree.entry_for_path("test.txt").is_none(),
1997            "First file should not reappear"
1998        );
1999        assert!(
2000            worktree.entry_for_path("dir1/dir2/dir3/test.txt").is_none(),
2001            "Old file should have been removed"
2002        );
2003        assert!(
2004            worktree.entry_for_path("dir1/dir2/test.txt").is_some(),
2005            "No error should have occurred after moving into existing directory"
2006        );
2007    });
2008    assert_eq!(
2009        worktree
2010            .update(cx, |worktree, cx| {
2011                worktree.load_file("dir1/dir2/test.txt".as_ref(), cx)
2012            })
2013            .await
2014            .unwrap()
2015            .text,
2016        expected_contents,
2017        "Moved file's contents should be preserved"
2018    );
2019}
2020
2021#[gpui::test]
2022async fn test_private_single_file_worktree(cx: &mut TestAppContext) {
2023    init_test(cx);
2024    let fs = FakeFs::new(cx.background_executor.clone());
2025    fs.insert_tree("/", json!({".env": "PRIVATE=secret\n"}))
2026        .await;
2027    let tree = Worktree::local(
2028        Path::new("/.env"),
2029        true,
2030        fs.clone(),
2031        Default::default(),
2032        &mut cx.to_async(),
2033    )
2034    .await
2035    .unwrap();
2036    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2037        .await;
2038    tree.read_with(cx, |tree, _| {
2039        let entry = tree.entry_for_path("").unwrap();
2040        assert!(entry.is_private);
2041    });
2042}
2043
2044#[gpui::test]
2045fn test_unrelativize() {
2046    let work_directory = WorkDirectory::in_project("");
2047    pretty_assertions::assert_eq!(
2048        work_directory.try_unrelativize(&"crates/gpui/gpui.rs".into()),
2049        Some(Path::new("crates/gpui/gpui.rs").into())
2050    );
2051
2052    let work_directory = WorkDirectory::in_project("vendor/some-submodule");
2053    pretty_assertions::assert_eq!(
2054        work_directory.try_unrelativize(&"src/thing.c".into()),
2055        Some(Path::new("vendor/some-submodule/src/thing.c").into())
2056    );
2057
2058    let work_directory = WorkDirectory::AboveProject {
2059        absolute_path: Path::new("/projects/zed").into(),
2060        location_in_repo: Path::new("crates/gpui").into(),
2061    };
2062
2063    pretty_assertions::assert_eq!(
2064        work_directory.try_unrelativize(&"crates/util/util.rs".into()),
2065        None,
2066    );
2067
2068    pretty_assertions::assert_eq!(
2069        work_directory.unrelativize(&"crates/util/util.rs".into()),
2070        Path::new("../util/util.rs").into()
2071    );
2072
2073    pretty_assertions::assert_eq!(work_directory.try_unrelativize(&"README.md".into()), None,);
2074
2075    pretty_assertions::assert_eq!(
2076        work_directory.unrelativize(&"README.md".into()),
2077        Path::new("../../README.md").into()
2078    );
2079}
2080
2081#[gpui::test]
2082async fn test_repository_above_root(executor: BackgroundExecutor, cx: &mut TestAppContext) {
2083    init_test(cx);
2084
2085    let fs = FakeFs::new(executor);
2086    fs.insert_tree(
2087        path!("/root"),
2088        json!({
2089            ".git": {},
2090            "subproject": {
2091                "a.txt": "A"
2092            }
2093        }),
2094    )
2095    .await;
2096    let worktree = Worktree::local(
2097        path!("/root/subproject").as_ref(),
2098        true,
2099        fs.clone(),
2100        Arc::default(),
2101        &mut cx.to_async(),
2102    )
2103    .await
2104    .unwrap();
2105    worktree
2106        .update(cx, |worktree, _| {
2107            worktree.as_local().unwrap().scan_complete()
2108        })
2109        .await;
2110    cx.run_until_parked();
2111    let repos = worktree.update(cx, |worktree, _| {
2112        worktree
2113            .as_local()
2114            .unwrap()
2115            .git_repositories
2116            .values()
2117            .map(|entry| entry.work_directory_abs_path.clone())
2118            .collect::<Vec<_>>()
2119    });
2120    pretty_assertions::assert_eq!(repos, [Path::new(path!("/root")).into()]);
2121
2122    fs.touch_path(path!("/root/subproject")).await;
2123    worktree
2124        .update(cx, |worktree, _| {
2125            worktree.as_local().unwrap().scan_complete()
2126        })
2127        .await;
2128    cx.run_until_parked();
2129
2130    let repos = worktree.update(cx, |worktree, _| {
2131        worktree
2132            .as_local()
2133            .unwrap()
2134            .git_repositories
2135            .values()
2136            .map(|entry| entry.work_directory_abs_path.clone())
2137            .collect::<Vec<_>>()
2138    });
2139    pretty_assertions::assert_eq!(repos, [Path::new(path!("/root")).into()]);
2140}
2141
2142#[gpui::test]
2143async fn test_global_gitignore(executor: BackgroundExecutor, cx: &mut TestAppContext) {
2144    init_test(cx);
2145
2146    let home = paths::home_dir();
2147    let fs = FakeFs::new(executor);
2148    fs.insert_tree(
2149        home,
2150        json!({
2151            ".config": {
2152                "git": {
2153                    "ignore": "foo\n/bar\nbaz\n"
2154                }
2155            },
2156            "project": {
2157                ".git": {},
2158                ".gitignore": "!baz",
2159                "foo": "",
2160                "bar": "",
2161                "sub": {
2162                    "bar": "",
2163                },
2164                "subrepo": {
2165                    ".git": {},
2166                    "bar": ""
2167                },
2168                "baz": ""
2169            }
2170        }),
2171    )
2172    .await;
2173    let worktree = Worktree::local(
2174        home.join("project"),
2175        true,
2176        fs.clone(),
2177        Arc::default(),
2178        &mut cx.to_async(),
2179    )
2180    .await
2181    .unwrap();
2182    worktree
2183        .update(cx, |worktree, _| {
2184            worktree.as_local().unwrap().scan_complete()
2185        })
2186        .await;
2187    cx.run_until_parked();
2188
2189    // .gitignore overrides excludesFile, and anchored paths in excludesFile are resolved
2190    // relative to the nearest containing repository
2191    worktree.update(cx, |worktree, _cx| {
2192        check_worktree_entries(
2193            worktree,
2194            &[],
2195            &["foo", "bar", "subrepo/bar"],
2196            &["sub/bar", "baz"],
2197            &[],
2198        );
2199    });
2200
2201    // Ignore statuses are updated when excludesFile changes
2202    fs.write(
2203        &home.join(".config").join("git").join("ignore"),
2204        "/bar\nbaz\n".as_bytes(),
2205    )
2206    .await
2207    .unwrap();
2208    worktree
2209        .update(cx, |worktree, _| {
2210            worktree.as_local().unwrap().scan_complete()
2211        })
2212        .await;
2213    cx.run_until_parked();
2214
2215    worktree.update(cx, |worktree, _cx| {
2216        check_worktree_entries(
2217            worktree,
2218            &[],
2219            &["bar", "subrepo/bar"],
2220            &["foo", "sub/bar", "baz"],
2221            &[],
2222        );
2223    });
2224
2225    // Statuses are updated when .git added/removed
2226    fs.remove_dir(
2227        &home.join("project").join("subrepo").join(".git"),
2228        RemoveOptions {
2229            recursive: true,
2230            ..Default::default()
2231        },
2232    )
2233    .await
2234    .unwrap();
2235    worktree
2236        .update(cx, |worktree, _| {
2237            worktree.as_local().unwrap().scan_complete()
2238        })
2239        .await;
2240    cx.run_until_parked();
2241
2242    worktree.update(cx, |worktree, _cx| {
2243        check_worktree_entries(
2244            worktree,
2245            &[],
2246            &["bar"],
2247            &["foo", "sub/bar", "baz", "subrepo/bar"],
2248            &[],
2249        );
2250    });
2251}
2252
2253#[track_caller]
2254fn check_worktree_entries(
2255    tree: &Worktree,
2256    expected_excluded_paths: &[&str],
2257    expected_ignored_paths: &[&str],
2258    expected_tracked_paths: &[&str],
2259    expected_included_paths: &[&str],
2260) {
2261    for path in expected_excluded_paths {
2262        let entry = tree.entry_for_path(path);
2263        assert!(
2264            entry.is_none(),
2265            "expected path '{path}' to be excluded, but got entry: {entry:?}",
2266        );
2267    }
2268    for path in expected_ignored_paths {
2269        let entry = tree
2270            .entry_for_path(path)
2271            .unwrap_or_else(|| panic!("Missing entry for expected ignored path '{path}'"));
2272        assert!(
2273            entry.is_ignored,
2274            "expected path '{path}' to be ignored, but got entry: {entry:?}",
2275        );
2276    }
2277    for path in expected_tracked_paths {
2278        let entry = tree
2279            .entry_for_path(path)
2280            .unwrap_or_else(|| panic!("Missing entry for expected tracked path '{path}'"));
2281        assert!(
2282            !entry.is_ignored || entry.is_always_included,
2283            "expected path '{path}' to be tracked, but got entry: {entry:?}",
2284        );
2285    }
2286    for path in expected_included_paths {
2287        let entry = tree
2288            .entry_for_path(path)
2289            .unwrap_or_else(|| panic!("Missing entry for expected included path '{path}'"));
2290        assert!(
2291            entry.is_always_included,
2292            "expected path '{path}' to always be included, but got entry: {entry:?}",
2293        );
2294    }
2295}
2296
2297fn init_test(cx: &mut gpui::TestAppContext) {
2298    zlog::init_test();
2299
2300    cx.update(|cx| {
2301        let settings_store = SettingsStore::test(cx);
2302        cx.set_global(settings_store);
2303        WorktreeSettings::register(cx);
2304    });
2305}