worktree_tests.rs

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