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