worktree_tests.rs

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