worktree_tests.rs

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