worktree_tests.rs

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