worktree_tests.rs

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