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