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