worktree_tests.rs

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