worktree_tests.rs

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