worktree_tests.rs

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