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, UnmergedStatus, UnmergedStatusCode,
  10    },
  11    GITIGNORE,
  12};
  13use gpui::{BorrowAppContext, ModelContext, Task, TestAppContext};
  14use parking_lot::Mutex;
  15use postage::stream::Stream;
  16use pretty_assertions::assert_eq;
  17use rand::prelude::*;
  18use serde_json::json;
  19use settings::{Settings, SettingsStore};
  20use std::{
  21    env,
  22    fmt::Write,
  23    mem,
  24    path::{Path, PathBuf},
  25    sync::Arc,
  26};
  27use util::{test::temp_tree, ResultExt};
  28
  29#[gpui::test]
  30async fn test_traversal(cx: &mut TestAppContext) {
  31    init_test(cx);
  32    let fs = FakeFs::new(cx.background_executor.clone());
  33    fs.insert_tree(
  34        "/root",
  35        json!({
  36           ".gitignore": "a/b\n",
  37           "a": {
  38               "b": "",
  39               "c": "",
  40           }
  41        }),
  42    )
  43    .await;
  44
  45    let tree = Worktree::local(
  46        Path::new("/root"),
  47        true,
  48        fs,
  49        Default::default(),
  50        &mut cx.to_async(),
  51    )
  52    .await
  53    .unwrap();
  54    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
  55        .await;
  56
  57    tree.read_with(cx, |tree, _| {
  58        assert_eq!(
  59            tree.entries(false, 0)
  60                .map(|entry| entry.path.as_ref())
  61                .collect::<Vec<_>>(),
  62            vec![
  63                Path::new(""),
  64                Path::new(".gitignore"),
  65                Path::new("a"),
  66                Path::new("a/c"),
  67            ]
  68        );
  69        assert_eq!(
  70            tree.entries(true, 0)
  71                .map(|entry| entry.path.as_ref())
  72                .collect::<Vec<_>>(),
  73            vec![
  74                Path::new(""),
  75                Path::new(".gitignore"),
  76                Path::new("a"),
  77                Path::new("a/b"),
  78                Path::new("a/c"),
  79            ]
  80        );
  81    })
  82}
  83
  84#[gpui::test(iterations = 10)]
  85async fn test_circular_symlinks(cx: &mut TestAppContext) {
  86    init_test(cx);
  87    let fs = FakeFs::new(cx.background_executor.clone());
  88    fs.insert_tree(
  89        "/root",
  90        json!({
  91            "lib": {
  92                "a": {
  93                    "a.txt": ""
  94                },
  95                "b": {
  96                    "b.txt": ""
  97                }
  98            }
  99        }),
 100    )
 101    .await;
 102    fs.create_symlink("/root/lib/a/lib".as_ref(), "..".into())
 103        .await
 104        .unwrap();
 105    fs.create_symlink("/root/lib/b/lib".as_ref(), "..".into())
 106        .await
 107        .unwrap();
 108
 109    let tree = Worktree::local(
 110        Path::new("/root"),
 111        true,
 112        fs.clone(),
 113        Default::default(),
 114        &mut cx.to_async(),
 115    )
 116    .await
 117    .unwrap();
 118
 119    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
 120        .await;
 121
 122    tree.read_with(cx, |tree, _| {
 123        assert_eq!(
 124            tree.entries(false, 0)
 125                .map(|entry| entry.path.as_ref())
 126                .collect::<Vec<_>>(),
 127            vec![
 128                Path::new(""),
 129                Path::new("lib"),
 130                Path::new("lib/a"),
 131                Path::new("lib/a/a.txt"),
 132                Path::new("lib/a/lib"),
 133                Path::new("lib/b"),
 134                Path::new("lib/b/b.txt"),
 135                Path::new("lib/b/lib"),
 136            ]
 137        );
 138    });
 139
 140    fs.rename(
 141        Path::new("/root/lib/a/lib"),
 142        Path::new("/root/lib/a/lib-2"),
 143        Default::default(),
 144    )
 145    .await
 146    .unwrap();
 147    cx.executor().run_until_parked();
 148    tree.read_with(cx, |tree, _| {
 149        assert_eq!(
 150            tree.entries(false, 0)
 151                .map(|entry| entry.path.as_ref())
 152                .collect::<Vec<_>>(),
 153            vec![
 154                Path::new(""),
 155                Path::new("lib"),
 156                Path::new("lib/a"),
 157                Path::new("lib/a/a.txt"),
 158                Path::new("lib/a/lib-2"),
 159                Path::new("lib/b"),
 160                Path::new("lib/b/b.txt"),
 161                Path::new("lib/b/lib"),
 162            ]
 163        );
 164    });
 165}
 166
 167#[gpui::test]
 168async fn test_symlinks_pointing_outside(cx: &mut TestAppContext) {
 169    init_test(cx);
 170    let fs = FakeFs::new(cx.background_executor.clone());
 171    fs.insert_tree(
 172        "/root",
 173        json!({
 174            "dir1": {
 175                "deps": {
 176                    // symlinks here
 177                },
 178                "src": {
 179                    "a.rs": "",
 180                    "b.rs": "",
 181                },
 182            },
 183            "dir2": {
 184                "src": {
 185                    "c.rs": "",
 186                    "d.rs": "",
 187                }
 188            },
 189            "dir3": {
 190                "deps": {},
 191                "src": {
 192                    "e.rs": "",
 193                    "f.rs": "",
 194                },
 195            }
 196        }),
 197    )
 198    .await;
 199
 200    // These symlinks point to directories outside of the worktree's root, dir1.
 201    fs.create_symlink("/root/dir1/deps/dep-dir2".as_ref(), "../../dir2".into())
 202        .await
 203        .unwrap();
 204    fs.create_symlink("/root/dir1/deps/dep-dir3".as_ref(), "../../dir3".into())
 205        .await
 206        .unwrap();
 207
 208    let tree = Worktree::local(
 209        Path::new("/root/dir1"),
 210        true,
 211        fs.clone(),
 212        Default::default(),
 213        &mut cx.to_async(),
 214    )
 215    .await
 216    .unwrap();
 217
 218    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
 219        .await;
 220
 221    let tree_updates = Arc::new(Mutex::new(Vec::new()));
 222    tree.update(cx, |_, cx| {
 223        let tree_updates = tree_updates.clone();
 224        cx.subscribe(&tree, move |_, _, event, _| {
 225            if let Event::UpdatedEntries(update) = event {
 226                tree_updates.lock().extend(
 227                    update
 228                        .iter()
 229                        .map(|(path, _, change)| (path.clone(), *change)),
 230                );
 231            }
 232        })
 233        .detach();
 234    });
 235
 236    // The symlinked directories are not scanned by default.
 237    tree.read_with(cx, |tree, _| {
 238        assert_eq!(
 239            tree.entries(true, 0)
 240                .map(|entry| (entry.path.as_ref(), entry.is_external))
 241                .collect::<Vec<_>>(),
 242            vec![
 243                (Path::new(""), false),
 244                (Path::new("deps"), false),
 245                (Path::new("deps/dep-dir2"), true),
 246                (Path::new("deps/dep-dir3"), true),
 247                (Path::new("src"), false),
 248                (Path::new("src/a.rs"), false),
 249                (Path::new("src/b.rs"), false),
 250            ]
 251        );
 252
 253        assert_eq!(
 254            tree.entry_for_path("deps/dep-dir2").unwrap().kind,
 255            EntryKind::UnloadedDir
 256        );
 257    });
 258
 259    // Expand one of the symlinked directories.
 260    tree.read_with(cx, |tree, _| {
 261        tree.as_local()
 262            .unwrap()
 263            .refresh_entries_for_paths(vec![Path::new("deps/dep-dir3").into()])
 264    })
 265    .recv()
 266    .await;
 267
 268    // The expanded directory's contents are loaded. Subdirectories are
 269    // not scanned yet.
 270    tree.read_with(cx, |tree, _| {
 271        assert_eq!(
 272            tree.entries(true, 0)
 273                .map(|entry| (entry.path.as_ref(), entry.is_external))
 274                .collect::<Vec<_>>(),
 275            vec![
 276                (Path::new(""), false),
 277                (Path::new("deps"), false),
 278                (Path::new("deps/dep-dir2"), true),
 279                (Path::new("deps/dep-dir3"), true),
 280                (Path::new("deps/dep-dir3/deps"), true),
 281                (Path::new("deps/dep-dir3/src"), true),
 282                (Path::new("src"), false),
 283                (Path::new("src/a.rs"), false),
 284                (Path::new("src/b.rs"), false),
 285            ]
 286        );
 287    });
 288    assert_eq!(
 289        mem::take(&mut *tree_updates.lock()),
 290        &[
 291            (Path::new("deps/dep-dir3").into(), PathChange::Loaded),
 292            (Path::new("deps/dep-dir3/deps").into(), PathChange::Loaded),
 293            (Path::new("deps/dep-dir3/src").into(), PathChange::Loaded)
 294        ]
 295    );
 296
 297    // Expand a subdirectory of one of the symlinked directories.
 298    tree.read_with(cx, |tree, _| {
 299        tree.as_local()
 300            .unwrap()
 301            .refresh_entries_for_paths(vec![Path::new("deps/dep-dir3/src").into()])
 302    })
 303    .recv()
 304    .await;
 305
 306    // The expanded subdirectory's contents are loaded.
 307    tree.read_with(cx, |tree, _| {
 308        assert_eq!(
 309            tree.entries(true, 0)
 310                .map(|entry| (entry.path.as_ref(), entry.is_external))
 311                .collect::<Vec<_>>(),
 312            vec![
 313                (Path::new(""), false),
 314                (Path::new("deps"), false),
 315                (Path::new("deps/dep-dir2"), true),
 316                (Path::new("deps/dep-dir3"), true),
 317                (Path::new("deps/dep-dir3/deps"), true),
 318                (Path::new("deps/dep-dir3/src"), true),
 319                (Path::new("deps/dep-dir3/src/e.rs"), true),
 320                (Path::new("deps/dep-dir3/src/f.rs"), true),
 321                (Path::new("src"), false),
 322                (Path::new("src/a.rs"), false),
 323                (Path::new("src/b.rs"), false),
 324            ]
 325        );
 326    });
 327
 328    assert_eq!(
 329        mem::take(&mut *tree_updates.lock()),
 330        &[
 331            (Path::new("deps/dep-dir3/src").into(), PathChange::Loaded),
 332            (
 333                Path::new("deps/dep-dir3/src/e.rs").into(),
 334                PathChange::Loaded
 335            ),
 336            (
 337                Path::new("deps/dep-dir3/src/f.rs").into(),
 338                PathChange::Loaded
 339            )
 340        ]
 341    );
 342}
 343
 344#[cfg(target_os = "macos")]
 345#[gpui::test]
 346async fn test_renaming_case_only(cx: &mut TestAppContext) {
 347    cx.executor().allow_parking();
 348    init_test(cx);
 349
 350    const OLD_NAME: &str = "aaa.rs";
 351    const NEW_NAME: &str = "AAA.rs";
 352
 353    let fs = Arc::new(RealFs::default());
 354    let temp_root = temp_tree(json!({
 355        OLD_NAME: "",
 356    }));
 357
 358    let tree = Worktree::local(
 359        temp_root.path(),
 360        true,
 361        fs.clone(),
 362        Default::default(),
 363        &mut cx.to_async(),
 364    )
 365    .await
 366    .unwrap();
 367
 368    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
 369        .await;
 370    tree.read_with(cx, |tree, _| {
 371        assert_eq!(
 372            tree.entries(true, 0)
 373                .map(|entry| entry.path.as_ref())
 374                .collect::<Vec<_>>(),
 375            vec![Path::new(""), Path::new(OLD_NAME)]
 376        );
 377    });
 378
 379    fs.rename(
 380        &temp_root.path().join(OLD_NAME),
 381        &temp_root.path().join(NEW_NAME),
 382        fs::RenameOptions {
 383            overwrite: true,
 384            ignore_if_exists: true,
 385        },
 386    )
 387    .await
 388    .unwrap();
 389
 390    tree.flush_fs_events(cx).await;
 391
 392    tree.read_with(cx, |tree, _| {
 393        assert_eq!(
 394            tree.entries(true, 0)
 395                .map(|entry| entry.path.as_ref())
 396                .collect::<Vec<_>>(),
 397            vec![Path::new(""), Path::new(NEW_NAME)]
 398        );
 399    });
 400}
 401
 402#[gpui::test]
 403async fn test_open_gitignored_files(cx: &mut TestAppContext) {
 404    init_test(cx);
 405    let fs = FakeFs::new(cx.background_executor.clone());
 406    fs.insert_tree(
 407        "/root",
 408        json!({
 409            ".gitignore": "node_modules\n",
 410            "one": {
 411                "node_modules": {
 412                    "a": {
 413                        "a1.js": "a1",
 414                        "a2.js": "a2",
 415                    },
 416                    "b": {
 417                        "b1.js": "b1",
 418                        "b2.js": "b2",
 419                    },
 420                    "c": {
 421                        "c1.js": "c1",
 422                        "c2.js": "c2",
 423                    }
 424                },
 425            },
 426            "two": {
 427                "x.js": "",
 428                "y.js": "",
 429            },
 430        }),
 431    )
 432    .await;
 433
 434    let tree = Worktree::local(
 435        Path::new("/root"),
 436        true,
 437        fs.clone(),
 438        Default::default(),
 439        &mut cx.to_async(),
 440    )
 441    .await
 442    .unwrap();
 443
 444    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
 445        .await;
 446
 447    tree.read_with(cx, |tree, _| {
 448        assert_eq!(
 449            tree.entries(true, 0)
 450                .map(|entry| (entry.path.as_ref(), entry.is_ignored))
 451                .collect::<Vec<_>>(),
 452            vec![
 453                (Path::new(""), false),
 454                (Path::new(".gitignore"), false),
 455                (Path::new("one"), false),
 456                (Path::new("one/node_modules"), true),
 457                (Path::new("two"), false),
 458                (Path::new("two/x.js"), false),
 459                (Path::new("two/y.js"), false),
 460            ]
 461        );
 462    });
 463
 464    // Open a file that is nested inside of a gitignored directory that
 465    // has not yet been expanded.
 466    let prev_read_dir_count = fs.read_dir_call_count();
 467    let loaded = tree
 468        .update(cx, |tree, cx| {
 469            tree.load_file("one/node_modules/b/b1.js".as_ref(), cx)
 470        })
 471        .await
 472        .unwrap();
 473
 474    tree.read_with(cx, |tree, _| {
 475        assert_eq!(
 476            tree.entries(true, 0)
 477                .map(|entry| (entry.path.as_ref(), entry.is_ignored))
 478                .collect::<Vec<_>>(),
 479            vec![
 480                (Path::new(""), false),
 481                (Path::new(".gitignore"), false),
 482                (Path::new("one"), false),
 483                (Path::new("one/node_modules"), true),
 484                (Path::new("one/node_modules/a"), true),
 485                (Path::new("one/node_modules/b"), true),
 486                (Path::new("one/node_modules/b/b1.js"), true),
 487                (Path::new("one/node_modules/b/b2.js"), true),
 488                (Path::new("one/node_modules/c"), true),
 489                (Path::new("two"), false),
 490                (Path::new("two/x.js"), false),
 491                (Path::new("two/y.js"), false),
 492            ]
 493        );
 494
 495        assert_eq!(
 496            loaded.file.path.as_ref(),
 497            Path::new("one/node_modules/b/b1.js")
 498        );
 499
 500        // Only the newly-expanded directories are scanned.
 501        assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 2);
 502    });
 503
 504    // Open another file in a different subdirectory of the same
 505    // gitignored directory.
 506    let prev_read_dir_count = fs.read_dir_call_count();
 507    let loaded = tree
 508        .update(cx, |tree, cx| {
 509            tree.load_file("one/node_modules/a/a2.js".as_ref(), cx)
 510        })
 511        .await
 512        .unwrap();
 513
 514    tree.read_with(cx, |tree, _| {
 515        assert_eq!(
 516            tree.entries(true, 0)
 517                .map(|entry| (entry.path.as_ref(), entry.is_ignored))
 518                .collect::<Vec<_>>(),
 519            vec![
 520                (Path::new(""), false),
 521                (Path::new(".gitignore"), false),
 522                (Path::new("one"), false),
 523                (Path::new("one/node_modules"), true),
 524                (Path::new("one/node_modules/a"), true),
 525                (Path::new("one/node_modules/a/a1.js"), true),
 526                (Path::new("one/node_modules/a/a2.js"), true),
 527                (Path::new("one/node_modules/b"), true),
 528                (Path::new("one/node_modules/b/b1.js"), true),
 529                (Path::new("one/node_modules/b/b2.js"), true),
 530                (Path::new("one/node_modules/c"), true),
 531                (Path::new("two"), false),
 532                (Path::new("two/x.js"), false),
 533                (Path::new("two/y.js"), false),
 534            ]
 535        );
 536
 537        assert_eq!(
 538            loaded.file.path.as_ref(),
 539            Path::new("one/node_modules/a/a2.js")
 540        );
 541
 542        // Only the newly-expanded directory is scanned.
 543        assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 1);
 544    });
 545
 546    let path = PathBuf::from("/root/one/node_modules/c/lib");
 547
 548    // No work happens when files and directories change within an unloaded directory.
 549    let prev_fs_call_count = fs.read_dir_call_count() + fs.metadata_call_count();
 550    // When we open a directory, we check each ancestor whether it's a git
 551    // repository. That means we have an fs.metadata call per ancestor that we
 552    // need to subtract here.
 553    let ancestors = path.ancestors().count();
 554
 555    fs.create_dir(path.as_ref()).await.unwrap();
 556    cx.executor().run_until_parked();
 557
 558    assert_eq!(
 559        fs.read_dir_call_count() + fs.metadata_call_count() - prev_fs_call_count - ancestors,
 560        0
 561    );
 562}
 563
 564#[gpui::test]
 565async fn test_dirs_no_longer_ignored(cx: &mut TestAppContext) {
 566    init_test(cx);
 567    let fs = FakeFs::new(cx.background_executor.clone());
 568    fs.insert_tree(
 569        "/root",
 570        json!({
 571            ".gitignore": "node_modules\n",
 572            "a": {
 573                "a.js": "",
 574            },
 575            "b": {
 576                "b.js": "",
 577            },
 578            "node_modules": {
 579                "c": {
 580                    "c.js": "",
 581                },
 582                "d": {
 583                    "d.js": "",
 584                    "e": {
 585                        "e1.js": "",
 586                        "e2.js": "",
 587                    },
 588                    "f": {
 589                        "f1.js": "",
 590                        "f2.js": "",
 591                    }
 592                },
 593            },
 594        }),
 595    )
 596    .await;
 597
 598    let tree = Worktree::local(
 599        Path::new("/root"),
 600        true,
 601        fs.clone(),
 602        Default::default(),
 603        &mut cx.to_async(),
 604    )
 605    .await
 606    .unwrap();
 607
 608    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
 609        .await;
 610
 611    // Open a file within the gitignored directory, forcing some of its
 612    // subdirectories to be read, but not all.
 613    let read_dir_count_1 = fs.read_dir_call_count();
 614    tree.read_with(cx, |tree, _| {
 615        tree.as_local()
 616            .unwrap()
 617            .refresh_entries_for_paths(vec![Path::new("node_modules/d/d.js").into()])
 618    })
 619    .recv()
 620    .await;
 621
 622    // Those subdirectories are now loaded.
 623    tree.read_with(cx, |tree, _| {
 624        assert_eq!(
 625            tree.entries(true, 0)
 626                .map(|e| (e.path.as_ref(), e.is_ignored))
 627                .collect::<Vec<_>>(),
 628            &[
 629                (Path::new(""), false),
 630                (Path::new(".gitignore"), false),
 631                (Path::new("a"), false),
 632                (Path::new("a/a.js"), false),
 633                (Path::new("b"), false),
 634                (Path::new("b/b.js"), false),
 635                (Path::new("node_modules"), true),
 636                (Path::new("node_modules/c"), true),
 637                (Path::new("node_modules/d"), true),
 638                (Path::new("node_modules/d/d.js"), true),
 639                (Path::new("node_modules/d/e"), true),
 640                (Path::new("node_modules/d/f"), true),
 641            ]
 642        );
 643    });
 644    let read_dir_count_2 = fs.read_dir_call_count();
 645    assert_eq!(read_dir_count_2 - read_dir_count_1, 2);
 646
 647    // Update the gitignore so that node_modules is no longer ignored,
 648    // but a subdirectory is ignored
 649    fs.save("/root/.gitignore".as_ref(), &"e".into(), Default::default())
 650        .await
 651        .unwrap();
 652    cx.executor().run_until_parked();
 653
 654    // All of the directories that are no longer ignored are now loaded.
 655    tree.read_with(cx, |tree, _| {
 656        assert_eq!(
 657            tree.entries(true, 0)
 658                .map(|e| (e.path.as_ref(), e.is_ignored))
 659                .collect::<Vec<_>>(),
 660            &[
 661                (Path::new(""), false),
 662                (Path::new(".gitignore"), false),
 663                (Path::new("a"), false),
 664                (Path::new("a/a.js"), false),
 665                (Path::new("b"), false),
 666                (Path::new("b/b.js"), false),
 667                // This directory is no longer ignored
 668                (Path::new("node_modules"), false),
 669                (Path::new("node_modules/c"), false),
 670                (Path::new("node_modules/c/c.js"), false),
 671                (Path::new("node_modules/d"), false),
 672                (Path::new("node_modules/d/d.js"), false),
 673                // This subdirectory is now ignored
 674                (Path::new("node_modules/d/e"), true),
 675                (Path::new("node_modules/d/f"), false),
 676                (Path::new("node_modules/d/f/f1.js"), false),
 677                (Path::new("node_modules/d/f/f2.js"), false),
 678            ]
 679        );
 680    });
 681
 682    // Each of the newly-loaded directories is scanned only once.
 683    let read_dir_count_3 = fs.read_dir_call_count();
 684    assert_eq!(read_dir_count_3 - read_dir_count_2, 2);
 685}
 686
 687#[gpui::test(iterations = 10)]
 688async fn test_rescan_with_gitignore(cx: &mut TestAppContext) {
 689    init_test(cx);
 690    cx.update(|cx| {
 691        cx.update_global::<SettingsStore, _>(|store, cx| {
 692            store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
 693                project_settings.file_scan_exclusions = Some(Vec::new());
 694            });
 695        });
 696    });
 697    let fs = FakeFs::new(cx.background_executor.clone());
 698    fs.insert_tree(
 699        "/root",
 700        json!({
 701            ".gitignore": "ancestor-ignored-file1\nancestor-ignored-file2\n",
 702            "tree": {
 703                ".git": {},
 704                ".gitignore": "ignored-dir\n",
 705                "tracked-dir": {
 706                    "tracked-file1": "",
 707                    "ancestor-ignored-file1": "",
 708                },
 709                "ignored-dir": {
 710                    "ignored-file1": ""
 711                }
 712            }
 713        }),
 714    )
 715    .await;
 716
 717    let tree = Worktree::local(
 718        "/root/tree".as_ref(),
 719        true,
 720        fs.clone(),
 721        Default::default(),
 722        &mut cx.to_async(),
 723    )
 724    .await
 725    .unwrap();
 726    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
 727        .await;
 728
 729    tree.read_with(cx, |tree, _| {
 730        tree.as_local()
 731            .unwrap()
 732            .refresh_entries_for_paths(vec![Path::new("ignored-dir").into()])
 733    })
 734    .recv()
 735    .await;
 736
 737    cx.read(|cx| {
 738        let tree = tree.read(cx);
 739        assert_entry_git_state(tree, "tracked-dir/tracked-file1", None, false);
 740        assert_entry_git_state(tree, "tracked-dir/ancestor-ignored-file1", None, false);
 741        assert_entry_git_state(tree, "ignored-dir/ignored-file1", None, true);
 742    });
 743
 744    fs.set_status_for_repo_via_working_copy_change(
 745        Path::new("/root/tree/.git"),
 746        &[(
 747            Path::new("tracked-dir/tracked-file2"),
 748            FileStatus::worktree(StatusCode::Added),
 749        )],
 750    );
 751
 752    fs.create_file(
 753        "/root/tree/tracked-dir/tracked-file2".as_ref(),
 754        Default::default(),
 755    )
 756    .await
 757    .unwrap();
 758    fs.create_file(
 759        "/root/tree/tracked-dir/ancestor-ignored-file2".as_ref(),
 760        Default::default(),
 761    )
 762    .await
 763    .unwrap();
 764    fs.create_file(
 765        "/root/tree/ignored-dir/ignored-file2".as_ref(),
 766        Default::default(),
 767    )
 768    .await
 769    .unwrap();
 770
 771    cx.executor().run_until_parked();
 772    cx.read(|cx| {
 773        let tree = tree.read(cx);
 774        assert_entry_git_state(
 775            tree,
 776            "tracked-dir/tracked-file2",
 777            Some(StatusCode::Added),
 778            false,
 779        );
 780        assert_entry_git_state(tree, "tracked-dir/ancestor-ignored-file2", None, false);
 781        assert_entry_git_state(tree, "ignored-dir/ignored-file2", None, true);
 782        assert!(tree.entry_for_path(".git").unwrap().is_ignored);
 783    });
 784}
 785
 786#[gpui::test]
 787async fn test_update_gitignore(cx: &mut TestAppContext) {
 788    init_test(cx);
 789    let fs = FakeFs::new(cx.background_executor.clone());
 790    fs.insert_tree(
 791        "/root",
 792        json!({
 793            ".git": {},
 794            ".gitignore": "*.txt\n",
 795            "a.xml": "<a></a>",
 796            "b.txt": "Some text"
 797        }),
 798    )
 799    .await;
 800
 801    let tree = Worktree::local(
 802        "/root".as_ref(),
 803        true,
 804        fs.clone(),
 805        Default::default(),
 806        &mut cx.to_async(),
 807    )
 808    .await
 809    .unwrap();
 810    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
 811        .await;
 812
 813    tree.read_with(cx, |tree, _| {
 814        tree.as_local()
 815            .unwrap()
 816            .refresh_entries_for_paths(vec![Path::new("").into()])
 817    })
 818    .recv()
 819    .await;
 820
 821    cx.read(|cx| {
 822        let tree = tree.read(cx);
 823        assert_entry_git_state(tree, "a.xml", None, false);
 824        assert_entry_git_state(tree, "b.txt", None, true);
 825    });
 826
 827    fs.atomic_write("/root/.gitignore".into(), "*.xml".into())
 828        .await
 829        .unwrap();
 830
 831    fs.set_status_for_repo_via_working_copy_change(
 832        Path::new("/root/.git"),
 833        &[(Path::new("b.txt"), FileStatus::worktree(StatusCode::Added))],
 834    );
 835
 836    cx.executor().run_until_parked();
 837    cx.read(|cx| {
 838        let tree = tree.read(cx);
 839        assert_entry_git_state(tree, "a.xml", None, true);
 840        assert_entry_git_state(tree, "b.txt", Some(StatusCode::Added), false);
 841    });
 842}
 843
 844#[gpui::test]
 845async fn test_write_file(cx: &mut TestAppContext) {
 846    init_test(cx);
 847    cx.executor().allow_parking();
 848    let dir = temp_tree(json!({
 849        ".git": {},
 850        ".gitignore": "ignored-dir\n",
 851        "tracked-dir": {},
 852        "ignored-dir": {}
 853    }));
 854
 855    let tree = Worktree::local(
 856        dir.path(),
 857        true,
 858        Arc::new(RealFs::default()),
 859        Default::default(),
 860        &mut cx.to_async(),
 861    )
 862    .await
 863    .unwrap();
 864
 865    #[cfg(not(target_os = "macos"))]
 866    fs::fs_watcher::global(|_| {}).unwrap();
 867
 868    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
 869        .await;
 870    tree.flush_fs_events(cx).await;
 871
 872    tree.update(cx, |tree, cx| {
 873        tree.write_file(
 874            Path::new("tracked-dir/file.txt"),
 875            "hello".into(),
 876            Default::default(),
 877            cx,
 878        )
 879    })
 880    .await
 881    .unwrap();
 882    tree.update(cx, |tree, cx| {
 883        tree.write_file(
 884            Path::new("ignored-dir/file.txt"),
 885            "world".into(),
 886            Default::default(),
 887            cx,
 888        )
 889    })
 890    .await
 891    .unwrap();
 892
 893    tree.read_with(cx, |tree, _| {
 894        let tracked = tree.entry_for_path("tracked-dir/file.txt").unwrap();
 895        let ignored = tree.entry_for_path("ignored-dir/file.txt").unwrap();
 896        assert!(!tracked.is_ignored);
 897        assert!(ignored.is_ignored);
 898    });
 899}
 900
 901#[gpui::test]
 902async fn test_file_scan_inclusions(cx: &mut TestAppContext) {
 903    init_test(cx);
 904    cx.executor().allow_parking();
 905    let dir = temp_tree(json!({
 906        ".gitignore": "**/target\n/node_modules\ntop_level.txt\n",
 907        "target": {
 908            "index": "blah2"
 909        },
 910        "node_modules": {
 911            ".DS_Store": "",
 912            "prettier": {
 913                "package.json": "{}",
 914            },
 915        },
 916        "src": {
 917            ".DS_Store": "",
 918            "foo": {
 919                "foo.rs": "mod another;\n",
 920                "another.rs": "// another",
 921            },
 922            "bar": {
 923                "bar.rs": "// bar",
 924            },
 925            "lib.rs": "mod foo;\nmod bar;\n",
 926        },
 927        "top_level.txt": "top level file",
 928        ".DS_Store": "",
 929    }));
 930    cx.update(|cx| {
 931        cx.update_global::<SettingsStore, _>(|store, cx| {
 932            store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
 933                project_settings.file_scan_exclusions = Some(vec![]);
 934                project_settings.file_scan_inclusions = Some(vec![
 935                    "node_modules/**/package.json".to_string(),
 936                    "**/.DS_Store".to_string(),
 937                ]);
 938            });
 939        });
 940    });
 941
 942    let tree = Worktree::local(
 943        dir.path(),
 944        true,
 945        Arc::new(RealFs::default()),
 946        Default::default(),
 947        &mut cx.to_async(),
 948    )
 949    .await
 950    .unwrap();
 951    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
 952        .await;
 953    tree.flush_fs_events(cx).await;
 954    tree.read_with(cx, |tree, _| {
 955        // Assert that file_scan_inclusions overrides  file_scan_exclusions.
 956        check_worktree_entries(
 957            tree,
 958            &[],
 959            &["target", "node_modules"],
 960            &["src/lib.rs", "src/bar/bar.rs", ".gitignore"],
 961            &[
 962                "node_modules/prettier/package.json",
 963                ".DS_Store",
 964                "node_modules/.DS_Store",
 965                "src/.DS_Store",
 966            ],
 967        )
 968    });
 969}
 970
 971#[gpui::test]
 972async fn test_file_scan_exclusions_overrules_inclusions(cx: &mut TestAppContext) {
 973    init_test(cx);
 974    cx.executor().allow_parking();
 975    let dir = temp_tree(json!({
 976        ".gitignore": "**/target\n/node_modules\n",
 977        "target": {
 978            "index": "blah2"
 979        },
 980        "node_modules": {
 981            ".DS_Store": "",
 982            "prettier": {
 983                "package.json": "{}",
 984            },
 985        },
 986        "src": {
 987            ".DS_Store": "",
 988            "foo": {
 989                "foo.rs": "mod another;\n",
 990                "another.rs": "// another",
 991            },
 992        },
 993        ".DS_Store": "",
 994    }));
 995
 996    cx.update(|cx| {
 997        cx.update_global::<SettingsStore, _>(|store, cx| {
 998            store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
 999                project_settings.file_scan_exclusions = Some(vec!["**/.DS_Store".to_string()]);
1000                project_settings.file_scan_inclusions = Some(vec!["**/.DS_Store".to_string()]);
1001            });
1002        });
1003    });
1004
1005    let tree = Worktree::local(
1006        dir.path(),
1007        true,
1008        Arc::new(RealFs::default()),
1009        Default::default(),
1010        &mut cx.to_async(),
1011    )
1012    .await
1013    .unwrap();
1014    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1015        .await;
1016    tree.flush_fs_events(cx).await;
1017    tree.read_with(cx, |tree, _| {
1018        // Assert that file_scan_inclusions overrides  file_scan_exclusions.
1019        check_worktree_entries(
1020            tree,
1021            &[".DS_Store, src/.DS_Store"],
1022            &["target", "node_modules"],
1023            &["src/foo/another.rs", "src/foo/foo.rs", ".gitignore"],
1024            &[],
1025        )
1026    });
1027}
1028
1029#[gpui::test]
1030async fn test_file_scan_inclusions_reindexes_on_setting_change(cx: &mut TestAppContext) {
1031    init_test(cx);
1032    cx.executor().allow_parking();
1033    let dir = temp_tree(json!({
1034        ".gitignore": "**/target\n/node_modules/\n",
1035        "target": {
1036            "index": "blah2"
1037        },
1038        "node_modules": {
1039            ".DS_Store": "",
1040            "prettier": {
1041                "package.json": "{}",
1042            },
1043        },
1044        "src": {
1045            ".DS_Store": "",
1046            "foo": {
1047                "foo.rs": "mod another;\n",
1048                "another.rs": "// another",
1049            },
1050        },
1051        ".DS_Store": "",
1052    }));
1053
1054    cx.update(|cx| {
1055        cx.update_global::<SettingsStore, _>(|store, cx| {
1056            store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
1057                project_settings.file_scan_exclusions = Some(vec![]);
1058                project_settings.file_scan_inclusions = Some(vec!["node_modules/**".to_string()]);
1059            });
1060        });
1061    });
1062    let tree = Worktree::local(
1063        dir.path(),
1064        true,
1065        Arc::new(RealFs::default()),
1066        Default::default(),
1067        &mut cx.to_async(),
1068    )
1069    .await
1070    .unwrap();
1071    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1072        .await;
1073    tree.flush_fs_events(cx).await;
1074
1075    tree.read_with(cx, |tree, _| {
1076        assert!(tree
1077            .entry_for_path("node_modules")
1078            .is_some_and(|f| f.is_always_included));
1079        assert!(tree
1080            .entry_for_path("node_modules/prettier/package.json")
1081            .is_some_and(|f| f.is_always_included));
1082    });
1083
1084    cx.update(|cx| {
1085        cx.update_global::<SettingsStore, _>(|store, cx| {
1086            store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
1087                project_settings.file_scan_exclusions = Some(vec![]);
1088                project_settings.file_scan_inclusions = Some(vec![]);
1089            });
1090        });
1091    });
1092    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1093        .await;
1094    tree.flush_fs_events(cx).await;
1095
1096    tree.read_with(cx, |tree, _| {
1097        assert!(tree
1098            .entry_for_path("node_modules")
1099            .is_some_and(|f| !f.is_always_included));
1100        assert!(tree
1101            .entry_for_path("node_modules/prettier/package.json")
1102            .is_some_and(|f| !f.is_always_included));
1103    });
1104}
1105
1106#[gpui::test]
1107async fn test_file_scan_exclusions(cx: &mut TestAppContext) {
1108    init_test(cx);
1109    cx.executor().allow_parking();
1110    let dir = temp_tree(json!({
1111        ".gitignore": "**/target\n/node_modules\n",
1112        "target": {
1113            "index": "blah2"
1114        },
1115        "node_modules": {
1116            ".DS_Store": "",
1117            "prettier": {
1118                "package.json": "{}",
1119            },
1120        },
1121        "src": {
1122            ".DS_Store": "",
1123            "foo": {
1124                "foo.rs": "mod another;\n",
1125                "another.rs": "// another",
1126            },
1127            "bar": {
1128                "bar.rs": "// bar",
1129            },
1130            "lib.rs": "mod foo;\nmod bar;\n",
1131        },
1132        ".DS_Store": "",
1133    }));
1134    cx.update(|cx| {
1135        cx.update_global::<SettingsStore, _>(|store, cx| {
1136            store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
1137                project_settings.file_scan_exclusions =
1138                    Some(vec!["**/foo/**".to_string(), "**/.DS_Store".to_string()]);
1139            });
1140        });
1141    });
1142
1143    let tree = Worktree::local(
1144        dir.path(),
1145        true,
1146        Arc::new(RealFs::default()),
1147        Default::default(),
1148        &mut cx.to_async(),
1149    )
1150    .await
1151    .unwrap();
1152    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1153        .await;
1154    tree.flush_fs_events(cx).await;
1155    tree.read_with(cx, |tree, _| {
1156        check_worktree_entries(
1157            tree,
1158            &[
1159                "src/foo/foo.rs",
1160                "src/foo/another.rs",
1161                "node_modules/.DS_Store",
1162                "src/.DS_Store",
1163                ".DS_Store",
1164            ],
1165            &["target", "node_modules"],
1166            &["src/lib.rs", "src/bar/bar.rs", ".gitignore"],
1167            &[],
1168        )
1169    });
1170
1171    cx.update(|cx| {
1172        cx.update_global::<SettingsStore, _>(|store, cx| {
1173            store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
1174                project_settings.file_scan_exclusions =
1175                    Some(vec!["**/node_modules/**".to_string()]);
1176            });
1177        });
1178    });
1179    tree.flush_fs_events(cx).await;
1180    cx.executor().run_until_parked();
1181    tree.read_with(cx, |tree, _| {
1182        check_worktree_entries(
1183            tree,
1184            &[
1185                "node_modules/prettier/package.json",
1186                "node_modules/.DS_Store",
1187                "node_modules",
1188            ],
1189            &["target"],
1190            &[
1191                ".gitignore",
1192                "src/lib.rs",
1193                "src/bar/bar.rs",
1194                "src/foo/foo.rs",
1195                "src/foo/another.rs",
1196                "src/.DS_Store",
1197                ".DS_Store",
1198            ],
1199            &[],
1200        )
1201    });
1202}
1203
1204#[gpui::test]
1205async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) {
1206    init_test(cx);
1207    cx.executor().allow_parking();
1208    let dir = temp_tree(json!({
1209        ".git": {
1210            "HEAD": "ref: refs/heads/main\n",
1211            "foo": "bar",
1212        },
1213        ".gitignore": "**/target\n/node_modules\ntest_output\n",
1214        "target": {
1215            "index": "blah2"
1216        },
1217        "node_modules": {
1218            ".DS_Store": "",
1219            "prettier": {
1220                "package.json": "{}",
1221            },
1222        },
1223        "src": {
1224            ".DS_Store": "",
1225            "foo": {
1226                "foo.rs": "mod another;\n",
1227                "another.rs": "// another",
1228            },
1229            "bar": {
1230                "bar.rs": "// bar",
1231            },
1232            "lib.rs": "mod foo;\nmod bar;\n",
1233        },
1234        ".DS_Store": "",
1235    }));
1236    cx.update(|cx| {
1237        cx.update_global::<SettingsStore, _>(|store, cx| {
1238            store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
1239                project_settings.file_scan_exclusions = Some(vec![
1240                    "**/.git".to_string(),
1241                    "node_modules/".to_string(),
1242                    "build_output".to_string(),
1243                ]);
1244            });
1245        });
1246    });
1247
1248    let tree = Worktree::local(
1249        dir.path(),
1250        true,
1251        Arc::new(RealFs::default()),
1252        Default::default(),
1253        &mut cx.to_async(),
1254    )
1255    .await
1256    .unwrap();
1257    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1258        .await;
1259    tree.flush_fs_events(cx).await;
1260    tree.read_with(cx, |tree, _| {
1261        check_worktree_entries(
1262            tree,
1263            &[
1264                ".git/HEAD",
1265                ".git/foo",
1266                "node_modules",
1267                "node_modules/.DS_Store",
1268                "node_modules/prettier",
1269                "node_modules/prettier/package.json",
1270            ],
1271            &["target"],
1272            &[
1273                ".DS_Store",
1274                "src/.DS_Store",
1275                "src/lib.rs",
1276                "src/foo/foo.rs",
1277                "src/foo/another.rs",
1278                "src/bar/bar.rs",
1279                ".gitignore",
1280            ],
1281            &[],
1282        )
1283    });
1284
1285    let new_excluded_dir = dir.path().join("build_output");
1286    let new_ignored_dir = dir.path().join("test_output");
1287    std::fs::create_dir_all(&new_excluded_dir)
1288        .unwrap_or_else(|e| panic!("Failed to create a {new_excluded_dir:?} directory: {e}"));
1289    std::fs::create_dir_all(&new_ignored_dir)
1290        .unwrap_or_else(|e| panic!("Failed to create a {new_ignored_dir:?} directory: {e}"));
1291    let node_modules_dir = dir.path().join("node_modules");
1292    let dot_git_dir = dir.path().join(".git");
1293    let src_dir = dir.path().join("src");
1294    for existing_dir in [&node_modules_dir, &dot_git_dir, &src_dir] {
1295        assert!(
1296            existing_dir.is_dir(),
1297            "Expect {existing_dir:?} to be present in the FS already"
1298        );
1299    }
1300
1301    for directory_for_new_file in [
1302        new_excluded_dir,
1303        new_ignored_dir,
1304        node_modules_dir,
1305        dot_git_dir,
1306        src_dir,
1307    ] {
1308        std::fs::write(directory_for_new_file.join("new_file"), "new file contents")
1309            .unwrap_or_else(|e| {
1310                panic!("Failed to create in {directory_for_new_file:?} a new file: {e}")
1311            });
1312    }
1313    tree.flush_fs_events(cx).await;
1314
1315    tree.read_with(cx, |tree, _| {
1316        check_worktree_entries(
1317            tree,
1318            &[
1319                ".git/HEAD",
1320                ".git/foo",
1321                ".git/new_file",
1322                "node_modules",
1323                "node_modules/.DS_Store",
1324                "node_modules/prettier",
1325                "node_modules/prettier/package.json",
1326                "node_modules/new_file",
1327                "build_output",
1328                "build_output/new_file",
1329                "test_output/new_file",
1330            ],
1331            &["target", "test_output"],
1332            &[
1333                ".DS_Store",
1334                "src/.DS_Store",
1335                "src/lib.rs",
1336                "src/foo/foo.rs",
1337                "src/foo/another.rs",
1338                "src/bar/bar.rs",
1339                "src/new_file",
1340                ".gitignore",
1341            ],
1342            &[],
1343        )
1344    });
1345}
1346
1347#[gpui::test]
1348async fn test_fs_events_in_dot_git_worktree(cx: &mut TestAppContext) {
1349    init_test(cx);
1350    cx.executor().allow_parking();
1351    let dir = temp_tree(json!({
1352        ".git": {
1353            "HEAD": "ref: refs/heads/main\n",
1354            "foo": "foo contents",
1355        },
1356    }));
1357    let dot_git_worktree_dir = dir.path().join(".git");
1358
1359    let tree = Worktree::local(
1360        dot_git_worktree_dir.clone(),
1361        true,
1362        Arc::new(RealFs::default()),
1363        Default::default(),
1364        &mut cx.to_async(),
1365    )
1366    .await
1367    .unwrap();
1368    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1369        .await;
1370    tree.flush_fs_events(cx).await;
1371    tree.read_with(cx, |tree, _| {
1372        check_worktree_entries(tree, &[], &["HEAD", "foo"], &[], &[])
1373    });
1374
1375    std::fs::write(dot_git_worktree_dir.join("new_file"), "new file contents")
1376        .unwrap_or_else(|e| panic!("Failed to create in {dot_git_worktree_dir:?} a new file: {e}"));
1377    tree.flush_fs_events(cx).await;
1378    tree.read_with(cx, |tree, _| {
1379        check_worktree_entries(tree, &[], &["HEAD", "foo", "new_file"], &[], &[])
1380    });
1381}
1382
1383#[gpui::test(iterations = 30)]
1384async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
1385    init_test(cx);
1386    let fs = FakeFs::new(cx.background_executor.clone());
1387    fs.insert_tree(
1388        "/root",
1389        json!({
1390            "b": {},
1391            "c": {},
1392            "d": {},
1393        }),
1394    )
1395    .await;
1396
1397    let tree = Worktree::local(
1398        "/root".as_ref(),
1399        true,
1400        fs,
1401        Default::default(),
1402        &mut cx.to_async(),
1403    )
1404    .await
1405    .unwrap();
1406
1407    let snapshot1 = tree.update(cx, |tree, cx| {
1408        let tree = tree.as_local_mut().unwrap();
1409        let snapshot = Arc::new(Mutex::new(tree.snapshot()));
1410        tree.observe_updates(0, cx, {
1411            let snapshot = snapshot.clone();
1412            let settings = tree.settings().clone();
1413            move |update| {
1414                snapshot
1415                    .lock()
1416                    .apply_remote_update(update, &settings.file_scan_inclusions)
1417                    .unwrap();
1418                async { true }
1419            }
1420        });
1421        snapshot
1422    });
1423
1424    let entry = tree
1425        .update(cx, |tree, cx| {
1426            tree.as_local_mut()
1427                .unwrap()
1428                .create_entry("a/e".as_ref(), true, cx)
1429        })
1430        .await
1431        .unwrap()
1432        .to_included()
1433        .unwrap();
1434    assert!(entry.is_dir());
1435
1436    cx.executor().run_until_parked();
1437    tree.read_with(cx, |tree, _| {
1438        assert_eq!(tree.entry_for_path("a/e").unwrap().kind, EntryKind::Dir);
1439    });
1440
1441    let snapshot2 = tree.update(cx, |tree, _| tree.as_local().unwrap().snapshot());
1442    assert_eq!(
1443        snapshot1.lock().entries(true, 0).collect::<Vec<_>>(),
1444        snapshot2.entries(true, 0).collect::<Vec<_>>()
1445    );
1446}
1447
1448#[gpui::test]
1449async fn test_bump_mtime_of_git_repo_workdir(cx: &mut TestAppContext) {
1450    init_test(cx);
1451
1452    // Create a worktree with a git directory.
1453    let fs = FakeFs::new(cx.background_executor.clone());
1454    fs.insert_tree(
1455        "/root",
1456        json!({
1457            ".git": {},
1458            "a.txt": "",
1459            "b":  {
1460                "c.txt": "",
1461            },
1462        }),
1463    )
1464    .await;
1465
1466    let tree = Worktree::local(
1467        "/root".as_ref(),
1468        true,
1469        fs.clone(),
1470        Default::default(),
1471        &mut cx.to_async(),
1472    )
1473    .await
1474    .unwrap();
1475    cx.executor().run_until_parked();
1476
1477    let (old_entry_ids, old_mtimes) = tree.read_with(cx, |tree, _| {
1478        (
1479            tree.entries(true, 0).map(|e| e.id).collect::<Vec<_>>(),
1480            tree.entries(true, 0).map(|e| e.mtime).collect::<Vec<_>>(),
1481        )
1482    });
1483
1484    // Regression test: after the directory is scanned, touch the git repo's
1485    // working directory, bumping its mtime. That directory keeps its project
1486    // entry id after the directories are re-scanned.
1487    fs.touch_path("/root").await;
1488    cx.executor().run_until_parked();
1489
1490    let (new_entry_ids, new_mtimes) = tree.read_with(cx, |tree, _| {
1491        (
1492            tree.entries(true, 0).map(|e| e.id).collect::<Vec<_>>(),
1493            tree.entries(true, 0).map(|e| e.mtime).collect::<Vec<_>>(),
1494        )
1495    });
1496    assert_eq!(new_entry_ids, old_entry_ids);
1497    assert_ne!(new_mtimes, old_mtimes);
1498
1499    // Regression test: changes to the git repository should still be
1500    // detected.
1501    fs.set_status_for_repo_via_git_operation(
1502        Path::new("/root/.git"),
1503        &[(
1504            Path::new("b/c.txt"),
1505            FileStatus::worktree(StatusCode::Modified),
1506        )],
1507    );
1508    cx.executor().run_until_parked();
1509
1510    let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
1511
1512    check_git_statuses(
1513        &snapshot,
1514        &[
1515            (Path::new(""), GitSummary::MODIFIED),
1516            (Path::new("a.txt"), GitSummary::UNCHANGED),
1517            (Path::new("b/c.txt"), GitSummary::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 = temp_tree(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 ModelContext<Worktree>) {
1888    let mut entries = tree.entries(true, 0).cloned().collect::<Vec<_>>();
1889    cx.subscribe(&cx.handle(), 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 ModelContext<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#[gpui::test]
2162async fn test_rename_work_directory(cx: &mut TestAppContext) {
2163    init_test(cx);
2164    cx.executor().allow_parking();
2165    let root = temp_tree(json!({
2166        "projects": {
2167            "project1": {
2168                "a": "",
2169                "b": "",
2170            }
2171        },
2172
2173    }));
2174    let root_path = root.path();
2175
2176    let tree = Worktree::local(
2177        root_path,
2178        true,
2179        Arc::new(RealFs::default()),
2180        Default::default(),
2181        &mut cx.to_async(),
2182    )
2183    .await
2184    .unwrap();
2185
2186    let repo = git_init(&root_path.join("projects/project1"));
2187    git_add("a", &repo);
2188    git_commit("init", &repo);
2189    std::fs::write(root_path.join("projects/project1/a"), "aa").ok();
2190
2191    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2192        .await;
2193
2194    tree.flush_fs_events(cx).await;
2195
2196    cx.read(|cx| {
2197        let tree = tree.read(cx);
2198        let repo = tree.repositories().iter().next().unwrap();
2199        assert_eq!(repo.path.as_ref(), Path::new("projects/project1"));
2200        assert_eq!(
2201            tree.status_for_file(Path::new("projects/project1/a")),
2202            Some(FileStatus::worktree(StatusCode::Modified)),
2203        );
2204        assert_eq!(
2205            tree.status_for_file(Path::new("projects/project1/b")),
2206            Some(FileStatus::Untracked),
2207        );
2208    });
2209
2210    std::fs::rename(
2211        root_path.join("projects/project1"),
2212        root_path.join("projects/project2"),
2213    )
2214    .ok();
2215    tree.flush_fs_events(cx).await;
2216
2217    cx.read(|cx| {
2218        let tree = tree.read(cx);
2219        let repo = tree.repositories().iter().next().unwrap();
2220        assert_eq!(repo.path.as_ref(), Path::new("projects/project2"));
2221        assert_eq!(
2222            tree.status_for_file(Path::new("projects/project2/a")),
2223            Some(FileStatus::worktree(StatusCode::Modified)),
2224        );
2225        assert_eq!(
2226            tree.status_for_file(Path::new("projects/project2/b")),
2227            Some(FileStatus::Untracked),
2228        );
2229    });
2230}
2231
2232#[gpui::test]
2233async fn test_git_repository_for_path(cx: &mut TestAppContext) {
2234    init_test(cx);
2235    cx.executor().allow_parking();
2236    let root = temp_tree(json!({
2237        "c.txt": "",
2238        "dir1": {
2239            ".git": {},
2240            "deps": {
2241                "dep1": {
2242                    ".git": {},
2243                    "src": {
2244                        "a.txt": ""
2245                    }
2246                }
2247            },
2248            "src": {
2249                "b.txt": ""
2250            }
2251        },
2252    }));
2253
2254    let tree = Worktree::local(
2255        root.path(),
2256        true,
2257        Arc::new(RealFs::default()),
2258        Default::default(),
2259        &mut cx.to_async(),
2260    )
2261    .await
2262    .unwrap();
2263
2264    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2265        .await;
2266    tree.flush_fs_events(cx).await;
2267
2268    tree.read_with(cx, |tree, _cx| {
2269        let tree = tree.as_local().unwrap();
2270
2271        assert!(tree.repository_for_path("c.txt".as_ref()).is_none());
2272
2273        let repo = tree.repository_for_path("dir1/src/b.txt".as_ref()).unwrap();
2274        assert_eq!(repo.path.as_ref(), Path::new("dir1"));
2275
2276        let repo = tree
2277            .repository_for_path("dir1/deps/dep1/src/a.txt".as_ref())
2278            .unwrap();
2279        assert_eq!(repo.path.as_ref(), Path::new("dir1/deps/dep1"));
2280
2281        let entries = tree.files(false, 0);
2282
2283        let paths_with_repos = tree
2284            .entries_with_repositories(entries)
2285            .map(|(entry, repo)| {
2286                (
2287                    entry.path.as_ref(),
2288                    repo.map(|repo| repo.path.to_path_buf()),
2289                )
2290            })
2291            .collect::<Vec<_>>();
2292
2293        assert_eq!(
2294            paths_with_repos,
2295            &[
2296                (Path::new("c.txt"), None),
2297                (
2298                    Path::new("dir1/deps/dep1/src/a.txt"),
2299                    Some(Path::new("dir1/deps/dep1").into())
2300                ),
2301                (Path::new("dir1/src/b.txt"), Some(Path::new("dir1").into())),
2302            ]
2303        );
2304    });
2305
2306    let repo_update_events = Arc::new(Mutex::new(vec![]));
2307    tree.update(cx, |_, cx| {
2308        let repo_update_events = repo_update_events.clone();
2309        cx.subscribe(&tree, move |_, _, event, _| {
2310            if let Event::UpdatedGitRepositories(update) = event {
2311                repo_update_events.lock().push(update.clone());
2312            }
2313        })
2314        .detach();
2315    });
2316
2317    std::fs::write(root.path().join("dir1/.git/random_new_file"), "hello").unwrap();
2318    tree.flush_fs_events(cx).await;
2319
2320    assert_eq!(
2321        repo_update_events.lock()[0]
2322            .iter()
2323            .map(|e| e.0.clone())
2324            .collect::<Vec<Arc<Path>>>(),
2325        vec![Path::new("dir1").into()]
2326    );
2327
2328    std::fs::remove_dir_all(root.path().join("dir1/.git")).unwrap();
2329    tree.flush_fs_events(cx).await;
2330
2331    tree.read_with(cx, |tree, _cx| {
2332        let tree = tree.as_local().unwrap();
2333
2334        assert!(tree
2335            .repository_for_path("dir1/src/b.txt".as_ref())
2336            .is_none());
2337    });
2338}
2339
2340#[gpui::test]
2341async fn test_file_status(cx: &mut TestAppContext) {
2342    init_test(cx);
2343    cx.executor().allow_parking();
2344    const IGNORE_RULE: &str = "**/target";
2345
2346    let root = temp_tree(json!({
2347        "project": {
2348            "a.txt": "a",
2349            "b.txt": "bb",
2350            "c": {
2351                "d": {
2352                    "e.txt": "eee"
2353                }
2354            },
2355            "f.txt": "ffff",
2356            "target": {
2357                "build_file": "???"
2358            },
2359            ".gitignore": IGNORE_RULE
2360        },
2361
2362    }));
2363
2364    const A_TXT: &str = "a.txt";
2365    const B_TXT: &str = "b.txt";
2366    const E_TXT: &str = "c/d/e.txt";
2367    const F_TXT: &str = "f.txt";
2368    const DOTGITIGNORE: &str = ".gitignore";
2369    const BUILD_FILE: &str = "target/build_file";
2370    let project_path = Path::new("project");
2371
2372    // Set up git repository before creating the worktree.
2373    let work_dir = root.path().join("project");
2374    let mut repo = git_init(work_dir.as_path());
2375    repo.add_ignore_rule(IGNORE_RULE).unwrap();
2376    git_add(A_TXT, &repo);
2377    git_add(E_TXT, &repo);
2378    git_add(DOTGITIGNORE, &repo);
2379    git_commit("Initial commit", &repo);
2380
2381    let tree = Worktree::local(
2382        root.path(),
2383        true,
2384        Arc::new(RealFs::default()),
2385        Default::default(),
2386        &mut cx.to_async(),
2387    )
2388    .await
2389    .unwrap();
2390
2391    tree.flush_fs_events(cx).await;
2392    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2393        .await;
2394    cx.executor().run_until_parked();
2395
2396    // Check that the right git state is observed on startup
2397    tree.read_with(cx, |tree, _cx| {
2398        let snapshot = tree.snapshot();
2399        assert_eq!(snapshot.repositories().iter().count(), 1);
2400        let repo_entry = snapshot.repositories().iter().next().unwrap();
2401        assert_eq!(repo_entry.path.as_ref(), Path::new("project"));
2402        assert!(repo_entry.location_in_repo.is_none());
2403
2404        assert_eq!(
2405            snapshot.status_for_file(project_path.join(B_TXT)),
2406            Some(FileStatus::Untracked),
2407        );
2408        assert_eq!(
2409            snapshot.status_for_file(project_path.join(F_TXT)),
2410            Some(FileStatus::Untracked),
2411        );
2412    });
2413
2414    // Modify a file in the working copy.
2415    std::fs::write(work_dir.join(A_TXT), "aa").unwrap();
2416    tree.flush_fs_events(cx).await;
2417    cx.executor().run_until_parked();
2418
2419    // The worktree detects that the file's git status has changed.
2420    tree.read_with(cx, |tree, _cx| {
2421        let snapshot = tree.snapshot();
2422        assert_eq!(
2423            snapshot.status_for_file(project_path.join(A_TXT)),
2424            Some(FileStatus::worktree(StatusCode::Modified)),
2425        );
2426    });
2427
2428    // Create a commit in the git repository.
2429    git_add(A_TXT, &repo);
2430    git_add(B_TXT, &repo);
2431    git_commit("Committing modified and added", &repo);
2432    tree.flush_fs_events(cx).await;
2433    cx.executor().run_until_parked();
2434
2435    // The worktree detects that the files' git status have changed.
2436    tree.read_with(cx, |tree, _cx| {
2437        let snapshot = tree.snapshot();
2438        assert_eq!(
2439            snapshot.status_for_file(project_path.join(F_TXT)),
2440            Some(FileStatus::Untracked),
2441        );
2442        assert_eq!(snapshot.status_for_file(project_path.join(B_TXT)), None);
2443        assert_eq!(snapshot.status_for_file(project_path.join(A_TXT)), None);
2444    });
2445
2446    // Modify files in the working copy and perform git operations on other files.
2447    git_reset(0, &repo);
2448    git_remove_index(Path::new(B_TXT), &repo);
2449    git_stash(&mut repo);
2450    std::fs::write(work_dir.join(E_TXT), "eeee").unwrap();
2451    std::fs::write(work_dir.join(BUILD_FILE), "this should be ignored").unwrap();
2452    tree.flush_fs_events(cx).await;
2453    cx.executor().run_until_parked();
2454
2455    // Check that more complex repo changes are tracked
2456    tree.read_with(cx, |tree, _cx| {
2457        let snapshot = tree.snapshot();
2458
2459        assert_eq!(snapshot.status_for_file(project_path.join(A_TXT)), None);
2460        assert_eq!(
2461            snapshot.status_for_file(project_path.join(B_TXT)),
2462            Some(FileStatus::Untracked),
2463        );
2464        assert_eq!(
2465            snapshot.status_for_file(project_path.join(E_TXT)),
2466            Some(FileStatus::worktree(StatusCode::Modified)),
2467        );
2468    });
2469
2470    std::fs::remove_file(work_dir.join(B_TXT)).unwrap();
2471    std::fs::remove_dir_all(work_dir.join("c")).unwrap();
2472    std::fs::write(
2473        work_dir.join(DOTGITIGNORE),
2474        [IGNORE_RULE, "f.txt"].join("\n"),
2475    )
2476    .unwrap();
2477
2478    git_add(Path::new(DOTGITIGNORE), &repo);
2479    git_commit("Committing modified git ignore", &repo);
2480
2481    tree.flush_fs_events(cx).await;
2482    cx.executor().run_until_parked();
2483
2484    let mut renamed_dir_name = "first_directory/second_directory";
2485    const RENAMED_FILE: &str = "rf.txt";
2486
2487    std::fs::create_dir_all(work_dir.join(renamed_dir_name)).unwrap();
2488    std::fs::write(
2489        work_dir.join(renamed_dir_name).join(RENAMED_FILE),
2490        "new-contents",
2491    )
2492    .unwrap();
2493
2494    tree.flush_fs_events(cx).await;
2495    cx.executor().run_until_parked();
2496
2497    tree.read_with(cx, |tree, _cx| {
2498        let snapshot = tree.snapshot();
2499        assert_eq!(
2500            snapshot.status_for_file(project_path.join(renamed_dir_name).join(RENAMED_FILE)),
2501            Some(FileStatus::Untracked),
2502        );
2503    });
2504
2505    renamed_dir_name = "new_first_directory/second_directory";
2506
2507    std::fs::rename(
2508        work_dir.join("first_directory"),
2509        work_dir.join("new_first_directory"),
2510    )
2511    .unwrap();
2512
2513    tree.flush_fs_events(cx).await;
2514    cx.executor().run_until_parked();
2515
2516    tree.read_with(cx, |tree, _cx| {
2517        let snapshot = tree.snapshot();
2518
2519        assert_eq!(
2520            snapshot.status_for_file(
2521                project_path
2522                    .join(Path::new(renamed_dir_name))
2523                    .join(RENAMED_FILE)
2524            ),
2525            Some(FileStatus::Untracked),
2526        );
2527    });
2528}
2529
2530#[gpui::test]
2531async fn test_git_repository_status(cx: &mut TestAppContext) {
2532    init_test(cx);
2533    cx.executor().allow_parking();
2534
2535    let root = temp_tree(json!({
2536        "project": {
2537            "a.txt": "a",    // Modified
2538            "b.txt": "bb",   // Added
2539            "c.txt": "ccc",  // Unchanged
2540            "d.txt": "dddd", // Deleted
2541        },
2542
2543    }));
2544
2545    // Set up git repository before creating the worktree.
2546    let work_dir = root.path().join("project");
2547    let repo = git_init(work_dir.as_path());
2548    git_add("a.txt", &repo);
2549    git_add("c.txt", &repo);
2550    git_add("d.txt", &repo);
2551    git_commit("Initial commit", &repo);
2552    std::fs::remove_file(work_dir.join("d.txt")).unwrap();
2553    std::fs::write(work_dir.join("a.txt"), "aa").unwrap();
2554
2555    let tree = Worktree::local(
2556        root.path(),
2557        true,
2558        Arc::new(RealFs::default()),
2559        Default::default(),
2560        &mut cx.to_async(),
2561    )
2562    .await
2563    .unwrap();
2564
2565    tree.flush_fs_events(cx).await;
2566    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2567        .await;
2568    cx.executor().run_until_parked();
2569
2570    // Check that the right git state is observed on startup
2571    tree.read_with(cx, |tree, _cx| {
2572        let snapshot = tree.snapshot();
2573        let repo = snapshot.repositories().iter().next().unwrap();
2574        let entries = repo.status().collect::<Vec<_>>();
2575
2576        assert_eq!(entries.len(), 3);
2577        assert_eq!(entries[0].repo_path.as_ref(), Path::new("a.txt"));
2578        assert_eq!(
2579            entries[0].status,
2580            FileStatus::worktree(StatusCode::Modified)
2581        );
2582        assert_eq!(entries[1].repo_path.as_ref(), Path::new("b.txt"));
2583        assert_eq!(entries[1].status, FileStatus::Untracked);
2584        assert_eq!(entries[2].repo_path.as_ref(), Path::new("d.txt"));
2585        assert_eq!(entries[2].status, FileStatus::worktree(StatusCode::Deleted));
2586    });
2587
2588    std::fs::write(work_dir.join("c.txt"), "some changes").unwrap();
2589    eprintln!("File c.txt has been modified");
2590
2591    tree.flush_fs_events(cx).await;
2592    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2593        .await;
2594    cx.executor().run_until_parked();
2595
2596    tree.read_with(cx, |tree, _cx| {
2597        let snapshot = tree.snapshot();
2598        let repository = snapshot.repositories().iter().next().unwrap();
2599        let entries = repository.status().collect::<Vec<_>>();
2600
2601        std::assert_eq!(entries.len(), 4, "entries: {entries:?}");
2602        assert_eq!(entries[0].repo_path.as_ref(), Path::new("a.txt"));
2603        assert_eq!(
2604            entries[0].status,
2605            FileStatus::worktree(StatusCode::Modified)
2606        );
2607        assert_eq!(entries[1].repo_path.as_ref(), Path::new("b.txt"));
2608        assert_eq!(entries[1].status, FileStatus::Untracked);
2609        // Status updated
2610        assert_eq!(entries[2].repo_path.as_ref(), Path::new("c.txt"));
2611        assert_eq!(
2612            entries[2].status,
2613            FileStatus::worktree(StatusCode::Modified)
2614        );
2615        assert_eq!(entries[3].repo_path.as_ref(), Path::new("d.txt"));
2616        assert_eq!(entries[3].status, FileStatus::worktree(StatusCode::Deleted));
2617    });
2618
2619    git_add("a.txt", &repo);
2620    git_add("c.txt", &repo);
2621    git_remove_index(Path::new("d.txt"), &repo);
2622    git_commit("Another commit", &repo);
2623    tree.flush_fs_events(cx).await;
2624    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2625        .await;
2626    cx.executor().run_until_parked();
2627
2628    std::fs::remove_file(work_dir.join("a.txt")).unwrap();
2629    std::fs::remove_file(work_dir.join("b.txt")).unwrap();
2630    tree.flush_fs_events(cx).await;
2631    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2632        .await;
2633    cx.executor().run_until_parked();
2634
2635    tree.read_with(cx, |tree, _cx| {
2636        let snapshot = tree.snapshot();
2637        let repo = snapshot.repositories().iter().next().unwrap();
2638        let entries = repo.status().collect::<Vec<_>>();
2639
2640        // Deleting an untracked entry, b.txt, should leave no status
2641        // a.txt was tracked, and so should have a status
2642        assert_eq!(
2643            entries.len(),
2644            1,
2645            "Entries length was incorrect\n{:#?}",
2646            &entries
2647        );
2648        assert_eq!(entries[0].repo_path.as_ref(), Path::new("a.txt"));
2649        assert_eq!(entries[0].status, FileStatus::worktree(StatusCode::Deleted));
2650    });
2651}
2652
2653#[gpui::test]
2654async fn test_repository_subfolder_git_status(cx: &mut TestAppContext) {
2655    init_test(cx);
2656    cx.executor().allow_parking();
2657
2658    let root = temp_tree(json!({
2659        "my-repo": {
2660            // .git folder will go here
2661            "a.txt": "a",
2662            "sub-folder-1": {
2663                "sub-folder-2": {
2664                    "c.txt": "cc",
2665                    "d": {
2666                        "e.txt": "eee"
2667                    }
2668                },
2669            }
2670        },
2671
2672    }));
2673
2674    const C_TXT: &str = "sub-folder-1/sub-folder-2/c.txt";
2675    const E_TXT: &str = "sub-folder-1/sub-folder-2/d/e.txt";
2676
2677    // Set up git repository before creating the worktree.
2678    let git_repo_work_dir = root.path().join("my-repo");
2679    let repo = git_init(git_repo_work_dir.as_path());
2680    git_add(C_TXT, &repo);
2681    git_commit("Initial commit", &repo);
2682
2683    // Open the worktree in subfolder
2684    let project_root = Path::new("my-repo/sub-folder-1/sub-folder-2");
2685    let tree = Worktree::local(
2686        root.path().join(project_root),
2687        true,
2688        Arc::new(RealFs::default()),
2689        Default::default(),
2690        &mut cx.to_async(),
2691    )
2692    .await
2693    .unwrap();
2694
2695    tree.flush_fs_events(cx).await;
2696    tree.flush_fs_events_in_root_git_repository(cx).await;
2697    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2698        .await;
2699    cx.executor().run_until_parked();
2700
2701    // Ensure that the git status is loaded correctly
2702    tree.read_with(cx, |tree, _cx| {
2703        let snapshot = tree.snapshot();
2704        assert_eq!(snapshot.repositories().iter().count(), 1);
2705        let repo = snapshot.repositories().iter().next().unwrap();
2706        // Path is blank because the working directory of
2707        // the git repository is located at the root of the project
2708        assert_eq!(repo.path.as_ref(), Path::new(""));
2709
2710        // This is the missing path between the root of the project (sub-folder-2) and its
2711        // location relative to the root of the repository.
2712        assert_eq!(
2713            repo.location_in_repo,
2714            Some(Arc::from(Path::new("sub-folder-1/sub-folder-2")))
2715        );
2716
2717        assert_eq!(snapshot.status_for_file("c.txt"), None);
2718        assert_eq!(
2719            snapshot.status_for_file("d/e.txt"),
2720            Some(FileStatus::Untracked)
2721        );
2722    });
2723
2724    // Now we simulate FS events, but ONLY in the .git folder that's outside
2725    // of out project root.
2726    // Meaning: we don't produce any FS events for files inside the project.
2727    git_add(E_TXT, &repo);
2728    git_commit("Second commit", &repo);
2729    tree.flush_fs_events_in_root_git_repository(cx).await;
2730    cx.executor().run_until_parked();
2731
2732    tree.read_with(cx, |tree, _cx| {
2733        let snapshot = tree.snapshot();
2734
2735        assert!(snapshot.repositories().iter().next().is_some());
2736
2737        assert_eq!(snapshot.status_for_file("c.txt"), None);
2738        assert_eq!(snapshot.status_for_file("d/e.txt"), None);
2739    });
2740}
2741
2742#[gpui::test]
2743async fn test_traverse_with_git_status(cx: &mut TestAppContext) {
2744    init_test(cx);
2745    let fs = FakeFs::new(cx.background_executor.clone());
2746    fs.insert_tree(
2747        "/root",
2748        json!({
2749            "x": {
2750                ".git": {},
2751                "x1.txt": "foo",
2752                "x2.txt": "bar",
2753                "y": {
2754                    ".git": {},
2755                    "y1.txt": "baz",
2756                    "y2.txt": "qux"
2757                },
2758                "z.txt": "sneaky..."
2759            },
2760            "z": {
2761                ".git": {},
2762                "z1.txt": "quux",
2763                "z2.txt": "quuux"
2764            }
2765        }),
2766    )
2767    .await;
2768
2769    fs.set_status_for_repo_via_git_operation(
2770        Path::new("/root/x/.git"),
2771        &[
2772            (
2773                Path::new("x2.txt"),
2774                FileStatus::worktree(StatusCode::Modified),
2775            ),
2776            (Path::new("z.txt"), FileStatus::worktree(StatusCode::Added)),
2777        ],
2778    );
2779    fs.set_status_for_repo_via_git_operation(
2780        Path::new("/root/x/y/.git"),
2781        &[(Path::new("y1.txt"), CONFLICT)],
2782    );
2783    fs.set_status_for_repo_via_git_operation(
2784        Path::new("/root/z/.git"),
2785        &[(Path::new("z2.txt"), FileStatus::worktree(StatusCode::Added))],
2786    );
2787
2788    let tree = Worktree::local(
2789        Path::new("/root"),
2790        true,
2791        fs.clone(),
2792        Default::default(),
2793        &mut cx.to_async(),
2794    )
2795    .await
2796    .unwrap();
2797
2798    tree.flush_fs_events(cx).await;
2799    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2800        .await;
2801    cx.executor().run_until_parked();
2802
2803    let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
2804
2805    let mut traversal = snapshot
2806        .traverse_from_path(true, false, true, Path::new("x"))
2807        .with_git_statuses();
2808
2809    let entry = traversal.next().unwrap();
2810    assert_eq!(entry.path.as_ref(), Path::new("x/x1.txt"));
2811    assert_eq!(entry.git_summary, GitSummary::UNCHANGED);
2812    let entry = traversal.next().unwrap();
2813    assert_eq!(entry.path.as_ref(), Path::new("x/x2.txt"));
2814    assert_eq!(entry.git_summary, GitSummary::MODIFIED);
2815    let entry = traversal.next().unwrap();
2816    assert_eq!(entry.path.as_ref(), Path::new("x/y/y1.txt"));
2817    assert_eq!(entry.git_summary, GitSummary::CONFLICT);
2818    let entry = traversal.next().unwrap();
2819    assert_eq!(entry.path.as_ref(), Path::new("x/y/y2.txt"));
2820    assert_eq!(entry.git_summary, GitSummary::UNCHANGED);
2821    let entry = traversal.next().unwrap();
2822    assert_eq!(entry.path.as_ref(), Path::new("x/z.txt"));
2823    assert_eq!(entry.git_summary, GitSummary::ADDED);
2824    let entry = traversal.next().unwrap();
2825    assert_eq!(entry.path.as_ref(), Path::new("z/z1.txt"));
2826    assert_eq!(entry.git_summary, GitSummary::UNCHANGED);
2827    let entry = traversal.next().unwrap();
2828    assert_eq!(entry.path.as_ref(), Path::new("z/z2.txt"));
2829    assert_eq!(entry.git_summary, GitSummary::ADDED);
2830}
2831
2832#[gpui::test]
2833async fn test_propagate_git_statuses(cx: &mut TestAppContext) {
2834    init_test(cx);
2835    let fs = FakeFs::new(cx.background_executor.clone());
2836    fs.insert_tree(
2837        "/root",
2838        json!({
2839            ".git": {},
2840            "a": {
2841                "b": {
2842                    "c1.txt": "",
2843                    "c2.txt": "",
2844                },
2845                "d": {
2846                    "e1.txt": "",
2847                    "e2.txt": "",
2848                    "e3.txt": "",
2849                }
2850            },
2851            "f": {
2852                "no-status.txt": ""
2853            },
2854            "g": {
2855                "h1.txt": "",
2856                "h2.txt": ""
2857            },
2858        }),
2859    )
2860    .await;
2861
2862    fs.set_status_for_repo_via_git_operation(
2863        Path::new("/root/.git"),
2864        &[
2865            (
2866                Path::new("a/b/c1.txt"),
2867                FileStatus::worktree(StatusCode::Added),
2868            ),
2869            (
2870                Path::new("a/d/e2.txt"),
2871                FileStatus::worktree(StatusCode::Modified),
2872            ),
2873            (Path::new("g/h2.txt"), CONFLICT),
2874        ],
2875    );
2876
2877    let tree = Worktree::local(
2878        Path::new("/root"),
2879        true,
2880        fs.clone(),
2881        Default::default(),
2882        &mut cx.to_async(),
2883    )
2884    .await
2885    .unwrap();
2886
2887    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2888        .await;
2889
2890    cx.executor().run_until_parked();
2891    let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
2892
2893    check_git_statuses(
2894        &snapshot,
2895        &[
2896            (
2897                Path::new(""),
2898                GitSummary::CONFLICT + GitSummary::MODIFIED + GitSummary::ADDED,
2899            ),
2900            (Path::new("g"), GitSummary::CONFLICT),
2901            (Path::new("g/h2.txt"), GitSummary::CONFLICT),
2902        ],
2903    );
2904
2905    check_git_statuses(
2906        &snapshot,
2907        &[
2908            (
2909                Path::new(""),
2910                GitSummary::CONFLICT + GitSummary::ADDED + GitSummary::MODIFIED,
2911            ),
2912            (Path::new("a"), GitSummary::ADDED + GitSummary::MODIFIED),
2913            (Path::new("a/b"), GitSummary::ADDED),
2914            (Path::new("a/b/c1.txt"), GitSummary::ADDED),
2915            (Path::new("a/b/c2.txt"), GitSummary::UNCHANGED),
2916            (Path::new("a/d"), GitSummary::MODIFIED),
2917            (Path::new("a/d/e2.txt"), GitSummary::MODIFIED),
2918            (Path::new("f"), GitSummary::UNCHANGED),
2919            (Path::new("f/no-status.txt"), GitSummary::UNCHANGED),
2920            (Path::new("g"), GitSummary::CONFLICT),
2921            (Path::new("g/h2.txt"), GitSummary::CONFLICT),
2922        ],
2923    );
2924
2925    check_git_statuses(
2926        &snapshot,
2927        &[
2928            (Path::new("a/b"), GitSummary::ADDED),
2929            (Path::new("a/b/c1.txt"), GitSummary::ADDED),
2930            (Path::new("a/b/c2.txt"), GitSummary::UNCHANGED),
2931            (Path::new("a/d"), GitSummary::MODIFIED),
2932            (Path::new("a/d/e1.txt"), GitSummary::UNCHANGED),
2933            (Path::new("a/d/e2.txt"), GitSummary::MODIFIED),
2934            (Path::new("f"), GitSummary::UNCHANGED),
2935            (Path::new("f/no-status.txt"), GitSummary::UNCHANGED),
2936            (Path::new("g"), GitSummary::CONFLICT),
2937        ],
2938    );
2939
2940    check_git_statuses(
2941        &snapshot,
2942        &[
2943            (Path::new("a/b/c1.txt"), GitSummary::ADDED),
2944            (Path::new("a/b/c2.txt"), GitSummary::UNCHANGED),
2945            (Path::new("a/d/e1.txt"), GitSummary::UNCHANGED),
2946            (Path::new("a/d/e2.txt"), GitSummary::MODIFIED),
2947            (Path::new("f/no-status.txt"), GitSummary::UNCHANGED),
2948        ],
2949    );
2950}
2951
2952#[gpui::test]
2953async fn test_propagate_statuses_for_repos_under_project(cx: &mut TestAppContext) {
2954    init_test(cx);
2955    let fs = FakeFs::new(cx.background_executor.clone());
2956    fs.insert_tree(
2957        "/root",
2958        json!({
2959            "x": {
2960                ".git": {},
2961                "x1.txt": "foo",
2962                "x2.txt": "bar"
2963            },
2964            "y": {
2965                ".git": {},
2966                "y1.txt": "baz",
2967                "y2.txt": "qux"
2968            },
2969            "z": {
2970                ".git": {},
2971                "z1.txt": "quux",
2972                "z2.txt": "quuux"
2973            }
2974        }),
2975    )
2976    .await;
2977
2978    fs.set_status_for_repo_via_git_operation(
2979        Path::new("/root/x/.git"),
2980        &[(Path::new("x1.txt"), FileStatus::worktree(StatusCode::Added))],
2981    );
2982    fs.set_status_for_repo_via_git_operation(
2983        Path::new("/root/y/.git"),
2984        &[
2985            (Path::new("y1.txt"), CONFLICT),
2986            (
2987                Path::new("y2.txt"),
2988                FileStatus::worktree(StatusCode::Modified),
2989            ),
2990        ],
2991    );
2992    fs.set_status_for_repo_via_git_operation(
2993        Path::new("/root/z/.git"),
2994        &[(
2995            Path::new("z2.txt"),
2996            FileStatus::worktree(StatusCode::Modified),
2997        )],
2998    );
2999
3000    let tree = Worktree::local(
3001        Path::new("/root"),
3002        true,
3003        fs.clone(),
3004        Default::default(),
3005        &mut cx.to_async(),
3006    )
3007    .await
3008    .unwrap();
3009
3010    tree.flush_fs_events(cx).await;
3011    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
3012        .await;
3013    cx.executor().run_until_parked();
3014
3015    let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
3016
3017    check_git_statuses(
3018        &snapshot,
3019        &[
3020            (Path::new("x"), GitSummary::ADDED),
3021            (Path::new("x/x1.txt"), GitSummary::ADDED),
3022        ],
3023    );
3024
3025    check_git_statuses(
3026        &snapshot,
3027        &[
3028            (Path::new("y"), GitSummary::CONFLICT + GitSummary::MODIFIED),
3029            (Path::new("y/y1.txt"), GitSummary::CONFLICT),
3030            (Path::new("y/y2.txt"), GitSummary::MODIFIED),
3031        ],
3032    );
3033
3034    check_git_statuses(
3035        &snapshot,
3036        &[
3037            (Path::new("z"), GitSummary::MODIFIED),
3038            (Path::new("z/z2.txt"), GitSummary::MODIFIED),
3039        ],
3040    );
3041
3042    check_git_statuses(
3043        &snapshot,
3044        &[
3045            (Path::new("x"), GitSummary::ADDED),
3046            (Path::new("x/x1.txt"), GitSummary::ADDED),
3047        ],
3048    );
3049
3050    check_git_statuses(
3051        &snapshot,
3052        &[
3053            (Path::new("x"), GitSummary::ADDED),
3054            (Path::new("x/x1.txt"), GitSummary::ADDED),
3055            (Path::new("x/x2.txt"), GitSummary::UNCHANGED),
3056            (Path::new("y"), GitSummary::CONFLICT + GitSummary::MODIFIED),
3057            (Path::new("y/y1.txt"), GitSummary::CONFLICT),
3058            (Path::new("y/y2.txt"), GitSummary::MODIFIED),
3059            (Path::new("z"), GitSummary::MODIFIED),
3060            (Path::new("z/z1.txt"), GitSummary::UNCHANGED),
3061            (Path::new("z/z2.txt"), GitSummary::MODIFIED),
3062        ],
3063    );
3064}
3065
3066#[gpui::test]
3067async fn test_propagate_statuses_for_nested_repos(cx: &mut TestAppContext) {
3068    init_test(cx);
3069    let fs = FakeFs::new(cx.background_executor.clone());
3070    fs.insert_tree(
3071        "/root",
3072        json!({
3073            "x": {
3074                ".git": {},
3075                "x1.txt": "foo",
3076                "x2.txt": "bar",
3077                "y": {
3078                    ".git": {},
3079                    "y1.txt": "baz",
3080                    "y2.txt": "qux"
3081                },
3082                "z.txt": "sneaky..."
3083            },
3084            "z": {
3085                ".git": {},
3086                "z1.txt": "quux",
3087                "z2.txt": "quuux"
3088            }
3089        }),
3090    )
3091    .await;
3092
3093    fs.set_status_for_repo_via_git_operation(
3094        Path::new("/root/x/.git"),
3095        &[
3096            (
3097                Path::new("x2.txt"),
3098                FileStatus::worktree(StatusCode::Modified),
3099            ),
3100            (Path::new("z.txt"), FileStatus::worktree(StatusCode::Added)),
3101        ],
3102    );
3103    fs.set_status_for_repo_via_git_operation(
3104        Path::new("/root/x/y/.git"),
3105        &[(Path::new("y1.txt"), CONFLICT)],
3106    );
3107
3108    fs.set_status_for_repo_via_git_operation(
3109        Path::new("/root/z/.git"),
3110        &[(Path::new("z2.txt"), FileStatus::worktree(StatusCode::Added))],
3111    );
3112
3113    let tree = Worktree::local(
3114        Path::new("/root"),
3115        true,
3116        fs.clone(),
3117        Default::default(),
3118        &mut cx.to_async(),
3119    )
3120    .await
3121    .unwrap();
3122
3123    tree.flush_fs_events(cx).await;
3124    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
3125        .await;
3126    cx.executor().run_until_parked();
3127
3128    let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
3129
3130    // Sanity check the propagation for x/y and z
3131    check_git_statuses(
3132        &snapshot,
3133        &[
3134            (Path::new("x/y"), GitSummary::CONFLICT),
3135            (Path::new("x/y/y1.txt"), GitSummary::CONFLICT),
3136            (Path::new("x/y/y2.txt"), GitSummary::UNCHANGED),
3137        ],
3138    );
3139    check_git_statuses(
3140        &snapshot,
3141        &[
3142            (Path::new("z"), GitSummary::ADDED),
3143            (Path::new("z/z1.txt"), GitSummary::UNCHANGED),
3144            (Path::new("z/z2.txt"), GitSummary::ADDED),
3145        ],
3146    );
3147
3148    // Test one of the fundamental cases of propagation blocking, the transition from one git repository to another
3149    check_git_statuses(
3150        &snapshot,
3151        &[
3152            (Path::new("x"), GitSummary::MODIFIED + GitSummary::ADDED),
3153            (Path::new("x/y"), GitSummary::CONFLICT),
3154            (Path::new("x/y/y1.txt"), GitSummary::CONFLICT),
3155        ],
3156    );
3157
3158    // Sanity check everything around it
3159    check_git_statuses(
3160        &snapshot,
3161        &[
3162            (Path::new("x"), GitSummary::MODIFIED + GitSummary::ADDED),
3163            (Path::new("x/x1.txt"), GitSummary::UNCHANGED),
3164            (Path::new("x/x2.txt"), GitSummary::MODIFIED),
3165            (Path::new("x/y"), GitSummary::CONFLICT),
3166            (Path::new("x/y/y1.txt"), GitSummary::CONFLICT),
3167            (Path::new("x/y/y2.txt"), GitSummary::UNCHANGED),
3168            (Path::new("x/z.txt"), GitSummary::ADDED),
3169        ],
3170    );
3171
3172    // Test the other fundamental case, transitioning from git repository to non-git repository
3173    check_git_statuses(
3174        &snapshot,
3175        &[
3176            (Path::new(""), GitSummary::UNCHANGED),
3177            (Path::new("x"), GitSummary::MODIFIED + GitSummary::ADDED),
3178            (Path::new("x/x1.txt"), GitSummary::UNCHANGED),
3179        ],
3180    );
3181
3182    // And all together now
3183    check_git_statuses(
3184        &snapshot,
3185        &[
3186            (Path::new(""), GitSummary::UNCHANGED),
3187            (Path::new("x"), GitSummary::MODIFIED + GitSummary::ADDED),
3188            (Path::new("x/x1.txt"), GitSummary::UNCHANGED),
3189            (Path::new("x/x2.txt"), GitSummary::MODIFIED),
3190            (Path::new("x/y"), GitSummary::CONFLICT),
3191            (Path::new("x/y/y1.txt"), GitSummary::CONFLICT),
3192            (Path::new("x/y/y2.txt"), GitSummary::UNCHANGED),
3193            (Path::new("x/z.txt"), GitSummary::ADDED),
3194            (Path::new("z"), GitSummary::ADDED),
3195            (Path::new("z/z1.txt"), GitSummary::UNCHANGED),
3196            (Path::new("z/z2.txt"), GitSummary::ADDED),
3197        ],
3198    );
3199}
3200
3201#[gpui::test]
3202async fn test_private_single_file_worktree(cx: &mut TestAppContext) {
3203    init_test(cx);
3204    let fs = FakeFs::new(cx.background_executor.clone());
3205    fs.insert_tree("/", json!({".env": "PRIVATE=secret\n"}))
3206        .await;
3207    let tree = Worktree::local(
3208        Path::new("/.env"),
3209        true,
3210        fs.clone(),
3211        Default::default(),
3212        &mut cx.to_async(),
3213    )
3214    .await
3215    .unwrap();
3216    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
3217        .await;
3218    tree.read_with(cx, |tree, _| {
3219        let entry = tree.entry_for_path("").unwrap();
3220        assert!(entry.is_private);
3221    });
3222}
3223
3224#[track_caller]
3225fn check_git_statuses(snapshot: &Snapshot, expected_statuses: &[(&Path, GitSummary)]) {
3226    let mut traversal = snapshot
3227        .traverse_from_path(true, true, false, "".as_ref())
3228        .with_git_statuses();
3229    let found_statuses = expected_statuses
3230        .iter()
3231        .map(|&(path, _)| {
3232            let git_entry = traversal
3233                .find(|git_entry| &*git_entry.path == path)
3234                .unwrap_or_else(|| panic!("Traversal has no entry for {path:?}"));
3235            (path, git_entry.git_summary)
3236        })
3237        .collect::<Vec<_>>();
3238    assert_eq!(found_statuses, expected_statuses);
3239}
3240
3241#[track_caller]
3242fn git_init(path: &Path) -> git2::Repository {
3243    git2::Repository::init(path).expect("Failed to initialize git repository")
3244}
3245
3246#[track_caller]
3247fn git_add<P: AsRef<Path>>(path: P, repo: &git2::Repository) {
3248    let path = path.as_ref();
3249    let mut index = repo.index().expect("Failed to get index");
3250    index.add_path(path).expect("Failed to add file");
3251    index.write().expect("Failed to write index");
3252}
3253
3254#[track_caller]
3255fn git_remove_index(path: &Path, repo: &git2::Repository) {
3256    let mut index = repo.index().expect("Failed to get index");
3257    index.remove_path(path).expect("Failed to add file");
3258    index.write().expect("Failed to write index");
3259}
3260
3261#[track_caller]
3262fn git_commit(msg: &'static str, repo: &git2::Repository) {
3263    use git2::Signature;
3264
3265    let signature = Signature::now("test", "test@zed.dev").unwrap();
3266    let oid = repo.index().unwrap().write_tree().unwrap();
3267    let tree = repo.find_tree(oid).unwrap();
3268    if let Ok(head) = repo.head() {
3269        let parent_obj = head.peel(git2::ObjectType::Commit).unwrap();
3270
3271        let parent_commit = parent_obj.as_commit().unwrap();
3272
3273        repo.commit(
3274            Some("HEAD"),
3275            &signature,
3276            &signature,
3277            msg,
3278            &tree,
3279            &[parent_commit],
3280        )
3281        .expect("Failed to commit with parent");
3282    } else {
3283        repo.commit(Some("HEAD"), &signature, &signature, msg, &tree, &[])
3284            .expect("Failed to commit");
3285    }
3286}
3287
3288#[track_caller]
3289fn git_stash(repo: &mut git2::Repository) {
3290    use git2::Signature;
3291
3292    let signature = Signature::now("test", "test@zed.dev").unwrap();
3293    repo.stash_save(&signature, "N/A", None)
3294        .expect("Failed to stash");
3295}
3296
3297#[track_caller]
3298fn git_reset(offset: usize, repo: &git2::Repository) {
3299    let head = repo.head().expect("Couldn't get repo head");
3300    let object = head.peel(git2::ObjectType::Commit).unwrap();
3301    let commit = object.as_commit().unwrap();
3302    let new_head = commit
3303        .parents()
3304        .inspect(|parnet| {
3305            parnet.message();
3306        })
3307        .nth(offset)
3308        .expect("Not enough history");
3309    repo.reset(new_head.as_object(), git2::ResetType::Soft, None)
3310        .expect("Could not reset");
3311}
3312
3313#[allow(dead_code)]
3314#[track_caller]
3315fn git_status(repo: &git2::Repository) -> collections::HashMap<String, git2::Status> {
3316    repo.statuses(None)
3317        .unwrap()
3318        .iter()
3319        .map(|status| (status.path().unwrap().to_string(), status.status()))
3320        .collect()
3321}
3322
3323#[track_caller]
3324fn check_worktree_entries(
3325    tree: &Worktree,
3326    expected_excluded_paths: &[&str],
3327    expected_ignored_paths: &[&str],
3328    expected_tracked_paths: &[&str],
3329    expected_included_paths: &[&str],
3330) {
3331    for path in expected_excluded_paths {
3332        let entry = tree.entry_for_path(path);
3333        assert!(
3334            entry.is_none(),
3335            "expected path '{path}' to be excluded, but got entry: {entry:?}",
3336        );
3337    }
3338    for path in expected_ignored_paths {
3339        let entry = tree
3340            .entry_for_path(path)
3341            .unwrap_or_else(|| panic!("Missing entry for expected ignored path '{path}'"));
3342        assert!(
3343            entry.is_ignored,
3344            "expected path '{path}' to be ignored, but got entry: {entry:?}",
3345        );
3346    }
3347    for path in expected_tracked_paths {
3348        let entry = tree
3349            .entry_for_path(path)
3350            .unwrap_or_else(|| panic!("Missing entry for expected tracked path '{path}'"));
3351        assert!(
3352            !entry.is_ignored || entry.is_always_included,
3353            "expected path '{path}' to be tracked, but got entry: {entry:?}",
3354        );
3355    }
3356    for path in expected_included_paths {
3357        let entry = tree
3358            .entry_for_path(path)
3359            .unwrap_or_else(|| panic!("Missing entry for expected included path '{path}'"));
3360        assert!(
3361            entry.is_always_included,
3362            "expected path '{path}' to always be included, but got entry: {entry:?}",
3363        );
3364    }
3365}
3366
3367fn init_test(cx: &mut gpui::TestAppContext) {
3368    if std::env::var("RUST_LOG").is_ok() {
3369        env_logger::try_init().ok();
3370    }
3371
3372    cx.update(|cx| {
3373        let settings_store = SettingsStore::test(cx);
3374        cx.set_global(settings_store);
3375        WorktreeSettings::register(cx);
3376    });
3377}
3378
3379fn assert_entry_git_state(
3380    tree: &Worktree,
3381    path: &str,
3382    worktree_status: Option<StatusCode>,
3383    is_ignored: bool,
3384) {
3385    let entry = tree.entry_for_path(path).expect("entry {path} not found");
3386    let status = tree.status_for_file(Path::new(path));
3387    let expected = worktree_status.map(|worktree_status| {
3388        TrackedStatus {
3389            worktree_status,
3390            index_status: StatusCode::Unmodified,
3391        }
3392        .into()
3393    });
3394    assert_eq!(
3395        status, expected,
3396        "expected {path} to have git status: {expected:?}"
3397    );
3398    assert_eq!(
3399        entry.is_ignored, is_ignored,
3400        "expected {path} to have is_ignored: {is_ignored}"
3401    );
3402}