worktree_tests.rs

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