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(not(target_os = "macos"))]
 858    fs::fs_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
1501    check_git_statuses(
1502        &snapshot,
1503        &[
1504            (Path::new(""), Some(GitFileStatus::Modified)),
1505            (Path::new("a.txt"), None),
1506            (Path::new("b/c.txt"), Some(GitFileStatus::Modified)),
1507        ],
1508    );
1509}
1510
1511#[gpui::test]
1512async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
1513    init_test(cx);
1514    cx.executor().allow_parking();
1515
1516    let fs_fake = FakeFs::new(cx.background_executor.clone());
1517    fs_fake
1518        .insert_tree(
1519            "/root",
1520            json!({
1521                "a": {},
1522            }),
1523        )
1524        .await;
1525
1526    let tree_fake = Worktree::local(
1527        "/root".as_ref(),
1528        true,
1529        fs_fake,
1530        Default::default(),
1531        &mut cx.to_async(),
1532    )
1533    .await
1534    .unwrap();
1535
1536    let entry = tree_fake
1537        .update(cx, |tree, cx| {
1538            tree.as_local_mut()
1539                .unwrap()
1540                .create_entry("a/b/c/d.txt".as_ref(), false, cx)
1541        })
1542        .await
1543        .unwrap()
1544        .to_included()
1545        .unwrap();
1546    assert!(entry.is_file());
1547
1548    cx.executor().run_until_parked();
1549    tree_fake.read_with(cx, |tree, _| {
1550        assert!(tree.entry_for_path("a/b/c/d.txt").unwrap().is_file());
1551        assert!(tree.entry_for_path("a/b/c/").unwrap().is_dir());
1552        assert!(tree.entry_for_path("a/b/").unwrap().is_dir());
1553    });
1554
1555    let fs_real = Arc::new(RealFs::default());
1556    let temp_root = temp_tree(json!({
1557        "a": {}
1558    }));
1559
1560    let tree_real = Worktree::local(
1561        temp_root.path(),
1562        true,
1563        fs_real,
1564        Default::default(),
1565        &mut cx.to_async(),
1566    )
1567    .await
1568    .unwrap();
1569
1570    let entry = tree_real
1571        .update(cx, |tree, cx| {
1572            tree.as_local_mut()
1573                .unwrap()
1574                .create_entry("a/b/c/d.txt".as_ref(), false, cx)
1575        })
1576        .await
1577        .unwrap()
1578        .to_included()
1579        .unwrap();
1580    assert!(entry.is_file());
1581
1582    cx.executor().run_until_parked();
1583    tree_real.read_with(cx, |tree, _| {
1584        assert!(tree.entry_for_path("a/b/c/d.txt").unwrap().is_file());
1585        assert!(tree.entry_for_path("a/b/c/").unwrap().is_dir());
1586        assert!(tree.entry_for_path("a/b/").unwrap().is_dir());
1587    });
1588
1589    // Test smallest change
1590    let entry = tree_real
1591        .update(cx, |tree, cx| {
1592            tree.as_local_mut()
1593                .unwrap()
1594                .create_entry("a/b/c/e.txt".as_ref(), false, cx)
1595        })
1596        .await
1597        .unwrap()
1598        .to_included()
1599        .unwrap();
1600    assert!(entry.is_file());
1601
1602    cx.executor().run_until_parked();
1603    tree_real.read_with(cx, |tree, _| {
1604        assert!(tree.entry_for_path("a/b/c/e.txt").unwrap().is_file());
1605    });
1606
1607    // Test largest change
1608    let entry = tree_real
1609        .update(cx, |tree, cx| {
1610            tree.as_local_mut()
1611                .unwrap()
1612                .create_entry("d/e/f/g.txt".as_ref(), false, cx)
1613        })
1614        .await
1615        .unwrap()
1616        .to_included()
1617        .unwrap();
1618    assert!(entry.is_file());
1619
1620    cx.executor().run_until_parked();
1621    tree_real.read_with(cx, |tree, _| {
1622        assert!(tree.entry_for_path("d/e/f/g.txt").unwrap().is_file());
1623        assert!(tree.entry_for_path("d/e/f").unwrap().is_dir());
1624        assert!(tree.entry_for_path("d/e/").unwrap().is_dir());
1625        assert!(tree.entry_for_path("d/").unwrap().is_dir());
1626    });
1627}
1628
1629#[gpui::test(iterations = 100)]
1630async fn test_random_worktree_operations_during_initial_scan(
1631    cx: &mut TestAppContext,
1632    mut rng: StdRng,
1633) {
1634    init_test(cx);
1635    let operations = env::var("OPERATIONS")
1636        .map(|o| o.parse().unwrap())
1637        .unwrap_or(5);
1638    let initial_entries = env::var("INITIAL_ENTRIES")
1639        .map(|o| o.parse().unwrap())
1640        .unwrap_or(20);
1641
1642    let root_dir = Path::new("/test");
1643    let fs = FakeFs::new(cx.background_executor.clone()) as Arc<dyn Fs>;
1644    fs.as_fake().insert_tree(root_dir, json!({})).await;
1645    for _ in 0..initial_entries {
1646        randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
1647    }
1648    log::info!("generated initial tree");
1649
1650    let worktree = Worktree::local(
1651        root_dir,
1652        true,
1653        fs.clone(),
1654        Default::default(),
1655        &mut cx.to_async(),
1656    )
1657    .await
1658    .unwrap();
1659
1660    let mut snapshots = vec![worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot())];
1661    let updates = Arc::new(Mutex::new(Vec::new()));
1662    worktree.update(cx, |tree, cx| {
1663        check_worktree_change_events(tree, cx);
1664
1665        tree.as_local_mut().unwrap().observe_updates(0, cx, {
1666            let updates = updates.clone();
1667            move |update| {
1668                updates.lock().push(update);
1669                async { true }
1670            }
1671        });
1672    });
1673
1674    for _ in 0..operations {
1675        worktree
1676            .update(cx, |worktree, cx| {
1677                randomly_mutate_worktree(worktree, &mut rng, cx)
1678            })
1679            .await
1680            .log_err();
1681        worktree.read_with(cx, |tree, _| {
1682            tree.as_local().unwrap().snapshot().check_invariants(true)
1683        });
1684
1685        if rng.gen_bool(0.6) {
1686            snapshots.push(worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot()));
1687        }
1688    }
1689
1690    worktree
1691        .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
1692        .await;
1693
1694    cx.executor().run_until_parked();
1695
1696    let final_snapshot = worktree.read_with(cx, |tree, _| {
1697        let tree = tree.as_local().unwrap();
1698        let snapshot = tree.snapshot();
1699        snapshot.check_invariants(true);
1700        snapshot
1701    });
1702
1703    let settings = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().settings());
1704
1705    for (i, snapshot) in snapshots.into_iter().enumerate().rev() {
1706        let mut updated_snapshot = snapshot.clone();
1707        for update in updates.lock().iter() {
1708            if update.scan_id >= updated_snapshot.scan_id() as u64 {
1709                updated_snapshot
1710                    .apply_remote_update(update.clone(), &settings.file_scan_inclusions)
1711                    .unwrap();
1712            }
1713        }
1714
1715        assert_eq!(
1716            updated_snapshot.entries(true, 0).collect::<Vec<_>>(),
1717            final_snapshot.entries(true, 0).collect::<Vec<_>>(),
1718            "wrong updates after snapshot {i}: {snapshot:#?} {updates:#?}",
1719        );
1720    }
1721}
1722
1723#[gpui::test(iterations = 100)]
1724async fn test_random_worktree_changes(cx: &mut TestAppContext, mut rng: StdRng) {
1725    init_test(cx);
1726    let operations = env::var("OPERATIONS")
1727        .map(|o| o.parse().unwrap())
1728        .unwrap_or(40);
1729    let initial_entries = env::var("INITIAL_ENTRIES")
1730        .map(|o| o.parse().unwrap())
1731        .unwrap_or(20);
1732
1733    let root_dir = Path::new("/test");
1734    let fs = FakeFs::new(cx.background_executor.clone()) as Arc<dyn Fs>;
1735    fs.as_fake().insert_tree(root_dir, json!({})).await;
1736    for _ in 0..initial_entries {
1737        randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
1738    }
1739    log::info!("generated initial tree");
1740
1741    let worktree = Worktree::local(
1742        root_dir,
1743        true,
1744        fs.clone(),
1745        Default::default(),
1746        &mut cx.to_async(),
1747    )
1748    .await
1749    .unwrap();
1750
1751    let updates = Arc::new(Mutex::new(Vec::new()));
1752    worktree.update(cx, |tree, cx| {
1753        check_worktree_change_events(tree, cx);
1754
1755        tree.as_local_mut().unwrap().observe_updates(0, cx, {
1756            let updates = updates.clone();
1757            move |update| {
1758                updates.lock().push(update);
1759                async { true }
1760            }
1761        });
1762    });
1763
1764    worktree
1765        .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
1766        .await;
1767
1768    fs.as_fake().pause_events();
1769    let mut snapshots = Vec::new();
1770    let mut mutations_len = operations;
1771    while mutations_len > 1 {
1772        if rng.gen_bool(0.2) {
1773            worktree
1774                .update(cx, |worktree, cx| {
1775                    randomly_mutate_worktree(worktree, &mut rng, cx)
1776                })
1777                .await
1778                .log_err();
1779        } else {
1780            randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
1781        }
1782
1783        let buffered_event_count = fs.as_fake().buffered_event_count();
1784        if buffered_event_count > 0 && rng.gen_bool(0.3) {
1785            let len = rng.gen_range(0..=buffered_event_count);
1786            log::info!("flushing {} events", len);
1787            fs.as_fake().flush_events(len);
1788        } else {
1789            randomly_mutate_fs(&fs, root_dir, 0.6, &mut rng).await;
1790            mutations_len -= 1;
1791        }
1792
1793        cx.executor().run_until_parked();
1794        if rng.gen_bool(0.2) {
1795            log::info!("storing snapshot {}", snapshots.len());
1796            let snapshot = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
1797            snapshots.push(snapshot);
1798        }
1799    }
1800
1801    log::info!("quiescing");
1802    fs.as_fake().flush_events(usize::MAX);
1803    cx.executor().run_until_parked();
1804
1805    let snapshot = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
1806    snapshot.check_invariants(true);
1807    let expanded_paths = snapshot
1808        .expanded_entries()
1809        .map(|e| e.path.clone())
1810        .collect::<Vec<_>>();
1811
1812    {
1813        let new_worktree = Worktree::local(
1814            root_dir,
1815            true,
1816            fs.clone(),
1817            Default::default(),
1818            &mut cx.to_async(),
1819        )
1820        .await
1821        .unwrap();
1822        new_worktree
1823            .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
1824            .await;
1825        new_worktree
1826            .update(cx, |tree, _| {
1827                tree.as_local_mut()
1828                    .unwrap()
1829                    .refresh_entries_for_paths(expanded_paths)
1830            })
1831            .recv()
1832            .await;
1833        let new_snapshot =
1834            new_worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
1835        assert_eq!(
1836            snapshot.entries_without_ids(true),
1837            new_snapshot.entries_without_ids(true)
1838        );
1839    }
1840
1841    let settings = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().settings());
1842
1843    for (i, mut prev_snapshot) in snapshots.into_iter().enumerate().rev() {
1844        for update in updates.lock().iter() {
1845            if update.scan_id >= prev_snapshot.scan_id() as u64 {
1846                prev_snapshot
1847                    .apply_remote_update(update.clone(), &settings.file_scan_inclusions)
1848                    .unwrap();
1849            }
1850        }
1851
1852        assert_eq!(
1853            prev_snapshot
1854                .entries(true, 0)
1855                .map(ignore_pending_dir)
1856                .collect::<Vec<_>>(),
1857            snapshot
1858                .entries(true, 0)
1859                .map(ignore_pending_dir)
1860                .collect::<Vec<_>>(),
1861            "wrong updates after snapshot {i}: {updates:#?}",
1862        );
1863    }
1864
1865    fn ignore_pending_dir(entry: &Entry) -> Entry {
1866        let mut entry = entry.clone();
1867        if entry.kind.is_dir() {
1868            entry.kind = EntryKind::Dir
1869        }
1870        entry
1871    }
1872}
1873
1874// The worktree's `UpdatedEntries` event can be used to follow along with
1875// all changes to the worktree's snapshot.
1876fn check_worktree_change_events(tree: &mut Worktree, cx: &mut ModelContext<Worktree>) {
1877    let mut entries = tree.entries(true, 0).cloned().collect::<Vec<_>>();
1878    cx.subscribe(&cx.handle(), move |tree, _, event, _| {
1879        if let Event::UpdatedEntries(changes) = event {
1880            for (path, _, change_type) in changes.iter() {
1881                let entry = tree.entry_for_path(path).cloned();
1882                let ix = match entries.binary_search_by_key(&path, |e| &e.path) {
1883                    Ok(ix) | Err(ix) => ix,
1884                };
1885                match change_type {
1886                    PathChange::Added => entries.insert(ix, entry.unwrap()),
1887                    PathChange::Removed => drop(entries.remove(ix)),
1888                    PathChange::Updated => {
1889                        let entry = entry.unwrap();
1890                        let existing_entry = entries.get_mut(ix).unwrap();
1891                        assert_eq!(existing_entry.path, entry.path);
1892                        *existing_entry = entry;
1893                    }
1894                    PathChange::AddedOrUpdated | PathChange::Loaded => {
1895                        let entry = entry.unwrap();
1896                        if entries.get(ix).map(|e| &e.path) == Some(&entry.path) {
1897                            *entries.get_mut(ix).unwrap() = entry;
1898                        } else {
1899                            entries.insert(ix, entry);
1900                        }
1901                    }
1902                }
1903            }
1904
1905            let new_entries = tree.entries(true, 0).cloned().collect::<Vec<_>>();
1906            assert_eq!(entries, new_entries, "incorrect changes: {:?}", changes);
1907        }
1908    })
1909    .detach();
1910}
1911
1912fn randomly_mutate_worktree(
1913    worktree: &mut Worktree,
1914    rng: &mut impl Rng,
1915    cx: &mut ModelContext<Worktree>,
1916) -> Task<Result<()>> {
1917    log::info!("mutating worktree");
1918    let worktree = worktree.as_local_mut().unwrap();
1919    let snapshot = worktree.snapshot();
1920    let entry = snapshot.entries(false, 0).choose(rng).unwrap();
1921
1922    match rng.gen_range(0_u32..100) {
1923        0..=33 if entry.path.as_ref() != Path::new("") => {
1924            log::info!("deleting entry {:?} ({})", entry.path, entry.id.0);
1925            worktree.delete_entry(entry.id, false, cx).unwrap()
1926        }
1927        ..=66 if entry.path.as_ref() != Path::new("") => {
1928            let other_entry = snapshot.entries(false, 0).choose(rng).unwrap();
1929            let new_parent_path = if other_entry.is_dir() {
1930                other_entry.path.clone()
1931            } else {
1932                other_entry.path.parent().unwrap().into()
1933            };
1934            let mut new_path = new_parent_path.join(random_filename(rng));
1935            if new_path.starts_with(&entry.path) {
1936                new_path = random_filename(rng).into();
1937            }
1938
1939            log::info!(
1940                "renaming entry {:?} ({}) to {:?}",
1941                entry.path,
1942                entry.id.0,
1943                new_path
1944            );
1945            let task = worktree.rename_entry(entry.id, new_path, cx);
1946            cx.background_executor().spawn(async move {
1947                task.await?.to_included().unwrap();
1948                Ok(())
1949            })
1950        }
1951        _ => {
1952            if entry.is_dir() {
1953                let child_path = entry.path.join(random_filename(rng));
1954                let is_dir = rng.gen_bool(0.3);
1955                log::info!(
1956                    "creating {} at {:?}",
1957                    if is_dir { "dir" } else { "file" },
1958                    child_path,
1959                );
1960                let task = worktree.create_entry(child_path, is_dir, cx);
1961                cx.background_executor().spawn(async move {
1962                    task.await?;
1963                    Ok(())
1964                })
1965            } else {
1966                log::info!("overwriting file {:?} ({})", entry.path, entry.id.0);
1967                let task =
1968                    worktree.write_file(entry.path.clone(), "".into(), Default::default(), cx);
1969                cx.background_executor().spawn(async move {
1970                    task.await?;
1971                    Ok(())
1972                })
1973            }
1974        }
1975    }
1976}
1977
1978async fn randomly_mutate_fs(
1979    fs: &Arc<dyn Fs>,
1980    root_path: &Path,
1981    insertion_probability: f64,
1982    rng: &mut impl Rng,
1983) {
1984    log::info!("mutating fs");
1985    let mut files = Vec::new();
1986    let mut dirs = Vec::new();
1987    for path in fs.as_fake().paths(false) {
1988        if path.starts_with(root_path) {
1989            if fs.is_file(&path).await {
1990                files.push(path);
1991            } else {
1992                dirs.push(path);
1993            }
1994        }
1995    }
1996
1997    if (files.is_empty() && dirs.len() == 1) || rng.gen_bool(insertion_probability) {
1998        let path = dirs.choose(rng).unwrap();
1999        let new_path = path.join(random_filename(rng));
2000
2001        if rng.gen() {
2002            log::info!(
2003                "creating dir {:?}",
2004                new_path.strip_prefix(root_path).unwrap()
2005            );
2006            fs.create_dir(&new_path).await.unwrap();
2007        } else {
2008            log::info!(
2009                "creating file {:?}",
2010                new_path.strip_prefix(root_path).unwrap()
2011            );
2012            fs.create_file(&new_path, Default::default()).await.unwrap();
2013        }
2014    } else if rng.gen_bool(0.05) {
2015        let ignore_dir_path = dirs.choose(rng).unwrap();
2016        let ignore_path = ignore_dir_path.join(*GITIGNORE);
2017
2018        let subdirs = dirs
2019            .iter()
2020            .filter(|d| d.starts_with(ignore_dir_path))
2021            .cloned()
2022            .collect::<Vec<_>>();
2023        let subfiles = files
2024            .iter()
2025            .filter(|d| d.starts_with(ignore_dir_path))
2026            .cloned()
2027            .collect::<Vec<_>>();
2028        let files_to_ignore = {
2029            let len = rng.gen_range(0..=subfiles.len());
2030            subfiles.choose_multiple(rng, len)
2031        };
2032        let dirs_to_ignore = {
2033            let len = rng.gen_range(0..subdirs.len());
2034            subdirs.choose_multiple(rng, len)
2035        };
2036
2037        let mut ignore_contents = String::new();
2038        for path_to_ignore in files_to_ignore.chain(dirs_to_ignore) {
2039            writeln!(
2040                ignore_contents,
2041                "{}",
2042                path_to_ignore
2043                    .strip_prefix(ignore_dir_path)
2044                    .unwrap()
2045                    .to_str()
2046                    .unwrap()
2047            )
2048            .unwrap();
2049        }
2050        log::info!(
2051            "creating gitignore {:?} with contents:\n{}",
2052            ignore_path.strip_prefix(root_path).unwrap(),
2053            ignore_contents
2054        );
2055        fs.save(
2056            &ignore_path,
2057            &ignore_contents.as_str().into(),
2058            Default::default(),
2059        )
2060        .await
2061        .unwrap();
2062    } else {
2063        let old_path = {
2064            let file_path = files.choose(rng);
2065            let dir_path = dirs[1..].choose(rng);
2066            file_path.into_iter().chain(dir_path).choose(rng).unwrap()
2067        };
2068
2069        let is_rename = rng.gen();
2070        if is_rename {
2071            let new_path_parent = dirs
2072                .iter()
2073                .filter(|d| !d.starts_with(old_path))
2074                .choose(rng)
2075                .unwrap();
2076
2077            let overwrite_existing_dir =
2078                !old_path.starts_with(new_path_parent) && rng.gen_bool(0.3);
2079            let new_path = if overwrite_existing_dir {
2080                fs.remove_dir(
2081                    new_path_parent,
2082                    RemoveOptions {
2083                        recursive: true,
2084                        ignore_if_not_exists: true,
2085                    },
2086                )
2087                .await
2088                .unwrap();
2089                new_path_parent.to_path_buf()
2090            } else {
2091                new_path_parent.join(random_filename(rng))
2092            };
2093
2094            log::info!(
2095                "renaming {:?} to {}{:?}",
2096                old_path.strip_prefix(root_path).unwrap(),
2097                if overwrite_existing_dir {
2098                    "overwrite "
2099                } else {
2100                    ""
2101                },
2102                new_path.strip_prefix(root_path).unwrap()
2103            );
2104            fs.rename(
2105                old_path,
2106                &new_path,
2107                fs::RenameOptions {
2108                    overwrite: true,
2109                    ignore_if_exists: true,
2110                },
2111            )
2112            .await
2113            .unwrap();
2114        } else if fs.is_file(old_path).await {
2115            log::info!(
2116                "deleting file {:?}",
2117                old_path.strip_prefix(root_path).unwrap()
2118            );
2119            fs.remove_file(old_path, Default::default()).await.unwrap();
2120        } else {
2121            log::info!(
2122                "deleting dir {:?}",
2123                old_path.strip_prefix(root_path).unwrap()
2124            );
2125            fs.remove_dir(
2126                old_path,
2127                RemoveOptions {
2128                    recursive: true,
2129                    ignore_if_not_exists: true,
2130                },
2131            )
2132            .await
2133            .unwrap();
2134        }
2135    }
2136}
2137
2138fn random_filename(rng: &mut impl Rng) -> String {
2139    (0..6)
2140        .map(|_| rng.sample(rand::distributions::Alphanumeric))
2141        .map(char::from)
2142        .collect()
2143}
2144
2145#[gpui::test]
2146async fn test_rename_work_directory(cx: &mut TestAppContext) {
2147    init_test(cx);
2148    cx.executor().allow_parking();
2149    let root = temp_tree(json!({
2150        "projects": {
2151            "project1": {
2152                "a": "",
2153                "b": "",
2154            }
2155        },
2156
2157    }));
2158    let root_path = root.path();
2159
2160    let tree = Worktree::local(
2161        root_path,
2162        true,
2163        Arc::new(RealFs::default()),
2164        Default::default(),
2165        &mut cx.to_async(),
2166    )
2167    .await
2168    .unwrap();
2169
2170    let repo = git_init(&root_path.join("projects/project1"));
2171    git_add("a", &repo);
2172    git_commit("init", &repo);
2173    std::fs::write(root_path.join("projects/project1/a"), "aa").ok();
2174
2175    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2176        .await;
2177
2178    tree.flush_fs_events(cx).await;
2179
2180    cx.read(|cx| {
2181        let tree = tree.read(cx);
2182        let repo = tree.repositories().next().unwrap();
2183        assert_eq!(repo.path.as_ref(), Path::new("projects/project1"));
2184        assert_eq!(
2185            tree.status_for_file(Path::new("projects/project1/a")),
2186            Some(GitFileStatus::Modified)
2187        );
2188        assert_eq!(
2189            tree.status_for_file(Path::new("projects/project1/b")),
2190            Some(GitFileStatus::Untracked)
2191        );
2192    });
2193
2194    std::fs::rename(
2195        root_path.join("projects/project1"),
2196        root_path.join("projects/project2"),
2197    )
2198    .ok();
2199    tree.flush_fs_events(cx).await;
2200
2201    cx.read(|cx| {
2202        let tree = tree.read(cx);
2203        let repo = tree.repositories().next().unwrap();
2204        assert_eq!(repo.path.as_ref(), Path::new("projects/project2"));
2205        assert_eq!(
2206            tree.status_for_file(Path::new("projects/project2/a")),
2207            Some(GitFileStatus::Modified)
2208        );
2209        assert_eq!(
2210            tree.status_for_file(Path::new("projects/project2/b")),
2211            Some(GitFileStatus::Untracked)
2212        );
2213    });
2214}
2215
2216#[gpui::test]
2217async fn test_git_repository_for_path(cx: &mut TestAppContext) {
2218    init_test(cx);
2219    cx.executor().allow_parking();
2220    let root = temp_tree(json!({
2221        "c.txt": "",
2222        "dir1": {
2223            ".git": {},
2224            "deps": {
2225                "dep1": {
2226                    ".git": {},
2227                    "src": {
2228                        "a.txt": ""
2229                    }
2230                }
2231            },
2232            "src": {
2233                "b.txt": ""
2234            }
2235        },
2236    }));
2237
2238    let tree = Worktree::local(
2239        root.path(),
2240        true,
2241        Arc::new(RealFs::default()),
2242        Default::default(),
2243        &mut cx.to_async(),
2244    )
2245    .await
2246    .unwrap();
2247
2248    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2249        .await;
2250    tree.flush_fs_events(cx).await;
2251
2252    tree.read_with(cx, |tree, _cx| {
2253        let tree = tree.as_local().unwrap();
2254
2255        assert!(tree.repository_for_path("c.txt".as_ref()).is_none());
2256
2257        let repo = tree.repository_for_path("dir1/src/b.txt".as_ref()).unwrap();
2258        assert_eq!(repo.path.as_ref(), Path::new("dir1"));
2259
2260        let repo = tree
2261            .repository_for_path("dir1/deps/dep1/src/a.txt".as_ref())
2262            .unwrap();
2263        assert_eq!(repo.path.as_ref(), Path::new("dir1/deps/dep1"));
2264
2265        let entries = tree.files(false, 0);
2266
2267        let paths_with_repos = tree
2268            .entries_with_repositories(entries)
2269            .map(|(entry, repo)| {
2270                (
2271                    entry.path.as_ref(),
2272                    repo.map(|repo| repo.path.to_path_buf()),
2273                )
2274            })
2275            .collect::<Vec<_>>();
2276
2277        assert_eq!(
2278            paths_with_repos,
2279            &[
2280                (Path::new("c.txt"), None),
2281                (
2282                    Path::new("dir1/deps/dep1/src/a.txt"),
2283                    Some(Path::new("dir1/deps/dep1").into())
2284                ),
2285                (Path::new("dir1/src/b.txt"), Some(Path::new("dir1").into())),
2286            ]
2287        );
2288    });
2289
2290    let repo_update_events = Arc::new(Mutex::new(vec![]));
2291    tree.update(cx, |_, cx| {
2292        let repo_update_events = repo_update_events.clone();
2293        cx.subscribe(&tree, move |_, _, event, _| {
2294            if let Event::UpdatedGitRepositories(update) = event {
2295                repo_update_events.lock().push(update.clone());
2296            }
2297        })
2298        .detach();
2299    });
2300
2301    std::fs::write(root.path().join("dir1/.git/random_new_file"), "hello").unwrap();
2302    tree.flush_fs_events(cx).await;
2303
2304    assert_eq!(
2305        repo_update_events.lock()[0]
2306            .iter()
2307            .map(|e| e.0.clone())
2308            .collect::<Vec<Arc<Path>>>(),
2309        vec![Path::new("dir1").into()]
2310    );
2311
2312    std::fs::remove_dir_all(root.path().join("dir1/.git")).unwrap();
2313    tree.flush_fs_events(cx).await;
2314
2315    tree.read_with(cx, |tree, _cx| {
2316        let tree = tree.as_local().unwrap();
2317
2318        assert!(tree
2319            .repository_for_path("dir1/src/b.txt".as_ref())
2320            .is_none());
2321    });
2322}
2323
2324#[gpui::test]
2325async fn test_file_status(cx: &mut TestAppContext) {
2326    init_test(cx);
2327    cx.executor().allow_parking();
2328    const IGNORE_RULE: &str = "**/target";
2329
2330    let root = temp_tree(json!({
2331        "project": {
2332            "a.txt": "a",
2333            "b.txt": "bb",
2334            "c": {
2335                "d": {
2336                    "e.txt": "eee"
2337                }
2338            },
2339            "f.txt": "ffff",
2340            "target": {
2341                "build_file": "???"
2342            },
2343            ".gitignore": IGNORE_RULE
2344        },
2345
2346    }));
2347
2348    const A_TXT: &str = "a.txt";
2349    const B_TXT: &str = "b.txt";
2350    const E_TXT: &str = "c/d/e.txt";
2351    const F_TXT: &str = "f.txt";
2352    const DOTGITIGNORE: &str = ".gitignore";
2353    const BUILD_FILE: &str = "target/build_file";
2354    let project_path = Path::new("project");
2355
2356    // Set up git repository before creating the worktree.
2357    let work_dir = root.path().join("project");
2358    let mut repo = git_init(work_dir.as_path());
2359    repo.add_ignore_rule(IGNORE_RULE).unwrap();
2360    git_add(A_TXT, &repo);
2361    git_add(E_TXT, &repo);
2362    git_add(DOTGITIGNORE, &repo);
2363    git_commit("Initial commit", &repo);
2364
2365    let tree = Worktree::local(
2366        root.path(),
2367        true,
2368        Arc::new(RealFs::default()),
2369        Default::default(),
2370        &mut cx.to_async(),
2371    )
2372    .await
2373    .unwrap();
2374
2375    tree.flush_fs_events(cx).await;
2376    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2377        .await;
2378    cx.executor().run_until_parked();
2379
2380    // Check that the right git state is observed on startup
2381    tree.read_with(cx, |tree, _cx| {
2382        let snapshot = tree.snapshot();
2383        assert_eq!(snapshot.repositories().count(), 1);
2384        let repo_entry = snapshot.repositories().next().unwrap();
2385        assert_eq!(repo_entry.path.as_ref(), Path::new("project"));
2386        assert!(repo_entry.location_in_repo.is_none());
2387
2388        assert_eq!(
2389            snapshot.status_for_file(project_path.join(B_TXT)),
2390            Some(GitFileStatus::Untracked)
2391        );
2392        assert_eq!(
2393            snapshot.status_for_file(project_path.join(F_TXT)),
2394            Some(GitFileStatus::Untracked)
2395        );
2396    });
2397
2398    // Modify a file in the working copy.
2399    std::fs::write(work_dir.join(A_TXT), "aa").unwrap();
2400    tree.flush_fs_events(cx).await;
2401    cx.executor().run_until_parked();
2402
2403    // The worktree detects that the file's git status has changed.
2404    tree.read_with(cx, |tree, _cx| {
2405        let snapshot = tree.snapshot();
2406        assert_eq!(
2407            snapshot.status_for_file(project_path.join(A_TXT)),
2408            Some(GitFileStatus::Modified)
2409        );
2410    });
2411
2412    // Create a commit in the git repository.
2413    git_add(A_TXT, &repo);
2414    git_add(B_TXT, &repo);
2415    git_commit("Committing modified and added", &repo);
2416    tree.flush_fs_events(cx).await;
2417    cx.executor().run_until_parked();
2418
2419    // The worktree detects that the files' git status have changed.
2420    tree.read_with(cx, |tree, _cx| {
2421        let snapshot = tree.snapshot();
2422        assert_eq!(
2423            snapshot.status_for_file(project_path.join(F_TXT)),
2424            Some(GitFileStatus::Untracked)
2425        );
2426        assert_eq!(snapshot.status_for_file(project_path.join(B_TXT)), None);
2427        assert_eq!(snapshot.status_for_file(project_path.join(A_TXT)), None);
2428    });
2429
2430    // Modify files in the working copy and perform git operations on other files.
2431    git_reset(0, &repo);
2432    git_remove_index(Path::new(B_TXT), &repo);
2433    git_stash(&mut repo);
2434    std::fs::write(work_dir.join(E_TXT), "eeee").unwrap();
2435    std::fs::write(work_dir.join(BUILD_FILE), "this should be ignored").unwrap();
2436    tree.flush_fs_events(cx).await;
2437    cx.executor().run_until_parked();
2438
2439    // Check that more complex repo changes are tracked
2440    tree.read_with(cx, |tree, _cx| {
2441        let snapshot = tree.snapshot();
2442
2443        assert_eq!(snapshot.status_for_file(project_path.join(A_TXT)), None);
2444        assert_eq!(
2445            snapshot.status_for_file(project_path.join(B_TXT)),
2446            Some(GitFileStatus::Untracked)
2447        );
2448        assert_eq!(
2449            snapshot.status_for_file(project_path.join(E_TXT)),
2450            Some(GitFileStatus::Modified)
2451        );
2452    });
2453
2454    std::fs::remove_file(work_dir.join(B_TXT)).unwrap();
2455    std::fs::remove_dir_all(work_dir.join("c")).unwrap();
2456    std::fs::write(
2457        work_dir.join(DOTGITIGNORE),
2458        [IGNORE_RULE, "f.txt"].join("\n"),
2459    )
2460    .unwrap();
2461
2462    git_add(Path::new(DOTGITIGNORE), &repo);
2463    git_commit("Committing modified git ignore", &repo);
2464
2465    tree.flush_fs_events(cx).await;
2466    cx.executor().run_until_parked();
2467
2468    let mut renamed_dir_name = "first_directory/second_directory";
2469    const RENAMED_FILE: &str = "rf.txt";
2470
2471    std::fs::create_dir_all(work_dir.join(renamed_dir_name)).unwrap();
2472    std::fs::write(
2473        work_dir.join(renamed_dir_name).join(RENAMED_FILE),
2474        "new-contents",
2475    )
2476    .unwrap();
2477
2478    tree.flush_fs_events(cx).await;
2479    cx.executor().run_until_parked();
2480
2481    tree.read_with(cx, |tree, _cx| {
2482        let snapshot = tree.snapshot();
2483        assert_eq!(
2484            snapshot.status_for_file(project_path.join(renamed_dir_name).join(RENAMED_FILE)),
2485            Some(GitFileStatus::Untracked)
2486        );
2487    });
2488
2489    renamed_dir_name = "new_first_directory/second_directory";
2490
2491    std::fs::rename(
2492        work_dir.join("first_directory"),
2493        work_dir.join("new_first_directory"),
2494    )
2495    .unwrap();
2496
2497    tree.flush_fs_events(cx).await;
2498    cx.executor().run_until_parked();
2499
2500    tree.read_with(cx, |tree, _cx| {
2501        let snapshot = tree.snapshot();
2502
2503        assert_eq!(
2504            snapshot.status_for_file(
2505                project_path
2506                    .join(Path::new(renamed_dir_name))
2507                    .join(RENAMED_FILE)
2508            ),
2509            Some(GitFileStatus::Untracked)
2510        );
2511    });
2512}
2513
2514#[gpui::test]
2515async fn test_git_repository_status(cx: &mut TestAppContext) {
2516    init_test(cx);
2517    cx.executor().allow_parking();
2518
2519    let root = temp_tree(json!({
2520        "project": {
2521            "a.txt": "a",    // Modified
2522            "b.txt": "bb",   // Added
2523            "c.txt": "ccc",  // Unchanged
2524            "d.txt": "dddd", // Deleted
2525        },
2526
2527    }));
2528
2529    // Set up git repository before creating the worktree.
2530    let work_dir = root.path().join("project");
2531    let repo = git_init(work_dir.as_path());
2532    git_add("a.txt", &repo);
2533    git_add("c.txt", &repo);
2534    git_add("d.txt", &repo);
2535    git_commit("Initial commit", &repo);
2536    std::fs::remove_file(work_dir.join("d.txt")).unwrap();
2537    std::fs::write(work_dir.join("a.txt"), "aa").unwrap();
2538
2539    let tree = Worktree::local(
2540        root.path(),
2541        true,
2542        Arc::new(RealFs::default()),
2543        Default::default(),
2544        &mut cx.to_async(),
2545    )
2546    .await
2547    .unwrap();
2548
2549    tree.flush_fs_events(cx).await;
2550    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2551        .await;
2552    cx.executor().run_until_parked();
2553
2554    // Check that the right git state is observed on startup
2555    tree.read_with(cx, |tree, _cx| {
2556        let snapshot = tree.snapshot();
2557        let repo = snapshot.repositories().next().unwrap();
2558        let entries = repo.status().collect::<Vec<_>>();
2559
2560        assert_eq!(entries.len(), 3);
2561        assert_eq!(entries[0].repo_path.as_ref(), Path::new("a.txt"));
2562        assert_eq!(entries[0].status, GitFileStatus::Modified);
2563        assert_eq!(entries[1].repo_path.as_ref(), Path::new("b.txt"));
2564        assert_eq!(entries[1].status, GitFileStatus::Untracked);
2565        assert_eq!(entries[2].repo_path.as_ref(), Path::new("d.txt"));
2566        assert_eq!(entries[2].status, GitFileStatus::Deleted);
2567    });
2568
2569    std::fs::write(work_dir.join("c.txt"), "some changes").unwrap();
2570    eprintln!("File c.txt has been modified");
2571
2572    tree.flush_fs_events(cx).await;
2573    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2574        .await;
2575    cx.executor().run_until_parked();
2576
2577    tree.read_with(cx, |tree, _cx| {
2578        let snapshot = tree.snapshot();
2579        let repository = snapshot.repositories().next().unwrap();
2580        let entries = repository.status().collect::<Vec<_>>();
2581
2582        std::assert_eq!(entries.len(), 4, "entries: {entries:?}");
2583        assert_eq!(entries[0].repo_path.as_ref(), Path::new("a.txt"));
2584        assert_eq!(entries[0].status, GitFileStatus::Modified);
2585        assert_eq!(entries[1].repo_path.as_ref(), Path::new("b.txt"));
2586        assert_eq!(entries[1].status, GitFileStatus::Untracked);
2587        // Status updated
2588        assert_eq!(entries[2].repo_path.as_ref(), Path::new("c.txt"));
2589        assert_eq!(entries[2].status, GitFileStatus::Modified);
2590        assert_eq!(entries[3].repo_path.as_ref(), Path::new("d.txt"));
2591        assert_eq!(entries[3].status, GitFileStatus::Deleted);
2592    });
2593
2594    git_add("a.txt", &repo);
2595    git_add("c.txt", &repo);
2596    git_remove_index(Path::new("d.txt"), &repo);
2597    git_commit("Another commit", &repo);
2598    tree.flush_fs_events(cx).await;
2599    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2600        .await;
2601    cx.executor().run_until_parked();
2602
2603    std::fs::remove_file(work_dir.join("a.txt")).unwrap();
2604    std::fs::remove_file(work_dir.join("b.txt")).unwrap();
2605    tree.flush_fs_events(cx).await;
2606    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2607        .await;
2608    cx.executor().run_until_parked();
2609
2610    tree.read_with(cx, |tree, _cx| {
2611        let snapshot = tree.snapshot();
2612        let repo = snapshot.repositories().next().unwrap();
2613        let entries = repo.status().collect::<Vec<_>>();
2614
2615        // Deleting an untracked entry, b.txt, should leave no status
2616        // a.txt was tracked, and so should have a status
2617        assert_eq!(
2618            entries.len(),
2619            1,
2620            "Entries length was incorrect\n{:#?}",
2621            &entries
2622        );
2623        assert_eq!(entries[0].repo_path.as_ref(), Path::new("a.txt"));
2624        assert_eq!(entries[0].status, GitFileStatus::Deleted);
2625    });
2626}
2627
2628#[gpui::test]
2629async fn test_repository_subfolder_git_status(cx: &mut TestAppContext) {
2630    init_test(cx);
2631    cx.executor().allow_parking();
2632
2633    let root = temp_tree(json!({
2634        "my-repo": {
2635            // .git folder will go here
2636            "a.txt": "a",
2637            "sub-folder-1": {
2638                "sub-folder-2": {
2639                    "c.txt": "cc",
2640                    "d": {
2641                        "e.txt": "eee"
2642                    }
2643                },
2644            }
2645        },
2646
2647    }));
2648
2649    const C_TXT: &str = "sub-folder-1/sub-folder-2/c.txt";
2650    const E_TXT: &str = "sub-folder-1/sub-folder-2/d/e.txt";
2651
2652    // Set up git repository before creating the worktree.
2653    let git_repo_work_dir = root.path().join("my-repo");
2654    let repo = git_init(git_repo_work_dir.as_path());
2655    git_add(C_TXT, &repo);
2656    git_commit("Initial commit", &repo);
2657
2658    // Open the worktree in subfolder
2659    let project_root = Path::new("my-repo/sub-folder-1/sub-folder-2");
2660    let tree = Worktree::local(
2661        root.path().join(project_root),
2662        true,
2663        Arc::new(RealFs::default()),
2664        Default::default(),
2665        &mut cx.to_async(),
2666    )
2667    .await
2668    .unwrap();
2669
2670    tree.flush_fs_events(cx).await;
2671    tree.flush_fs_events_in_root_git_repository(cx).await;
2672    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2673        .await;
2674    cx.executor().run_until_parked();
2675
2676    // Ensure that the git status is loaded correctly
2677    tree.read_with(cx, |tree, _cx| {
2678        let snapshot = tree.snapshot();
2679        assert_eq!(snapshot.repositories().count(), 1);
2680        let repo = snapshot.repositories().next().unwrap();
2681        // Path is blank because the working directory of
2682        // the git repository is located at the root of the project
2683        assert_eq!(repo.path.as_ref(), Path::new(""));
2684
2685        // This is the missing path between the root of the project (sub-folder-2) and its
2686        // location relative to the root of the repository.
2687        assert_eq!(
2688            repo.location_in_repo,
2689            Some(Arc::from(Path::new("sub-folder-1/sub-folder-2")))
2690        );
2691
2692        assert_eq!(snapshot.status_for_file("c.txt"), None);
2693        assert_eq!(
2694            snapshot.status_for_file("d/e.txt"),
2695            Some(GitFileStatus::Untracked)
2696        );
2697    });
2698
2699    // Now we simulate FS events, but ONLY in the .git folder that's outside
2700    // of out project root.
2701    // Meaning: we don't produce any FS events for files inside the project.
2702    git_add(E_TXT, &repo);
2703    git_commit("Second commit", &repo);
2704    tree.flush_fs_events_in_root_git_repository(cx).await;
2705    cx.executor().run_until_parked();
2706
2707    tree.read_with(cx, |tree, _cx| {
2708        let snapshot = tree.snapshot();
2709
2710        assert!(snapshot.repositories().next().is_some());
2711
2712        assert_eq!(snapshot.status_for_file("c.txt"), None);
2713        assert_eq!(snapshot.status_for_file("d/e.txt"), None);
2714    });
2715}
2716
2717#[gpui::test]
2718async fn test_traverse_with_git_status(cx: &mut TestAppContext) {
2719    init_test(cx);
2720    let fs = FakeFs::new(cx.background_executor.clone());
2721    fs.insert_tree(
2722        "/root",
2723        json!({
2724            "x": {
2725                ".git": {},
2726                "x1.txt": "foo",
2727                "x2.txt": "bar",
2728                "y": {
2729                    ".git": {},
2730                    "y1.txt": "baz",
2731                    "y2.txt": "qux"
2732                },
2733                "z.txt": "sneaky..."
2734            },
2735            "z": {
2736                ".git": {},
2737                "z1.txt": "quux",
2738                "z2.txt": "quuux"
2739            }
2740        }),
2741    )
2742    .await;
2743
2744    fs.set_status_for_repo_via_git_operation(
2745        Path::new("/root/x/.git"),
2746        &[
2747            (Path::new("x2.txt"), GitFileStatus::Modified),
2748            (Path::new("z.txt"), GitFileStatus::Added),
2749        ],
2750    );
2751    fs.set_status_for_repo_via_git_operation(
2752        Path::new("/root/x/y/.git"),
2753        &[(Path::new("y1.txt"), GitFileStatus::Conflict)],
2754    );
2755    fs.set_status_for_repo_via_git_operation(
2756        Path::new("/root/z/.git"),
2757        &[(Path::new("z2.txt"), GitFileStatus::Added)],
2758    );
2759
2760    let tree = Worktree::local(
2761        Path::new("/root"),
2762        true,
2763        fs.clone(),
2764        Default::default(),
2765        &mut cx.to_async(),
2766    )
2767    .await
2768    .unwrap();
2769
2770    tree.flush_fs_events(cx).await;
2771    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2772        .await;
2773    cx.executor().run_until_parked();
2774
2775    let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
2776
2777    let mut traversal = snapshot
2778        .traverse_from_path(true, false, true, Path::new("x"))
2779        .with_git_statuses();
2780
2781    let entry = traversal.next().unwrap();
2782    assert_eq!(entry.path.as_ref(), Path::new("x/x1.txt"));
2783    assert_eq!(entry.git_status, None);
2784    let entry = traversal.next().unwrap();
2785    assert_eq!(entry.path.as_ref(), Path::new("x/x2.txt"));
2786    assert_eq!(entry.git_status, Some(GitFileStatus::Modified));
2787    let entry = traversal.next().unwrap();
2788    assert_eq!(entry.path.as_ref(), Path::new("x/y/y1.txt"));
2789    assert_eq!(entry.git_status, Some(GitFileStatus::Conflict));
2790    let entry = traversal.next().unwrap();
2791    assert_eq!(entry.path.as_ref(), Path::new("x/y/y2.txt"));
2792    assert_eq!(entry.git_status, None);
2793    let entry = traversal.next().unwrap();
2794    assert_eq!(entry.path.as_ref(), Path::new("x/z.txt"));
2795    assert_eq!(entry.git_status, Some(GitFileStatus::Added));
2796    let entry = traversal.next().unwrap();
2797    assert_eq!(entry.path.as_ref(), Path::new("z/z1.txt"));
2798    assert_eq!(entry.git_status, None);
2799    let entry = traversal.next().unwrap();
2800    assert_eq!(entry.path.as_ref(), Path::new("z/z2.txt"));
2801    assert_eq!(entry.git_status, Some(GitFileStatus::Added));
2802}
2803
2804#[gpui::test]
2805async fn test_propagate_git_statuses(cx: &mut TestAppContext) {
2806    init_test(cx);
2807    let fs = FakeFs::new(cx.background_executor.clone());
2808    fs.insert_tree(
2809        "/root",
2810        json!({
2811            ".git": {},
2812            "a": {
2813                "b": {
2814                    "c1.txt": "",
2815                    "c2.txt": "",
2816                },
2817                "d": {
2818                    "e1.txt": "",
2819                    "e2.txt": "",
2820                    "e3.txt": "",
2821                }
2822            },
2823            "f": {
2824                "no-status.txt": ""
2825            },
2826            "g": {
2827                "h1.txt": "",
2828                "h2.txt": ""
2829            },
2830        }),
2831    )
2832    .await;
2833
2834    fs.set_status_for_repo_via_git_operation(
2835        Path::new("/root/.git"),
2836        &[
2837            (Path::new("a/b/c1.txt"), GitFileStatus::Added),
2838            (Path::new("a/d/e2.txt"), GitFileStatus::Modified),
2839            (Path::new("g/h2.txt"), GitFileStatus::Conflict),
2840        ],
2841    );
2842
2843    let tree = Worktree::local(
2844        Path::new("/root"),
2845        true,
2846        fs.clone(),
2847        Default::default(),
2848        &mut cx.to_async(),
2849    )
2850    .await
2851    .unwrap();
2852
2853    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2854        .await;
2855
2856    cx.executor().run_until_parked();
2857    let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
2858
2859    check_git_statuses(
2860        &snapshot,
2861        &[
2862            (Path::new(""), Some(GitFileStatus::Conflict)),
2863            (Path::new("g"), Some(GitFileStatus::Conflict)),
2864            (Path::new("g/h2.txt"), Some(GitFileStatus::Conflict)),
2865        ],
2866    );
2867
2868    check_git_statuses(
2869        &snapshot,
2870        &[
2871            (Path::new(""), Some(GitFileStatus::Conflict)),
2872            (Path::new("a"), Some(GitFileStatus::Modified)),
2873            (Path::new("a/b"), Some(GitFileStatus::Added)),
2874            (Path::new("a/b/c1.txt"), Some(GitFileStatus::Added)),
2875            (Path::new("a/b/c2.txt"), None),
2876            (Path::new("a/d"), Some(GitFileStatus::Modified)),
2877            (Path::new("a/d/e2.txt"), Some(GitFileStatus::Modified)),
2878            (Path::new("f"), None),
2879            (Path::new("f/no-status.txt"), None),
2880            (Path::new("g"), Some(GitFileStatus::Conflict)),
2881            (Path::new("g/h2.txt"), Some(GitFileStatus::Conflict)),
2882        ],
2883    );
2884
2885    check_git_statuses(
2886        &snapshot,
2887        &[
2888            (Path::new("a/b"), Some(GitFileStatus::Added)),
2889            (Path::new("a/b/c1.txt"), Some(GitFileStatus::Added)),
2890            (Path::new("a/b/c2.txt"), None),
2891            (Path::new("a/d"), Some(GitFileStatus::Modified)),
2892            (Path::new("a/d/e1.txt"), None),
2893            (Path::new("a/d/e2.txt"), Some(GitFileStatus::Modified)),
2894            (Path::new("f"), None),
2895            (Path::new("f/no-status.txt"), None),
2896            (Path::new("g"), Some(GitFileStatus::Conflict)),
2897        ],
2898    );
2899
2900    check_git_statuses(
2901        &snapshot,
2902        &[
2903            (Path::new("a/b/c1.txt"), Some(GitFileStatus::Added)),
2904            (Path::new("a/b/c2.txt"), None),
2905            (Path::new("a/d/e1.txt"), None),
2906            (Path::new("a/d/e2.txt"), Some(GitFileStatus::Modified)),
2907            (Path::new("f/no-status.txt"), None),
2908        ],
2909    );
2910}
2911
2912#[gpui::test]
2913async fn test_propagate_statuses_for_repos_under_project(cx: &mut TestAppContext) {
2914    init_test(cx);
2915    let fs = FakeFs::new(cx.background_executor.clone());
2916    fs.insert_tree(
2917        "/root",
2918        json!({
2919            "x": {
2920                ".git": {},
2921                "x1.txt": "foo",
2922                "x2.txt": "bar"
2923            },
2924            "y": {
2925                ".git": {},
2926                "y1.txt": "baz",
2927                "y2.txt": "qux"
2928            },
2929            "z": {
2930                ".git": {},
2931                "z1.txt": "quux",
2932                "z2.txt": "quuux"
2933            }
2934        }),
2935    )
2936    .await;
2937
2938    fs.set_status_for_repo_via_git_operation(
2939        Path::new("/root/x/.git"),
2940        &[(Path::new("x1.txt"), GitFileStatus::Added)],
2941    );
2942    fs.set_status_for_repo_via_git_operation(
2943        Path::new("/root/y/.git"),
2944        &[
2945            (Path::new("y1.txt"), GitFileStatus::Conflict),
2946            (Path::new("y2.txt"), GitFileStatus::Modified),
2947        ],
2948    );
2949    fs.set_status_for_repo_via_git_operation(
2950        Path::new("/root/z/.git"),
2951        &[(Path::new("z2.txt"), GitFileStatus::Modified)],
2952    );
2953
2954    let tree = Worktree::local(
2955        Path::new("/root"),
2956        true,
2957        fs.clone(),
2958        Default::default(),
2959        &mut cx.to_async(),
2960    )
2961    .await
2962    .unwrap();
2963
2964    tree.flush_fs_events(cx).await;
2965    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2966        .await;
2967    cx.executor().run_until_parked();
2968
2969    let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
2970
2971    check_git_statuses(
2972        &snapshot,
2973        &[
2974            (Path::new("x"), Some(GitFileStatus::Added)),
2975            (Path::new("x/x1.txt"), Some(GitFileStatus::Added)),
2976        ],
2977    );
2978
2979    check_git_statuses(
2980        &snapshot,
2981        &[
2982            (Path::new("y"), Some(GitFileStatus::Conflict)),
2983            (Path::new("y/y1.txt"), Some(GitFileStatus::Conflict)),
2984            (Path::new("y/y2.txt"), Some(GitFileStatus::Modified)),
2985        ],
2986    );
2987
2988    check_git_statuses(
2989        &snapshot,
2990        &[
2991            (Path::new("z"), Some(GitFileStatus::Modified)),
2992            (Path::new("z/z2.txt"), Some(GitFileStatus::Modified)),
2993        ],
2994    );
2995
2996    check_git_statuses(
2997        &snapshot,
2998        &[
2999            (Path::new("x"), Some(GitFileStatus::Added)),
3000            (Path::new("x/x1.txt"), Some(GitFileStatus::Added)),
3001        ],
3002    );
3003
3004    check_git_statuses(
3005        &snapshot,
3006        &[
3007            (Path::new("x"), Some(GitFileStatus::Added)),
3008            (Path::new("x/x1.txt"), Some(GitFileStatus::Added)),
3009            (Path::new("x/x2.txt"), None),
3010            (Path::new("y"), Some(GitFileStatus::Conflict)),
3011            (Path::new("y/y1.txt"), Some(GitFileStatus::Conflict)),
3012            (Path::new("y/y2.txt"), Some(GitFileStatus::Modified)),
3013            (Path::new("z"), Some(GitFileStatus::Modified)),
3014            (Path::new("z/z1.txt"), None),
3015            (Path::new("z/z2.txt"), Some(GitFileStatus::Modified)),
3016        ],
3017    );
3018}
3019
3020#[gpui::test]
3021async fn test_propagate_statuses_for_nested_repos(cx: &mut TestAppContext) {
3022    init_test(cx);
3023    let fs = FakeFs::new(cx.background_executor.clone());
3024    fs.insert_tree(
3025        "/root",
3026        json!({
3027            "x": {
3028                ".git": {},
3029                "x1.txt": "foo",
3030                "x2.txt": "bar",
3031                "y": {
3032                    ".git": {},
3033                    "y1.txt": "baz",
3034                    "y2.txt": "qux"
3035                },
3036                "z.txt": "sneaky..."
3037            },
3038            "z": {
3039                ".git": {},
3040                "z1.txt": "quux",
3041                "z2.txt": "quuux"
3042            }
3043        }),
3044    )
3045    .await;
3046
3047    fs.set_status_for_repo_via_git_operation(
3048        Path::new("/root/x/.git"),
3049        &[
3050            (Path::new("x2.txt"), GitFileStatus::Modified),
3051            (Path::new("z.txt"), GitFileStatus::Added),
3052        ],
3053    );
3054    fs.set_status_for_repo_via_git_operation(
3055        Path::new("/root/x/y/.git"),
3056        &[(Path::new("y1.txt"), GitFileStatus::Conflict)],
3057    );
3058
3059    fs.set_status_for_repo_via_git_operation(
3060        Path::new("/root/z/.git"),
3061        &[(Path::new("z2.txt"), GitFileStatus::Added)],
3062    );
3063
3064    let tree = Worktree::local(
3065        Path::new("/root"),
3066        true,
3067        fs.clone(),
3068        Default::default(),
3069        &mut cx.to_async(),
3070    )
3071    .await
3072    .unwrap();
3073
3074    tree.flush_fs_events(cx).await;
3075    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
3076        .await;
3077    cx.executor().run_until_parked();
3078
3079    let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
3080
3081    // Sanity check the propagation for x/y and z
3082    check_git_statuses(
3083        &snapshot,
3084        &[
3085            (Path::new("x/y"), Some(GitFileStatus::Conflict)), // the y git repository has conflict file in it, and so should have a conflict status
3086            (Path::new("x/y/y1.txt"), Some(GitFileStatus::Conflict)),
3087            (Path::new("x/y/y2.txt"), None),
3088        ],
3089    );
3090    check_git_statuses(
3091        &snapshot,
3092        &[
3093            (Path::new("z"), Some(GitFileStatus::Added)),
3094            (Path::new("z/z1.txt"), None),
3095            (Path::new("z/z2.txt"), Some(GitFileStatus::Added)),
3096        ],
3097    );
3098
3099    // Test one of the fundamental cases of propagation blocking, the transition from one git repository to another
3100    check_git_statuses(
3101        &snapshot,
3102        &[
3103            (Path::new("x"), Some(GitFileStatus::Modified)),
3104            (Path::new("x/y"), Some(GitFileStatus::Conflict)),
3105            (Path::new("x/y/y1.txt"), Some(GitFileStatus::Conflict)),
3106        ],
3107    );
3108
3109    // Sanity check everything around it
3110    check_git_statuses(
3111        &snapshot,
3112        &[
3113            (Path::new("x"), Some(GitFileStatus::Modified)),
3114            (Path::new("x/x1.txt"), None),
3115            (Path::new("x/x2.txt"), Some(GitFileStatus::Modified)),
3116            (Path::new("x/y"), Some(GitFileStatus::Conflict)),
3117            (Path::new("x/y/y1.txt"), Some(GitFileStatus::Conflict)),
3118            (Path::new("x/y/y2.txt"), None),
3119            (Path::new("x/z.txt"), Some(GitFileStatus::Added)),
3120        ],
3121    );
3122
3123    // Test the other fundamental case, transitioning from git repository to non-git repository
3124    check_git_statuses(
3125        &snapshot,
3126        &[
3127            (Path::new(""), None),
3128            (Path::new("x"), Some(GitFileStatus::Modified)),
3129            (Path::new("x/x1.txt"), None),
3130        ],
3131    );
3132
3133    // And all together now
3134    check_git_statuses(
3135        &snapshot,
3136        &[
3137            (Path::new(""), None),
3138            (Path::new("x"), Some(GitFileStatus::Modified)),
3139            (Path::new("x/x1.txt"), None),
3140            (Path::new("x/x2.txt"), Some(GitFileStatus::Modified)),
3141            (Path::new("x/y"), Some(GitFileStatus::Conflict)),
3142            (Path::new("x/y/y1.txt"), Some(GitFileStatus::Conflict)),
3143            (Path::new("x/y/y2.txt"), None),
3144            (Path::new("x/z.txt"), Some(GitFileStatus::Added)),
3145            (Path::new("z"), Some(GitFileStatus::Added)),
3146            (Path::new("z/z1.txt"), None),
3147            (Path::new("z/z2.txt"), Some(GitFileStatus::Added)),
3148        ],
3149    );
3150}
3151
3152#[gpui::test]
3153async fn test_private_single_file_worktree(cx: &mut TestAppContext) {
3154    init_test(cx);
3155    let fs = FakeFs::new(cx.background_executor.clone());
3156    fs.insert_tree("/", json!({".env": "PRIVATE=secret\n"}))
3157        .await;
3158    let tree = Worktree::local(
3159        Path::new("/.env"),
3160        true,
3161        fs.clone(),
3162        Default::default(),
3163        &mut cx.to_async(),
3164    )
3165    .await
3166    .unwrap();
3167    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
3168        .await;
3169    tree.read_with(cx, |tree, _| {
3170        let entry = tree.entry_for_path("").unwrap();
3171        assert!(entry.is_private);
3172    });
3173}
3174
3175#[track_caller]
3176fn check_git_statuses(snapshot: &Snapshot, expected_statuses: &[(&Path, Option<GitFileStatus>)]) {
3177    let mut traversal = snapshot
3178        .traverse_from_path(true, true, false, "".as_ref())
3179        .with_git_statuses();
3180    let found_statuses = expected_statuses
3181        .iter()
3182        .map(|&(path, _)| {
3183            let git_entry = traversal
3184                .find(|git_entry| &*git_entry.path == path)
3185                .expect("Traversal has no entry for {path:?}");
3186            (path, git_entry.git_status)
3187        })
3188        .collect::<Vec<_>>();
3189    assert_eq!(found_statuses, expected_statuses);
3190}
3191
3192#[track_caller]
3193fn git_init(path: &Path) -> git2::Repository {
3194    git2::Repository::init(path).expect("Failed to initialize git repository")
3195}
3196
3197#[track_caller]
3198fn git_add<P: AsRef<Path>>(path: P, repo: &git2::Repository) {
3199    let path = path.as_ref();
3200    let mut index = repo.index().expect("Failed to get index");
3201    index.add_path(path).expect("Failed to add file");
3202    index.write().expect("Failed to write index");
3203}
3204
3205#[track_caller]
3206fn git_remove_index(path: &Path, repo: &git2::Repository) {
3207    let mut index = repo.index().expect("Failed to get index");
3208    index.remove_path(path).expect("Failed to add file");
3209    index.write().expect("Failed to write index");
3210}
3211
3212#[track_caller]
3213fn git_commit(msg: &'static str, repo: &git2::Repository) {
3214    use git2::Signature;
3215
3216    let signature = Signature::now("test", "test@zed.dev").unwrap();
3217    let oid = repo.index().unwrap().write_tree().unwrap();
3218    let tree = repo.find_tree(oid).unwrap();
3219    if let Ok(head) = repo.head() {
3220        let parent_obj = head.peel(git2::ObjectType::Commit).unwrap();
3221
3222        let parent_commit = parent_obj.as_commit().unwrap();
3223
3224        repo.commit(
3225            Some("HEAD"),
3226            &signature,
3227            &signature,
3228            msg,
3229            &tree,
3230            &[parent_commit],
3231        )
3232        .expect("Failed to commit with parent");
3233    } else {
3234        repo.commit(Some("HEAD"), &signature, &signature, msg, &tree, &[])
3235            .expect("Failed to commit");
3236    }
3237}
3238
3239#[track_caller]
3240fn git_stash(repo: &mut git2::Repository) {
3241    use git2::Signature;
3242
3243    let signature = Signature::now("test", "test@zed.dev").unwrap();
3244    repo.stash_save(&signature, "N/A", None)
3245        .expect("Failed to stash");
3246}
3247
3248#[track_caller]
3249fn git_reset(offset: usize, repo: &git2::Repository) {
3250    let head = repo.head().expect("Couldn't get repo head");
3251    let object = head.peel(git2::ObjectType::Commit).unwrap();
3252    let commit = object.as_commit().unwrap();
3253    let new_head = commit
3254        .parents()
3255        .inspect(|parnet| {
3256            parnet.message();
3257        })
3258        .nth(offset)
3259        .expect("Not enough history");
3260    repo.reset(new_head.as_object(), git2::ResetType::Soft, None)
3261        .expect("Could not reset");
3262}
3263
3264#[allow(dead_code)]
3265#[track_caller]
3266fn git_status(repo: &git2::Repository) -> collections::HashMap<String, git2::Status> {
3267    repo.statuses(None)
3268        .unwrap()
3269        .iter()
3270        .map(|status| (status.path().unwrap().to_string(), status.status()))
3271        .collect()
3272}
3273
3274#[track_caller]
3275fn check_worktree_entries(
3276    tree: &Worktree,
3277    expected_excluded_paths: &[&str],
3278    expected_ignored_paths: &[&str],
3279    expected_tracked_paths: &[&str],
3280    expected_included_paths: &[&str],
3281) {
3282    for path in expected_excluded_paths {
3283        let entry = tree.entry_for_path(path);
3284        assert!(
3285            entry.is_none(),
3286            "expected path '{path}' to be excluded, but got entry: {entry:?}",
3287        );
3288    }
3289    for path in expected_ignored_paths {
3290        let entry = tree
3291            .entry_for_path(path)
3292            .unwrap_or_else(|| panic!("Missing entry for expected ignored path '{path}'"));
3293        assert!(
3294            entry.is_ignored,
3295            "expected path '{path}' to be ignored, but got entry: {entry:?}",
3296        );
3297    }
3298    for path in expected_tracked_paths {
3299        let entry = tree
3300            .entry_for_path(path)
3301            .unwrap_or_else(|| panic!("Missing entry for expected tracked path '{path}'"));
3302        assert!(
3303            !entry.is_ignored || entry.is_always_included,
3304            "expected path '{path}' to be tracked, but got entry: {entry:?}",
3305        );
3306    }
3307    for path in expected_included_paths {
3308        let entry = tree
3309            .entry_for_path(path)
3310            .unwrap_or_else(|| panic!("Missing entry for expected included path '{path}'"));
3311        assert!(
3312            entry.is_always_included,
3313            "expected path '{path}' to always be included, but got entry: {entry:?}",
3314        );
3315    }
3316}
3317
3318fn init_test(cx: &mut gpui::TestAppContext) {
3319    if std::env::var("RUST_LOG").is_ok() {
3320        env_logger::try_init().ok();
3321    }
3322
3323    cx.update(|cx| {
3324        let settings_store = SettingsStore::test(cx);
3325        cx.set_global(settings_store);
3326        WorktreeSettings::register(cx);
3327    });
3328}
3329
3330fn assert_entry_git_state(
3331    tree: &Worktree,
3332    path: &str,
3333    git_status: Option<GitFileStatus>,
3334    is_ignored: bool,
3335) {
3336    let entry = tree.entry_for_path(path).expect("entry {path} not found");
3337    assert_eq!(
3338        tree.status_for_file(Path::new(path)),
3339        git_status,
3340        "expected {path} to have git status: {git_status:?}"
3341    );
3342    assert_eq!(
3343        entry.is_ignored, is_ignored,
3344        "expected {path} to have is_ignored: {is_ignored}"
3345    );
3346}