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