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