worktree_tests.rs

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