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