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::<WorktreeSettings>(cx, |project_settings| {
 774                project_settings.file_scan_exclusions = Some(vec![]);
 775                project_settings.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::<WorktreeSettings>(cx, |project_settings| {
 840                project_settings.file_scan_exclusions = Some(vec!["**/.DS_Store".to_string()]);
 841                project_settings.file_scan_inclusions = Some(vec!["**/.DS_Store".to_string()]);
 842            });
 843        });
 844    });
 845
 846    let tree = Worktree::local(
 847        dir.path(),
 848        true,
 849        Arc::new(RealFs::new(None, cx.executor())),
 850        Default::default(),
 851        &mut cx.to_async(),
 852    )
 853    .await
 854    .unwrap();
 855    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
 856        .await;
 857    tree.flush_fs_events(cx).await;
 858    tree.read_with(cx, |tree, _| {
 859        // Assert that file_scan_inclusions overrides  file_scan_exclusions.
 860        check_worktree_entries(
 861            tree,
 862            &[".DS_Store, src/.DS_Store"],
 863            &["target", "node_modules"],
 864            &["src/foo/another.rs", "src/foo/foo.rs", ".gitignore"],
 865            &[],
 866        )
 867    });
 868}
 869
 870#[gpui::test]
 871async fn test_file_scan_inclusions_reindexes_on_setting_change(cx: &mut TestAppContext) {
 872    init_test(cx);
 873    cx.executor().allow_parking();
 874    let dir = TempTree::new(json!({
 875        ".gitignore": "**/target\n/node_modules/\n",
 876        "target": {
 877            "index": "blah2"
 878        },
 879        "node_modules": {
 880            ".DS_Store": "",
 881            "prettier": {
 882                "package.json": "{}",
 883            },
 884        },
 885        "src": {
 886            ".DS_Store": "",
 887            "foo": {
 888                "foo.rs": "mod another;\n",
 889                "another.rs": "// another",
 890            },
 891        },
 892        ".DS_Store": "",
 893    }));
 894
 895    cx.update(|cx| {
 896        cx.update_global::<SettingsStore, _>(|store, cx| {
 897            store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
 898                project_settings.file_scan_exclusions = Some(vec![]);
 899                project_settings.file_scan_inclusions = Some(vec!["node_modules/**".to_string()]);
 900            });
 901        });
 902    });
 903    let tree = Worktree::local(
 904        dir.path(),
 905        true,
 906        Arc::new(RealFs::new(None, cx.executor())),
 907        Default::default(),
 908        &mut cx.to_async(),
 909    )
 910    .await
 911    .unwrap();
 912    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
 913        .await;
 914    tree.flush_fs_events(cx).await;
 915
 916    tree.read_with(cx, |tree, _| {
 917        assert!(
 918            tree.entry_for_path("node_modules")
 919                .is_some_and(|f| f.is_always_included)
 920        );
 921        assert!(
 922            tree.entry_for_path("node_modules/prettier/package.json")
 923                .is_some_and(|f| f.is_always_included)
 924        );
 925    });
 926
 927    cx.update(|cx| {
 928        cx.update_global::<SettingsStore, _>(|store, cx| {
 929            store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
 930                project_settings.file_scan_exclusions = Some(vec![]);
 931                project_settings.file_scan_inclusions = Some(vec![]);
 932            });
 933        });
 934    });
 935    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
 936        .await;
 937    tree.flush_fs_events(cx).await;
 938
 939    tree.read_with(cx, |tree, _| {
 940        assert!(
 941            tree.entry_for_path("node_modules")
 942                .is_some_and(|f| !f.is_always_included)
 943        );
 944        assert!(
 945            tree.entry_for_path("node_modules/prettier/package.json")
 946                .is_some_and(|f| !f.is_always_included)
 947        );
 948    });
 949}
 950
 951#[gpui::test]
 952async fn test_file_scan_exclusions(cx: &mut TestAppContext) {
 953    init_test(cx);
 954    cx.executor().allow_parking();
 955    let dir = TempTree::new(json!({
 956        ".gitignore": "**/target\n/node_modules\n",
 957        "target": {
 958            "index": "blah2"
 959        },
 960        "node_modules": {
 961            ".DS_Store": "",
 962            "prettier": {
 963                "package.json": "{}",
 964            },
 965        },
 966        "src": {
 967            ".DS_Store": "",
 968            "foo": {
 969                "foo.rs": "mod another;\n",
 970                "another.rs": "// another",
 971            },
 972            "bar": {
 973                "bar.rs": "// bar",
 974            },
 975            "lib.rs": "mod foo;\nmod bar;\n",
 976        },
 977        ".DS_Store": "",
 978    }));
 979    cx.update(|cx| {
 980        cx.update_global::<SettingsStore, _>(|store, cx| {
 981            store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
 982                project_settings.file_scan_exclusions =
 983                    Some(vec!["**/foo/**".to_string(), "**/.DS_Store".to_string()]);
 984            });
 985        });
 986    });
 987
 988    let tree = Worktree::local(
 989        dir.path(),
 990        true,
 991        Arc::new(RealFs::new(None, cx.executor())),
 992        Default::default(),
 993        &mut cx.to_async(),
 994    )
 995    .await
 996    .unwrap();
 997    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
 998        .await;
 999    tree.flush_fs_events(cx).await;
