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