worktree_tests.rs

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