worktree_tests.rs

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