worktree_tests.rs

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