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(""), MODIFIED),
1516            (Path::new("a.txt"), GitSummary::UNCHANGED),
1517            (Path::new("b/c.txt"), MODIFIED),
1518        ],
1519    );
1520}
1521
1522#[gpui::test]
1523async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
1524    init_test(cx);
1525    cx.executor().allow_parking();
1526
1527    let fs_fake = FakeFs::new(cx.background_executor.clone());
1528    fs_fake
1529        .insert_tree(
1530            "/root",
1531            json!({
1532                "a": {},
1533            }),
1534        )
1535        .await;
1536
1537    let tree_fake = Worktree::local(
1538        "/root".as_ref(),
1539        true,
1540        fs_fake,
1541        Default::default(),
1542        &mut cx.to_async(),
1543    )
1544    .await
1545    .unwrap();
1546
1547    let entry = tree_fake
1548        .update(cx, |tree, cx| {
1549            tree.as_local_mut()
1550                .unwrap()
1551                .create_entry("a/b/c/d.txt".as_ref(), false, cx)
1552        })
1553        .await
1554        .unwrap()
1555        .to_included()
1556        .unwrap();
1557    assert!(entry.is_file());
1558
1559    cx.executor().run_until_parked();
1560    tree_fake.read_with(cx, |tree, _| {
1561        assert!(tree.entry_for_path("a/b/c/d.txt").unwrap().is_file());
1562        assert!(tree.entry_for_path("a/b/c/").unwrap().is_dir());
1563        assert!(tree.entry_for_path("a/b/").unwrap().is_dir());
1564    });
1565
1566    let fs_real = Arc::new(RealFs::default());
1567    let temp_root = 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, 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, 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, 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            (Path::new(""), GitSummary::CONFLICT + MODIFIED + ADDED),
2897            (Path::new("g"), GitSummary::CONFLICT),
2898            (Path::new("g/h2.txt"), GitSummary::CONFLICT),
2899        ],
2900    );
2901
2902    check_git_statuses(
2903        &snapshot,
2904        &[
2905            (Path::new(""), GitSummary::CONFLICT + ADDED + MODIFIED),
2906            (Path::new("a"), ADDED + MODIFIED),
2907            (Path::new("a/b"), ADDED),
2908            (Path::new("a/b/c1.txt"), ADDED),
2909            (Path::new("a/b/c2.txt"), GitSummary::UNCHANGED),
2910            (Path::new("a/d"), MODIFIED),
2911            (Path::new("a/d/e2.txt"), MODIFIED),
2912            (Path::new("f"), GitSummary::UNCHANGED),
2913            (Path::new("f/no-status.txt"), GitSummary::UNCHANGED),
2914            (Path::new("g"), GitSummary::CONFLICT),
2915            (Path::new("g/h2.txt"), GitSummary::CONFLICT),
2916        ],
2917    );
2918
2919    check_git_statuses(
2920        &snapshot,
2921        &[
2922            (Path::new("a/b"), ADDED),
2923            (Path::new("a/b/c1.txt"), ADDED),
2924            (Path::new("a/b/c2.txt"), GitSummary::UNCHANGED),
2925            (Path::new("a/d"), MODIFIED),
2926            (Path::new("a/d/e1.txt"), GitSummary::UNCHANGED),
2927            (Path::new("a/d/e2.txt"), MODIFIED),
2928            (Path::new("f"), GitSummary::UNCHANGED),
2929            (Path::new("f/no-status.txt"), GitSummary::UNCHANGED),
2930            (Path::new("g"), GitSummary::CONFLICT),
2931        ],
2932    );
2933
2934    check_git_statuses(
2935        &snapshot,
2936        &[
2937            (Path::new("a/b/c1.txt"), ADDED),
2938            (Path::new("a/b/c2.txt"), GitSummary::UNCHANGED),
2939            (Path::new("a/d/e1.txt"), GitSummary::UNCHANGED),
2940            (Path::new("a/d/e2.txt"), MODIFIED),
2941            (Path::new("f/no-status.txt"), GitSummary::UNCHANGED),
2942        ],
2943    );
2944}
2945
2946#[gpui::test]
2947async fn test_propagate_statuses_for_repos_under_project(cx: &mut TestAppContext) {
2948    init_test(cx);
2949    let fs = FakeFs::new(cx.background_executor.clone());
2950    fs.insert_tree(
2951        "/root",
2952        json!({
2953            "x": {
2954                ".git": {},
2955                "x1.txt": "foo",
2956                "x2.txt": "bar"
2957            },
2958            "y": {
2959                ".git": {},
2960                "y1.txt": "baz",
2961                "y2.txt": "qux"
2962            },
2963            "z": {
2964                ".git": {},
2965                "z1.txt": "quux",
2966                "z2.txt": "quuux"
2967            }
2968        }),
2969    )
2970    .await;
2971
2972    fs.set_status_for_repo_via_git_operation(
2973        Path::new("/root/x/.git"),
2974        &[(Path::new("x1.txt"), FileStatus::worktree(StatusCode::Added))],
2975    );
2976    fs.set_status_for_repo_via_git_operation(
2977        Path::new("/root/y/.git"),
2978        &[
2979            (Path::new("y1.txt"), CONFLICT),
2980            (
2981                Path::new("y2.txt"),
2982                FileStatus::worktree(StatusCode::Modified),
2983            ),
2984        ],
2985    );
2986    fs.set_status_for_repo_via_git_operation(
2987        Path::new("/root/z/.git"),
2988        &[(
2989            Path::new("z2.txt"),
2990            FileStatus::worktree(StatusCode::Modified),
2991        )],
2992    );
2993
2994    let tree = Worktree::local(
2995        Path::new("/root"),
2996        true,
2997        fs.clone(),
2998        Default::default(),
2999        &mut cx.to_async(),
3000    )
3001    .await
3002    .unwrap();
3003
3004    tree.flush_fs_events(cx).await;
3005    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
3006        .await;
3007    cx.executor().run_until_parked();
3008
3009    let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
3010
3011    check_git_statuses(
3012        &snapshot,
3013        &[(Path::new("x"), ADDED), (Path::new("x/x1.txt"), ADDED)],
3014    );
3015
3016    check_git_statuses(
3017        &snapshot,
3018        &[
3019            (Path::new("y"), GitSummary::CONFLICT + MODIFIED),
3020            (Path::new("y/y1.txt"), GitSummary::CONFLICT),
3021            (Path::new("y/y2.txt"), MODIFIED),
3022        ],
3023    );
3024
3025    check_git_statuses(
3026        &snapshot,
3027        &[
3028            (Path::new("z"), MODIFIED),
3029            (Path::new("z/z2.txt"), MODIFIED),
3030        ],
3031    );
3032
3033    check_git_statuses(
3034        &snapshot,
3035        &[(Path::new("x"), ADDED), (Path::new("x/x1.txt"), ADDED)],
3036    );
3037
3038    check_git_statuses(
3039        &snapshot,
3040        &[
3041            (Path::new("x"), ADDED),
3042            (Path::new("x/x1.txt"), ADDED),
3043            (Path::new("x/x2.txt"), GitSummary::UNCHANGED),
3044            (Path::new("y"), GitSummary::CONFLICT + MODIFIED),
3045            (Path::new("y/y1.txt"), GitSummary::CONFLICT),
3046            (Path::new("y/y2.txt"), MODIFIED),
3047            (Path::new("z"), MODIFIED),
3048            (Path::new("z/z1.txt"), GitSummary::UNCHANGED),
3049            (Path::new("z/z2.txt"), MODIFIED),
3050        ],
3051    );
3052}
3053
3054#[gpui::test]
3055async fn test_propagate_statuses_for_nested_repos(cx: &mut TestAppContext) {
3056    init_test(cx);
3057    let fs = FakeFs::new(cx.background_executor.clone());
3058    fs.insert_tree(
3059        "/root",
3060        json!({
3061            "x": {
3062                ".git": {},
3063                "x1.txt": "foo",
3064                "x2.txt": "bar",
3065                "y": {
3066                    ".git": {},
3067                    "y1.txt": "baz",
3068                    "y2.txt": "qux"
3069                },
3070                "z.txt": "sneaky..."
3071            },
3072            "z": {
3073                ".git": {},
3074                "z1.txt": "quux",
3075                "z2.txt": "quuux"
3076            }
3077        }),
3078    )
3079    .await;
3080
3081    fs.set_status_for_repo_via_git_operation(
3082        Path::new("/root/x/.git"),
3083        &[
3084            (
3085                Path::new("x2.txt"),
3086                FileStatus::worktree(StatusCode::Modified),
3087            ),
3088            (Path::new("z.txt"), FileStatus::worktree(StatusCode::Added)),
3089        ],
3090    );
3091    fs.set_status_for_repo_via_git_operation(
3092        Path::new("/root/x/y/.git"),
3093        &[(Path::new("y1.txt"), CONFLICT)],
3094    );
3095
3096    fs.set_status_for_repo_via_git_operation(
3097        Path::new("/root/z/.git"),
3098        &[(Path::new("z2.txt"), FileStatus::worktree(StatusCode::Added))],
3099    );
3100
3101    let tree = Worktree::local(
3102        Path::new("/root"),
3103        true,
3104        fs.clone(),
3105        Default::default(),
3106        &mut cx.to_async(),
3107    )
3108    .await
3109    .unwrap();
3110
3111    tree.flush_fs_events(cx).await;
3112    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
3113        .await;
3114    cx.executor().run_until_parked();
3115
3116    let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
3117
3118    // Sanity check the propagation for x/y and z
3119    check_git_statuses(
3120        &snapshot,
3121        &[
3122            (Path::new("x/y"), GitSummary::CONFLICT),
3123            (Path::new("x/y/y1.txt"), GitSummary::CONFLICT),
3124            (Path::new("x/y/y2.txt"), GitSummary::UNCHANGED),
3125        ],
3126    );
3127    check_git_statuses(
3128        &snapshot,
3129        &[
3130            (Path::new("z"), ADDED),
3131            (Path::new("z/z1.txt"), GitSummary::UNCHANGED),
3132            (Path::new("z/z2.txt"), ADDED),
3133        ],
3134    );
3135
3136    // Test one of the fundamental cases of propagation blocking, the transition from one git repository to another
3137    check_git_statuses(
3138        &snapshot,
3139        &[
3140            (Path::new("x"), MODIFIED + ADDED),
3141            (Path::new("x/y"), GitSummary::CONFLICT),
3142            (Path::new("x/y/y1.txt"), GitSummary::CONFLICT),
3143        ],
3144    );
3145
3146    // Sanity check everything around it
3147    check_git_statuses(
3148        &snapshot,
3149        &[
3150            (Path::new("x"), MODIFIED + ADDED),
3151            (Path::new("x/x1.txt"), GitSummary::UNCHANGED),
3152            (Path::new("x/x2.txt"), MODIFIED),
3153            (Path::new("x/y"), GitSummary::CONFLICT),
3154            (Path::new("x/y/y1.txt"), GitSummary::CONFLICT),
3155            (Path::new("x/y/y2.txt"), GitSummary::UNCHANGED),
3156            (Path::new("x/z.txt"), ADDED),
3157        ],
3158    );
3159
3160    // Test the other fundamental case, transitioning from git repository to non-git repository
3161    check_git_statuses(
3162        &snapshot,
3163        &[
3164            (Path::new(""), GitSummary::UNCHANGED),
3165            (Path::new("x"), MODIFIED + ADDED),
3166            (Path::new("x/x1.txt"), GitSummary::UNCHANGED),
3167        ],
3168    );
3169
3170    // And all together now
3171    check_git_statuses(
3172        &snapshot,
3173        &[
3174            (Path::new(""), GitSummary::UNCHANGED),
3175            (Path::new("x"), MODIFIED + ADDED),
3176            (Path::new("x/x1.txt"), GitSummary::UNCHANGED),
3177            (Path::new("x/x2.txt"), MODIFIED),
3178            (Path::new("x/y"), GitSummary::CONFLICT),
3179            (Path::new("x/y/y1.txt"), GitSummary::CONFLICT),
3180            (Path::new("x/y/y2.txt"), GitSummary::UNCHANGED),
3181            (Path::new("x/z.txt"), ADDED),
3182            (Path::new("z"), ADDED),
3183            (Path::new("z/z1.txt"), GitSummary::UNCHANGED),
3184            (Path::new("z/z2.txt"), ADDED),
3185        ],
3186    );
3187}
3188
3189#[gpui::test]
3190async fn test_private_single_file_worktree(cx: &mut TestAppContext) {
3191    init_test(cx);
3192    let fs = FakeFs::new(cx.background_executor.clone());
3193    fs.insert_tree("/", json!({".env": "PRIVATE=secret\n"}))
3194        .await;
3195    let tree = Worktree::local(
3196        Path::new("/.env"),
3197        true,
3198        fs.clone(),
3199        Default::default(),
3200        &mut cx.to_async(),
3201    )
3202    .await
3203    .unwrap();
3204    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
3205        .await;
3206    tree.read_with(cx, |tree, _| {
3207        let entry = tree.entry_for_path("").unwrap();
3208        assert!(entry.is_private);
3209    });
3210}
3211
3212#[track_caller]
3213fn check_git_statuses(snapshot: &Snapshot, expected_statuses: &[(&Path, GitSummary)]) {
3214    let mut traversal = snapshot
3215        .traverse_from_path(true, true, false, "".as_ref())
3216        .with_git_statuses();
3217    let found_statuses = expected_statuses
3218        .iter()
3219        .map(|&(path, _)| {
3220            let git_entry = traversal
3221                .find(|git_entry| &*git_entry.path == path)
3222                .unwrap_or_else(|| panic!("Traversal has no entry for {path:?}"));
3223            (path, git_entry.git_summary)
3224        })
3225        .collect::<Vec<_>>();
3226    assert_eq!(found_statuses, expected_statuses);
3227}
3228
3229const ADDED: GitSummary = GitSummary {
3230    added: 1,
3231    count: 1,
3232    ..GitSummary::UNCHANGED
3233};
3234const MODIFIED: GitSummary = GitSummary {
3235    modified: 1,
3236    count: 1,
3237    ..GitSummary::UNCHANGED
3238};
3239
3240#[track_caller]
3241fn git_init(path: &Path) -> git2::Repository {
3242    git2::Repository::init(path).expect("Failed to initialize git repository")
3243}
3244
3245#[track_caller]
3246fn git_add<P: AsRef<Path>>(path: P, repo: &git2::Repository) {
3247    let path = path.as_ref();
3248    let mut index = repo.index().expect("Failed to get index");
3249    index.add_path(path).expect("Failed to add file");
3250    index.write().expect("Failed to write index");
3251}
3252
3253#[track_caller]
3254fn git_remove_index(path: &Path, repo: &git2::Repository) {
3255    let mut index = repo.index().expect("Failed to get index");
3256    index.remove_path(path).expect("Failed to add file");
3257    index.write().expect("Failed to write index");
3258}
3259
3260#[track_caller]
3261fn git_commit(msg: &'static str, repo: &git2::Repository) {
3262    use git2::Signature;
3263
3264    let signature = Signature::now("test", "test@zed.dev").unwrap();
3265    let oid = repo.index().unwrap().write_tree().unwrap();
3266    let tree = repo.find_tree(oid).unwrap();
3267    if let Ok(head) = repo.head() {
3268        let parent_obj = head.peel(git2::ObjectType::Commit).unwrap();
3269
3270        let parent_commit = parent_obj.as_commit().unwrap();
3271
3272        repo.commit(
3273            Some("HEAD"),
3274            &signature,
3275            &signature,
3276            msg,
3277            &tree,
3278            &[parent_commit],
3279        )
3280        .expect("Failed to commit with parent");
3281    } else {
3282        repo.commit(Some("HEAD"), &signature, &signature, msg, &tree, &[])
3283            .expect("Failed to commit");
3284    }
3285}
3286
3287#[track_caller]
3288fn git_stash(repo: &mut git2::Repository) {
3289    use git2::Signature;
3290
3291    let signature = Signature::now("test", "test@zed.dev").unwrap();
3292    repo.stash_save(&signature, "N/A", None)
3293        .expect("Failed to stash");
3294}
3295
3296#[track_caller]
3297fn git_reset(offset: usize, repo: &git2::Repository) {
3298    let head = repo.head().expect("Couldn't get repo head");
3299    let object = head.peel(git2::ObjectType::Commit).unwrap();
3300    let commit = object.as_commit().unwrap();
3301    let new_head = commit
3302        .parents()
3303        .inspect(|parnet| {
3304            parnet.message();
3305        })
3306        .nth(offset)
3307        .expect("Not enough history");
3308    repo.reset(new_head.as_object(), git2::ResetType::Soft, None)
3309        .expect("Could not reset");
3310}
3311
3312#[allow(dead_code)]
3313#[track_caller]
3314fn git_status(repo: &git2::Repository) -> collections::HashMap<String, git2::Status> {
3315    repo.statuses(None)
3316        .unwrap()
3317        .iter()
3318        .map(|status| (status.path().unwrap().to_string(), status.status()))
3319        .collect()
3320}
3321
3322#[track_caller]
3323fn check_worktree_entries(
3324    tree: &Worktree,
3325    expected_excluded_paths: &[&str],
3326    expected_ignored_paths: &[&str],
3327    expected_tracked_paths: &[&str],
3328    expected_included_paths: &[&str],
3329) {
3330    for path in expected_excluded_paths {
3331        let entry = tree.entry_for_path(path);
3332        assert!(
3333            entry.is_none(),
3334            "expected path '{path}' to be excluded, but got entry: {entry:?}",
3335        );
3336    }
3337    for path in expected_ignored_paths {
3338        let entry = tree
3339            .entry_for_path(path)
3340            .unwrap_or_else(|| panic!("Missing entry for expected ignored path '{path}'"));
3341        assert!(
3342            entry.is_ignored,
3343            "expected path '{path}' to be ignored, but got entry: {entry:?}",
3344        );
3345    }
3346    for path in expected_tracked_paths {
3347        let entry = tree
3348            .entry_for_path(path)
3349            .unwrap_or_else(|| panic!("Missing entry for expected tracked path '{path}'"));
3350        assert!(
3351            !entry.is_ignored || entry.is_always_included,
3352            "expected path '{path}' to be tracked, but got entry: {entry:?}",
3353        );
3354    }
3355    for path in expected_included_paths {
3356        let entry = tree
3357            .entry_for_path(path)
3358            .unwrap_or_else(|| panic!("Missing entry for expected included path '{path}'"));
3359        assert!(
3360            entry.is_always_included,
3361            "expected path '{path}' to always be included, but got entry: {entry:?}",
3362        );
3363    }
3364}
3365
3366fn init_test(cx: &mut gpui::TestAppContext) {
3367    if std::env::var("RUST_LOG").is_ok() {
3368        env_logger::try_init().ok();
3369    }
3370
3371    cx.update(|cx| {
3372        let settings_store = SettingsStore::test(cx);
3373        cx.set_global(settings_store);
3374        WorktreeSettings::register(cx);
3375    });
3376}
3377
3378fn assert_entry_git_state(
3379    tree: &Worktree,
3380    path: &str,
3381    worktree_status: Option<StatusCode>,
3382    is_ignored: bool,
3383) {
3384    let entry = tree.entry_for_path(path).expect("entry {path} not found");
3385    let status = tree.status_for_file(Path::new(path));
3386    let expected = worktree_status.map(|worktree_status| {
3387        TrackedStatus {
3388            worktree_status,
3389            index_status: StatusCode::Unmodified,
3390        }
3391        .into()
3392    });
3393    assert_eq!(
3394        status, expected,
3395        "expected {path} to have git status: {expected:?}"
3396    );
3397    assert_eq!(
3398        entry.is_ignored, is_ignored,
3399        "expected {path} to have is_ignored: {is_ignored}"
3400    );
3401}