worktree_tests.rs

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