worktree_tests.rs

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