worktree_tests.rs

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