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