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