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