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