1000    tree.read_with(cx, |tree, _| {
1001        check_worktree_entries(
1002            tree,
1003            &[
1004                "src/foo/foo.rs",
1005                "src/foo/another.rs",
1006                "node_modules/.DS_Store",
1007                "src/.DS_Store",
1008                ".DS_Store",
1009            ],
1010            &["target", "node_modules"],
1011            &["src/lib.rs", "src/bar/bar.rs", ".gitignore"],
1012            &[],
1013        )
1014    });
1015
1016    cx.update(|cx| {
1017        cx.update_global::<SettingsStore, _>(|store, cx| {
1018            store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
1019                project_settings.file_scan_exclusions =
1020                    Some(vec!["**/node_modules/**".to_string()]);
1021            });
1022        });
1023    });
1024    tree.flush_fs_events(cx).await;
1025    cx.executor().run_until_parked();
1026    tree.read_with(cx, |tree, _| {
1027        check_worktree_entries(
1028            tree,
1029            &[
1030                "node_modules/prettier/package.json",
1031                "node_modules/.DS_Store",
1032                "node_modules",
1033            ],
1034            &["target"],
1035            &[
1036                ".gitignore",
1037                "src/lib.rs",
1038                "src/bar/bar.rs",
1039                "src/foo/foo.rs",
1040                "src/foo/another.rs",
1041                "src/.DS_Store",
1042                ".DS_Store",
1043            ],
1044            &[],
1045        )
1046    });
1047}
1048
1049#[gpui::test]
1050async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) {
1051    init_test(cx);
1052    cx.executor().allow_parking();
1053    let dir = TempTree::new(json!({
1054        ".git": {
1055            "HEAD": "ref: refs/heads/main\n",
1056            "foo": "bar",
1057        },
1058        ".gitignore": "**/target\n/node_modules\ntest_output\n",
1059        "target": {
1060            "index": "blah2"
1061        },
1062        "node_modules": {
1063            ".DS_Store": "",
1064            "prettier": {
1065                "package.json": "{}",
1066            },
1067        },
1068        "src": {
1069            ".DS_Store": "",
1070            "foo": {
1071                "foo.rs": "mod another;\n",
1072                "another.rs": "// another",
1073            },
1074            "bar": {
1075                "bar.rs": "// bar",
1076            },
1077            "lib.rs": "mod foo;\nmod bar;\n",
1078        },
1079        ".DS_Store": "",
1080    }));
1081    cx.update(|cx| {
1082        cx.update_global::<SettingsStore, _>(|store, cx| {
1083            store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
1084                project_settings.file_scan_exclusions = Some(vec![
1085                    "**/.git".to_string(),
1086                    "node_modules/".to_string(),
1087                    "build_output".to_string(),
1088                ]);
1089            });
1090        });
1091    });
1092
1093    let tree = Worktree::local(
1094        dir.path(),
1095        true,
1096        Arc::new(RealFs::new(None, cx.executor())),
1097        Default::default(),
1098        &mut cx.to_async(),
1099    )
1100    .await
1101    .unwrap();
1102    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1103        .await;
1104    tree.flush_fs_events(cx).await;
1105    tree.read_with(cx, |tree, _| {
1106        check_worktree_entries(
1107            tree,
1108            &[
1109                ".git/HEAD",
1110                ".git/foo",
1111                "node_modules",
1112                "node_modules/.DS_Store",
1113                "node_modules/prettier",
1114                "node_modules/prettier/package.json",
1115            ],
1116            &["target"],
1117            &[
1118                ".DS_Store",
1119                "src/.DS_Store",
1120                "src/lib.rs",
1121                "src/foo/foo.rs",
1122                "src/foo/another.rs",
1123                "src/bar/bar.rs",
1124                ".gitignore",
1125            ],
1126            &[],
1127        )
1128    });
1129
1130    let new_excluded_dir = dir.path().join("build_output");
1131    let new_ignored_dir = dir.path().join("test_output");
1132    std::fs::create_dir_all(&new_excluded_dir)
1133        .unwrap_or_else(|e| panic!("Failed to create a {new_excluded_dir:?} directory: {e}"));
1134    std::fs::create_dir_all(&new_ignored_dir)
1135        .unwrap_or_else(|e| panic!("Failed to create a {new_ignored_dir:?} directory: {e}"));
1136    let node_modules_dir = dir.path().join("node_modules");
1137    let dot_git_dir = dir.path().join(".git");
1138    let src_dir = dir.path().join("src");
1139    for existing_dir in [&node_modules_dir, &dot_git_dir, &src_dir] {
1140        assert!(
1141            existing_dir.is_dir(),
1142            "Expect {existing_dir:?} to be present in the FS already"
1143        );
1144    }
1145
1146    for directory_for_new_file in [
1147        new_excluded_dir,
1148        new_ignored_dir,
1149        node_modules_dir,
1150        dot_git_dir,
1151        src_dir,
1152    ] {
1153        std::fs::write(directory_for_new_file.join("new_file"), "new file contents")
1154            .unwrap_or_else(|e| {
1155                panic!("Failed to create in {directory_for_new_file:?} a new file: {e}")
1156            });
1157    }
1158    tree.flush_fs_events(cx).await;
1159
1160    tree.read_with(cx, |tree, _| {
1161        check_worktree_entries(
1162            tree,
1163            &[
1164                ".git/HEAD",
1165                ".git/foo",
1166                ".git/new_file",
1167                "node_modules",
1168                "node_modules/.DS_Store",
1169                "node_modules/prettier",
1170                "node_modules/prettier/package.json",
1171                "node_modules/new_file",
1172                "build_output",
1173                "build_output/new_file",
1174                "test_output/new_file",
1175            ],
1176            &["target", "test_output"],
1177            &[
1178                ".DS_Store",
1179                "src/.DS_Store",
1180                "src/lib.rs",
1181                "src/foo/foo.rs",
1182                "src/foo/another.rs",
1183                "src/bar/bar.rs",
1184                "src/new_file",
1185                ".gitignore",
1186            ],
1187            &[],
1188        )
1189    });
1190}
1191
1192#[gpui::test]
1193async fn test_fs_events_in_dot_git_worktree(cx: &mut TestAppContext) {
1194    init_test(cx);
1195    cx.executor().allow_parking();
1196    let dir = TempTree::new(json!({
1197        ".git": {
1198            "HEAD": "ref: refs/heads/main\n",
1199            "foo": "foo contents",
1200        },
1201    }));
1202    let dot_git_worktree_dir = dir.path().join(".git");
1203
1204    let tree = Worktree::local(
1205        dot_git_worktree_dir.clone(),
1206        true,
1207        Arc::new(RealFs::new(None, cx.executor())),
1208        Default::default(),
1209        &mut cx.to_async(),
1210    )
1211    .await
1212    .unwrap();
1213    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1214        .await;
1215    tree.flush_fs_events(cx).await;
1216    tree.read_with(cx, |tree, _| {
1217        check_worktree_entries(tree, &[], &["HEAD", "foo"], &[], &[])
1218    });
1219
1220    std::fs::write(dot_git_worktree_dir.join("new_file"), "new file contents")
1221        .unwrap_or_else(|e| panic!("Failed to create in {dot_git_worktree_dir:?} a new file: {e}"));
1222    tree.flush_fs_events(cx).await;
1223    tree.read_with(cx, |tree, _| {
1224        check_worktree_entries(tree, &[], &["HEAD", "foo", "new_file"], &[], &[])
1225    });
1226}
1227
1228#[gpui::test(iterations = 30)]
1229async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
1230    init_test(cx);
1231    let fs = FakeFs::new(cx.background_executor.clone());
1232    fs.insert_tree(
1233        "/root",
1234        json!({
1235            "b": {},
1236            "c": {},
1237            "d": {},
1238        }),
1239    )
1240    .await;
1241
1242    let tree = Worktree::local(
1243        "/root".as_ref(),
1244        true,
1245        fs,
1246        Default::default(),
1247        &mut cx.to_async(),
1248    )
1249    .await
1250    .unwrap();
1251
1252    let snapshot1 = tree.update(cx, |tree, cx| {
1253        let tree = tree.as_local_mut().unwrap();
1254        let snapshot = Arc::new(Mutex::new(tree.snapshot()));
1255        tree.observe_updates(0, cx, {
1256            let snapshot = snapshot.clone();
1257            let settings = tree.settings();
1258            move |update| {
1259                snapshot
1260                    .lock()
1261                    .apply_remote_update(update, &settings.file_scan_inclusions);
1262                async { true }
1263            }
1264        });
1265        snapshot
1266    });
1267
1268    let entry = tree
1269        .update(cx, |tree, cx| {
1270            tree.as_local_mut()
1271                .unwrap()
1272                .create_entry("a/e".as_ref(), true, None, cx)
1273        })
1274        .await
1275        .unwrap()
1276        .into_included()
1277        .unwrap();
1278    assert!(entry.is_dir());
1279
1280    cx.executor().run_until_parked();
1281    tree.read_with(cx, |tree, _| {
1282        assert_eq!(tree.entry_for_path("a/e").unwrap().kind, EntryKind::Dir);
1283    });
1284
1285    let snapshot2 = tree.update(cx, |tree, _| tree.as_local().unwrap().snapshot());
1286    assert_eq!(
1287        snapshot1.lock().entries(true, 0).collect::<Vec<_>>(),
1288        snapshot2.entries(true, 0).collect::<Vec<_>>()
1289    );
1290}
1291
1292#[gpui::test]
1293async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
1294    init_test(cx);
1295    cx.executor().allow_parking();
1296
1297    let fs_fake = FakeFs::new(cx.background_executor.clone());
1298    fs_fake
1299        .insert_tree(
1300            "/root",
1301            json!({
1302                "a": {},
1303            }),
1304        )
1305        .await;
1306
1307    let tree_fake = Worktree::local(
1308        "/root".as_ref(),
1309        true,
1310        fs_fake,
1311        Default::default(),
1312        &mut cx.to_async(),
1313    )
1314    .await
1315    .unwrap();
1316
1317    let entry = tree_fake
1318        .update(cx, |tree, cx| {
1319            tree.as_local_mut()
1320                .unwrap()
1321                .create_entry("a/b/c/d.txt".as_ref(), false, None, cx)
1322        })
1323        .await
1324        .unwrap()
1325        .into_included()
1326        .unwrap();
1327    assert!(entry.is_file());
1328
1329    cx.executor().run_until_parked();
1330    tree_fake.read_with(cx, |tree, _| {
1331        assert!(tree.entry_for_path("a/b/c/d.txt").unwrap().is_file());
1332        assert!(tree.entry_for_path("a/b/c/").unwrap().is_dir());
1333        assert!(tree.entry_for_path("a/b/").unwrap().is_dir());
1334    });
1335
1336    let fs_real = Arc::new(RealFs::new(None, cx.executor()));
1337    let temp_root = TempTree::new(json!({
1338        "a": {}
1339    }));
1340
1341    let tree_real = Worktree::local(
1342        temp_root.path(),
1343        true,
1344        fs_real,
1345        Default::default(),
1346        &mut cx.to_async(),
1347    )
1348    .await
1349    .unwrap();
1350
1351    let entry = tree_real
1352        .update(cx, |tree, cx| {
1353            tree.as_local_mut()
1354                .unwrap()
1355                .create_entry("a/b/c/d.txt".as_ref(), false, None, cx)
1356        })
1357        .await
1358        .unwrap()
1359        .into_included()
1360        .unwrap();
1361    assert!(entry.is_file());
1362
1363    cx.executor().run_until_parked();
1364    tree_real.read_with(cx, |tree, _| {
1365        assert!(tree.entry_for_path("a/b/c/d.txt").unwrap().is_file());
1366        assert!(tree.entry_for_path("a/b/c/").unwrap().is_dir());
1367        assert!(tree.entry_for_path("a/b/").unwrap().is_dir());
1368    });
1369
1370    // Test smallest change
1371    let entry = tree_real
1372        .update(cx, |tree, cx| {
1373            tree.as_local_mut()
1374                .unwrap()
1375                .create_entry("a/b/c/e.txt".as_ref(), false, None, cx)
1376        })
1377        .await
1378        .unwrap()
1379        .into_included()
1380        .unwrap();
1381    assert!(entry.is_file());
1382
1383    cx.executor().run_until_parked();
1384    tree_real.read_with(cx, |tree, _| {
1385        assert!(tree.entry_for_path("a/b/c/e.txt").unwrap().is_file());
1386    });
1387
1388    // Test largest change
1389    let entry = tree_real
1390        .update(cx, |tree, cx| {
1391            tree.as_local_mut()
1392                .unwrap()
1393                .create_entry("d/e/f/g.txt".as_ref(), false, None, cx)
1394        })
1395        .await
1396        .unwrap()
1397        .into_included()
1398        .unwrap();
1399    assert!(entry.is_file());
1400
1401    cx.executor().run_until_parked();
1402    tree_real.read_with(cx, |tree, _| {
1403        assert!(tree.entry_for_path("d/e/f/g.txt").unwrap().is_file());
1404        assert!(tree.entry_for_path("d/e/f").unwrap().is_dir());
1405        assert!(tree.entry_for_path("d/e/").unwrap().is_dir());
1406        assert!(tree.entry_for_path("d/").unwrap().is_dir());
1407    });
1408}
1409
1410#[gpui::test(iterations = 100)]
1411async fn test_random_worktree_operations_during_initial_scan(
1412    cx: &mut TestAppContext,
1413    mut rng: StdRng,
1414) {
1415    init_test(cx);
1416    let operations = env::var("OPERATIONS")
1417        .map(|o| o.parse().unwrap())
1418        .unwrap_or(5);
1419    let initial_entries = env::var("INITIAL_ENTRIES")
1420        .map(|o| o.parse().unwrap())
1421        .unwrap_or(20);
1422
1423    let root_dir = Path::new(path!("/test"));
1424    let fs = FakeFs::new(cx.background_executor.clone()) as Arc<dyn Fs>;
1425    fs.as_fake().insert_tree(root_dir, json!({})).await;
1426    for _ in 0..initial_entries {
1427        randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
1428    }
1429    log::info!("generated initial tree");
1430
1431    let worktree = Worktree::local(
1432        root_dir,
1433        true,
1434        fs.clone(),
1435        Default::default(),
1436        &mut cx.to_async(),
1437    )
1438    .await
1439    .unwrap();
1440
1441    let mut snapshots = vec![worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot())];
1442    let updates = Arc::new(Mutex::new(Vec::new()));
1443    worktree.update(cx, |tree, cx| {
1444        check_worktree_change_events(tree, cx);
1445
1446        tree.as_local_mut().unwrap().observe_updates(0, cx, {
1447            let updates = updates.clone();
1448            move |update| {
1449                updates.lock().push(update);
1450                async { true }
1451            }
1452        });
1453    });
1454
1455    for _ in 0..operations {
1456        worktree
1457            .update(cx, |worktree, cx| {
1458                randomly_mutate_worktree(worktree, &mut rng, cx)
1459            })
1460            .await
1461            .log_err();
1462        worktree.read_with(cx, |tree, _| {
1463            tree.as_local().unwrap().snapshot().check_invariants(true)
1464        });
1465
1466        if rng.random_bool(0.6) {
1467            snapshots.push(worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot()));
1468        }
1469    }
1470
1471    worktree
1472        .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
1473        .await;
1474
1475    cx.executor().run_until_parked();
1476
1477    let final_snapshot = worktree.read_with(cx, |tree, _| {
1478        let tree = tree.as_local().unwrap();
1479        let snapshot = tree.snapshot();
1480        snapshot.check_invariants(true);
1481        snapshot
1482    });
1483
1484    let settings = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().settings());
1485
1486    for (i, snapshot) in snapshots.into_iter().enumerate().rev() {
1487        let mut updated_snapshot = snapshot.clone();
1488        for update in updates.lock().iter() {
1489            if update.scan_id >= updated_snapshot.scan_id() as u64 {
1490                updated_snapshot
1491                    .apply_remote_update(update.clone(), &settings.file_scan_inclusions);
1492            }
1493        }
1494
1495        assert_eq!(
1496            updated_snapshot.entries(true, 0).collect::<Vec<_>>(),
1497            final_snapshot.entries(true, 0).collect::<Vec<_>>(),
1498            "wrong updates after snapshot {i}: {snapshot:#?} {updates:#?}",
1499        );
1500    }
1501}
1502
1503#[gpui::test(iterations = 100)]
1504async fn test_random_worktree_changes(cx: &mut TestAppContext, mut rng: StdRng) {
1505    init_test(cx);
1506    let operations = env::var("OPERATIONS")
1507        .map(|o| o.parse().unwrap())
1508        .unwrap_or(40);
1509    let initial_entries = env::var("INITIAL_ENTRIES")
1510        .map(|o| o.parse().unwrap())
1511        .unwrap_or(20);
1512
1513    let root_dir = Path::new(path!("/test"));
1514    let fs = FakeFs::new(cx.background_executor.clone()) as Arc<dyn Fs>;
1515    fs.as_fake().insert_tree(root_dir, json!({})).await;
1516    for _ in 0..initial_entries {
1517        randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
1518    }
1519    log::info!("generated initial tree");
1520
1521    let worktree = Worktree::local(
1522        root_dir,
1523        true,
1524        fs.clone(),
1525        Default::default(),
1526        &mut cx.to_async(),
1527    )
1528    .await
1529    .unwrap();
1530
1531    let updates = Arc::new(Mutex::new(Vec::new()));
1532    worktree.update(cx, |tree, cx| {
1533        check_worktree_change_events(tree, cx);
1534
1535        tree.as_local_mut().unwrap().observe_updates(0, cx, {
1536            let updates = updates.clone();
1537            move |update| {
1538                updates.lock().push(update);
1539                async { true }
1540            }
1541        });
1542    });
1543
1544    worktree
1545        .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
1546        .await;
1547
1548    fs.as_fake().pause_events();
1549    let mut snapshots = Vec::new();
1550    let mut mutations_len = operations;
1551    while mutations_len > 1 {
1552        if rng.random_bool(0.2) {
1553            worktree
1554                .update(cx, |worktree, cx| {
1555                    randomly_mutate_worktree(worktree, &mut rng, cx)
1556                })
1557                .await
1558                .log_err();
1559        } else {
1560            randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
1561        }
1562
1563        let buffered_event_count = fs.as_fake().buffered_event_count();
1564        if buffered_event_count > 0 && rng.random_bool(0.3) {
1565            let len = rng.random_range(0..=buffered_event_count);
1566            log::info!("flushing {} events", len);
1567            fs.as_fake().flush_events(len);
1568        } else {
1569            randomly_mutate_fs(&fs, root_dir, 0.6, &mut rng).await;
1570            mutations_len -= 1;
1571        }
1572
1573        cx.executor().run_until_parked();
1574        if rng.random_bool(0.2) {
1575            log::info!("storing snapshot {}", snapshots.len());
1576            let snapshot = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
1577            snapshots.push(snapshot);
1578        }
1579    }
1580
1581    log::info!("quiescing");
1582    fs.as_fake().flush_events(usize::MAX);
1583    cx.executor().run_until_parked();
1584
1585    let snapshot = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
1586    snapshot.check_invariants(true);
1587    let expanded_paths = snapshot
1588        .expanded_entries()
1589        .map(|e| e.path.clone())
1590        .collect::<Vec<_>>();
1591
1592    {
1593        let new_worktree = Worktree::local(
1594            root_dir,
1595            true,
1596            fs.clone(),
1597            Default::default(),
1598            &mut cx.to_async(),
1599        )
1600        .await
1601        .unwrap();
1602        new_worktree
1603            .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
1604            .await;
1605        new_worktree
1606            .update(cx, |tree, _| {
1607                tree.as_local_mut()
1608                    .unwrap()
1609                    .refresh_entries_for_paths(expanded_paths)
1610            })
1611            .recv()
1612            .await;
1613        let new_snapshot =
1614            new_worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
1615        assert_eq!(
1616            snapshot.entries_without_ids(true),
1617            new_snapshot.entries_without_ids(true)
1618        );
1619    }
1620
1621    let settings = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().settings());
1622
1623    for (i, mut prev_snapshot) in snapshots.into_iter().enumerate().rev() {
1624        for update in updates.lock().iter() {
1625            if update.scan_id >= prev_snapshot.scan_id() as u64 {
1626                prev_snapshot.apply_remote_update(update.clone(), &settings.file_scan_inclusions);
1627            }
1628        }
1629
1630        assert_eq!(
1631            prev_snapshot
1632                .entries(true, 0)
1633                .map(ignore_pending_dir)
1634                .collect::<Vec<_>>(),
1635            snapshot
1636                .entries(true, 0)
1637                .map(ignore_pending_dir)
1638                .collect::<Vec<_>>(),
1639            "wrong updates after snapshot {i}: {updates:#?}",
1640        );
1641    }
1642
1643    fn ignore_pending_dir(entry: &Entry) -> Entry {
1644        let mut entry = entry.clone();
1645        if entry.kind.is_dir() {
1646            entry.kind = EntryKind::Dir
1647        }
1648        entry
1649    }
1650}
1651
1652// The worktree's `UpdatedEntries` event can be used to follow along with
1653// all changes to the worktree's snapshot.
1654fn check_worktree_change_events(tree: &mut Worktree, cx: &mut Context<Worktree>) {
1655    let mut entries = tree.entries(true, 0).cloned().collect::<Vec<_>>();
1656    cx.subscribe(&cx.entity(), move |tree, _, event, _| {
1657        if let Event::UpdatedEntries(changes) = event {
1658            for (path, _, change_type) in changes.iter() {
1659                let entry = tree.entry_for_path(path).cloned();
1660                let ix = match entries.binary_search_by_key(&path, |e| &e.path) {
1661                    Ok(ix) | Err(ix) => ix,
1662                };
1663                match change_type {
1664                    PathChange::Added => entries.insert(ix, entry.unwrap()),
1665                    PathChange::Removed => drop(entries.remove(ix)),
1666                    PathChange::Updated => {
1667                        let entry = entry.unwrap();
1668                        let existing_entry = entries.get_mut(ix).unwrap();
1669                        assert_eq!(existing_entry.path, entry.path);
1670                        *existing_entry = entry;
1671                    }
1672                    PathChange::AddedOrUpdated | PathChange::Loaded => {
1673                        let entry = entry.unwrap();
1674                        if entries.get(ix).map(|e| &e.path) == Some(&entry.path) {
1675                            *entries.get_mut(ix).unwrap() = entry;
1676                        } else {
1677                            entries.insert(ix, entry);
1678                        }
1679                    }
1680                }
1681            }
1682
1683            let new_entries = tree.entries(true, 0).cloned().collect::<Vec<_>>();
1684            assert_eq!(entries, new_entries, "incorrect changes: {:?}", changes);
1685        }
1686    })
1687    .detach();
1688}
1689
1690fn randomly_mutate_worktree(
1691    worktree: &mut Worktree,
1692    rng: &mut impl Rng,
1693    cx: &mut Context<Worktree>,
1694) -> Task<Result<()>> {
1695    log::info!("mutating worktree");
1696    let worktree = worktree.as_local_mut().unwrap();
1697    let snapshot = worktree.snapshot();
1698    let entry = snapshot.entries(false, 0).choose(rng).unwrap();
1699
1700    match rng.random_range(0_u32..100) {
1701        0..=33 if entry.path.as_ref() != Path::new("") => {
1702            log::info!("deleting entry {:?} ({})", entry.path, entry.id.0);
1703            worktree.delete_entry(entry.id, false, cx).unwrap()
1704        }
1705        ..=66 if entry.path.as_ref() != Path::new("") => {
1706            let other_entry = snapshot.entries(false, 0).choose(rng).unwrap();
1707            let new_parent_path = if other_entry.is_dir() {
1708                other_entry.path.clone()
1709            } else {
1710                other_entry.path.parent().unwrap().into()
1711            };
1712            let mut new_path = new_parent_path.join(random_filename(rng));
1713            if new_path.starts_with(&entry.path) {
1714                new_path = random_filename(rng).into();
1715            }
1716
1717            log::info!(
1718                "renaming entry {:?} ({}) to {:?}",
1719                entry.path,
1720                entry.id.0,
1721                new_path
1722            );
1723            let task = worktree.rename_entry(entry.id, new_path, cx);
1724            cx.background_spawn(async move {
1725                task.await?.into_included().unwrap();
1726                Ok(())
1727            })
1728        }
1729        _ => {
1730            if entry.is_dir() {
1731                let child_path = entry.path.join(random_filename(rng));
1732                let is_dir = rng.random_bool(0.3);
1733                log::info!(
1734                    "creating {} at {:?}",
1735                    if is_dir { "dir" } else { "file" },
1736                    child_path,
1737                );
1738                let task = worktree.create_entry(child_path, is_dir, None, cx);
1739                cx.background_spawn(async move {
1740                    task.await?;
1741                    Ok(())
1742                })
1743            } else {
1744                log::info!("overwriting file {:?} ({})", entry.path, entry.id.0);
1745                let task =
1746                    worktree.write_file(entry.path.clone(), "".into(), Default::default(), cx);
1747                cx.background_spawn(async move {
1748                    task.await?;
1749                    Ok(())
1750                })
1751            }
1752        }
1753    }
1754}
1755
1756async fn randomly_mutate_fs(
1757    fs: &Arc<dyn Fs>,
1758    root_path: &Path,
1759    insertion_probability: f64,
1760    rng: &mut impl Rng,
1761) {
1762    log::info!("mutating fs");
1763    let mut files = Vec::new();
1764    let mut dirs = Vec::new();
1765    for path in fs.as_fake().paths(false) {
1766        if path.starts_with(root_path) {
1767            if fs.is_file(&path).await {
1768                files.push(path);
1769            } else {
1770                dirs.push(path);
1771            }
1772        }
1773    }
1774
1775    if (files.is_empty() && dirs.len() == 1) || rng.random_bool(insertion_probability) {
1776        let path = dirs.choose(rng).unwrap();
1777        let new_path = path.join(random_filename(rng));
1778
1779        if rng.random() {
1780            log::info!(
1781                "creating dir {:?}",
1782                new_path.strip_prefix(root_path).unwrap()
1783            );
1784            fs.create_dir(&new_path).await.unwrap();
1785        } else {
1786            log::info!(
1787                "creating file {:?}",
1788                new_path.strip_prefix(root_path).unwrap()
1789            );
1790            fs.create_file(&new_path, Default::default()).await.unwrap();
1791        }
1792    } else if rng.random_bool(0.05) {
1793        let ignore_dir_path = dirs.choose(rng).unwrap();
1794        let ignore_path = ignore_dir_path.join(*GITIGNORE);
1795
1796        let subdirs = dirs
1797            .iter()
1798            .filter(|d| d.starts_with(ignore_dir_path))
1799            .cloned()
1800            .collect::<Vec<_>>();
1801        let subfiles = files
1802            .iter()
1803            .filter(|d| d.starts_with(ignore_dir_path))
1804            .cloned()
1805            .collect::<Vec<_>>();
1806        let files_to_ignore = {
1807            let len = rng.random_range(0..=subfiles.len());
1808            subfiles.choose_multiple(rng, len)
1809        };
1810        let dirs_to_ignore = {
1811            let len = rng.random_range(0..subdirs.len());
1812            subdirs.choose_multiple(rng, len)
1813        };
1814
1815        let mut ignore_contents = String::new();
1816        for path_to_ignore in files_to_ignore.chain(dirs_to_ignore) {
1817            writeln!(
1818                ignore_contents,
1819                "{}",
1820                path_to_ignore
1821                    .strip_prefix(ignore_dir_path)
1822                    .unwrap()
1823                    .to_str()
1824                    .unwrap()
1825            )
1826            .unwrap();
1827        }
1828        log::info!(
1829            "creating gitignore {:?} with contents:\n{}",
1830            ignore_path.strip_prefix(root_path).unwrap(),
1831            ignore_contents
1832        );
1833        fs.save(
1834            &ignore_path,
1835            &ignore_contents.as_str().into(),
1836            Default::default(),
1837        )
1838        .await
1839        .unwrap();
1840    } else {
1841        let old_path = {
1842            let file_path = files.choose(rng);
1843            let dir_path = dirs[1..].choose(rng);
1844            file_path.into_iter().chain(dir_path).choose(rng).unwrap()
1845        };
1846
1847        let is_rename = rng.random();
1848        if is_rename {
1849            let new_path_parent = dirs
1850                .iter()
1851                .filter(|d| !d.starts_with(old_path))
1852                .choose(rng)
1853                .unwrap();
1854
1855            let overwrite_existing_dir =
1856                !old_path.starts_with(new_path_parent) && rng.random_bool(0.3);
1857            let new_path = if overwrite_existing_dir {
1858                fs.remove_dir(
1859                    new_path_parent,
1860                    RemoveOptions {
1861                        recursive: true,
1862                        ignore_if_not_exists: true,
1863                    },
1864                )
1865                .await
1866                .unwrap();
1867                new_path_parent.to_path_buf()
1868            } else {
1869                new_path_parent.join(random_filename(rng))
1870            };
1871
1872            log::info!(
1873                "renaming {:?} to {}{:?}",
1874                old_path.strip_prefix(root_path).unwrap(),
1875                if overwrite_existing_dir {
1876                    "overwrite "
1877                } else {
1878                    ""
1879                },
1880                new_path.strip_prefix(root_path).unwrap()
1881            );
1882            fs.rename(
1883                old_path,
1884                &new_path,
1885                fs::RenameOptions {
1886                    overwrite: true,
1887                    ignore_if_exists: true,
1888                },
1889            )
1890            .await
1891            .unwrap();
1892        } else if fs.is_file(old_path).await {
1893            log::info!(
1894                "deleting file {:?}",
1895                old_path.strip_prefix(root_path).unwrap()
1896            );
1897            fs.remove_file(old_path, Default::default()).await.unwrap();
1898        } else {
1899            log::info!(
1900                "deleting dir {:?}",
1901                old_path.strip_prefix(root_path).unwrap()
1902            );
1903            fs.remove_dir(
1904                old_path,
1905                RemoveOptions {
1906                    recursive: true,
1907                    ignore_if_not_exists: true,
1908                },
1909            )
1910            .await
1911            .unwrap();
1912        }
1913    }
1914}
1915
1916fn random_filename(rng: &mut impl Rng) -> String {
1917    (0..6)
1918        .map(|_| rng.sample(rand::distr::Alphanumeric))
1919        .map(char::from)
1920        .collect()
1921}
1922
1923#[gpui::test]
1924async fn test_rename_file_to_new_directory(cx: &mut TestAppContext) {
1925    init_test(cx);
1926    let fs = FakeFs::new(cx.background_executor.clone());
1927    let expected_contents = "content";
1928    fs.as_fake()
1929        .insert_tree(
1930            "/root",
1931            json!({
1932                "test.txt": expected_contents
1933            }),
1934        )
1935        .await;
1936    let worktree = Worktree::local(
1937        Path::new("/root"),
1938        true,
1939        fs.clone(),
1940        Arc::default(),
1941        &mut cx.to_async(),
1942    )
1943    .await
1944    .unwrap();
1945    cx.read(|cx| worktree.read(cx).as_local().unwrap().scan_complete())
1946        .await;
1947
1948    let entry_id = worktree.read_with(cx, |worktree, _| {
1949        worktree.entry_for_path("test.txt").unwrap().id
1950    });
1951    let _result = worktree
1952        .update(cx, |worktree, cx| {
1953            worktree.rename_entry(entry_id, Path::new("dir1/dir2/dir3/test.txt"), cx)
1954        })
1955        .await
1956        .unwrap();
1957    worktree.read_with(cx, |worktree, _| {
1958        assert!(
1959            worktree.entry_for_path("test.txt").is_none(),
1960            "Old file should have been removed"
1961        );
1962        assert!(
1963            worktree.entry_for_path("dir1/dir2/dir3/test.txt").is_some(),
1964            "Whole directory hierarchy and the new file should have been created"
1965        );
1966    });
1967    assert_eq!(
1968        worktree
1969            .update(cx, |worktree, cx| {
1970                worktree.load_file("dir1/dir2/dir3/test.txt".as_ref(), cx)
1971            })
1972            .await
1973            .unwrap()
1974            .text,
1975        expected_contents,
1976        "Moved file's contents should be preserved"
1977    );
1978
1979    let entry_id = worktree.read_with(cx, |worktree, _| {
1980        worktree
1981            .entry_for_path("dir1/dir2/dir3/test.txt")
1982            .unwrap()
1983            .id
1984    });
1985    let _result = worktree
1986        .update(cx, |worktree, cx| {
1987            worktree.rename_entry(entry_id, Path::new("dir1/dir2/test.txt"), cx)
1988        })
1989        .await
1990        .unwrap();
1991    worktree.read_with(cx, |worktree, _| {
1992        assert!(
1993            worktree.entry_for_path("test.txt").is_none(),
1994            "First file should not reappear"
1995        );
1996        assert!(
1997            worktree.entry_for_path("dir1/dir2/dir3/test.txt").is_none(),
1998            "Old file should have been removed"
1999        );
2000        assert!(
2001            worktree.entry_for_path("dir1/dir2/test.txt").is_some(),
2002            "No error should have occurred after moving into existing directory"
2003        );
2004    });
2005    assert_eq!(
2006        worktree
2007            .update(cx, |worktree, cx| {
2008                worktree.load_file("dir1/dir2/test.txt".as_ref(), cx)
2009            })
2010            .await
2011            .unwrap()
2012            .text,
2013        expected_contents,
2014        "Moved file's contents should be preserved"
2015    );
2016}
2017
2018#[gpui::test]
2019async fn test_private_single_file_worktree(cx: &mut TestAppContext) {
2020    init_test(cx);
2021    let fs = FakeFs::new(cx.background_executor.clone());
2022    fs.insert_tree("/", json!({".env": "PRIVATE=secret\n"}))
2023        .await;
2024    let tree = Worktree::local(
2025        Path::new("/.env"),
2026        true,
2027        fs.clone(),
2028        Default::default(),
2029        &mut cx.to_async(),
2030    )
2031    .await
2032    .unwrap();
2033    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2034        .await;
2035    tree.read_with(cx, |tree, _| {
2036        let entry = tree.entry_for_path("").unwrap();
2037        assert!(entry.is_private);
2038    });
2039}
2040
2041#[gpui::test]
2042fn test_unrelativize() {
2043    let work_directory = WorkDirectory::in_project("");
2044    pretty_assertions::assert_eq!(
2045        work_directory.try_unrelativize(&"crates/gpui/gpui.rs".into()),
2046        Some(Path::new("crates/gpui/gpui.rs").into())
2047    );
2048
2049    let work_directory = WorkDirectory::in_project("vendor/some-submodule");
2050    pretty_assertions::assert_eq!(
2051        work_directory.try_unrelativize(&"src/thing.c".into()),
2052        Some(Path::new("vendor/some-submodule/src/thing.c").into())
2053    );
2054
2055    let work_directory = WorkDirectory::AboveProject {
2056        absolute_path: Path::new("/projects/zed").into(),
2057        location_in_repo: Path::new("crates/gpui").into(),
2058    };
2059
2060    pretty_assertions::assert_eq!(
2061        work_directory.try_unrelativize(&"crates/util/util.rs".into()),
2062        None,
2063    );
2064
2065    pretty_assertions::assert_eq!(
2066        work_directory.unrelativize(&"crates/util/util.rs".into()),
2067        Path::new("../util/util.rs").into()
2068    );
2069
2070    pretty_assertions::assert_eq!(work_directory.try_unrelativize(&"README.md".into()), None,);
2071
2072    pretty_assertions::assert_eq!(
2073        work_directory.unrelativize(&"README.md".into()),
2074        Path::new("../../README.md").into()
2075    );
2076}
2077
2078#[gpui::test]
2079async fn test_repository_above_root(executor: BackgroundExecutor, cx: &mut TestAppContext) {
2080    init_test(cx);
2081
2082    let fs = FakeFs::new(executor);
2083    fs.insert_tree(
2084        path!("/root"),
2085        json!({
2086            ".git": {},
2087            "subproject": {
2088                "a.txt": "A"
2089            }
2090        }),
2091    )
2092    .await;
2093    let worktree = Worktree::local(
2094        path!("/root/subproject").as_ref(),
2095        true,
2096        fs.clone(),
2097        Arc::default(),
2098        &mut cx.to_async(),
2099    )
2100    .await
2101    .unwrap();
2102    worktree
2103        .update(cx, |worktree, _| {
2104            worktree.as_local().unwrap().scan_complete()
2105        })
2106        .await;
2107    cx.run_until_parked();
2108    let repos = worktree.update(cx, |worktree, _| {
2109        worktree
2110            .as_local()
2111            .unwrap()
2112            .git_repositories
2113            .values()
2114            .map(|entry| entry.work_directory_abs_path.clone())
2115            .collect::<Vec<_>>()
2116    });
2117    pretty_assertions::assert_eq!(repos, [Path::new(path!("/root")).into()]);
2118
2119    fs.touch_path(path!("/root/subproject")).await;
2120    worktree
2121        .update(cx, |worktree, _| {
2122            worktree.as_local().unwrap().scan_complete()
2123        })
2124        .await;
2125    cx.run_until_parked();
2126
2127    let repos = worktree.update(cx, |worktree, _| {
2128        worktree
2129            .as_local()
2130            .unwrap()
2131            .git_repositories
2132            .values()
2133            .map(|entry| entry.work_directory_abs_path.clone())
2134            .collect::<Vec<_>>()
2135    });
2136    pretty_assertions::assert_eq!(repos, [Path::new(path!("/root")).into()]);
2137}
2138
2139#[gpui::test]
2140async fn test_global_gitignore(executor: BackgroundExecutor, cx: &mut TestAppContext) {
2141    init_test(cx);
2142
2143    let home = paths::home_dir();
2144    let fs = FakeFs::new(executor);
2145    fs.insert_tree(
2146        home,
2147        json!({
2148            ".config": {
2149                "git": {
2150                    "ignore": "foo\n/bar\nbaz\n"
2151                }
2152            },
2153            "project": {
2154                ".git": {},
2155                ".gitignore": "!baz",
2156                "foo": "",
2157                "bar": "",
2158                "sub": {
2159                    "bar": "",
2160                },
2161                "subrepo": {
2162                    ".git": {},
2163                    "bar": ""
2164                },
2165                "baz": ""
2166            }
2167        }),
2168    )
2169    .await;
2170    let worktree = Worktree::local(
2171        home.join("project"),
2172        true,
2173        fs.clone(),
2174        Arc::default(),
2175        &mut cx.to_async(),
2176    )
2177    .await
2178    .unwrap();
2179    worktree
2180        .update(cx, |worktree, _| {
2181            worktree.as_local().unwrap().scan_complete()
2182        })
2183        .await;
2184    cx.run_until_parked();
2185
2186    // .gitignore overrides excludesFile, and anchored paths in excludesFile are resolved
2187    // relative to the nearest containing repository
2188    worktree.update(cx, |worktree, _cx| {
2189        check_worktree_entries(
2190            worktree,
2191            &[],
2192            &["foo", "bar", "subrepo/bar"],
2193            &["sub/bar", "baz"],
2194            &[],
2195        );
2196    });
2197
2198    // Ignore statuses are updated when excludesFile changes
2199    fs.write(
2200        &home.join(".config").join("git").join("ignore"),
2201        "/bar\nbaz\n".as_bytes(),
2202    )
2203    .await
2204    .unwrap();
2205    worktree
2206        .update(cx, |worktree, _| {
2207            worktree.as_local().unwrap().scan_complete()
2208        })
2209        .await;
2210    cx.run_until_parked();
2211
2212    worktree.update(cx, |worktree, _cx| {
2213        check_worktree_entries(
2214            worktree,
2215            &[],
2216            &["bar", "subrepo/bar"],
2217            &["foo", "sub/bar", "baz"],
2218            &[],
2219        );
2220    });
2221
2222    // Statuses are updated when .git added/removed
2223    fs.remove_dir(
2224        &home.join("project").join("subrepo").join(".git"),
2225        RemoveOptions {
2226            recursive: true,
2227            ..Default::default()
2228        },
2229    )
2230    .await
2231    .unwrap();
2232    worktree
2233        .update(cx, |worktree, _| {
2234            worktree.as_local().unwrap().scan_complete()
2235        })
2236        .await;
2237    cx.run_until_parked();
2238
2239    worktree.update(cx, |worktree, _cx| {
2240        check_worktree_entries(
2241            worktree,
2242            &[],
2243            &["bar"],
2244            &["foo", "sub/bar", "baz", "subrepo/bar"],
2245            &[],
2246        );
2247    });
2248}
2249
2250#[track_caller]
2251fn check_worktree_entries(
2252    tree: &Worktree,
2253    expected_excluded_paths: &[&str],
2254    expected_ignored_paths: &[&str],
2255    expected_tracked_paths: &[&str],
2256    expected_included_paths: &[&str],
2257) {
2258    for path in expected_excluded_paths {
2259        let entry = tree.entry_for_path(path);
2260        assert!(
2261            entry.is_none(),
2262            "expected path '{path}' to be excluded, but got entry: {entry:?}",
2263        );
2264    }
2265    for path in expected_ignored_paths {
2266        let entry = tree
2267            .entry_for_path(path)
2268            .unwrap_or_else(|| panic!("Missing entry for expected ignored path '{path}'"));
2269        assert!(
2270            entry.is_ignored,
2271            "expected path '{path}' to be ignored, but got entry: {entry:?}",
2272        );
2273    }
2274    for path in expected_tracked_paths {
2275        let entry = tree
2276            .entry_for_path(path)
2277            .unwrap_or_else(|| panic!("Missing entry for expected tracked path '{path}'"));
2278        assert!(
2279            !entry.is_ignored || entry.is_always_included,
2280            "expected path '{path}' to be tracked, but got entry: {entry:?}",
2281        );
2282    }
2283    for path in expected_included_paths {
2284        let entry = tree
2285            .entry_for_path(path)
2286            .unwrap_or_else(|| panic!("Missing entry for expected included path '{path}'"));
2287        assert!(
2288            entry.is_always_included,
2289            "expected path '{path}' to always be included, but got entry: {entry:?}",
2290        );
2291    }
2292}
2293
2294fn init_test(cx: &mut gpui::TestAppContext) {
2295    zlog::init_test();
2296
2297    cx.update(|cx| {
2298        let settings_store = SettingsStore::test(cx);
2299        cx.set_global(settings_store);
2300        WorktreeSettings::register(cx);
2301    });
2302}