worktree_tests.rs

   1use crate::{
   2    worktree_settings::WorktreeSettings, Entry, EntryKind, Event, PathChange, Snapshot,
   3    WorkDirectory, Worktree, WorktreeModelHandle,
   4};
   5use anyhow::Result;
   6use fs::{FakeFs, Fs, RealFs, RemoveOptions};
   7use git::{
   8    status::{
   9        FileStatus, GitSummary, StatusCode, TrackedStatus, TrackedSummary, UnmergedStatus,
  10        UnmergedStatusCode,
  11    },
  12    GITIGNORE,
  13};
  14use gpui::{BorrowAppContext, Context, Task, TestAppContext};
  15use parking_lot::Mutex;
  16use postage::stream::Stream;
  17use pretty_assertions::assert_eq;
  18use rand::prelude::*;
  19use serde_json::json;
  20use settings::{Settings, SettingsStore};
  21use std::{
  22    env,
  23    fmt::Write,
  24    mem,
  25    path::{Path, PathBuf},
  26    sync::Arc,
  27};
  28use util::{test::TempTree, ResultExt};
  29
  30#[gpui::test]
  31async fn test_traversal(cx: &mut TestAppContext) {
  32    init_test(cx);
  33    let fs = FakeFs::new(cx.background_executor.clone());
  34    fs.insert_tree(
  35        "/root",
  36        json!({
  37           ".gitignore": "a/b\n",
  38           "a": {
  39               "b": "",
  40               "c": "",
  41           }
  42        }),
  43    )
  44    .await;
  45
  46    let tree = Worktree::local(
  47        Path::new("/root"),
  48        true,
  49        fs,
  50        Default::default(),
  51        &mut cx.to_async(),
  52    )
  53    .await
  54    .unwrap();
  55    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
  56        .await;
  57
  58    tree.read_with(cx, |tree, _| {
  59        assert_eq!(
  60            tree.entries(false, 0)
  61                .map(|entry| entry.path.as_ref())
  62                .collect::<Vec<_>>(),
  63            vec![
  64                Path::new(""),
  65                Path::new(".gitignore"),
  66                Path::new("a"),
  67                Path::new("a/c"),
  68            ]
  69        );
  70        assert_eq!(
  71            tree.entries(true, 0)
  72                .map(|entry| entry.path.as_ref())
  73                .collect::<Vec<_>>(),
  74            vec![
  75                Path::new(""),
  76                Path::new(".gitignore"),
  77                Path::new("a"),
  78                Path::new("a/b"),
  79                Path::new("a/c"),
  80            ]
  81        );
  82    })
  83}
  84
  85#[gpui::test(iterations = 10)]
  86async fn test_circular_symlinks(cx: &mut TestAppContext) {
  87    init_test(cx);
  88    let fs = FakeFs::new(cx.background_executor.clone());
  89    fs.insert_tree(
  90        "/root",
  91        json!({
  92            "lib": {
  93                "a": {
  94                    "a.txt": ""
  95                },
  96                "b": {
  97                    "b.txt": ""
  98                }
  99            }
 100        }),
 101    )
 102    .await;
 103    fs.create_symlink("/root/lib/a/lib".as_ref(), "..".into())
 104        .await
 105        .unwrap();
 106    fs.create_symlink("/root/lib/b/lib".as_ref(), "..".into())
 107        .await
 108        .unwrap();
 109
 110    let tree = Worktree::local(
 111        Path::new("/root"),
 112        true,
 113        fs.clone(),
 114        Default::default(),
 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                Path::new(""),
 130                Path::new("lib"),
 131                Path::new("lib/a"),
 132                Path::new("lib/a/a.txt"),
 133                Path::new("lib/a/lib"),
 134                Path::new("lib/b"),
 135                Path::new("lib/b/b.txt"),
 136                Path::new("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                Path::new(""),
 156                Path::new("lib"),
 157                Path::new("lib/a"),
 158                Path::new("lib/a/a.txt"),
 159                Path::new("lib/a/lib-2"),
 160                Path::new("lib/b"),
 161                Path::new("lib/b/b.txt"),
 162                Path::new("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        &mut cx.to_async(),
 215    )
 216    .await
 217    .unwrap();
 218
 219    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
 220        .await;
 221
 222    let tree_updates = Arc::new(Mutex::new(Vec::new()));
 223    tree.update(cx, |_, cx| {
 224        let tree_updates = tree_updates.clone();
 225        cx.subscribe(&tree, move |_, _, event, _| {
 226            if let Event::UpdatedEntries(update) = event {
 227                tree_updates.lock().extend(
 228                    update
 229                        .iter()
 230                        .map(|(path, _, change)| (path.clone(), *change)),
 231                );
 232            }
 233        })
 234        .detach();
 235    });
 236
 237    // The symlinked directories are not scanned by default.
 238    tree.read_with(cx, |tree, _| {
 239        assert_eq!(
 240            tree.entries(true, 0)
 241                .map(|entry| (entry.path.as_ref(), entry.is_external))
 242                .collect::<Vec<_>>(),
 243            vec![
 244                (Path::new(""), false),
 245                (Path::new("deps"), false),
 246                (Path::new("deps/dep-dir2"), true),
 247                (Path::new("deps/dep-dir3"), true),
 248                (Path::new("src"), false),
 249                (Path::new("src/a.rs"), false),
 250                (Path::new("src/b.rs"), false),
 251            ]
 252        );
 253
 254        assert_eq!(
 255            tree.entry_for_path("deps/dep-dir2").unwrap().kind,
 256            EntryKind::UnloadedDir
 257        );
 258    });
 259
 260    // Expand one of the symlinked directories.
 261    tree.read_with(cx, |tree, _| {
 262        tree.as_local()
 263            .unwrap()
 264            .refresh_entries_for_paths(vec![Path::new("deps/dep-dir3").into()])
 265    })
 266    .recv()
 267    .await;
 268
 269    // The expanded directory's contents are loaded. Subdirectories are
 270    // not scanned yet.
 271    tree.read_with(cx, |tree, _| {
 272        assert_eq!(
 273            tree.entries(true, 0)
 274                .map(|entry| (entry.path.as_ref(), entry.is_external))
 275                .collect::<Vec<_>>(),
 276            vec![
 277                (Path::new(""), false),
 278                (Path::new("deps"), false),
 279                (Path::new("deps/dep-dir2"), true),
 280                (Path::new("deps/dep-dir3"), true),
 281                (Path::new("deps/dep-dir3/deps"), true),
 282                (Path::new("deps/dep-dir3/src"), true),
 283                (Path::new("src"), false),
 284                (Path::new("src/a.rs"), false),
 285                (Path::new("src/b.rs"), false),
 286            ]
 287        );
 288    });
 289    assert_eq!(
 290        mem::take(&mut *tree_updates.lock()),
 291        &[
 292            (Path::new("deps/dep-dir3").into(), PathChange::Loaded),
 293            (Path::new("deps/dep-dir3/deps").into(), PathChange::Loaded),
 294            (Path::new("deps/dep-dir3/src").into(), PathChange::Loaded)
 295        ]
 296    );
 297
 298    // Expand a subdirectory of one of the symlinked directories.
 299    tree.read_with(cx, |tree, _| {
 300        tree.as_local()
 301            .unwrap()
 302            .refresh_entries_for_paths(vec![Path::new("deps/dep-dir3/src").into()])
 303    })
 304    .recv()
 305    .await;
 306
 307    // The expanded subdirectory's contents are loaded.
 308    tree.read_with(cx, |tree, _| {
 309        assert_eq!(
 310            tree.entries(true, 0)
 311                .map(|entry| (entry.path.as_ref(), entry.is_external))
 312                .collect::<Vec<_>>(),
 313            vec![
 314                (Path::new(""), false),
 315                (Path::new("deps"), false),
 316                (Path::new("deps/dep-dir2"), true),
 317                (Path::new("deps/dep-dir3"), true),
 318                (Path::new("deps/dep-dir3/deps"), true),
 319                (Path::new("deps/dep-dir3/src"), true),
 320                (Path::new("deps/dep-dir3/src/e.rs"), true),
 321                (Path::new("deps/dep-dir3/src/f.rs"), true),
 322                (Path::new("src"), false),
 323                (Path::new("src/a.rs"), false),
 324                (Path::new("src/b.rs"), false),
 325            ]
 326        );
 327    });
 328
 329    assert_eq!(
 330        mem::take(&mut *tree_updates.lock()),
 331        &[
 332            (Path::new("deps/dep-dir3/src").into(), PathChange::Loaded),
 333            (
 334                Path::new("deps/dep-dir3/src/e.rs").into(),
 335                PathChange::Loaded
 336            ),
 337            (
 338                Path::new("deps/dep-dir3/src/f.rs").into(),
 339                PathChange::Loaded
 340            )
 341        ]
 342    );
 343}
 344
 345#[cfg(target_os = "macos")]
 346#[gpui::test]
 347async fn test_renaming_case_only(cx: &mut TestAppContext) {
 348    cx.executor().allow_parking();
 349    init_test(cx);
 350
 351    const OLD_NAME: &str = "aaa.rs";
 352    const NEW_NAME: &str = "AAA.rs";
 353
 354    let fs = Arc::new(RealFs::default());
 355    let temp_root = TempTree::new(json!({
 356        OLD_NAME: "",
 357    }));
 358
 359    let tree = Worktree::local(
 360        temp_root.path(),
 361        true,
 362        fs.clone(),
 363        Default::default(),
 364        &mut cx.to_async(),
 365    )
 366    .await
 367    .unwrap();
 368
 369    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
 370        .await;
 371    tree.read_with(cx, |tree, _| {
 372        assert_eq!(
 373            tree.entries(true, 0)
 374                .map(|entry| entry.path.as_ref())
 375                .collect::<Vec<_>>(),
 376            vec![Path::new(""), Path::new(OLD_NAME)]
 377        );
 378    });
 379
 380    fs.rename(
 381        &temp_root.path().join(OLD_NAME),
 382        &temp_root.path().join(NEW_NAME),
 383        fs::RenameOptions {
 384            overwrite: true,
 385            ignore_if_exists: true,
 386        },
 387    )
 388    .await
 389    .unwrap();
 390
 391    tree.flush_fs_events(cx).await;
 392
 393    tree.read_with(cx, |tree, _| {
 394        assert_eq!(
 395            tree.entries(true, 0)
 396                .map(|entry| entry.path.as_ref())
 397                .collect::<Vec<_>>(),
 398            vec![Path::new(""), Path::new(NEW_NAME)]
 399        );
 400    });
 401}
 402
 403#[gpui::test]
 404async fn test_open_gitignored_files(cx: &mut TestAppContext) {
 405    init_test(cx);
 406    let fs = FakeFs::new(cx.background_executor.clone());
 407    fs.insert_tree(
 408        "/root",
 409        json!({
 410            ".gitignore": "node_modules\n",
 411            "one": {
 412                "node_modules": {
 413                    "a": {
 414                        "a1.js": "a1",
 415                        "a2.js": "a2",
 416                    },
 417                    "b": {
 418                        "b1.js": "b1",
 419                        "b2.js": "b2",
 420                    },
 421                    "c": {
 422                        "c1.js": "c1",
 423                        "c2.js": "c2",
 424                    }
 425                },
 426            },
 427            "two": {
 428                "x.js": "",
 429                "y.js": "",
 430            },
 431        }),
 432    )
 433    .await;
 434
 435    let tree = Worktree::local(
 436        Path::new("/root"),
 437        true,
 438        fs.clone(),
 439        Default::default(),
 440        &mut cx.to_async(),
 441    )
 442    .await
 443    .unwrap();
 444
 445    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
 446        .await;
 447
 448    tree.read_with(cx, |tree, _| {
 449        assert_eq!(
 450            tree.entries(true, 0)
 451                .map(|entry| (entry.path.as_ref(), entry.is_ignored))
 452                .collect::<Vec<_>>(),
 453            vec![
 454                (Path::new(""), false),
 455                (Path::new(".gitignore"), false),
 456                (Path::new("one"), false),
 457                (Path::new("one/node_modules"), true),
 458                (Path::new("two"), false),
 459                (Path::new("two/x.js"), false),
 460                (Path::new("two/y.js"), false),
 461            ]
 462        );
 463    });
 464
 465    // Open a file that is nested inside of a gitignored directory that
 466    // has not yet been expanded.
 467    let prev_read_dir_count = fs.read_dir_call_count();
 468    let loaded = tree
 469        .update(cx, |tree, cx| {
 470            tree.load_file("one/node_modules/b/b1.js".as_ref(), cx)
 471        })
 472        .await
 473        .unwrap();
 474
 475    tree.read_with(cx, |tree, _| {
 476        assert_eq!(
 477            tree.entries(true, 0)
 478                .map(|entry| (entry.path.as_ref(), entry.is_ignored))
 479                .collect::<Vec<_>>(),
 480            vec![
 481                (Path::new(""), false),
 482                (Path::new(".gitignore"), false),
 483                (Path::new("one"), false),
 484                (Path::new("one/node_modules"), true),
 485                (Path::new("one/node_modules/a"), true),
 486                (Path::new("one/node_modules/b"), true),
 487                (Path::new("one/node_modules/b/b1.js"), true),
 488                (Path::new("one/node_modules/b/b2.js"), true),
 489                (Path::new("one/node_modules/c"), true),
 490                (Path::new("two"), false),
 491                (Path::new("two/x.js"), false),
 492                (Path::new("two/y.js"), false),
 493            ]
 494        );
 495
 496        assert_eq!(
 497            loaded.file.path.as_ref(),
 498            Path::new("one/node_modules/b/b1.js")
 499        );
 500
 501        // Only the newly-expanded directories are scanned.
 502        assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 2);
 503    });
 504
 505    // Open another file in a different subdirectory of the same
 506    // gitignored directory.
 507    let prev_read_dir_count = fs.read_dir_call_count();
 508    let loaded = tree
 509        .update(cx, |tree, cx| {
 510            tree.load_file("one/node_modules/a/a2.js".as_ref(), cx)
 511        })
 512        .await
 513        .unwrap();
 514
 515    tree.read_with(cx, |tree, _| {
 516        assert_eq!(
 517            tree.entries(true, 0)
 518                .map(|entry| (entry.path.as_ref(), entry.is_ignored))
 519                .collect::<Vec<_>>(),
 520            vec![
 521                (Path::new(""), false),
 522                (Path::new(".gitignore"), false),
 523                (Path::new("one"), false),
 524                (Path::new("one/node_modules"), true),
 525                (Path::new("one/node_modules/a"), true),
 526                (Path::new("one/node_modules/a/a1.js"), true),
 527                (Path::new("one/node_modules/a/a2.js"), true),
 528                (Path::new("one/node_modules/b"), true),
 529                (Path::new("one/node_modules/b/b1.js"), true),
 530                (Path::new("one/node_modules/b/b2.js"), true),
 531                (Path::new("one/node_modules/c"), true),
 532                (Path::new("two"), false),
 533                (Path::new("two/x.js"), false),
 534                (Path::new("two/y.js"), false),
 535            ]
 536        );
 537
 538        assert_eq!(
 539            loaded.file.path.as_ref(),
 540            Path::new("one/node_modules/a/a2.js")
 541        );
 542
 543        // Only the newly-expanded directory is scanned.
 544        assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 1);
 545    });
 546
 547    let path = PathBuf::from("/root/one/node_modules/c/lib");
 548
 549    // No work happens when files and directories change within an unloaded directory.
 550    let prev_fs_call_count = fs.read_dir_call_count() + fs.metadata_call_count();
 551    // When we open a directory, we check each ancestor whether it's a git
 552    // repository. That means we have an fs.metadata call per ancestor that we
 553    // need to subtract here.
 554    let ancestors = path.ancestors().count();
 555
 556    fs.create_dir(path.as_ref()).await.unwrap();
 557    cx.executor().run_until_parked();
 558
 559    assert_eq!(
 560        fs.read_dir_call_count() + fs.metadata_call_count() - prev_fs_call_count - ancestors,
 561        0
 562    );
 563}
 564
 565#[gpui::test]
 566async fn test_dirs_no_longer_ignored(cx: &mut TestAppContext) {
 567    init_test(cx);
 568    let fs = FakeFs::new(cx.background_executor.clone());
 569    fs.insert_tree(
 570        "/root",
 571        json!({
 572            ".gitignore": "node_modules\n",
 573            "a": {
 574                "a.js": "",
 575            },
 576            "b": {
 577                "b.js": "",
 578            },
 579            "node_modules": {
 580                "c": {
 581                    "c.js": "",
 582                },
 583                "d": {
 584                    "d.js": "",
 585                    "e": {
 586                        "e1.js": "",
 587                        "e2.js": "",
 588                    },
 589                    "f": {
 590                        "f1.js": "",
 591                        "f2.js": "",
 592                    }
 593                },
 594            },
 595        }),
 596    )
 597    .await;
 598
 599    let tree = Worktree::local(
 600        Path::new("/root"),
 601        true,
 602        fs.clone(),
 603        Default::default(),
 604        &mut cx.to_async(),
 605    )
 606    .await
 607    .unwrap();
 608
 609    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
 610        .await;
 611
 612    // Open a file within the gitignored directory, forcing some of its
 613    // subdirectories to be read, but not all.
 614    let read_dir_count_1 = fs.read_dir_call_count();
 615    tree.read_with(cx, |tree, _| {
 616        tree.as_local()
 617            .unwrap()
 618            .refresh_entries_for_paths(vec![Path::new("node_modules/d/d.js").into()])
 619    })
 620    .recv()
 621    .await;
 622
 623    // Those subdirectories are now loaded.
 624    tree.read_with(cx, |tree, _| {
 625        assert_eq!(
 626            tree.entries(true, 0)
 627                .map(|e| (e.path.as_ref(), e.is_ignored))
 628                .collect::<Vec<_>>(),
 629            &[
 630                (Path::new(""), false),
 631                (Path::new(".gitignore"), false),
 632                (Path::new("a"), false),
 633                (Path::new("a/a.js"), false),
 634                (Path::new("b"), false),
 635                (Path::new("b/b.js"), false),
 636                (Path::new("node_modules"), true),
 637                (Path::new("node_modules/c"), true),
 638                (Path::new("node_modules/d"), true),
 639                (Path::new("node_modules/d/d.js"), true),
 640                (Path::new("node_modules/d/e"), true),
 641                (Path::new("node_modules/d/f"), true),
 642            ]
 643        );
 644    });
 645    let read_dir_count_2 = fs.read_dir_call_count();
 646    assert_eq!(read_dir_count_2 - read_dir_count_1, 2);
 647
 648    // Update the gitignore so that node_modules is no longer ignored,
 649    // but a subdirectory is ignored
 650    fs.save("/root/.gitignore".as_ref(), &"e".into(), Default::default())
 651        .await
 652        .unwrap();
 653    cx.executor().run_until_parked();
 654
 655    // All of the directories that are no longer ignored are now loaded.
 656    tree.read_with(cx, |tree, _| {
 657        assert_eq!(
 658            tree.entries(true, 0)
 659                .map(|e| (e.path.as_ref(), e.is_ignored))
 660                .collect::<Vec<_>>(),
 661            &[
 662                (Path::new(""), false),
 663                (Path::new(".gitignore"), false),
 664                (Path::new("a"), false),
 665                (Path::new("a/a.js"), false),
 666                (Path::new("b"), false),
 667                (Path::new("b/b.js"), false),
 668                // This directory is no longer ignored
 669                (Path::new("node_modules"), false),
 670                (Path::new("node_modules/c"), false),
 671                (Path::new("node_modules/c/c.js"), false),
 672                (Path::new("node_modules/d"), false),
 673                (Path::new("node_modules/d/d.js"), false),
 674                // This subdirectory is now ignored
 675                (Path::new("node_modules/d/e"), true),
 676                (Path::new("node_modules/d/f"), false),
 677                (Path::new("node_modules/d/f/f1.js"), false),
 678                (Path::new("node_modules/d/f/f2.js"), false),
 679            ]
 680        );
 681    });
 682
 683    // Each of the newly-loaded directories is scanned only once.
 684    let read_dir_count_3 = fs.read_dir_call_count();
 685    assert_eq!(read_dir_count_3 - read_dir_count_2, 2);
 686}
 687
 688#[gpui::test(iterations = 10)]
 689async fn test_rescan_with_gitignore(cx: &mut TestAppContext) {
 690    init_test(cx);
 691    cx.update(|cx| {
 692        cx.update_global::<SettingsStore, _>(|store, cx| {
 693            store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
 694                project_settings.file_scan_exclusions = Some(Vec::new());
 695            });
 696        });
 697    });
 698    let fs = FakeFs::new(cx.background_executor.clone());
 699    fs.insert_tree(
 700        "/root",
 701        json!({
 702            ".gitignore": "ancestor-ignored-file1\nancestor-ignored-file2\n",
 703            "tree": {
 704                ".git": {},
 705                ".gitignore": "ignored-dir\n",
 706                "tracked-dir": {
 707                    "tracked-file1": "",
 708                    "ancestor-ignored-file1": "",
 709                },
 710                "ignored-dir": {
 711                    "ignored-file1": ""
 712                }
 713            }
 714        }),
 715    )
 716    .await;
 717
 718    let tree = Worktree::local(
 719        "/root/tree".as_ref(),
 720        true,
 721        fs.clone(),
 722        Default::default(),
 723        &mut cx.to_async(),
 724    )
 725    .await
 726    .unwrap();
 727    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
 728        .await;
 729
 730    tree.read_with(cx, |tree, _| {
 731        tree.as_local()
 732            .unwrap()
 733            .refresh_entries_for_paths(vec![Path::new("ignored-dir").into()])
 734    })
 735    .recv()
 736    .await;
 737
 738    cx.read(|cx| {
 739        let tree = tree.read(cx);
 740        assert_entry_git_state(tree, "tracked-dir/tracked-file1", None, false);
 741        assert_entry_git_state(tree, "tracked-dir/ancestor-ignored-file1", None, false);
 742        assert_entry_git_state(tree, "ignored-dir/ignored-file1", None, true);
 743    });
 744
 745    fs.set_status_for_repo_via_working_copy_change(
 746        Path::new("/root/tree/.git"),
 747        &[(
 748            Path::new("tracked-dir/tracked-file2"),
 749            StatusCode::Added.index(),
 750        )],
 751    );
 752
 753    fs.create_file(
 754        "/root/tree/tracked-dir/tracked-file2".as_ref(),
 755        Default::default(),
 756    )
 757    .await
 758    .unwrap();
 759    fs.create_file(
 760        "/root/tree/tracked-dir/ancestor-ignored-file2".as_ref(),
 761        Default::default(),
 762    )
 763    .await
 764    .unwrap();
 765    fs.create_file(
 766        "/root/tree/ignored-dir/ignored-file2".as_ref(),
 767        Default::default(),
 768    )
 769    .await
 770    .unwrap();
 771
 772    cx.executor().run_until_parked();
 773    cx.read(|cx| {
 774        let tree = tree.read(cx);
 775        assert_entry_git_state(
 776            tree,
 777            "tracked-dir/tracked-file2",
 778            Some(StatusCode::Added),
 779            false,
 780        );
 781        assert_entry_git_state(tree, "tracked-dir/ancestor-ignored-file2", None, false);
 782        assert_entry_git_state(tree, "ignored-dir/ignored-file2", None, true);
 783        assert!(tree.entry_for_path(".git").unwrap().is_ignored);
 784    });
 785}
 786
 787#[gpui::test]
 788async fn test_update_gitignore(cx: &mut TestAppContext) {
 789    init_test(cx);
 790    let fs = FakeFs::new(cx.background_executor.clone());
 791    fs.insert_tree(
 792        "/root",
 793        json!({
 794            ".git": {},
 795            ".gitignore": "*.txt\n",
 796            "a.xml": "<a></a>",
 797            "b.txt": "Some text"
 798        }),
 799    )
 800    .await;
 801
 802    let tree = Worktree::local(
 803        "/root".as_ref(),
 804        true,
 805        fs.clone(),
 806        Default::default(),
 807        &mut cx.to_async(),
 808    )
 809    .await
 810    .unwrap();
 811    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
 812        .await;
 813
 814    tree.read_with(cx, |tree, _| {
 815        tree.as_local()
 816            .unwrap()
 817            .refresh_entries_for_paths(vec![Path::new("").into()])
 818    })
 819    .recv()
 820    .await;
 821
 822    cx.read(|cx| {
 823        let tree = tree.read(cx);
 824        assert_entry_git_state(tree, "a.xml", None, false);
 825        assert_entry_git_state(tree, "b.txt", None, true);
 826    });
 827
 828    fs.atomic_write("/root/.gitignore".into(), "*.xml".into())
 829        .await
 830        .unwrap();
 831
 832    fs.set_status_for_repo_via_working_copy_change(
 833        Path::new("/root/.git"),
 834        &[(Path::new("b.txt"), StatusCode::Added.index())],
 835    );
 836
 837    cx.executor().run_until_parked();
 838    cx.read(|cx| {
 839        let tree = tree.read(cx);
 840        assert_entry_git_state(tree, "a.xml", None, true);
 841        assert_entry_git_state(tree, "b.txt", Some(StatusCode::Added), false);
 842    });
 843}
 844
 845#[gpui::test]
 846async fn test_write_file(cx: &mut TestAppContext) {
 847    init_test(cx);
 848    cx.executor().allow_parking();
 849    let dir = TempTree::new(json!({
 850        ".git": {},
 851        ".gitignore": "ignored-dir\n",
 852        "tracked-dir": {},
 853        "ignored-dir": {}
 854    }));
 855
 856    let tree = Worktree::local(
 857        dir.path(),
 858        true,
 859        Arc::new(RealFs::default()),
 860        Default::default(),
 861        &mut cx.to_async(),
 862    )
 863    .await
 864    .unwrap();
 865
 866    #[cfg(not(target_os = "macos"))]
 867    fs::fs_watcher::global(|_| {}).unwrap();
 868
 869    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
 870        .await;
 871    tree.flush_fs_events(cx).await;
 872
 873    tree.update(cx, |tree, cx| {
 874        tree.write_file(
 875            Path::new("tracked-dir/file.txt"),
 876            "hello".into(),
 877            Default::default(),
 878            cx,
 879        )
 880    })
 881    .await
 882    .unwrap();
 883    tree.update(cx, |tree, cx| {
 884        tree.write_file(
 885            Path::new("ignored-dir/file.txt"),
 886            "world".into(),
 887            Default::default(),
 888            cx,
 889        )
 890    })
 891    .await
 892    .unwrap();
 893
 894    tree.read_with(cx, |tree, _| {
 895        let tracked = tree.entry_for_path("tracked-dir/file.txt").unwrap();
 896        let ignored = tree.entry_for_path("ignored-dir/file.txt").unwrap();
 897        assert!(!tracked.is_ignored);
 898        assert!(ignored.is_ignored);
 899    });
 900}
 901
 902#[gpui::test]
 903async fn test_file_scan_inclusions(cx: &mut TestAppContext) {
 904    init_test(cx);
 905    cx.executor().allow_parking();
 906    let dir = TempTree::new(json!({
 907        ".gitignore": "**/target\n/node_modules\ntop_level.txt\n",
 908        "target": {
 909            "index": "blah2"
 910        },
 911        "node_modules": {
 912            ".DS_Store": "",
 913            "prettier": {
 914                "package.json": "{}",
 915            },
 916        },
 917        "src": {
 918            ".DS_Store": "",
 919            "foo": {
 920                "foo.rs": "mod another;\n",
 921                "another.rs": "// another",
 922            },
 923            "bar": {
 924                "bar.rs": "// bar",
 925            },
 926            "lib.rs": "mod foo;\nmod bar;\n",
 927        },
 928        "top_level.txt": "top level file",
 929        ".DS_Store": "",
 930    }));
 931    cx.update(|cx| {
 932        cx.update_global::<SettingsStore, _>(|store, cx| {
 933            store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
 934                project_settings.file_scan_exclusions = Some(vec![]);
 935                project_settings.file_scan_inclusions = Some(vec![
 936                    "node_modules/**/package.json".to_string(),
 937                    "**/.DS_Store".to_string(),
 938                ]);
 939            });
 940        });
 941    });
 942
 943    let tree = Worktree::local(
 944        dir.path(),
 945        true,
 946        Arc::new(RealFs::default()),
 947        Default::default(),
 948        &mut cx.to_async(),
 949    )
 950    .await
 951    .unwrap();
 952    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
 953        .await;
 954    tree.flush_fs_events(cx).await;
 955    tree.read_with(cx, |tree, _| {
 956        // Assert that file_scan_inclusions overrides  file_scan_exclusions.
 957        check_worktree_entries(
 958            tree,
 959            &[],
 960            &["target", "node_modules"],
 961            &["src/lib.rs", "src/bar/bar.rs", ".gitignore"],
 962            &[
 963                "node_modules/prettier/package.json",
 964                ".DS_Store",
 965                "node_modules/.DS_Store",
 966                "src/.DS_Store",
 967            ],
 968        )
 969    });
 970}
 971
 972#[gpui::test]
 973async fn test_file_scan_exclusions_overrules_inclusions(cx: &mut TestAppContext) {
 974    init_test(cx);
 975    cx.executor().allow_parking();
 976    let dir = TempTree::new(json!({
 977        ".gitignore": "**/target\n/node_modules\n",
 978        "target": {
 979            "index": "blah2"
 980        },
 981        "node_modules": {
 982            ".DS_Store": "",
 983            "prettier": {
 984                "package.json": "{}",
 985            },
 986        },
 987        "src": {
 988            ".DS_Store": "",
 989            "foo": {
 990                "foo.rs": "mod another;\n",
 991                "another.rs": "// another",
 992            },
 993        },
 994        ".DS_Store": "",
 995    }));
 996
 997    cx.update(|cx| {
 998        cx.update_global::<SettingsStore, _>(|store, cx| {
 999            store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
1000                project_settings.file_scan_exclusions = Some(vec!["**/.DS_Store".to_string()]);
1001                project_settings.file_scan_inclusions = Some(vec!["**/.DS_Store".to_string()]);
1002            });
1003        });
1004    });
1005
1006    let tree = Worktree::local(
1007        dir.path(),
1008        true,
1009        Arc::new(RealFs::default()),
1010        Default::default(),
1011        &mut cx.to_async(),
1012    )
1013    .await
1014    .unwrap();
1015    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1016        .await;
1017    tree.flush_fs_events(cx).await;
1018    tree.read_with(cx, |tree, _| {
1019        // Assert that file_scan_inclusions overrides  file_scan_exclusions.
1020        check_worktree_entries(
1021            tree,
1022            &[".DS_Store, src/.DS_Store"],
1023            &["target", "node_modules"],
1024            &["src/foo/another.rs", "src/foo/foo.rs", ".gitignore"],
1025            &[],
1026        )
1027    });
1028}
1029
1030#[gpui::test]
1031async fn test_file_scan_inclusions_reindexes_on_setting_change(cx: &mut TestAppContext) {
1032    init_test(cx);
1033    cx.executor().allow_parking();
1034    let dir = TempTree::new(json!({
1035        ".gitignore": "**/target\n/node_modules/\n",
1036        "target": {
1037            "index": "blah2"
1038        },
1039        "node_modules": {
1040            ".DS_Store": "",
1041            "prettier": {
1042                "package.json": "{}",
1043            },
1044        },
1045        "src": {
1046            ".DS_Store": "",
1047            "foo": {
1048                "foo.rs": "mod another;\n",
1049                "another.rs": "// another",
1050            },
1051        },
1052        ".DS_Store": "",
1053    }));
1054
1055    cx.update(|cx| {
1056        cx.update_global::<SettingsStore, _>(|store, cx| {
1057            store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
1058                project_settings.file_scan_exclusions = Some(vec![]);
1059                project_settings.file_scan_inclusions = Some(vec!["node_modules/**".to_string()]);
1060            });
1061        });
1062    });
1063    let tree = Worktree::local(
1064        dir.path(),
1065        true,
1066        Arc::new(RealFs::default()),
1067        Default::default(),
1068        &mut cx.to_async(),
1069    )
1070    .await
1071    .unwrap();
1072    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1073        .await;
1074    tree.flush_fs_events(cx).await;
1075
1076    tree.read_with(cx, |tree, _| {
1077        assert!(tree
1078            .entry_for_path("node_modules")
1079            .is_some_and(|f| f.is_always_included));
1080        assert!(tree
1081            .entry_for_path("node_modules/prettier/package.json")
1082            .is_some_and(|f| f.is_always_included));
1083    });
1084
1085    cx.update(|cx| {
1086        cx.update_global::<SettingsStore, _>(|store, cx| {
1087            store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
1088                project_settings.file_scan_exclusions = Some(vec![]);
1089                project_settings.file_scan_inclusions = Some(vec![]);
1090            });
1091        });
1092    });
1093    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1094        .await;
1095    tree.flush_fs_events(cx).await;
1096
1097    tree.read_with(cx, |tree, _| {
1098        assert!(tree
1099            .entry_for_path("node_modules")
1100            .is_some_and(|f| !f.is_always_included));
1101        assert!(tree
1102            .entry_for_path("node_modules/prettier/package.json")
1103            .is_some_and(|f| !f.is_always_included));
1104    });
1105}
1106
1107#[gpui::test]
1108async fn test_file_scan_exclusions(cx: &mut TestAppContext) {
1109    init_test(cx);
1110    cx.executor().allow_parking();
1111    let dir = TempTree::new(json!({
1112        ".gitignore": "**/target\n/node_modules\n",
1113        "target": {
1114            "index": "blah2"
1115        },
1116        "node_modules": {
1117            ".DS_Store": "",
1118            "prettier": {
1119                "package.json": "{}",
1120            },
1121        },
1122        "src": {
1123            ".DS_Store": "",
1124            "foo": {
1125                "foo.rs": "mod another;\n",
1126                "another.rs": "// another",
1127            },
1128            "bar": {
1129                "bar.rs": "// bar",
1130            },
1131            "lib.rs": "mod foo;\nmod bar;\n",
1132        },
1133        ".DS_Store": "",
1134    }));
1135    cx.update(|cx| {
1136        cx.update_global::<SettingsStore, _>(|store, cx| {
1137            store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
1138                project_settings.file_scan_exclusions =
1139                    Some(vec!["**/foo/**".to_string(), "**/.DS_Store".to_string()]);
1140            });
1141        });
1142    });
1143
1144    let tree = Worktree::local(
1145        dir.path(),
1146        true,
1147        Arc::new(RealFs::default()),
1148        Default::default(),
1149        &mut cx.to_async(),
1150    )
1151    .await
1152    .unwrap();
1153    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1154        .await;
1155    tree.flush_fs_events(cx).await;
1156    tree.read_with(cx, |tree, _| {
1157        check_worktree_entries(
1158            tree,
1159            &[
1160                "src/foo/foo.rs",
1161                "src/foo/another.rs",
1162                "node_modules/.DS_Store",
1163                "src/.DS_Store",
1164                ".DS_Store",
1165            ],
1166            &["target", "node_modules"],
1167            &["src/lib.rs", "src/bar/bar.rs", ".gitignore"],
1168            &[],
1169        )
1170    });
1171
1172    cx.update(|cx| {
1173        cx.update_global::<SettingsStore, _>(|store, cx| {
1174            store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
1175                project_settings.file_scan_exclusions =
1176                    Some(vec!["**/node_modules/**".to_string()]);
1177            });
1178        });
1179    });
1180    tree.flush_fs_events(cx).await;
1181    cx.executor().run_until_parked();
1182    tree.read_with(cx, |tree, _| {
1183        check_worktree_entries(
1184            tree,
1185            &[
1186                "node_modules/prettier/package.json",
1187                "node_modules/.DS_Store",
1188                "node_modules",
1189            ],
1190            &["target"],
1191            &[
1192                ".gitignore",
1193                "src/lib.rs",
1194                "src/bar/bar.rs",
1195                "src/foo/foo.rs",
1196                "src/foo/another.rs",
1197                "src/.DS_Store",
1198                ".DS_Store",
1199            ],
1200            &[],
1201        )
1202    });
1203}
1204
1205#[gpui::test]
1206async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) {
1207    init_test(cx);
1208    cx.executor().allow_parking();
1209    let dir = TempTree::new(json!({
1210        ".git": {
1211            "HEAD": "ref: refs/heads/main\n",
1212            "foo": "bar",
1213        },
1214        ".gitignore": "**/target\n/node_modules\ntest_output\n",
1215        "target": {
1216            "index": "blah2"
1217        },
1218        "node_modules": {
1219            ".DS_Store": "",
1220            "prettier": {
1221                "package.json": "{}",
1222            },
1223        },
1224        "src": {
1225            ".DS_Store": "",
1226            "foo": {
1227                "foo.rs": "mod another;\n",
1228                "another.rs": "// another",
1229            },
1230            "bar": {
1231                "bar.rs": "// bar",
1232            },
1233            "lib.rs": "mod foo;\nmod bar;\n",
1234        },
1235        ".DS_Store": "",
1236    }));
1237    cx.update(|cx| {
1238        cx.update_global::<SettingsStore, _>(|store, cx| {
1239            store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
1240                project_settings.file_scan_exclusions = Some(vec![
1241                    "**/.git".to_string(),
1242                    "node_modules/".to_string(),
1243                    "build_output".to_string(),
1244                ]);
1245            });
1246        });
1247    });
1248
1249    let tree = Worktree::local(
1250        dir.path(),
1251        true,
1252        Arc::new(RealFs::default()),
1253        Default::default(),
1254        &mut cx.to_async(),
1255    )
1256    .await
1257    .unwrap();
1258    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1259        .await;
1260    tree.flush_fs_events(cx).await;
1261    tree.read_with(cx, |tree, _| {
1262        check_worktree_entries(
1263            tree,
1264            &[
1265                ".git/HEAD",
1266                ".git/foo",
1267                "node_modules",
1268                "node_modules/.DS_Store",
1269                "node_modules/prettier",
1270                "node_modules/prettier/package.json",
1271            ],
1272            &["target"],
1273            &[
1274                ".DS_Store",
1275                "src/.DS_Store",
1276                "src/lib.rs",
1277                "src/foo/foo.rs",
1278                "src/foo/another.rs",
1279                "src/bar/bar.rs",
1280                ".gitignore",
1281            ],
1282            &[],
1283        )
1284    });
1285
1286    let new_excluded_dir = dir.path().join("build_output");
1287    let new_ignored_dir = dir.path().join("test_output");
1288    std::fs::create_dir_all(&new_excluded_dir)
1289        .unwrap_or_else(|e| panic!("Failed to create a {new_excluded_dir:?} directory: {e}"));
1290    std::fs::create_dir_all(&new_ignored_dir)
1291        .unwrap_or_else(|e| panic!("Failed to create a {new_ignored_dir:?} directory: {e}"));
1292    let node_modules_dir = dir.path().join("node_modules");
1293    let dot_git_dir = dir.path().join(".git");
1294    let src_dir = dir.path().join("src");
1295    for existing_dir in [&node_modules_dir, &dot_git_dir, &src_dir] {
1296        assert!(
1297            existing_dir.is_dir(),
1298            "Expect {existing_dir:?} to be present in the FS already"
1299        );
1300    }
1301
1302    for directory_for_new_file in [
1303        new_excluded_dir,
1304        new_ignored_dir,
1305        node_modules_dir,
1306        dot_git_dir,
1307        src_dir,
1308    ] {
1309        std::fs::write(directory_for_new_file.join("new_file"), "new file contents")
1310            .unwrap_or_else(|e| {
1311                panic!("Failed to create in {directory_for_new_file:?} a new file: {e}")
1312            });
1313    }
1314    tree.flush_fs_events(cx).await;
1315
1316    tree.read_with(cx, |tree, _| {
1317        check_worktree_entries(
1318            tree,
1319            &[
1320                ".git/HEAD",
1321                ".git/foo",
1322                ".git/new_file",
1323                "node_modules",
1324                "node_modules/.DS_Store",
1325                "node_modules/prettier",
1326                "node_modules/prettier/package.json",
1327                "node_modules/new_file",
1328                "build_output",
1329                "build_output/new_file",
1330                "test_output/new_file",
1331            ],
1332            &["target", "test_output"],
1333            &[
1334                ".DS_Store",
1335                "src/.DS_Store",
1336                "src/lib.rs",
1337                "src/foo/foo.rs",
1338                "src/foo/another.rs",
1339                "src/bar/bar.rs",
1340                "src/new_file",
1341                ".gitignore",
1342            ],
1343            &[],
1344        )
1345    });
1346}
1347
1348#[gpui::test]
1349async fn test_fs_events_in_dot_git_worktree(cx: &mut TestAppContext) {
1350    init_test(cx);
1351    cx.executor().allow_parking();
1352    let dir = TempTree::new(json!({
1353        ".git": {
1354            "HEAD": "ref: refs/heads/main\n",
1355            "foo": "foo contents",
1356        },
1357    }));
1358    let dot_git_worktree_dir = dir.path().join(".git");
1359
1360    let tree = Worktree::local(
1361        dot_git_worktree_dir.clone(),
1362        true,
1363        Arc::new(RealFs::default()),
1364        Default::default(),
1365        &mut cx.to_async(),
1366    )
1367    .await
1368    .unwrap();
1369    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1370        .await;
1371    tree.flush_fs_events(cx).await;
1372    tree.read_with(cx, |tree, _| {
1373        check_worktree_entries(tree, &[], &["HEAD", "foo"], &[], &[])
1374    });
1375
1376    std::fs::write(dot_git_worktree_dir.join("new_file"), "new file contents")
1377        .unwrap_or_else(|e| panic!("Failed to create in {dot_git_worktree_dir:?} a new file: {e}"));
1378    tree.flush_fs_events(cx).await;
1379    tree.read_with(cx, |tree, _| {
1380        check_worktree_entries(tree, &[], &["HEAD", "foo", "new_file"], &[], &[])
1381    });
1382}
1383
1384#[gpui::test(iterations = 30)]
1385async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
1386    init_test(cx);
1387    let fs = FakeFs::new(cx.background_executor.clone());
1388    fs.insert_tree(
1389        "/root",
1390        json!({
1391            "b": {},
1392            "c": {},
1393            "d": {},
1394        }),
1395    )
1396    .await;
1397
1398    let tree = Worktree::local(
1399        "/root".as_ref(),
1400        true,
1401        fs,
1402        Default::default(),
1403        &mut cx.to_async(),
1404    )
1405    .await
1406    .unwrap();
1407
1408    let snapshot1 = tree.update(cx, |tree, cx| {
1409        let tree = tree.as_local_mut().unwrap();
1410        let snapshot = Arc::new(Mutex::new(tree.snapshot()));
1411        tree.observe_updates(0, cx, {
1412            let snapshot = snapshot.clone();
1413            let settings = tree.settings().clone();
1414            move |update| {
1415                snapshot
1416                    .lock()
1417                    .apply_remote_update(update, &settings.file_scan_inclusions)
1418                    .unwrap();
1419                async { true }
1420            }
1421        });
1422        snapshot
1423    });
1424
1425    let entry = tree
1426        .update(cx, |tree, cx| {
1427            tree.as_local_mut()
1428                .unwrap()
1429                .create_entry("a/e".as_ref(), true, cx)
1430        })
1431        .await
1432        .unwrap()
1433        .to_included()
1434        .unwrap();
1435    assert!(entry.is_dir());
1436
1437    cx.executor().run_until_parked();
1438    tree.read_with(cx, |tree, _| {
1439        assert_eq!(tree.entry_for_path("a/e").unwrap().kind, EntryKind::Dir);
1440    });
1441
1442    let snapshot2 = tree.update(cx, |tree, _| tree.as_local().unwrap().snapshot());
1443    assert_eq!(
1444        snapshot1.lock().entries(true, 0).collect::<Vec<_>>(),
1445        snapshot2.entries(true, 0).collect::<Vec<_>>()
1446    );
1447}
1448
1449#[gpui::test]
1450async fn test_bump_mtime_of_git_repo_workdir(cx: &mut TestAppContext) {
1451    init_test(cx);
1452
1453    // Create a worktree with a git directory.
1454    let fs = FakeFs::new(cx.background_executor.clone());
1455    fs.insert_tree(
1456        "/root",
1457        json!({
1458            ".git": {},
1459            "a.txt": "",
1460            "b":  {
1461                "c.txt": "",
1462            },
1463        }),
1464    )
1465    .await;
1466
1467    let tree = Worktree::local(
1468        "/root".as_ref(),
1469        true,
1470        fs.clone(),
1471        Default::default(),
1472        &mut cx.to_async(),
1473    )
1474    .await
1475    .unwrap();
1476    cx.executor().run_until_parked();
1477
1478    let (old_entry_ids, old_mtimes) = tree.read_with(cx, |tree, _| {
1479        (
1480            tree.entries(true, 0).map(|e| e.id).collect::<Vec<_>>(),
1481            tree.entries(true, 0).map(|e| e.mtime).collect::<Vec<_>>(),
1482        )
1483    });
1484
1485    // Regression test: after the directory is scanned, touch the git repo's
1486    // working directory, bumping its mtime. That directory keeps its project
1487    // entry id after the directories are re-scanned.
1488    fs.touch_path("/root").await;
1489    cx.executor().run_until_parked();
1490
1491    let (new_entry_ids, new_mtimes) = tree.read_with(cx, |tree, _| {
1492        (
1493            tree.entries(true, 0).map(|e| e.id).collect::<Vec<_>>(),
1494            tree.entries(true, 0).map(|e| e.mtime).collect::<Vec<_>>(),
1495        )
1496    });
1497    assert_eq!(new_entry_ids, old_entry_ids);
1498    assert_ne!(new_mtimes, old_mtimes);
1499
1500    // Regression test: changes to the git repository should still be
1501    // detected.
1502    fs.set_status_for_repo_via_git_operation(
1503        Path::new("/root/.git"),
1504        &[(Path::new("b/c.txt"), StatusCode::Modified.index())],
1505    );
1506    cx.executor().run_until_parked();
1507
1508    let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
1509
1510    check_git_statuses(
1511        &snapshot,
1512        &[
1513            (Path::new(""), MODIFIED),
1514            (Path::new("a.txt"), GitSummary::UNCHANGED),
1515            (Path::new("b/c.txt"), MODIFIED),
1516        ],
1517    );
1518}
1519
1520#[gpui::test]
1521async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
1522    init_test(cx);
1523    cx.executor().allow_parking();
1524
1525    let fs_fake = FakeFs::new(cx.background_executor.clone());
1526    fs_fake
1527        .insert_tree(
1528            "/root",
1529            json!({
1530                "a": {},
1531            }),
1532        )
1533        .await;
1534
1535    let tree_fake = Worktree::local(
1536        "/root".as_ref(),
1537        true,
1538        fs_fake,
1539        Default::default(),
1540        &mut cx.to_async(),
1541    )
1542    .await
1543    .unwrap();
1544
1545    let entry = tree_fake
1546        .update(cx, |tree, cx| {
1547            tree.as_local_mut()
1548                .unwrap()
1549                .create_entry("a/b/c/d.txt".as_ref(), false, cx)
1550        })
1551        .await
1552        .unwrap()
1553        .to_included()
1554        .unwrap();
1555    assert!(entry.is_file());
1556
1557    cx.executor().run_until_parked();
1558    tree_fake.read_with(cx, |tree, _| {
1559        assert!(tree.entry_for_path("a/b/c/d.txt").unwrap().is_file());
1560        assert!(tree.entry_for_path("a/b/c/").unwrap().is_dir());
1561        assert!(tree.entry_for_path("a/b/").unwrap().is_dir());
1562    });
1563
1564    let fs_real = Arc::new(RealFs::default());
1565    let temp_root = TempTree::new(json!({
1566        "a": {}
1567    }));
1568
1569    let tree_real = Worktree::local(
1570        temp_root.path(),
1571        true,
1572        fs_real,
1573        Default::default(),
1574        &mut cx.to_async(),
1575    )
1576    .await
1577    .unwrap();
1578
1579    let entry = tree_real
1580        .update(cx, |tree, cx| {
1581            tree.as_local_mut()
1582                .unwrap()
1583                .create_entry("a/b/c/d.txt".as_ref(), false, cx)
1584        })
1585        .await
1586        .unwrap()
1587        .to_included()
1588        .unwrap();
1589    assert!(entry.is_file());
1590
1591    cx.executor().run_until_parked();
1592    tree_real.read_with(cx, |tree, _| {
1593        assert!(tree.entry_for_path("a/b/c/d.txt").unwrap().is_file());
1594        assert!(tree.entry_for_path("a/b/c/").unwrap().is_dir());
1595        assert!(tree.entry_for_path("a/b/").unwrap().is_dir());
1596    });
1597
1598    // Test smallest change
1599    let entry = tree_real
1600        .update(cx, |tree, cx| {
1601            tree.as_local_mut()
1602                .unwrap()
1603                .create_entry("a/b/c/e.txt".as_ref(), false, cx)
1604        })
1605        .await
1606        .unwrap()
1607        .to_included()
1608        .unwrap();
1609    assert!(entry.is_file());
1610
1611    cx.executor().run_until_parked();
1612    tree_real.read_with(cx, |tree, _| {
1613        assert!(tree.entry_for_path("a/b/c/e.txt").unwrap().is_file());
1614    });
1615
1616    // Test largest change
1617    let entry = tree_real
1618        .update(cx, |tree, cx| {
1619            tree.as_local_mut()
1620                .unwrap()
1621                .create_entry("d/e/f/g.txt".as_ref(), false, cx)
1622        })
1623        .await
1624        .unwrap()
1625        .to_included()
1626        .unwrap();
1627    assert!(entry.is_file());
1628
1629    cx.executor().run_until_parked();
1630    tree_real.read_with(cx, |tree, _| {
1631        assert!(tree.entry_for_path("d/e/f/g.txt").unwrap().is_file());
1632        assert!(tree.entry_for_path("d/e/f").unwrap().is_dir());
1633        assert!(tree.entry_for_path("d/e/").unwrap().is_dir());
1634        assert!(tree.entry_for_path("d/").unwrap().is_dir());
1635    });
1636}
1637
1638#[gpui::test(iterations = 100)]
1639async fn test_random_worktree_operations_during_initial_scan(
1640    cx: &mut TestAppContext,
1641    mut rng: StdRng,
1642) {
1643    init_test(cx);
1644    let operations = env::var("OPERATIONS")
1645        .map(|o| o.parse().unwrap())
1646        .unwrap_or(5);
1647    let initial_entries = env::var("INITIAL_ENTRIES")
1648        .map(|o| o.parse().unwrap())
1649        .unwrap_or(20);
1650
1651    let root_dir = Path::new("/test");
1652    let fs = FakeFs::new(cx.background_executor.clone()) as Arc<dyn Fs>;
1653    fs.as_fake().insert_tree(root_dir, json!({})).await;
1654    for _ in 0..initial_entries {
1655        randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
1656    }
1657    log::info!("generated initial tree");
1658
1659    let worktree = Worktree::local(
1660        root_dir,
1661        true,
1662        fs.clone(),
1663        Default::default(),
1664        &mut cx.to_async(),
1665    )
1666    .await
1667    .unwrap();
1668
1669    let mut snapshots = vec![worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot())];
1670    let updates = Arc::new(Mutex::new(Vec::new()));
1671    worktree.update(cx, |tree, cx| {
1672        check_worktree_change_events(tree, cx);
1673
1674        tree.as_local_mut().unwrap().observe_updates(0, cx, {
1675            let updates = updates.clone();
1676            move |update| {
1677                updates.lock().push(update);
1678                async { true }
1679            }
1680        });
1681    });
1682
1683    for _ in 0..operations {
1684        worktree
1685            .update(cx, |worktree, cx| {
1686                randomly_mutate_worktree(worktree, &mut rng, cx)
1687            })
1688            .await
1689            .log_err();
1690        worktree.read_with(cx, |tree, _| {
1691            tree.as_local().unwrap().snapshot().check_invariants(true)
1692        });
1693
1694        if rng.gen_bool(0.6) {
1695            snapshots.push(worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot()));
1696        }
1697    }
1698
1699    worktree
1700        .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
1701        .await;
1702
1703    cx.executor().run_until_parked();
1704
1705    let final_snapshot = worktree.read_with(cx, |tree, _| {
1706        let tree = tree.as_local().unwrap();
1707        let snapshot = tree.snapshot();
1708        snapshot.check_invariants(true);
1709        snapshot
1710    });
1711
1712    let settings = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().settings());
1713
1714    for (i, snapshot) in snapshots.into_iter().enumerate().rev() {
1715        let mut updated_snapshot = snapshot.clone();
1716        for update in updates.lock().iter() {
1717            if update.scan_id >= updated_snapshot.scan_id() as u64 {
1718                updated_snapshot
1719                    .apply_remote_update(update.clone(), &settings.file_scan_inclusions)
1720                    .unwrap();
1721            }
1722        }
1723
1724        assert_eq!(
1725            updated_snapshot.entries(true, 0).collect::<Vec<_>>(),
1726            final_snapshot.entries(true, 0).collect::<Vec<_>>(),
1727            "wrong updates after snapshot {i}: {snapshot:#?} {updates:#?}",
1728        );
1729    }
1730}
1731
1732#[gpui::test(iterations = 100)]
1733async fn test_random_worktree_changes(cx: &mut TestAppContext, mut rng: StdRng) {
1734    init_test(cx);
1735    let operations = env::var("OPERATIONS")
1736        .map(|o| o.parse().unwrap())
1737        .unwrap_or(40);
1738    let initial_entries = env::var("INITIAL_ENTRIES")
1739        .map(|o| o.parse().unwrap())
1740        .unwrap_or(20);
1741
1742    let root_dir = Path::new("/test");
1743    let fs = FakeFs::new(cx.background_executor.clone()) as Arc<dyn Fs>;
1744    fs.as_fake().insert_tree(root_dir, json!({})).await;
1745    for _ in 0..initial_entries {
1746        randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
1747    }
1748    log::info!("generated initial tree");
1749
1750    let worktree = Worktree::local(
1751        root_dir,
1752        true,
1753        fs.clone(),
1754        Default::default(),
1755        &mut cx.to_async(),
1756    )
1757    .await
1758    .unwrap();
1759
1760    let updates = Arc::new(Mutex::new(Vec::new()));
1761    worktree.update(cx, |tree, cx| {
1762        check_worktree_change_events(tree, cx);
1763
1764        tree.as_local_mut().unwrap().observe_updates(0, cx, {
1765            let updates = updates.clone();
1766            move |update| {
1767                updates.lock().push(update);
1768                async { true }
1769            }
1770        });
1771    });
1772
1773    worktree
1774        .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
1775        .await;
1776
1777    fs.as_fake().pause_events();
1778    let mut snapshots = Vec::new();
1779    let mut mutations_len = operations;
1780    while mutations_len > 1 {
1781        if rng.gen_bool(0.2) {
1782            worktree
1783                .update(cx, |worktree, cx| {
1784                    randomly_mutate_worktree(worktree, &mut rng, cx)
1785                })
1786                .await
1787                .log_err();
1788        } else {
1789            randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
1790        }
1791
1792        let buffered_event_count = fs.as_fake().buffered_event_count();
1793        if buffered_event_count > 0 && rng.gen_bool(0.3) {
1794            let len = rng.gen_range(0..=buffered_event_count);
1795            log::info!("flushing {} events", len);
1796            fs.as_fake().flush_events(len);
1797        } else {
1798            randomly_mutate_fs(&fs, root_dir, 0.6, &mut rng).await;
1799            mutations_len -= 1;
1800        }
1801
1802        cx.executor().run_until_parked();
1803        if rng.gen_bool(0.2) {
1804            log::info!("storing snapshot {}", snapshots.len());
1805            let snapshot = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
1806            snapshots.push(snapshot);
1807        }
1808    }
1809
1810    log::info!("quiescing");
1811    fs.as_fake().flush_events(usize::MAX);
1812    cx.executor().run_until_parked();
1813
1814    let snapshot = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
1815    snapshot.check_invariants(true);
1816    let expanded_paths = snapshot
1817        .expanded_entries()
1818        .map(|e| e.path.clone())
1819        .collect::<Vec<_>>();
1820
1821    {
1822        let new_worktree = Worktree::local(
1823            root_dir,
1824            true,
1825            fs.clone(),
1826            Default::default(),
1827            &mut cx.to_async(),
1828        )
1829        .await
1830        .unwrap();
1831        new_worktree
1832            .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
1833            .await;
1834        new_worktree
1835            .update(cx, |tree, _| {
1836                tree.as_local_mut()
1837                    .unwrap()
1838                    .refresh_entries_for_paths(expanded_paths)
1839            })
1840            .recv()
1841            .await;
1842        let new_snapshot =
1843            new_worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
1844        assert_eq!(
1845            snapshot.entries_without_ids(true),
1846            new_snapshot.entries_without_ids(true)
1847        );
1848    }
1849
1850    let settings = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().settings());
1851
1852    for (i, mut prev_snapshot) in snapshots.into_iter().enumerate().rev() {
1853        for update in updates.lock().iter() {
1854            if update.scan_id >= prev_snapshot.scan_id() as u64 {
1855                prev_snapshot
1856                    .apply_remote_update(update.clone(), &settings.file_scan_inclusions)
1857                    .unwrap();
1858            }
1859        }
1860
1861        assert_eq!(
1862            prev_snapshot
1863                .entries(true, 0)
1864                .map(ignore_pending_dir)
1865                .collect::<Vec<_>>(),
1866            snapshot
1867                .entries(true, 0)
1868                .map(ignore_pending_dir)
1869                .collect::<Vec<_>>(),
1870            "wrong updates after snapshot {i}: {updates:#?}",
1871        );
1872    }
1873
1874    fn ignore_pending_dir(entry: &Entry) -> Entry {
1875        let mut entry = entry.clone();
1876        if entry.kind.is_dir() {
1877            entry.kind = EntryKind::Dir
1878        }
1879        entry
1880    }
1881}
1882
1883// The worktree's `UpdatedEntries` event can be used to follow along with
1884// all changes to the worktree's snapshot.
1885fn check_worktree_change_events(tree: &mut Worktree, cx: &mut Context<Worktree>) {
1886    let mut entries = tree.entries(true, 0).cloned().collect::<Vec<_>>();
1887    cx.subscribe(&cx.entity(), move |tree, _, event, _| {
1888        if let Event::UpdatedEntries(changes) = event {
1889            for (path, _, change_type) in changes.iter() {
1890                let entry = tree.entry_for_path(path).cloned();
1891                let ix = match entries.binary_search_by_key(&path, |e| &e.path) {
1892                    Ok(ix) | Err(ix) => ix,
1893                };
1894                match change_type {
1895                    PathChange::Added => entries.insert(ix, entry.unwrap()),
1896                    PathChange::Removed => drop(entries.remove(ix)),
1897                    PathChange::Updated => {
1898                        let entry = entry.unwrap();
1899                        let existing_entry = entries.get_mut(ix).unwrap();
1900                        assert_eq!(existing_entry.path, entry.path);
1901                        *existing_entry = entry;
1902                    }
1903                    PathChange::AddedOrUpdated | PathChange::Loaded => {
1904                        let entry = entry.unwrap();
1905                        if entries.get(ix).map(|e| &e.path) == Some(&entry.path) {
1906                            *entries.get_mut(ix).unwrap() = entry;
1907                        } else {
1908                            entries.insert(ix, entry);
1909                        }
1910                    }
1911                }
1912            }
1913
1914            let new_entries = tree.entries(true, 0).cloned().collect::<Vec<_>>();
1915            assert_eq!(entries, new_entries, "incorrect changes: {:?}", changes);
1916        }
1917    })
1918    .detach();
1919}
1920
1921fn randomly_mutate_worktree(
1922    worktree: &mut Worktree,
1923    rng: &mut impl Rng,
1924    cx: &mut Context<Worktree>,
1925) -> Task<Result<()>> {
1926    log::info!("mutating worktree");
1927    let worktree = worktree.as_local_mut().unwrap();
1928    let snapshot = worktree.snapshot();
1929    let entry = snapshot.entries(false, 0).choose(rng).unwrap();
1930
1931    match rng.gen_range(0_u32..100) {
1932        0..=33 if entry.path.as_ref() != Path::new("") => {
1933            log::info!("deleting entry {:?} ({})", entry.path, entry.id.0);
1934            worktree.delete_entry(entry.id, false, cx).unwrap()
1935        }
1936        ..=66 if entry.path.as_ref() != Path::new("") => {
1937            let other_entry = snapshot.entries(false, 0).choose(rng).unwrap();
1938            let new_parent_path = if other_entry.is_dir() {
1939                other_entry.path.clone()
1940            } else {
1941                other_entry.path.parent().unwrap().into()
1942            };
1943            let mut new_path = new_parent_path.join(random_filename(rng));
1944            if new_path.starts_with(&entry.path) {
1945                new_path = random_filename(rng).into();
1946            }
1947
1948            log::info!(
1949                "renaming entry {:?} ({}) to {:?}",
1950                entry.path,
1951                entry.id.0,
1952                new_path
1953            );
1954            let task = worktree.rename_entry(entry.id, new_path, cx);
1955            cx.background_executor().spawn(async move {
1956                task.await?.to_included().unwrap();
1957                Ok(())
1958            })
1959        }
1960        _ => {
1961            if entry.is_dir() {
1962                let child_path = entry.path.join(random_filename(rng));
1963                let is_dir = rng.gen_bool(0.3);
1964                log::info!(
1965                    "creating {} at {:?}",
1966                    if is_dir { "dir" } else { "file" },
1967                    child_path,
1968                );
1969                let task = worktree.create_entry(child_path, is_dir, cx);
1970                cx.background_executor().spawn(async move {
1971                    task.await?;
1972                    Ok(())
1973                })
1974            } else {
1975                log::info!("overwriting file {:?} ({})", entry.path, entry.id.0);
1976                let task =
1977                    worktree.write_file(entry.path.clone(), "".into(), Default::default(), cx);
1978                cx.background_executor().spawn(async move {
1979                    task.await?;
1980                    Ok(())
1981                })
1982            }
1983        }
1984    }
1985}
1986
1987async fn randomly_mutate_fs(
1988    fs: &Arc<dyn Fs>,
1989    root_path: &Path,
1990    insertion_probability: f64,
1991    rng: &mut impl Rng,
1992) {
1993    log::info!("mutating fs");
1994    let mut files = Vec::new();
1995    let mut dirs = Vec::new();
1996    for path in fs.as_fake().paths(false) {
1997        if path.starts_with(root_path) {
1998            if fs.is_file(&path).await {
1999                files.push(path);
2000            } else {
2001                dirs.push(path);
2002            }
2003        }
2004    }
2005
2006    if (files.is_empty() && dirs.len() == 1) || rng.gen_bool(insertion_probability) {
2007        let path = dirs.choose(rng).unwrap();
2008        let new_path = path.join(random_filename(rng));
2009
2010        if rng.gen() {
2011            log::info!(
2012                "creating dir {:?}",
2013                new_path.strip_prefix(root_path).unwrap()
2014            );
2015            fs.create_dir(&new_path).await.unwrap();
2016        } else {
2017            log::info!(
2018                "creating file {:?}",
2019                new_path.strip_prefix(root_path).unwrap()
2020            );
2021            fs.create_file(&new_path, Default::default()).await.unwrap();
2022        }
2023    } else if rng.gen_bool(0.05) {
2024        let ignore_dir_path = dirs.choose(rng).unwrap();
2025        let ignore_path = ignore_dir_path.join(*GITIGNORE);
2026
2027        let subdirs = dirs
2028            .iter()
2029            .filter(|d| d.starts_with(ignore_dir_path))
2030            .cloned()
2031            .collect::<Vec<_>>();
2032        let subfiles = files
2033            .iter()
2034            .filter(|d| d.starts_with(ignore_dir_path))
2035            .cloned()
2036            .collect::<Vec<_>>();
2037        let files_to_ignore = {
2038            let len = rng.gen_range(0..=subfiles.len());
2039            subfiles.choose_multiple(rng, len)
2040        };
2041        let dirs_to_ignore = {
2042            let len = rng.gen_range(0..subdirs.len());
2043            subdirs.choose_multiple(rng, len)
2044        };
2045
2046        let mut ignore_contents = String::new();
2047        for path_to_ignore in files_to_ignore.chain(dirs_to_ignore) {
2048            writeln!(
2049                ignore_contents,
2050                "{}",
2051                path_to_ignore
2052                    .strip_prefix(ignore_dir_path)
2053                    .unwrap()
2054                    .to_str()
2055                    .unwrap()
2056            )
2057            .unwrap();
2058        }
2059        log::info!(
2060            "creating gitignore {:?} with contents:\n{}",
2061            ignore_path.strip_prefix(root_path).unwrap(),
2062            ignore_contents
2063        );
2064        fs.save(
2065            &ignore_path,
2066            &ignore_contents.as_str().into(),
2067            Default::default(),
2068        )
2069        .await
2070        .unwrap();
2071    } else {
2072        let old_path = {
2073            let file_path = files.choose(rng);
2074            let dir_path = dirs[1..].choose(rng);
2075            file_path.into_iter().chain(dir_path).choose(rng).unwrap()
2076        };
2077
2078        let is_rename = rng.gen();
2079        if is_rename {
2080            let new_path_parent = dirs
2081                .iter()
2082                .filter(|d| !d.starts_with(old_path))
2083                .choose(rng)
2084                .unwrap();
2085
2086            let overwrite_existing_dir =
2087                !old_path.starts_with(new_path_parent) && rng.gen_bool(0.3);
2088            let new_path = if overwrite_existing_dir {
2089                fs.remove_dir(
2090                    new_path_parent,
2091                    RemoveOptions {
2092                        recursive: true,
2093                        ignore_if_not_exists: true,
2094                    },
2095                )
2096                .await
2097                .unwrap();
2098                new_path_parent.to_path_buf()
2099            } else {
2100                new_path_parent.join(random_filename(rng))
2101            };
2102
2103            log::info!(
2104                "renaming {:?} to {}{:?}",
2105                old_path.strip_prefix(root_path).unwrap(),
2106                if overwrite_existing_dir {
2107                    "overwrite "
2108                } else {
2109                    ""
2110                },
2111                new_path.strip_prefix(root_path).unwrap()
2112            );
2113            fs.rename(
2114                old_path,
2115                &new_path,
2116                fs::RenameOptions {
2117                    overwrite: true,
2118                    ignore_if_exists: true,
2119                },
2120            )
2121            .await
2122            .unwrap();
2123        } else if fs.is_file(old_path).await {
2124            log::info!(
2125                "deleting file {:?}",
2126                old_path.strip_prefix(root_path).unwrap()
2127            );
2128            fs.remove_file(old_path, Default::default()).await.unwrap();
2129        } else {
2130            log::info!(
2131                "deleting dir {:?}",
2132                old_path.strip_prefix(root_path).unwrap()
2133            );
2134            fs.remove_dir(
2135                old_path,
2136                RemoveOptions {
2137                    recursive: true,
2138                    ignore_if_not_exists: true,
2139                },
2140            )
2141            .await
2142            .unwrap();
2143        }
2144    }
2145}
2146
2147fn random_filename(rng: &mut impl Rng) -> String {
2148    (0..6)
2149        .map(|_| rng.sample(rand::distributions::Alphanumeric))
2150        .map(char::from)
2151        .collect()
2152}
2153
2154const CONFLICT: FileStatus = FileStatus::Unmerged(UnmergedStatus {
2155    first_head: UnmergedStatusCode::Updated,
2156    second_head: UnmergedStatusCode::Updated,
2157});
2158
2159// NOTE:
2160// This test always fails on Windows, because on Windows, unlike on Unix, you can't rename
2161// a directory which some program has already open.
2162// This is a limitation of the Windows.
2163// See: https://stackoverflow.com/questions/41365318/access-is-denied-when-renaming-folder
2164#[gpui::test]
2165#[cfg_attr(target_os = "windows", ignore)]
2166async fn test_rename_work_directory(cx: &mut TestAppContext) {
2167    init_test(cx);
2168    cx.executor().allow_parking();
2169    let root = TempTree::new(json!({
2170        "projects": {
2171            "project1": {
2172                "a": "",
2173                "b": "",
2174            }
2175        },
2176
2177    }));
2178    let root_path = root.path();
2179
2180    let tree = Worktree::local(
2181        root_path,
2182        true,
2183        Arc::new(RealFs::default()),
2184        Default::default(),
2185        &mut cx.to_async(),
2186    )
2187    .await
2188    .unwrap();
2189
2190    let repo = git_init(&root_path.join("projects/project1"));
2191    git_add("a", &repo);
2192    git_commit("init", &repo);
2193    std::fs::write(root_path.join("projects/project1/a"), "aa").unwrap();
2194
2195    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2196        .await;
2197
2198    tree.flush_fs_events(cx).await;
2199
2200    cx.read(|cx| {
2201        let tree = tree.read(cx);
2202        let repo = tree.repositories().iter().next().unwrap();
2203        assert_eq!(
2204            repo.work_directory,
2205            WorkDirectory::in_project("projects/project1")
2206        );
2207        assert_eq!(
2208            tree.status_for_file(Path::new("projects/project1/a")),
2209            Some(StatusCode::Modified.worktree()),
2210        );
2211        assert_eq!(
2212            tree.status_for_file(Path::new("projects/project1/b")),
2213            Some(FileStatus::Untracked),
2214        );
2215    });
2216
2217    std::fs::rename(
2218        root_path.join("projects/project1"),
2219        root_path.join("projects/project2"),
2220    )
2221    .unwrap();
2222    tree.flush_fs_events(cx).await;
2223
2224    cx.read(|cx| {
2225        let tree = tree.read(cx);
2226        let repo = tree.repositories().iter().next().unwrap();
2227        assert_eq!(
2228            repo.work_directory,
2229            WorkDirectory::in_project("projects/project2")
2230        );
2231        assert_eq!(
2232            tree.status_for_file(Path::new("projects/project2/a")),
2233            Some(StatusCode::Modified.worktree()),
2234        );
2235        assert_eq!(
2236            tree.status_for_file(Path::new("projects/project2/b")),
2237            Some(FileStatus::Untracked),
2238        );
2239    });
2240}
2241
2242#[gpui::test]
2243async fn test_git_repository_for_path(cx: &mut TestAppContext) {
2244    init_test(cx);
2245    cx.executor().allow_parking();
2246    let root = TempTree::new(json!({
2247        "c.txt": "",
2248        "dir1": {
2249            ".git": {},
2250            "deps": {
2251                "dep1": {
2252                    ".git": {},
2253                    "src": {
2254                        "a.txt": ""
2255                    }
2256                }
2257            },
2258            "src": {
2259                "b.txt": ""
2260            }
2261        },
2262    }));
2263
2264    let tree = Worktree::local(
2265        root.path(),
2266        true,
2267        Arc::new(RealFs::default()),
2268        Default::default(),
2269        &mut cx.to_async(),
2270    )
2271    .await
2272    .unwrap();
2273
2274    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2275        .await;
2276    tree.flush_fs_events(cx).await;
2277
2278    tree.read_with(cx, |tree, _cx| {
2279        let tree = tree.as_local().unwrap();
2280
2281        assert!(tree.repository_for_path("c.txt".as_ref()).is_none());
2282
2283        let repo = tree.repository_for_path("dir1/src/b.txt".as_ref()).unwrap();
2284        assert_eq!(repo.work_directory, WorkDirectory::in_project("dir1"));
2285
2286        let repo = tree
2287            .repository_for_path("dir1/deps/dep1/src/a.txt".as_ref())
2288            .unwrap();
2289        assert_eq!(
2290            repo.work_directory,
2291            WorkDirectory::in_project("dir1/deps/dep1")
2292        );
2293
2294        let entries = tree.files(false, 0);
2295
2296        let paths_with_repos = tree
2297            .entries_with_repositories(entries)
2298            .map(|(entry, repo)| {
2299                (
2300                    entry.path.as_ref(),
2301                    repo.map(|repo| repo.work_directory.clone()),
2302                )
2303            })
2304            .collect::<Vec<_>>();
2305
2306        assert_eq!(
2307            paths_with_repos,
2308            &[
2309                (Path::new("c.txt"), None),
2310                (
2311                    Path::new("dir1/deps/dep1/src/a.txt"),
2312                    Some(WorkDirectory::in_project("dir1/deps/dep1"))
2313                ),
2314                (
2315                    Path::new("dir1/src/b.txt"),
2316                    Some(WorkDirectory::in_project("dir1"))
2317                ),
2318            ]
2319        );
2320    });
2321
2322    let repo_update_events = Arc::new(Mutex::new(vec![]));
2323    tree.update(cx, |_, cx| {
2324        let repo_update_events = repo_update_events.clone();
2325        cx.subscribe(&tree, move |_, _, event, _| {
2326            if let Event::UpdatedGitRepositories(update) = event {
2327                repo_update_events.lock().push(update.clone());
2328            }
2329        })
2330        .detach();
2331    });
2332
2333    std::fs::write(root.path().join("dir1/.git/random_new_file"), "hello").unwrap();
2334    tree.flush_fs_events(cx).await;
2335
2336    assert_eq!(
2337        repo_update_events.lock()[0]
2338            .iter()
2339            .map(|e| e.0.clone())
2340            .collect::<Vec<Arc<Path>>>(),
2341        vec![Path::new("dir1").into()]
2342    );
2343
2344    std::fs::remove_dir_all(root.path().join("dir1/.git")).unwrap();
2345    tree.flush_fs_events(cx).await;
2346
2347    tree.read_with(cx, |tree, _cx| {
2348        let tree = tree.as_local().unwrap();
2349
2350        assert!(tree
2351            .repository_for_path("dir1/src/b.txt".as_ref())
2352            .is_none());
2353    });
2354}
2355
2356// NOTE:
2357// This test always fails on Windows, because on Windows, unlike on Unix, you can't rename
2358// a directory which some program has already open.
2359// This is a limitation of the Windows.
2360// See: https://stackoverflow.com/questions/41365318/access-is-denied-when-renaming-folder
2361#[gpui::test]
2362#[cfg_attr(target_os = "windows", ignore)]
2363async fn test_file_status(cx: &mut TestAppContext) {
2364    init_test(cx);
2365    cx.executor().allow_parking();
2366    const IGNORE_RULE: &str = "**/target";
2367
2368    let root = TempTree::new(json!({
2369        "project": {
2370            "a.txt": "a",
2371            "b.txt": "bb",
2372            "c": {
2373                "d": {
2374                    "e.txt": "eee"
2375                }
2376            },
2377            "f.txt": "ffff",
2378            "target": {
2379                "build_file": "???"
2380            },
2381            ".gitignore": IGNORE_RULE
2382        },
2383
2384    }));
2385
2386    const A_TXT: &str = "a.txt";
2387    const B_TXT: &str = "b.txt";
2388    const E_TXT: &str = "c/d/e.txt";
2389    const F_TXT: &str = "f.txt";
2390    const DOTGITIGNORE: &str = ".gitignore";
2391    const BUILD_FILE: &str = "target/build_file";
2392    let project_path = Path::new("project");
2393
2394    // Set up git repository before creating the worktree.
2395    let work_dir = root.path().join("project");
2396    let mut repo = git_init(work_dir.as_path());
2397    repo.add_ignore_rule(IGNORE_RULE).unwrap();
2398    git_add(A_TXT, &repo);
2399    git_add(E_TXT, &repo);
2400    git_add(DOTGITIGNORE, &repo);
2401    git_commit("Initial commit", &repo);
2402
2403    let tree = Worktree::local(
2404        root.path(),
2405        true,
2406        Arc::new(RealFs::default()),
2407        Default::default(),
2408        &mut cx.to_async(),
2409    )
2410    .await
2411    .unwrap();
2412
2413    tree.flush_fs_events(cx).await;
2414    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2415        .await;
2416    cx.executor().run_until_parked();
2417
2418    // Check that the right git state is observed on startup
2419    tree.read_with(cx, |tree, _cx| {
2420        let snapshot = tree.snapshot();
2421        assert_eq!(snapshot.repositories().iter().count(), 1);
2422        let repo_entry = snapshot.repositories().iter().next().unwrap();
2423        assert_eq!(
2424            repo_entry.work_directory,
2425            WorkDirectory::in_project("project")
2426        );
2427
2428        assert_eq!(
2429            snapshot.status_for_file(project_path.join(B_TXT)),
2430            Some(FileStatus::Untracked),
2431        );
2432        assert_eq!(
2433            snapshot.status_for_file(project_path.join(F_TXT)),
2434            Some(FileStatus::Untracked),
2435        );
2436    });
2437
2438    // Modify a file in the working copy.
2439    std::fs::write(work_dir.join(A_TXT), "aa").unwrap();
2440    tree.flush_fs_events(cx).await;
2441    cx.executor().run_until_parked();
2442
2443    // The worktree detects that the file's git status has changed.
2444    tree.read_with(cx, |tree, _cx| {
2445        let snapshot = tree.snapshot();
2446        assert_eq!(
2447            snapshot.status_for_file(project_path.join(A_TXT)),
2448            Some(StatusCode::Modified.worktree()),
2449        );
2450    });
2451
2452    // Create a commit in the git repository.
2453    git_add(A_TXT, &repo);
2454    git_add(B_TXT, &repo);
2455    git_commit("Committing modified and added", &repo);
2456    tree.flush_fs_events(cx).await;
2457    cx.executor().run_until_parked();
2458
2459    // The worktree detects that the files' git status have changed.
2460    tree.read_with(cx, |tree, _cx| {
2461        let snapshot = tree.snapshot();
2462        assert_eq!(
2463            snapshot.status_for_file(project_path.join(F_TXT)),
2464            Some(FileStatus::Untracked),
2465        );
2466        assert_eq!(snapshot.status_for_file(project_path.join(B_TXT)), None);
2467        assert_eq!(snapshot.status_for_file(project_path.join(A_TXT)), None);
2468    });
2469
2470    // Modify files in the working copy and perform git operations on other files.
2471    git_reset(0, &repo);
2472    git_remove_index(Path::new(B_TXT), &repo);
2473    git_stash(&mut repo);
2474    std::fs::write(work_dir.join(E_TXT), "eeee").unwrap();
2475    std::fs::write(work_dir.join(BUILD_FILE), "this should be ignored").unwrap();
2476    tree.flush_fs_events(cx).await;
2477    cx.executor().run_until_parked();
2478
2479    // Check that more complex repo changes are tracked
2480    tree.read_with(cx, |tree, _cx| {
2481        let snapshot = tree.snapshot();
2482
2483        assert_eq!(snapshot.status_for_file(project_path.join(A_TXT)), None);
2484        assert_eq!(
2485            snapshot.status_for_file(project_path.join(B_TXT)),
2486            Some(FileStatus::Untracked),
2487        );
2488        assert_eq!(
2489            snapshot.status_for_file(project_path.join(E_TXT)),
2490            Some(StatusCode::Modified.worktree()),
2491        );
2492    });
2493
2494    std::fs::remove_file(work_dir.join(B_TXT)).unwrap();
2495    std::fs::remove_dir_all(work_dir.join("c")).unwrap();
2496    std::fs::write(
2497        work_dir.join(DOTGITIGNORE),
2498        [IGNORE_RULE, "f.txt"].join("\n"),
2499    )
2500    .unwrap();
2501
2502    git_add(Path::new(DOTGITIGNORE), &repo);
2503    git_commit("Committing modified git ignore", &repo);
2504
2505    tree.flush_fs_events(cx).await;
2506    cx.executor().run_until_parked();
2507
2508    let mut renamed_dir_name = "first_directory/second_directory";
2509    const RENAMED_FILE: &str = "rf.txt";
2510
2511    std::fs::create_dir_all(work_dir.join(renamed_dir_name)).unwrap();
2512    std::fs::write(
2513        work_dir.join(renamed_dir_name).join(RENAMED_FILE),
2514        "new-contents",
2515    )
2516    .unwrap();
2517
2518    tree.flush_fs_events(cx).await;
2519    cx.executor().run_until_parked();
2520
2521    tree.read_with(cx, |tree, _cx| {
2522        let snapshot = tree.snapshot();
2523        assert_eq!(
2524            snapshot.status_for_file(project_path.join(renamed_dir_name).join(RENAMED_FILE)),
2525            Some(FileStatus::Untracked),
2526        );
2527    });
2528
2529    renamed_dir_name = "new_first_directory/second_directory";
2530
2531    std::fs::rename(
2532        work_dir.join("first_directory"),
2533        work_dir.join("new_first_directory"),
2534    )
2535    .unwrap();
2536
2537    tree.flush_fs_events(cx).await;
2538    cx.executor().run_until_parked();
2539
2540    tree.read_with(cx, |tree, _cx| {
2541        let snapshot = tree.snapshot();
2542
2543        assert_eq!(
2544            snapshot.status_for_file(
2545                project_path
2546                    .join(Path::new(renamed_dir_name))
2547                    .join(RENAMED_FILE)
2548            ),
2549            Some(FileStatus::Untracked),
2550        );
2551    });
2552}
2553
2554#[gpui::test]
2555async fn test_git_repository_status(cx: &mut TestAppContext) {
2556    init_test(cx);
2557    cx.executor().allow_parking();
2558
2559    let root = TempTree::new(json!({
2560        "project": {
2561            "a.txt": "a",    // Modified
2562            "b.txt": "bb",   // Added
2563            "c.txt": "ccc",  // Unchanged
2564            "d.txt": "dddd", // Deleted
2565        },
2566
2567    }));
2568
2569    // Set up git repository before creating the worktree.
2570    let work_dir = root.path().join("project");
2571    let repo = git_init(work_dir.as_path());
2572    git_add("a.txt", &repo);
2573    git_add("c.txt", &repo);
2574    git_add("d.txt", &repo);
2575    git_commit("Initial commit", &repo);
2576    std::fs::remove_file(work_dir.join("d.txt")).unwrap();
2577    std::fs::write(work_dir.join("a.txt"), "aa").unwrap();
2578
2579    let tree = Worktree::local(
2580        root.path(),
2581        true,
2582        Arc::new(RealFs::default()),
2583        Default::default(),
2584        &mut cx.to_async(),
2585    )
2586    .await
2587    .unwrap();
2588
2589    tree.flush_fs_events(cx).await;
2590    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2591        .await;
2592    cx.executor().run_until_parked();
2593
2594    // Check that the right git state is observed on startup
2595    tree.read_with(cx, |tree, _cx| {
2596        let snapshot = tree.snapshot();
2597        let repo = snapshot.repositories().iter().next().unwrap();
2598        let entries = repo.status().collect::<Vec<_>>();
2599
2600        assert_eq!(entries.len(), 3);
2601        assert_eq!(entries[0].repo_path.as_ref(), Path::new("a.txt"));
2602        assert_eq!(entries[0].status, StatusCode::Modified.worktree());
2603        assert_eq!(entries[1].repo_path.as_ref(), Path::new("b.txt"));
2604        assert_eq!(entries[1].status, FileStatus::Untracked);
2605        assert_eq!(entries[2].repo_path.as_ref(), Path::new("d.txt"));
2606        assert_eq!(entries[2].status, StatusCode::Deleted.worktree());
2607    });
2608
2609    std::fs::write(work_dir.join("c.txt"), "some changes").unwrap();
2610    eprintln!("File c.txt has been modified");
2611
2612    tree.flush_fs_events(cx).await;
2613    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2614        .await;
2615    cx.executor().run_until_parked();
2616
2617    tree.read_with(cx, |tree, _cx| {
2618        let snapshot = tree.snapshot();
2619        let repository = snapshot.repositories().iter().next().unwrap();
2620        let entries = repository.status().collect::<Vec<_>>();
2621
2622        std::assert_eq!(entries.len(), 4, "entries: {entries:?}");
2623        assert_eq!(entries[0].repo_path.as_ref(), Path::new("a.txt"));
2624        assert_eq!(entries[0].status, StatusCode::Modified.worktree());
2625        assert_eq!(entries[1].repo_path.as_ref(), Path::new("b.txt"));
2626        assert_eq!(entries[1].status, FileStatus::Untracked);
2627        // Status updated
2628        assert_eq!(entries[2].repo_path.as_ref(), Path::new("c.txt"));
2629        assert_eq!(entries[2].status, StatusCode::Modified.worktree());
2630        assert_eq!(entries[3].repo_path.as_ref(), Path::new("d.txt"));
2631        assert_eq!(entries[3].status, StatusCode::Deleted.worktree());
2632    });
2633
2634    git_add("a.txt", &repo);
2635    git_add("c.txt", &repo);
2636    git_remove_index(Path::new("d.txt"), &repo);
2637    git_commit("Another commit", &repo);
2638    tree.flush_fs_events(cx).await;
2639    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2640        .await;
2641    cx.executor().run_until_parked();
2642
2643    std::fs::remove_file(work_dir.join("a.txt")).unwrap();
2644    std::fs::remove_file(work_dir.join("b.txt")).unwrap();
2645    tree.flush_fs_events(cx).await;
2646    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2647        .await;
2648    cx.executor().run_until_parked();
2649
2650    tree.read_with(cx, |tree, _cx| {
2651        let snapshot = tree.snapshot();
2652        let repo = snapshot.repositories().iter().next().unwrap();
2653        let entries = repo.status().collect::<Vec<_>>();
2654
2655        // Deleting an untracked entry, b.txt, should leave no status
2656        // a.txt was tracked, and so should have a status
2657        assert_eq!(
2658            entries.len(),
2659            1,
2660            "Entries length was incorrect\n{:#?}",
2661            &entries
2662        );
2663        assert_eq!(entries[0].repo_path.as_ref(), Path::new("a.txt"));
2664        assert_eq!(entries[0].status, StatusCode::Deleted.worktree());
2665    });
2666}
2667
2668#[gpui::test]
2669async fn test_git_status_postprocessing(cx: &mut TestAppContext) {
2670    init_test(cx);
2671    cx.executor().allow_parking();
2672
2673    let root = TempTree::new(json!({
2674        "project": {
2675            "sub": {},
2676            "a.txt": "",
2677        },
2678    }));
2679
2680    let work_dir = root.path().join("project");
2681    let repo = git_init(work_dir.as_path());
2682    // a.txt exists in HEAD and the working copy but is deleted in the index.
2683    git_add("a.txt", &repo);
2684    git_commit("Initial commit", &repo);
2685    git_remove_index("a.txt".as_ref(), &repo);
2686    // `sub` is a nested git repository.
2687    let _sub = git_init(&work_dir.join("sub"));
2688
2689    let tree = Worktree::local(
2690        root.path(),
2691        true,
2692        Arc::new(RealFs::default()),
2693        Default::default(),
2694        &mut cx.to_async(),
2695    )
2696    .await
2697    .unwrap();
2698
2699    tree.flush_fs_events(cx).await;
2700    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2701        .await;
2702    cx.executor().run_until_parked();
2703
2704    tree.read_with(cx, |tree, _cx| {
2705        let snapshot = tree.snapshot();
2706        let repo = snapshot.repositories().iter().next().unwrap();
2707        let entries = repo.status().collect::<Vec<_>>();
2708
2709        // `sub` doesn't appear in our computed statuses.
2710        assert_eq!(entries.len(), 1);
2711        assert_eq!(entries[0].repo_path.as_ref(), Path::new("a.txt"));
2712        // a.txt appears with a combined `DA` status.
2713        assert_eq!(
2714            entries[0].status,
2715            TrackedStatus {
2716                index_status: StatusCode::Deleted,
2717                worktree_status: StatusCode::Added
2718            }
2719            .into()
2720        );
2721    });
2722}
2723
2724#[gpui::test]
2725async fn test_repository_subfolder_git_status(cx: &mut TestAppContext) {
2726    init_test(cx);
2727    cx.executor().allow_parking();
2728
2729    let root = TempTree::new(json!({
2730        "my-repo": {
2731            // .git folder will go here
2732            "a.txt": "a",
2733            "sub-folder-1": {
2734                "sub-folder-2": {
2735                    "c.txt": "cc",
2736                    "d": {
2737                        "e.txt": "eee"
2738                    }
2739                },
2740            }
2741        },
2742
2743    }));
2744
2745    const C_TXT: &str = "sub-folder-1/sub-folder-2/c.txt";
2746    const E_TXT: &str = "sub-folder-1/sub-folder-2/d/e.txt";
2747
2748    // Set up git repository before creating the worktree.
2749    let git_repo_work_dir = root.path().join("my-repo");
2750    let repo = git_init(git_repo_work_dir.as_path());
2751    git_add(C_TXT, &repo);
2752    git_commit("Initial commit", &repo);
2753
2754    // Open the worktree in subfolder
2755    let project_root = Path::new("my-repo/sub-folder-1/sub-folder-2");
2756    let tree = Worktree::local(
2757        root.path().join(project_root),
2758        true,
2759        Arc::new(RealFs::default()),
2760        Default::default(),
2761        &mut cx.to_async(),
2762    )
2763    .await
2764    .unwrap();
2765
2766    tree.flush_fs_events(cx).await;
2767    tree.flush_fs_events_in_root_git_repository(cx).await;
2768    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2769        .await;
2770    cx.executor().run_until_parked();
2771
2772    // Ensure that the git status is loaded correctly
2773    tree.read_with(cx, |tree, _cx| {
2774        let snapshot = tree.snapshot();
2775        assert_eq!(snapshot.repositories().iter().count(), 1);
2776        let repo = snapshot.repositories().iter().next().unwrap();
2777        assert_eq!(
2778            repo.work_directory.canonicalize(),
2779            WorkDirectory::AboveProject {
2780                absolute_path: Arc::from(root.path().join("my-repo").canonicalize().unwrap()),
2781                location_in_repo: Arc::from(Path::new(util::separator!(
2782                    "sub-folder-1/sub-folder-2"
2783                )))
2784            }
2785        );
2786
2787        assert_eq!(snapshot.status_for_file("c.txt"), None);
2788        assert_eq!(
2789            snapshot.status_for_file("d/e.txt"),
2790            Some(FileStatus::Untracked)
2791        );
2792    });
2793
2794    // Now we simulate FS events, but ONLY in the .git folder that's outside
2795    // of out project root.
2796    // Meaning: we don't produce any FS events for files inside the project.
2797    git_add(E_TXT, &repo);
2798    git_commit("Second commit", &repo);
2799    tree.flush_fs_events_in_root_git_repository(cx).await;
2800    cx.executor().run_until_parked();
2801
2802    tree.read_with(cx, |tree, _cx| {
2803        let snapshot = tree.snapshot();
2804
2805        assert!(snapshot.repositories().iter().next().is_some());
2806
2807        assert_eq!(snapshot.status_for_file("c.txt"), None);
2808        assert_eq!(snapshot.status_for_file("d/e.txt"), None);
2809    });
2810}
2811
2812#[gpui::test]
2813async fn test_traverse_with_git_status(cx: &mut TestAppContext) {
2814    init_test(cx);
2815    let fs = FakeFs::new(cx.background_executor.clone());
2816    fs.insert_tree(
2817        "/root",
2818        json!({
2819            "x": {
2820                ".git": {},
2821                "x1.txt": "foo",
2822                "x2.txt": "bar",
2823                "y": {
2824                    ".git": {},
2825                    "y1.txt": "baz",
2826                    "y2.txt": "qux"
2827                },
2828                "z.txt": "sneaky..."
2829            },
2830            "z": {
2831                ".git": {},
2832                "z1.txt": "quux",
2833                "z2.txt": "quuux"
2834            }
2835        }),
2836    )
2837    .await;
2838
2839    fs.set_status_for_repo_via_git_operation(
2840        Path::new("/root/x/.git"),
2841        &[
2842            (Path::new("x2.txt"), StatusCode::Modified.index()),
2843            (Path::new("z.txt"), StatusCode::Added.index()),
2844        ],
2845    );
2846    fs.set_status_for_repo_via_git_operation(
2847        Path::new("/root/x/y/.git"),
2848        &[(Path::new("y1.txt"), CONFLICT)],
2849    );
2850    fs.set_status_for_repo_via_git_operation(
2851        Path::new("/root/z/.git"),
2852        &[(Path::new("z2.txt"), StatusCode::Added.index())],
2853    );
2854
2855    let tree = Worktree::local(
2856        Path::new("/root"),
2857        true,
2858        fs.clone(),
2859        Default::default(),
2860        &mut cx.to_async(),
2861    )
2862    .await
2863    .unwrap();
2864
2865    tree.flush_fs_events(cx).await;
2866    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2867        .await;
2868    cx.executor().run_until_parked();
2869
2870    let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
2871
2872    let mut traversal = snapshot
2873        .traverse_from_path(true, false, true, Path::new("x"))
2874        .with_git_statuses();
2875
2876    let entry = traversal.next().unwrap();
2877    assert_eq!(entry.path.as_ref(), Path::new("x/x1.txt"));
2878    assert_eq!(entry.git_summary, GitSummary::UNCHANGED);
2879    let entry = traversal.next().unwrap();
2880    assert_eq!(entry.path.as_ref(), Path::new("x/x2.txt"));
2881    assert_eq!(entry.git_summary, MODIFIED);
2882    let entry = traversal.next().unwrap();
2883    assert_eq!(entry.path.as_ref(), Path::new("x/y/y1.txt"));
2884    assert_eq!(entry.git_summary, GitSummary::CONFLICT);
2885    let entry = traversal.next().unwrap();
2886    assert_eq!(entry.path.as_ref(), Path::new("x/y/y2.txt"));
2887    assert_eq!(entry.git_summary, GitSummary::UNCHANGED);
2888    let entry = traversal.next().unwrap();
2889    assert_eq!(entry.path.as_ref(), Path::new("x/z.txt"));
2890    assert_eq!(entry.git_summary, ADDED);
2891    let entry = traversal.next().unwrap();
2892    assert_eq!(entry.path.as_ref(), Path::new("z/z1.txt"));
2893    assert_eq!(entry.git_summary, GitSummary::UNCHANGED);
2894    let entry = traversal.next().unwrap();
2895    assert_eq!(entry.path.as_ref(), Path::new("z/z2.txt"));
2896    assert_eq!(entry.git_summary, ADDED);
2897}
2898
2899#[gpui::test]
2900async fn test_propagate_git_statuses(cx: &mut TestAppContext) {
2901    init_test(cx);
2902    let fs = FakeFs::new(cx.background_executor.clone());
2903    fs.insert_tree(
2904        "/root",
2905        json!({
2906            ".git": {},
2907            "a": {
2908                "b": {
2909                    "c1.txt": "",
2910                    "c2.txt": "",
2911                },
2912                "d": {
2913                    "e1.txt": "",
2914                    "e2.txt": "",
2915                    "e3.txt": "",
2916                }
2917            },
2918            "f": {
2919                "no-status.txt": ""
2920            },
2921            "g": {
2922                "h1.txt": "",
2923                "h2.txt": ""
2924            },
2925        }),
2926    )
2927    .await;
2928
2929    fs.set_status_for_repo_via_git_operation(
2930        Path::new("/root/.git"),
2931        &[
2932            (Path::new("a/b/c1.txt"), StatusCode::Added.index()),
2933            (Path::new("a/d/e2.txt"), StatusCode::Modified.index()),
2934            (Path::new("g/h2.txt"), CONFLICT),
2935        ],
2936    );
2937
2938    let tree = Worktree::local(
2939        Path::new("/root"),
2940        true,
2941        fs.clone(),
2942        Default::default(),
2943        &mut cx.to_async(),
2944    )
2945    .await
2946    .unwrap();
2947
2948    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2949        .await;
2950
2951    cx.executor().run_until_parked();
2952    let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
2953
2954    check_git_statuses(
2955        &snapshot,
2956        &[
2957            (Path::new(""), GitSummary::CONFLICT + MODIFIED + ADDED),
2958            (Path::new("g"), GitSummary::CONFLICT),
2959            (Path::new("g/h2.txt"), GitSummary::CONFLICT),
2960        ],
2961    );
2962
2963    check_git_statuses(
2964        &snapshot,
2965        &[
2966            (Path::new(""), GitSummary::CONFLICT + ADDED + MODIFIED),
2967            (Path::new("a"), ADDED + MODIFIED),
2968            (Path::new("a/b"), ADDED),
2969            (Path::new("a/b/c1.txt"), ADDED),
2970            (Path::new("a/b/c2.txt"), GitSummary::UNCHANGED),
2971            (Path::new("a/d"), MODIFIED),
2972            (Path::new("a/d/e2.txt"), MODIFIED),
2973            (Path::new("f"), GitSummary::UNCHANGED),
2974            (Path::new("f/no-status.txt"), GitSummary::UNCHANGED),
2975            (Path::new("g"), GitSummary::CONFLICT),
2976            (Path::new("g/h2.txt"), GitSummary::CONFLICT),
2977        ],
2978    );
2979
2980    check_git_statuses(
2981        &snapshot,
2982        &[
2983            (Path::new("a/b"), ADDED),
2984            (Path::new("a/b/c1.txt"), ADDED),
2985            (Path::new("a/b/c2.txt"), GitSummary::UNCHANGED),
2986            (Path::new("a/d"), MODIFIED),
2987            (Path::new("a/d/e1.txt"), GitSummary::UNCHANGED),
2988            (Path::new("a/d/e2.txt"), MODIFIED),
2989            (Path::new("f"), GitSummary::UNCHANGED),
2990            (Path::new("f/no-status.txt"), GitSummary::UNCHANGED),
2991            (Path::new("g"), GitSummary::CONFLICT),
2992        ],
2993    );
2994
2995    check_git_statuses(
2996        &snapshot,
2997        &[
2998            (Path::new("a/b/c1.txt"), ADDED),
2999            (Path::new("a/b/c2.txt"), GitSummary::UNCHANGED),
3000            (Path::new("a/d/e1.txt"), GitSummary::UNCHANGED),
3001            (Path::new("a/d/e2.txt"), MODIFIED),
3002            (Path::new("f/no-status.txt"), GitSummary::UNCHANGED),
3003        ],
3004    );
3005}
3006
3007#[gpui::test]
3008async fn test_propagate_statuses_for_repos_under_project(cx: &mut TestAppContext) {
3009    init_test(cx);
3010    let fs = FakeFs::new(cx.background_executor.clone());
3011    fs.insert_tree(
3012        "/root",
3013        json!({
3014            "x": {
3015                ".git": {},
3016                "x1.txt": "foo",
3017                "x2.txt": "bar"
3018            },
3019            "y": {
3020                ".git": {},
3021                "y1.txt": "baz",
3022                "y2.txt": "qux"
3023            },
3024            "z": {
3025                ".git": {},
3026                "z1.txt": "quux",
3027                "z2.txt": "quuux"
3028            }
3029        }),
3030    )
3031    .await;
3032
3033    fs.set_status_for_repo_via_git_operation(
3034        Path::new("/root/x/.git"),
3035        &[(Path::new("x1.txt"), StatusCode::Added.index())],
3036    );
3037    fs.set_status_for_repo_via_git_operation(
3038        Path::new("/root/y/.git"),
3039        &[
3040            (Path::new("y1.txt"), CONFLICT),
3041            (Path::new("y2.txt"), StatusCode::Modified.index()),
3042        ],
3043    );
3044    fs.set_status_for_repo_via_git_operation(
3045        Path::new("/root/z/.git"),
3046        &[(Path::new("z2.txt"), StatusCode::Modified.index())],
3047    );
3048
3049    let tree = Worktree::local(
3050        Path::new("/root"),
3051        true,
3052        fs.clone(),
3053        Default::default(),
3054        &mut cx.to_async(),
3055    )
3056    .await
3057    .unwrap();
3058
3059    tree.flush_fs_events(cx).await;
3060    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
3061        .await;
3062    cx.executor().run_until_parked();
3063
3064    let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
3065
3066    check_git_statuses(
3067        &snapshot,
3068        &[(Path::new("x"), ADDED), (Path::new("x/x1.txt"), ADDED)],
3069    );
3070
3071    check_git_statuses(
3072        &snapshot,
3073        &[
3074            (Path::new("y"), GitSummary::CONFLICT + MODIFIED),
3075            (Path::new("y/y1.txt"), GitSummary::CONFLICT),
3076            (Path::new("y/y2.txt"), MODIFIED),
3077        ],
3078    );
3079
3080    check_git_statuses(
3081        &snapshot,
3082        &[
3083            (Path::new("z"), MODIFIED),
3084            (Path::new("z/z2.txt"), MODIFIED),
3085        ],
3086    );
3087
3088    check_git_statuses(
3089        &snapshot,
3090        &[(Path::new("x"), ADDED), (Path::new("x/x1.txt"), ADDED)],
3091    );
3092
3093    check_git_statuses(
3094        &snapshot,
3095        &[
3096            (Path::new("x"), ADDED),
3097            (Path::new("x/x1.txt"), ADDED),
3098            (Path::new("x/x2.txt"), GitSummary::UNCHANGED),
3099            (Path::new("y"), GitSummary::CONFLICT + MODIFIED),
3100            (Path::new("y/y1.txt"), GitSummary::CONFLICT),
3101            (Path::new("y/y2.txt"), MODIFIED),
3102            (Path::new("z"), MODIFIED),
3103            (Path::new("z/z1.txt"), GitSummary::UNCHANGED),
3104            (Path::new("z/z2.txt"), MODIFIED),
3105        ],
3106    );
3107}
3108
3109#[gpui::test]
3110async fn test_propagate_statuses_for_nested_repos(cx: &mut TestAppContext) {
3111    init_test(cx);
3112    let fs = FakeFs::new(cx.background_executor.clone());
3113    fs.insert_tree(
3114        "/root",
3115        json!({
3116            "x": {
3117                ".git": {},
3118                "x1.txt": "foo",
3119                "x2.txt": "bar",
3120                "y": {
3121                    ".git": {},
3122                    "y1.txt": "baz",
3123                    "y2.txt": "qux"
3124                },
3125                "z.txt": "sneaky..."
3126            },
3127            "z": {
3128                ".git": {},
3129                "z1.txt": "quux",
3130                "z2.txt": "quuux"
3131            }
3132        }),
3133    )
3134    .await;
3135
3136    fs.set_status_for_repo_via_git_operation(
3137        Path::new("/root/x/.git"),
3138        &[
3139            (Path::new("x2.txt"), StatusCode::Modified.index()),
3140            (Path::new("z.txt"), StatusCode::Added.index()),
3141        ],
3142    );
3143    fs.set_status_for_repo_via_git_operation(
3144        Path::new("/root/x/y/.git"),
3145        &[(Path::new("y1.txt"), CONFLICT)],
3146    );
3147
3148    fs.set_status_for_repo_via_git_operation(
3149        Path::new("/root/z/.git"),
3150        &[(Path::new("z2.txt"), StatusCode::Added.index())],
3151    );
3152
3153    let tree = Worktree::local(
3154        Path::new("/root"),
3155        true,
3156        fs.clone(),
3157        Default::default(),
3158        &mut cx.to_async(),
3159    )
3160    .await
3161    .unwrap();
3162
3163    tree.flush_fs_events(cx).await;
3164    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
3165        .await;
3166    cx.executor().run_until_parked();
3167
3168    let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
3169
3170    // Sanity check the propagation for x/y and z
3171    check_git_statuses(
3172        &snapshot,
3173        &[
3174            (Path::new("x/y"), GitSummary::CONFLICT),
3175            (Path::new("x/y/y1.txt"), GitSummary::CONFLICT),
3176            (Path::new("x/y/y2.txt"), GitSummary::UNCHANGED),
3177        ],
3178    );
3179    check_git_statuses(
3180        &snapshot,
3181        &[
3182            (Path::new("z"), ADDED),
3183            (Path::new("z/z1.txt"), GitSummary::UNCHANGED),
3184            (Path::new("z/z2.txt"), ADDED),
3185        ],
3186    );
3187
3188    // Test one of the fundamental cases of propagation blocking, the transition from one git repository to another
3189    check_git_statuses(
3190        &snapshot,
3191        &[
3192            (Path::new("x"), MODIFIED + ADDED),
3193            (Path::new("x/y"), GitSummary::CONFLICT),
3194            (Path::new("x/y/y1.txt"), GitSummary::CONFLICT),
3195        ],
3196    );
3197
3198    // Sanity check everything around it
3199    check_git_statuses(
3200        &snapshot,
3201        &[
3202            (Path::new("x"), MODIFIED + ADDED),
3203            (Path::new("x/x1.txt"), GitSummary::UNCHANGED),
3204            (Path::new("x/x2.txt"), MODIFIED),
3205            (Path::new("x/y"), GitSummary::CONFLICT),
3206            (Path::new("x/y/y1.txt"), GitSummary::CONFLICT),
3207            (Path::new("x/y/y2.txt"), GitSummary::UNCHANGED),
3208            (Path::new("x/z.txt"), ADDED),
3209        ],
3210    );
3211
3212    // Test the other fundamental case, transitioning from git repository to non-git repository
3213    check_git_statuses(
3214        &snapshot,
3215        &[
3216            (Path::new(""), GitSummary::UNCHANGED),
3217            (Path::new("x"), MODIFIED + ADDED),
3218            (Path::new("x/x1.txt"), GitSummary::UNCHANGED),
3219        ],
3220    );
3221
3222    // And all together now
3223    check_git_statuses(
3224        &snapshot,
3225        &[
3226            (Path::new(""), GitSummary::UNCHANGED),
3227            (Path::new("x"), MODIFIED + ADDED),
3228            (Path::new("x/x1.txt"), GitSummary::UNCHANGED),
3229            (Path::new("x/x2.txt"), MODIFIED),
3230            (Path::new("x/y"), GitSummary::CONFLICT),
3231            (Path::new("x/y/y1.txt"), GitSummary::CONFLICT),
3232            (Path::new("x/y/y2.txt"), GitSummary::UNCHANGED),
3233            (Path::new("x/z.txt"), ADDED),
3234            (Path::new("z"), ADDED),
3235            (Path::new("z/z1.txt"), GitSummary::UNCHANGED),
3236            (Path::new("z/z2.txt"), ADDED),
3237        ],
3238    );
3239}
3240
3241#[gpui::test]
3242async fn test_private_single_file_worktree(cx: &mut TestAppContext) {
3243    init_test(cx);
3244    let fs = FakeFs::new(cx.background_executor.clone());
3245    fs.insert_tree("/", json!({".env": "PRIVATE=secret\n"}))
3246        .await;
3247    let tree = Worktree::local(
3248        Path::new("/.env"),
3249        true,
3250        fs.clone(),
3251        Default::default(),
3252        &mut cx.to_async(),
3253    )
3254    .await
3255    .unwrap();
3256    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
3257        .await;
3258    tree.read_with(cx, |tree, _| {
3259        let entry = tree.entry_for_path("").unwrap();
3260        assert!(entry.is_private);
3261    });
3262}
3263
3264#[track_caller]
3265fn check_git_statuses(snapshot: &Snapshot, expected_statuses: &[(&Path, GitSummary)]) {
3266    let mut traversal = snapshot
3267        .traverse_from_path(true, true, false, "".as_ref())
3268        .with_git_statuses();
3269    let found_statuses = expected_statuses
3270        .iter()
3271        .map(|&(path, _)| {
3272            let git_entry = traversal
3273                .find(|git_entry| &*git_entry.path == path)
3274                .unwrap_or_else(|| panic!("Traversal has no entry for {path:?}"));
3275            (path, git_entry.git_summary)
3276        })
3277        .collect::<Vec<_>>();
3278    assert_eq!(found_statuses, expected_statuses);
3279}
3280
3281const ADDED: GitSummary = GitSummary {
3282    index: TrackedSummary::ADDED,
3283    count: 1,
3284    ..GitSummary::UNCHANGED
3285};
3286const MODIFIED: GitSummary = GitSummary {
3287    index: TrackedSummary::MODIFIED,
3288    count: 1,
3289    ..GitSummary::UNCHANGED
3290};
3291
3292#[track_caller]
3293fn git_init(path: &Path) -> git2::Repository {
3294    git2::Repository::init(path).expect("Failed to initialize git repository")
3295}
3296
3297#[track_caller]
3298fn git_add<P: AsRef<Path>>(path: P, repo: &git2::Repository) {
3299    let path = path.as_ref();
3300    let mut index = repo.index().expect("Failed to get index");
3301    index.add_path(path).expect("Failed to add file");
3302    index.write().expect("Failed to write index");
3303}
3304
3305#[track_caller]
3306fn git_remove_index(path: &Path, repo: &git2::Repository) {
3307    let mut index = repo.index().expect("Failed to get index");
3308    index.remove_path(path).expect("Failed to add file");
3309    index.write().expect("Failed to write index");
3310}
3311
3312#[track_caller]
3313fn git_commit(msg: &'static str, repo: &git2::Repository) {
3314    use git2::Signature;
3315
3316    let signature = Signature::now("test", "test@zed.dev").unwrap();
3317    let oid = repo.index().unwrap().write_tree().unwrap();
3318    let tree = repo.find_tree(oid).unwrap();
3319    if let Ok(head) = repo.head() {
3320        let parent_obj = head.peel(git2::ObjectType::Commit).unwrap();
3321
3322        let parent_commit = parent_obj.as_commit().unwrap();
3323
3324        repo.commit(
3325            Some("HEAD"),
3326            &signature,
3327            &signature,
3328            msg,
3329            &tree,
3330            &[parent_commit],
3331        )
3332        .expect("Failed to commit with parent");
3333    } else {
3334        repo.commit(Some("HEAD"), &signature, &signature, msg, &tree, &[])
3335            .expect("Failed to commit");
3336    }
3337}
3338
3339#[track_caller]
3340fn git_stash(repo: &mut git2::Repository) {
3341    use git2::Signature;
3342
3343    let signature = Signature::now("test", "test@zed.dev").unwrap();
3344    repo.stash_save(&signature, "N/A", None)
3345        .expect("Failed to stash");
3346}
3347
3348#[track_caller]
3349fn git_reset(offset: usize, repo: &git2::Repository) {
3350    let head = repo.head().expect("Couldn't get repo head");
3351    let object = head.peel(git2::ObjectType::Commit).unwrap();
3352    let commit = object.as_commit().unwrap();
3353    let new_head = commit
3354        .parents()
3355        .inspect(|parnet| {
3356            parnet.message();
3357        })
3358        .nth(offset)
3359        .expect("Not enough history");
3360    repo.reset(new_head.as_object(), git2::ResetType::Soft, None)
3361        .expect("Could not reset");
3362}
3363
3364#[allow(dead_code)]
3365#[track_caller]
3366fn git_status(repo: &git2::Repository) -> collections::HashMap<String, git2::Status> {
3367    repo.statuses(None)
3368        .unwrap()
3369        .iter()
3370        .map(|status| (status.path().unwrap().to_string(), status.status()))
3371        .collect()
3372}
3373
3374#[track_caller]
3375fn check_worktree_entries(
3376    tree: &Worktree,
3377    expected_excluded_paths: &[&str],
3378    expected_ignored_paths: &[&str],
3379    expected_tracked_paths: &[&str],
3380    expected_included_paths: &[&str],
3381) {
3382    for path in expected_excluded_paths {
3383        let entry = tree.entry_for_path(path);
3384        assert!(
3385            entry.is_none(),
3386            "expected path '{path}' to be excluded, but got entry: {entry:?}",
3387        );
3388    }
3389    for path in expected_ignored_paths {
3390        let entry = tree
3391            .entry_for_path(path)
3392            .unwrap_or_else(|| panic!("Missing entry for expected ignored path '{path}'"));
3393        assert!(
3394            entry.is_ignored,
3395            "expected path '{path}' to be ignored, but got entry: {entry:?}",
3396        );
3397    }
3398    for path in expected_tracked_paths {
3399        let entry = tree
3400            .entry_for_path(path)
3401            .unwrap_or_else(|| panic!("Missing entry for expected tracked path '{path}'"));
3402        assert!(
3403            !entry.is_ignored || entry.is_always_included,
3404            "expected path '{path}' to be tracked, but got entry: {entry:?}",
3405        );
3406    }
3407    for path in expected_included_paths {
3408        let entry = tree
3409            .entry_for_path(path)
3410            .unwrap_or_else(|| panic!("Missing entry for expected included path '{path}'"));
3411        assert!(
3412            entry.is_always_included,
3413            "expected path '{path}' to always be included, but got entry: {entry:?}",
3414        );
3415    }
3416}
3417
3418fn init_test(cx: &mut gpui::TestAppContext) {
3419    if std::env::var("RUST_LOG").is_ok() {
3420        env_logger::try_init().ok();
3421    }
3422
3423    cx.update(|cx| {
3424        let settings_store = SettingsStore::test(cx);
3425        cx.set_global(settings_store);
3426        WorktreeSettings::register(cx);
3427    });
3428}
3429
3430fn assert_entry_git_state(
3431    tree: &Worktree,
3432    path: &str,
3433    index_status: Option<StatusCode>,
3434    is_ignored: bool,
3435) {
3436    let entry = tree.entry_for_path(path).expect("entry {path} not found");
3437    let status = tree.status_for_file(Path::new(path));
3438    let expected = index_status.map(|index_status| {
3439        TrackedStatus {
3440            index_status,
3441            worktree_status: StatusCode::Unmodified,
3442        }
3443        .into()
3444    });
3445    assert_eq!(
3446        status, expected,
3447        "expected {path} to have git status: {expected:?}"
3448    );
3449    assert_eq!(
3450        entry.is_ignored, is_ignored,
3451        "expected {path} to have is_ignored: {is_ignored}"
3452    );
3453}