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(target_os = "linux")]
 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_exclusions(cx: &mut TestAppContext) {
 883    init_test(cx);
 884    cx.executor().allow_parking();
 885    let dir = temp_tree(json!({
 886        ".gitignore": "**/target\n/node_modules\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        ".DS_Store": "",
 908    }));
 909    cx.update(|cx| {
 910        cx.update_global::<SettingsStore, _>(|store, cx| {
 911            store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
 912                project_settings.file_scan_exclusions =
 913                    Some(vec!["**/foo/**".to_string(), "**/.DS_Store".to_string()]);
 914            });
 915        });
 916    });
 917
 918    let tree = Worktree::local(
 919        dir.path(),
 920        true,
 921        Arc::new(RealFs::default()),
 922        Default::default(),
 923        &mut cx.to_async(),
 924    )
 925    .await
 926    .unwrap();
 927    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
 928        .await;
 929    tree.flush_fs_events(cx).await;
 930    tree.read_with(cx, |tree, _| {
 931        check_worktree_entries(
 932            tree,
 933            &[
 934                "src/foo/foo.rs",
 935                "src/foo/another.rs",
 936                "node_modules/.DS_Store",
 937                "src/.DS_Store",
 938                ".DS_Store",
 939            ],
 940            &["target", "node_modules"],
 941            &["src/lib.rs", "src/bar/bar.rs", ".gitignore"],
 942        )
 943    });
 944
 945    cx.update(|cx| {
 946        cx.update_global::<SettingsStore, _>(|store, cx| {
 947            store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
 948                project_settings.file_scan_exclusions =
 949                    Some(vec!["**/node_modules/**".to_string()]);
 950            });
 951        });
 952    });
 953    tree.flush_fs_events(cx).await;
 954    cx.executor().run_until_parked();
 955    tree.read_with(cx, |tree, _| {
 956        check_worktree_entries(
 957            tree,
 958            &[
 959                "node_modules/prettier/package.json",
 960                "node_modules/.DS_Store",
 961                "node_modules",
 962            ],
 963            &["target"],
 964            &[
 965                ".gitignore",
 966                "src/lib.rs",
 967                "src/bar/bar.rs",
 968                "src/foo/foo.rs",
 969                "src/foo/another.rs",
 970                "src/.DS_Store",
 971                ".DS_Store",
 972            ],
 973        )
 974    });
 975}
 976
 977#[gpui::test]
 978async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) {
 979    init_test(cx);
 980    cx.executor().allow_parking();
 981    let dir = temp_tree(json!({
 982        ".git": {
 983            "HEAD": "ref: refs/heads/main\n",
 984            "foo": "bar",
 985        },
 986        ".gitignore": "**/target\n/node_modules\ntest_output\n",
 987        "target": {
 988            "index": "blah2"
 989        },
 990        "node_modules": {
 991            ".DS_Store": "",
 992            "prettier": {
 993                "package.json": "{}",
 994            },
 995        },
 996        "src": {
 997            ".DS_Store": "",
 998            "foo": {
 999                "foo.rs": "mod another;\n",
1000                "another.rs": "// another",
1001            },
1002            "bar": {
1003                "bar.rs": "// bar",
1004            },
1005            "lib.rs": "mod foo;\nmod bar;\n",
1006        },
1007        ".DS_Store": "",
1008    }));
1009    cx.update(|cx| {
1010        cx.update_global::<SettingsStore, _>(|store, cx| {
1011            store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
1012                project_settings.file_scan_exclusions = Some(vec![
1013                    "**/.git".to_string(),
1014                    "node_modules/".to_string(),
1015                    "build_output".to_string(),
1016                ]);
1017            });
1018        });
1019    });
1020
1021    let tree = Worktree::local(
1022        dir.path(),
1023        true,
1024        Arc::new(RealFs::default()),
1025        Default::default(),
1026        &mut cx.to_async(),
1027    )
1028    .await
1029    .unwrap();
1030    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1031        .await;
1032    tree.flush_fs_events(cx).await;
1033    tree.read_with(cx, |tree, _| {
1034        check_worktree_entries(
1035            tree,
1036            &[
1037                ".git/HEAD",
1038                ".git/foo",
1039                "node_modules",
1040                "node_modules/.DS_Store",
1041                "node_modules/prettier",
1042                "node_modules/prettier/package.json",
1043            ],
1044            &["target"],
1045            &[
1046                ".DS_Store",
1047                "src/.DS_Store",
1048                "src/lib.rs",
1049                "src/foo/foo.rs",
1050                "src/foo/another.rs",
1051                "src/bar/bar.rs",
1052                ".gitignore",
1053            ],
1054        )
1055    });
1056
1057    let new_excluded_dir = dir.path().join("build_output");
1058    let new_ignored_dir = dir.path().join("test_output");
1059    std::fs::create_dir_all(&new_excluded_dir)
1060        .unwrap_or_else(|e| panic!("Failed to create a {new_excluded_dir:?} directory: {e}"));
1061    std::fs::create_dir_all(&new_ignored_dir)
1062        .unwrap_or_else(|e| panic!("Failed to create a {new_ignored_dir:?} directory: {e}"));
1063    let node_modules_dir = dir.path().join("node_modules");
1064    let dot_git_dir = dir.path().join(".git");
1065    let src_dir = dir.path().join("src");
1066    for existing_dir in [&node_modules_dir, &dot_git_dir, &src_dir] {
1067        assert!(
1068            existing_dir.is_dir(),
1069            "Expect {existing_dir:?} to be present in the FS already"
1070        );
1071    }
1072
1073    for directory_for_new_file in [
1074        new_excluded_dir,
1075        new_ignored_dir,
1076        node_modules_dir,
1077        dot_git_dir,
1078        src_dir,
1079    ] {
1080        std::fs::write(directory_for_new_file.join("new_file"), "new file contents")
1081            .unwrap_or_else(|e| {
1082                panic!("Failed to create in {directory_for_new_file:?} a new file: {e}")
1083            });
1084    }
1085    tree.flush_fs_events(cx).await;
1086
1087    tree.read_with(cx, |tree, _| {
1088        check_worktree_entries(
1089            tree,
1090            &[
1091                ".git/HEAD",
1092                ".git/foo",
1093                ".git/new_file",
1094                "node_modules",
1095                "node_modules/.DS_Store",
1096                "node_modules/prettier",
1097                "node_modules/prettier/package.json",
1098                "node_modules/new_file",
1099                "build_output",
1100                "build_output/new_file",
1101                "test_output/new_file",
1102            ],
1103            &["target", "test_output"],
1104            &[
1105                ".DS_Store",
1106                "src/.DS_Store",
1107                "src/lib.rs",
1108                "src/foo/foo.rs",
1109                "src/foo/another.rs",
1110                "src/bar/bar.rs",
1111                "src/new_file",
1112                ".gitignore",
1113            ],
1114        )
1115    });
1116}
1117
1118#[gpui::test]
1119async fn test_fs_events_in_dot_git_worktree(cx: &mut TestAppContext) {
1120    init_test(cx);
1121    cx.executor().allow_parking();
1122    let dir = temp_tree(json!({
1123        ".git": {
1124            "HEAD": "ref: refs/heads/main\n",
1125            "foo": "foo contents",
1126        },
1127    }));
1128    let dot_git_worktree_dir = dir.path().join(".git");
1129
1130    let tree = Worktree::local(
1131        dot_git_worktree_dir.clone(),
1132        true,
1133        Arc::new(RealFs::default()),
1134        Default::default(),
1135        &mut cx.to_async(),
1136    )
1137    .await
1138    .unwrap();
1139    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1140        .await;
1141    tree.flush_fs_events(cx).await;
1142    tree.read_with(cx, |tree, _| {
1143        check_worktree_entries(tree, &[], &["HEAD", "foo"], &[])
1144    });
1145
1146    std::fs::write(dot_git_worktree_dir.join("new_file"), "new file contents")
1147        .unwrap_or_else(|e| panic!("Failed to create in {dot_git_worktree_dir:?} a new file: {e}"));
1148    tree.flush_fs_events(cx).await;
1149    tree.read_with(cx, |tree, _| {
1150        check_worktree_entries(tree, &[], &["HEAD", "foo", "new_file"], &[])
1151    });
1152}
1153
1154#[gpui::test(iterations = 30)]
1155async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
1156    init_test(cx);
1157    let fs = FakeFs::new(cx.background_executor.clone());
1158    fs.insert_tree(
1159        "/root",
1160        json!({
1161            "b": {},
1162            "c": {},
1163            "d": {},
1164        }),
1165    )
1166    .await;
1167
1168    let tree = Worktree::local(
1169        "/root".as_ref(),
1170        true,
1171        fs,
1172        Default::default(),
1173        &mut cx.to_async(),
1174    )
1175    .await
1176    .unwrap();
1177
1178    let snapshot1 = tree.update(cx, |tree, cx| {
1179        let tree = tree.as_local_mut().unwrap();
1180        let snapshot = Arc::new(Mutex::new(tree.snapshot()));
1181        tree.observe_updates(0, cx, {
1182            let snapshot = snapshot.clone();
1183            move |update| {
1184                snapshot.lock().apply_remote_update(update).unwrap();
1185                async { true }
1186            }
1187        });
1188        snapshot
1189    });
1190
1191    let entry = tree
1192        .update(cx, |tree, cx| {
1193            tree.as_local_mut()
1194                .unwrap()
1195                .create_entry("a/e".as_ref(), true, cx)
1196        })
1197        .await
1198        .unwrap()
1199        .to_included()
1200        .unwrap();
1201    assert!(entry.is_dir());
1202
1203    cx.executor().run_until_parked();
1204    tree.read_with(cx, |tree, _| {
1205        assert_eq!(tree.entry_for_path("a/e").unwrap().kind, EntryKind::Dir);
1206    });
1207
1208    let snapshot2 = tree.update(cx, |tree, _| tree.as_local().unwrap().snapshot());
1209    assert_eq!(
1210        snapshot1.lock().entries(true, 0).collect::<Vec<_>>(),
1211        snapshot2.entries(true, 0).collect::<Vec<_>>()
1212    );
1213}
1214
1215#[gpui::test]
1216async fn test_bump_mtime_of_git_repo_workdir(cx: &mut TestAppContext) {
1217    init_test(cx);
1218
1219    // Create a worktree with a git directory.
1220    let fs = FakeFs::new(cx.background_executor.clone());
1221    fs.insert_tree(
1222        "/root",
1223        json!({
1224            ".git": {},
1225            "a.txt": "",
1226            "b":  {
1227                "c.txt": "",
1228            },
1229        }),
1230    )
1231    .await;
1232
1233    let tree = Worktree::local(
1234        "/root".as_ref(),
1235        true,
1236        fs.clone(),
1237        Default::default(),
1238        &mut cx.to_async(),
1239    )
1240    .await
1241    .unwrap();
1242    cx.executor().run_until_parked();
1243
1244    let (old_entry_ids, old_mtimes) = tree.read_with(cx, |tree, _| {
1245        (
1246            tree.entries(true, 0).map(|e| e.id).collect::<Vec<_>>(),
1247            tree.entries(true, 0).map(|e| e.mtime).collect::<Vec<_>>(),
1248        )
1249    });
1250
1251    // Regression test: after the directory is scanned, touch the git repo's
1252    // working directory, bumping its mtime. That directory keeps its project
1253    // entry id after the directories are re-scanned.
1254    fs.touch_path("/root").await;
1255    cx.executor().run_until_parked();
1256
1257    let (new_entry_ids, new_mtimes) = tree.read_with(cx, |tree, _| {
1258        (
1259            tree.entries(true, 0).map(|e| e.id).collect::<Vec<_>>(),
1260            tree.entries(true, 0).map(|e| e.mtime).collect::<Vec<_>>(),
1261        )
1262    });
1263    assert_eq!(new_entry_ids, old_entry_ids);
1264    assert_ne!(new_mtimes, old_mtimes);
1265
1266    // Regression test: changes to the git repository should still be
1267    // detected.
1268    fs.set_status_for_repo_via_git_operation(
1269        Path::new("/root/.git"),
1270        &[(Path::new("b/c.txt"), GitFileStatus::Modified)],
1271    );
1272    cx.executor().run_until_parked();
1273
1274    let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
1275    check_propagated_statuses(
1276        &snapshot,
1277        &[
1278            (Path::new(""), Some(GitFileStatus::Modified)),
1279            (Path::new("a.txt"), None),
1280            (Path::new("b/c.txt"), Some(GitFileStatus::Modified)),
1281        ],
1282    );
1283}
1284
1285#[gpui::test]
1286async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
1287    init_test(cx);
1288    cx.executor().allow_parking();
1289
1290    let fs_fake = FakeFs::new(cx.background_executor.clone());
1291    fs_fake
1292        .insert_tree(
1293            "/root",
1294            json!({
1295                "a": {},
1296            }),
1297        )
1298        .await;
1299
1300    let tree_fake = Worktree::local(
1301        "/root".as_ref(),
1302        true,
1303        fs_fake,
1304        Default::default(),
1305        &mut cx.to_async(),
1306    )
1307    .await
1308    .unwrap();
1309
1310    let entry = tree_fake
1311        .update(cx, |tree, cx| {
1312            tree.as_local_mut()
1313                .unwrap()
1314                .create_entry("a/b/c/d.txt".as_ref(), false, cx)
1315        })
1316        .await
1317        .unwrap()
1318        .to_included()
1319        .unwrap();
1320    assert!(entry.is_file());
1321
1322    cx.executor().run_until_parked();
1323    tree_fake.read_with(cx, |tree, _| {
1324        assert!(tree.entry_for_path("a/b/c/d.txt").unwrap().is_file());
1325        assert!(tree.entry_for_path("a/b/c/").unwrap().is_dir());
1326        assert!(tree.entry_for_path("a/b/").unwrap().is_dir());
1327    });
1328
1329    let fs_real = Arc::new(RealFs::default());
1330    let temp_root = temp_tree(json!({
1331        "a": {}
1332    }));
1333
1334    let tree_real = Worktree::local(
1335        temp_root.path(),
1336        true,
1337        fs_real,
1338        Default::default(),
1339        &mut cx.to_async(),
1340    )
1341    .await
1342    .unwrap();
1343
1344    let entry = tree_real
1345        .update(cx, |tree, cx| {
1346            tree.as_local_mut()
1347                .unwrap()
1348                .create_entry("a/b/c/d.txt".as_ref(), false, cx)
1349        })
1350        .await
1351        .unwrap()
1352        .to_included()
1353        .unwrap();
1354    assert!(entry.is_file());
1355
1356    cx.executor().run_until_parked();
1357    tree_real.read_with(cx, |tree, _| {
1358        assert!(tree.entry_for_path("a/b/c/d.txt").unwrap().is_file());
1359        assert!(tree.entry_for_path("a/b/c/").unwrap().is_dir());
1360        assert!(tree.entry_for_path("a/b/").unwrap().is_dir());
1361    });
1362
1363    // Test smallest change
1364    let entry = tree_real
1365        .update(cx, |tree, cx| {
1366            tree.as_local_mut()
1367                .unwrap()
1368                .create_entry("a/b/c/e.txt".as_ref(), false, cx)
1369        })
1370        .await
1371        .unwrap()
1372        .to_included()
1373        .unwrap();
1374    assert!(entry.is_file());
1375
1376    cx.executor().run_until_parked();
1377    tree_real.read_with(cx, |tree, _| {
1378        assert!(tree.entry_for_path("a/b/c/e.txt").unwrap().is_file());
1379    });
1380
1381    // Test largest change
1382    let entry = tree_real
1383        .update(cx, |tree, cx| {
1384            tree.as_local_mut()
1385                .unwrap()
1386                .create_entry("d/e/f/g.txt".as_ref(), false, cx)
1387        })
1388        .await
1389        .unwrap()
1390        .to_included()
1391        .unwrap();
1392    assert!(entry.is_file());
1393
1394    cx.executor().run_until_parked();
1395    tree_real.read_with(cx, |tree, _| {
1396        assert!(tree.entry_for_path("d/e/f/g.txt").unwrap().is_file());
1397        assert!(tree.entry_for_path("d/e/f").unwrap().is_dir());
1398        assert!(tree.entry_for_path("d/e/").unwrap().is_dir());
1399        assert!(tree.entry_for_path("d/").unwrap().is_dir());
1400    });
1401}
1402
1403#[gpui::test(iterations = 100)]
1404async fn test_random_worktree_operations_during_initial_scan(
1405    cx: &mut TestAppContext,
1406    mut rng: StdRng,
1407) {
1408    init_test(cx);
1409    let operations = env::var("OPERATIONS")
1410        .map(|o| o.parse().unwrap())
1411        .unwrap_or(5);
1412    let initial_entries = env::var("INITIAL_ENTRIES")
1413        .map(|o| o.parse().unwrap())
1414        .unwrap_or(20);
1415
1416    let root_dir = Path::new("/test");
1417    let fs = FakeFs::new(cx.background_executor.clone()) as Arc<dyn Fs>;
1418    fs.as_fake().insert_tree(root_dir, json!({})).await;
1419    for _ in 0..initial_entries {
1420        randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
1421    }
1422    log::info!("generated initial tree");
1423
1424    let worktree = Worktree::local(
1425        root_dir,
1426        true,
1427        fs.clone(),
1428        Default::default(),
1429        &mut cx.to_async(),
1430    )
1431    .await
1432    .unwrap();
1433
1434    let mut snapshots = vec![worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot())];
1435    let updates = Arc::new(Mutex::new(Vec::new()));
1436    worktree.update(cx, |tree, cx| {
1437        check_worktree_change_events(tree, cx);
1438
1439        tree.as_local_mut().unwrap().observe_updates(0, cx, {
1440            let updates = updates.clone();
1441            move |update| {
1442                updates.lock().push(update);
1443                async { true }
1444            }
1445        });
1446    });
1447
1448    for _ in 0..operations {
1449        worktree
1450            .update(cx, |worktree, cx| {
1451                randomly_mutate_worktree(worktree, &mut rng, cx)
1452            })
1453            .await
1454            .log_err();
1455        worktree.read_with(cx, |tree, _| {
1456            tree.as_local().unwrap().snapshot().check_invariants(true)
1457        });
1458
1459        if rng.gen_bool(0.6) {
1460            snapshots.push(worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot()));
1461        }
1462    }
1463
1464    worktree
1465        .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
1466        .await;
1467
1468    cx.executor().run_until_parked();
1469
1470    let final_snapshot = worktree.read_with(cx, |tree, _| {
1471        let tree = tree.as_local().unwrap();
1472        let snapshot = tree.snapshot();
1473        snapshot.check_invariants(true);
1474        snapshot
1475    });
1476
1477    for (i, snapshot) in snapshots.into_iter().enumerate().rev() {
1478        let mut updated_snapshot = snapshot.clone();
1479        for update in updates.lock().iter() {
1480            if update.scan_id >= updated_snapshot.scan_id() as u64 {
1481                updated_snapshot
1482                    .apply_remote_update(update.clone())
1483                    .unwrap();
1484            }
1485        }
1486
1487        assert_eq!(
1488            updated_snapshot.entries(true, 0).collect::<Vec<_>>(),
1489            final_snapshot.entries(true, 0).collect::<Vec<_>>(),
1490            "wrong updates after snapshot {i}: {snapshot:#?} {updates:#?}",
1491        );
1492    }
1493}
1494
1495#[gpui::test(iterations = 100)]
1496async fn test_random_worktree_changes(cx: &mut TestAppContext, mut rng: StdRng) {
1497    init_test(cx);
1498    let operations = env::var("OPERATIONS")
1499        .map(|o| o.parse().unwrap())
1500        .unwrap_or(40);
1501    let initial_entries = env::var("INITIAL_ENTRIES")
1502        .map(|o| o.parse().unwrap())
1503        .unwrap_or(20);
1504
1505    let root_dir = Path::new("/test");
1506    let fs = FakeFs::new(cx.background_executor.clone()) as Arc<dyn Fs>;
1507    fs.as_fake().insert_tree(root_dir, json!({})).await;
1508    for _ in 0..initial_entries {
1509        randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
1510    }
1511    log::info!("generated initial tree");
1512
1513    let worktree = Worktree::local(
1514        root_dir,
1515        true,
1516        fs.clone(),
1517        Default::default(),
1518        &mut cx.to_async(),
1519    )
1520    .await
1521    .unwrap();
1522
1523    let updates = Arc::new(Mutex::new(Vec::new()));
1524    worktree.update(cx, |tree, cx| {
1525        check_worktree_change_events(tree, cx);
1526
1527        tree.as_local_mut().unwrap().observe_updates(0, cx, {
1528            let updates = updates.clone();
1529            move |update| {
1530                updates.lock().push(update);
1531                async { true }
1532            }
1533        });
1534    });
1535
1536    worktree
1537        .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
1538        .await;
1539
1540    fs.as_fake().pause_events();
1541    let mut snapshots = Vec::new();
1542    let mut mutations_len = operations;
1543    while mutations_len > 1 {
1544        if rng.gen_bool(0.2) {
1545            worktree
1546                .update(cx, |worktree, cx| {
1547                    randomly_mutate_worktree(worktree, &mut rng, cx)
1548                })
1549                .await
1550                .log_err();
1551        } else {
1552            randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
1553        }
1554
1555        let buffered_event_count = fs.as_fake().buffered_event_count();
1556        if buffered_event_count > 0 && rng.gen_bool(0.3) {
1557            let len = rng.gen_range(0..=buffered_event_count);
1558            log::info!("flushing {} events", len);
1559            fs.as_fake().flush_events(len);
1560        } else {
1561            randomly_mutate_fs(&fs, root_dir, 0.6, &mut rng).await;
1562            mutations_len -= 1;
1563        }
1564
1565        cx.executor().run_until_parked();
1566        if rng.gen_bool(0.2) {
1567            log::info!("storing snapshot {}", snapshots.len());
1568            let snapshot = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
1569            snapshots.push(snapshot);
1570        }
1571    }
1572
1573    log::info!("quiescing");
1574    fs.as_fake().flush_events(usize::MAX);
1575    cx.executor().run_until_parked();
1576
1577    let snapshot = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
1578    snapshot.check_invariants(true);
1579    let expanded_paths = snapshot
1580        .expanded_entries()
1581        .map(|e| e.path.clone())
1582        .collect::<Vec<_>>();
1583
1584    {
1585        let new_worktree = Worktree::local(
1586            root_dir,
1587            true,
1588            fs.clone(),
1589            Default::default(),
1590            &mut cx.to_async(),
1591        )
1592        .await
1593        .unwrap();
1594        new_worktree
1595            .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
1596            .await;
1597        new_worktree
1598            .update(cx, |tree, _| {
1599                tree.as_local_mut()
1600                    .unwrap()
1601                    .refresh_entries_for_paths(expanded_paths)
1602            })
1603            .recv()
1604            .await;
1605        let new_snapshot =
1606            new_worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
1607        assert_eq!(
1608            snapshot.entries_without_ids(true),
1609            new_snapshot.entries_without_ids(true)
1610        );
1611    }
1612
1613    for (i, mut prev_snapshot) in snapshots.into_iter().enumerate().rev() {
1614        for update in updates.lock().iter() {
1615            if update.scan_id >= prev_snapshot.scan_id() as u64 {
1616                prev_snapshot.apply_remote_update(update.clone()).unwrap();
1617            }
1618        }
1619
1620        assert_eq!(
1621            prev_snapshot
1622                .entries(true, 0)
1623                .map(ignore_pending_dir)
1624                .collect::<Vec<_>>(),
1625            snapshot
1626                .entries(true, 0)
1627                .map(ignore_pending_dir)
1628                .collect::<Vec<_>>(),
1629            "wrong updates after snapshot {i}: {updates:#?}",
1630        );
1631    }
1632
1633    fn ignore_pending_dir(entry: &Entry) -> Entry {
1634        let mut entry = entry.clone();
1635        if entry.kind.is_dir() {
1636            entry.kind = EntryKind::Dir
1637        }
1638        entry
1639    }
1640}
1641
1642// The worktree's `UpdatedEntries` event can be used to follow along with
1643// all changes to the worktree's snapshot.
1644fn check_worktree_change_events(tree: &mut Worktree, cx: &mut ModelContext<Worktree>) {
1645    let mut entries = tree.entries(true, 0).cloned().collect::<Vec<_>>();
1646    cx.subscribe(&cx.handle(), move |tree, _, event, _| {
1647        if let Event::UpdatedEntries(changes) = event {
1648            for (path, _, change_type) in changes.iter() {
1649                let entry = tree.entry_for_path(path).cloned();
1650                let ix = match entries.binary_search_by_key(&path, |e| &e.path) {
1651                    Ok(ix) | Err(ix) => ix,
1652                };
1653                match change_type {
1654                    PathChange::Added => entries.insert(ix, entry.unwrap()),
1655                    PathChange::Removed => drop(entries.remove(ix)),
1656                    PathChange::Updated => {
1657                        let entry = entry.unwrap();
1658                        let existing_entry = entries.get_mut(ix).unwrap();
1659                        assert_eq!(existing_entry.path, entry.path);
1660                        *existing_entry = entry;
1661                    }
1662                    PathChange::AddedOrUpdated | PathChange::Loaded => {
1663                        let entry = entry.unwrap();
1664                        if entries.get(ix).map(|e| &e.path) == Some(&entry.path) {
1665                            *entries.get_mut(ix).unwrap() = entry;
1666                        } else {
1667                            entries.insert(ix, entry);
1668                        }
1669                    }
1670                }
1671            }
1672
1673            let new_entries = tree.entries(true, 0).cloned().collect::<Vec<_>>();
1674            assert_eq!(entries, new_entries, "incorrect changes: {:?}", changes);
1675        }
1676    })
1677    .detach();
1678}
1679
1680fn randomly_mutate_worktree(
1681    worktree: &mut Worktree,
1682    rng: &mut impl Rng,
1683    cx: &mut ModelContext<Worktree>,
1684) -> Task<Result<()>> {
1685    log::info!("mutating worktree");
1686    let worktree = worktree.as_local_mut().unwrap();
1687    let snapshot = worktree.snapshot();
1688    let entry = snapshot.entries(false, 0).choose(rng).unwrap();
1689
1690    match rng.gen_range(0_u32..100) {
1691        0..=33 if entry.path.as_ref() != Path::new("") => {
1692            log::info!("deleting entry {:?} ({})", entry.path, entry.id.0);
1693            worktree.delete_entry(entry.id, false, cx).unwrap()
1694        }
1695        ..=66 if entry.path.as_ref() != Path::new("") => {
1696            let other_entry = snapshot.entries(false, 0).choose(rng).unwrap();
1697            let new_parent_path = if other_entry.is_dir() {
1698                other_entry.path.clone()
1699            } else {
1700                other_entry.path.parent().unwrap().into()
1701            };
1702            let mut new_path = new_parent_path.join(random_filename(rng));
1703            if new_path.starts_with(&entry.path) {
1704                new_path = random_filename(rng).into();
1705            }
1706
1707            log::info!(
1708                "renaming entry {:?} ({}) to {:?}",
1709                entry.path,
1710                entry.id.0,
1711                new_path
1712            );
1713            let task = worktree.rename_entry(entry.id, new_path, cx);
1714            cx.background_executor().spawn(async move {
1715                task.await?.to_included().unwrap();
1716                Ok(())
1717            })
1718        }
1719        _ => {
1720            if entry.is_dir() {
1721                let child_path = entry.path.join(random_filename(rng));
1722                let is_dir = rng.gen_bool(0.3);
1723                log::info!(
1724                    "creating {} at {:?}",
1725                    if is_dir { "dir" } else { "file" },
1726                    child_path,
1727                );
1728                let task = worktree.create_entry(child_path, is_dir, cx);
1729                cx.background_executor().spawn(async move {
1730                    task.await?;
1731                    Ok(())
1732                })
1733            } else {
1734                log::info!("overwriting file {:?} ({})", entry.path, entry.id.0);
1735                let task =
1736                    worktree.write_file(entry.path.clone(), "".into(), Default::default(), cx);
1737                cx.background_executor().spawn(async move {
1738                    task.await?;
1739                    Ok(())
1740                })
1741            }
1742        }
1743    }
1744}
1745
1746async fn randomly_mutate_fs(
1747    fs: &Arc<dyn Fs>,
1748    root_path: &Path,
1749    insertion_probability: f64,
1750    rng: &mut impl Rng,
1751) {
1752    log::info!("mutating fs");
1753    let mut files = Vec::new();
1754    let mut dirs = Vec::new();
1755    for path in fs.as_fake().paths(false) {
1756        if path.starts_with(root_path) {
1757            if fs.is_file(&path).await {
1758                files.push(path);
1759            } else {
1760                dirs.push(path);
1761            }
1762        }
1763    }
1764
1765    if (files.is_empty() && dirs.len() == 1) || rng.gen_bool(insertion_probability) {
1766        let path = dirs.choose(rng).unwrap();
1767        let new_path = path.join(random_filename(rng));
1768
1769        if rng.gen() {
1770            log::info!(
1771                "creating dir {:?}",
1772                new_path.strip_prefix(root_path).unwrap()
1773            );
1774            fs.create_dir(&new_path).await.unwrap();
1775        } else {
1776            log::info!(
1777                "creating file {:?}",
1778                new_path.strip_prefix(root_path).unwrap()
1779            );
1780            fs.create_file(&new_path, Default::default()).await.unwrap();
1781        }
1782    } else if rng.gen_bool(0.05) {
1783        let ignore_dir_path = dirs.choose(rng).unwrap();
1784        let ignore_path = ignore_dir_path.join(*GITIGNORE);
1785
1786        let subdirs = dirs
1787            .iter()
1788            .filter(|d| d.starts_with(ignore_dir_path))
1789            .cloned()
1790            .collect::<Vec<_>>();
1791        let subfiles = files
1792            .iter()
1793            .filter(|d| d.starts_with(ignore_dir_path))
1794            .cloned()
1795            .collect::<Vec<_>>();
1796        let files_to_ignore = {
1797            let len = rng.gen_range(0..=subfiles.len());
1798            subfiles.choose_multiple(rng, len)
1799        };
1800        let dirs_to_ignore = {
1801            let len = rng.gen_range(0..subdirs.len());
1802            subdirs.choose_multiple(rng, len)
1803        };
1804
1805        let mut ignore_contents = String::new();
1806        for path_to_ignore in files_to_ignore.chain(dirs_to_ignore) {
1807            writeln!(
1808                ignore_contents,
1809                "{}",
1810                path_to_ignore
1811                    .strip_prefix(ignore_dir_path)
1812                    .unwrap()
1813                    .to_str()
1814                    .unwrap()
1815            )
1816            .unwrap();
1817        }
1818        log::info!(
1819            "creating gitignore {:?} with contents:\n{}",
1820            ignore_path.strip_prefix(root_path).unwrap(),
1821            ignore_contents
1822        );
1823        fs.save(
1824            &ignore_path,
1825            &ignore_contents.as_str().into(),
1826            Default::default(),
1827        )
1828        .await
1829        .unwrap();
1830    } else {
1831        let old_path = {
1832            let file_path = files.choose(rng);
1833            let dir_path = dirs[1..].choose(rng);
1834            file_path.into_iter().chain(dir_path).choose(rng).unwrap()
1835        };
1836
1837        let is_rename = rng.gen();
1838        if is_rename {
1839            let new_path_parent = dirs
1840                .iter()
1841                .filter(|d| !d.starts_with(old_path))
1842                .choose(rng)
1843                .unwrap();
1844
1845            let overwrite_existing_dir =
1846                !old_path.starts_with(new_path_parent) && rng.gen_bool(0.3);
1847            let new_path = if overwrite_existing_dir {
1848                fs.remove_dir(
1849                    new_path_parent,
1850                    RemoveOptions {
1851                        recursive: true,
1852                        ignore_if_not_exists: true,
1853                    },
1854                )
1855                .await
1856                .unwrap();
1857                new_path_parent.to_path_buf()
1858            } else {
1859                new_path_parent.join(random_filename(rng))
1860            };
1861
1862            log::info!(
1863                "renaming {:?} to {}{:?}",
1864                old_path.strip_prefix(root_path).unwrap(),
1865                if overwrite_existing_dir {
1866                    "overwrite "
1867                } else {
1868                    ""
1869                },
1870                new_path.strip_prefix(root_path).unwrap()
1871            );
1872            fs.rename(
1873                old_path,
1874                &new_path,
1875                fs::RenameOptions {
1876                    overwrite: true,
1877                    ignore_if_exists: true,
1878                },
1879            )
1880            .await
1881            .unwrap();
1882        } else if fs.is_file(old_path).await {
1883            log::info!(
1884                "deleting file {:?}",
1885                old_path.strip_prefix(root_path).unwrap()
1886            );
1887            fs.remove_file(old_path, Default::default()).await.unwrap();
1888        } else {
1889            log::info!(
1890                "deleting dir {:?}",
1891                old_path.strip_prefix(root_path).unwrap()
1892            );
1893            fs.remove_dir(
1894                old_path,
1895                RemoveOptions {
1896                    recursive: true,
1897                    ignore_if_not_exists: true,
1898                },
1899            )
1900            .await
1901            .unwrap();
1902        }
1903    }
1904}
1905
1906fn random_filename(rng: &mut impl Rng) -> String {
1907    (0..6)
1908        .map(|_| rng.sample(rand::distributions::Alphanumeric))
1909        .map(char::from)
1910        .collect()
1911}
1912
1913#[gpui::test]
1914async fn test_rename_work_directory(cx: &mut TestAppContext) {
1915    init_test(cx);
1916    cx.executor().allow_parking();
1917    let root = temp_tree(json!({
1918        "projects": {
1919            "project1": {
1920                "a": "",
1921                "b": "",
1922            }
1923        },
1924
1925    }));
1926    let root_path = root.path();
1927
1928    let tree = Worktree::local(
1929        root_path,
1930        true,
1931        Arc::new(RealFs::default()),
1932        Default::default(),
1933        &mut cx.to_async(),
1934    )
1935    .await
1936    .unwrap();
1937
1938    let repo = git_init(&root_path.join("projects/project1"));
1939    git_add("a", &repo);
1940    git_commit("init", &repo);
1941    std::fs::write(root_path.join("projects/project1/a"), "aa").ok();
1942
1943    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1944        .await;
1945
1946    tree.flush_fs_events(cx).await;
1947
1948    cx.read(|cx| {
1949        let tree = tree.read(cx);
1950        let (work_dir, _) = tree.repositories().next().unwrap();
1951        assert_eq!(work_dir.as_ref(), Path::new("projects/project1"));
1952        assert_eq!(
1953            tree.status_for_file(Path::new("projects/project1/a")),
1954            Some(GitFileStatus::Modified)
1955        );
1956        assert_eq!(
1957            tree.status_for_file(Path::new("projects/project1/b")),
1958            Some(GitFileStatus::Added)
1959        );
1960    });
1961
1962    std::fs::rename(
1963        root_path.join("projects/project1"),
1964        root_path.join("projects/project2"),
1965    )
1966    .ok();
1967    tree.flush_fs_events(cx).await;
1968
1969    cx.read(|cx| {
1970        let tree = tree.read(cx);
1971        let (work_dir, _) = tree.repositories().next().unwrap();
1972        assert_eq!(work_dir.as_ref(), Path::new("projects/project2"));
1973        assert_eq!(
1974            tree.status_for_file(Path::new("projects/project2/a")),
1975            Some(GitFileStatus::Modified)
1976        );
1977        assert_eq!(
1978            tree.status_for_file(Path::new("projects/project2/b")),
1979            Some(GitFileStatus::Added)
1980        );
1981    });
1982}
1983
1984#[gpui::test]
1985async fn test_git_repository_for_path(cx: &mut TestAppContext) {
1986    init_test(cx);
1987    cx.executor().allow_parking();
1988    let root = temp_tree(json!({
1989        "c.txt": "",
1990        "dir1": {
1991            ".git": {},
1992            "deps": {
1993                "dep1": {
1994                    ".git": {},
1995                    "src": {
1996                        "a.txt": ""
1997                    }
1998                }
1999            },
2000            "src": {
2001                "b.txt": ""
2002            }
2003        },
2004    }));
2005
2006    let tree = Worktree::local(
2007        root.path(),
2008        true,
2009        Arc::new(RealFs::default()),
2010        Default::default(),
2011        &mut cx.to_async(),
2012    )
2013    .await
2014    .unwrap();
2015
2016    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2017        .await;
2018    tree.flush_fs_events(cx).await;
2019
2020    tree.read_with(cx, |tree, _cx| {
2021        let tree = tree.as_local().unwrap();
2022
2023        assert!(tree.repository_for_path("c.txt".as_ref()).is_none());
2024
2025        let entry = tree.repository_for_path("dir1/src/b.txt".as_ref()).unwrap();
2026        assert_eq!(
2027            entry
2028                .work_directory(tree)
2029                .map(|directory| directory.as_ref().to_owned()),
2030            Some(Path::new("dir1").to_owned())
2031        );
2032
2033        let entry = tree
2034            .repository_for_path("dir1/deps/dep1/src/a.txt".as_ref())
2035            .unwrap();
2036        assert_eq!(
2037            entry
2038                .work_directory(tree)
2039                .map(|directory| directory.as_ref().to_owned()),
2040            Some(Path::new("dir1/deps/dep1").to_owned())
2041        );
2042
2043        let entries = tree.files(false, 0);
2044
2045        let paths_with_repos = tree
2046            .entries_with_repositories(entries)
2047            .map(|(entry, repo)| {
2048                (
2049                    entry.path.as_ref(),
2050                    repo.and_then(|repo| {
2051                        repo.work_directory(tree)
2052                            .map(|work_directory| work_directory.0.to_path_buf())
2053                    }),
2054                )
2055            })
2056            .collect::<Vec<_>>();
2057
2058        assert_eq!(
2059            paths_with_repos,
2060            &[
2061                (Path::new("c.txt"), None),
2062                (
2063                    Path::new("dir1/deps/dep1/src/a.txt"),
2064                    Some(Path::new("dir1/deps/dep1").into())
2065                ),
2066                (Path::new("dir1/src/b.txt"), Some(Path::new("dir1").into())),
2067            ]
2068        );
2069    });
2070
2071    let repo_update_events = Arc::new(Mutex::new(vec![]));
2072    tree.update(cx, |_, cx| {
2073        let repo_update_events = repo_update_events.clone();
2074        cx.subscribe(&tree, move |_, _, event, _| {
2075            if let Event::UpdatedGitRepositories(update) = event {
2076                repo_update_events.lock().push(update.clone());
2077            }
2078        })
2079        .detach();
2080    });
2081
2082    std::fs::write(root.path().join("dir1/.git/random_new_file"), "hello").unwrap();
2083    tree.flush_fs_events(cx).await;
2084
2085    assert_eq!(
2086        repo_update_events.lock()[0]
2087            .iter()
2088            .map(|e| e.0.clone())
2089            .collect::<Vec<Arc<Path>>>(),
2090        vec![Path::new("dir1").into()]
2091    );
2092
2093    std::fs::remove_dir_all(root.path().join("dir1/.git")).unwrap();
2094    tree.flush_fs_events(cx).await;
2095
2096    tree.read_with(cx, |tree, _cx| {
2097        let tree = tree.as_local().unwrap();
2098
2099        assert!(tree
2100            .repository_for_path("dir1/src/b.txt".as_ref())
2101            .is_none());
2102    });
2103}
2104
2105#[gpui::test]
2106async fn test_git_status(cx: &mut TestAppContext) {
2107    init_test(cx);
2108    cx.executor().allow_parking();
2109    const IGNORE_RULE: &str = "**/target";
2110
2111    let root = temp_tree(json!({
2112        "project": {
2113            "a.txt": "a",
2114            "b.txt": "bb",
2115            "c": {
2116                "d": {
2117                    "e.txt": "eee"
2118                }
2119            },
2120            "f.txt": "ffff",
2121            "target": {
2122                "build_file": "???"
2123            },
2124            ".gitignore": IGNORE_RULE
2125        },
2126
2127    }));
2128
2129    const A_TXT: &str = "a.txt";
2130    const B_TXT: &str = "b.txt";
2131    const E_TXT: &str = "c/d/e.txt";
2132    const F_TXT: &str = "f.txt";
2133    const DOTGITIGNORE: &str = ".gitignore";
2134    const BUILD_FILE: &str = "target/build_file";
2135    let project_path = Path::new("project");
2136
2137    // Set up git repository before creating the worktree.
2138    let work_dir = root.path().join("project");
2139    let mut repo = git_init(work_dir.as_path());
2140    repo.add_ignore_rule(IGNORE_RULE).unwrap();
2141    git_add(A_TXT, &repo);
2142    git_add(E_TXT, &repo);
2143    git_add(DOTGITIGNORE, &repo);
2144    git_commit("Initial commit", &repo);
2145
2146    let tree = Worktree::local(
2147        root.path(),
2148        true,
2149        Arc::new(RealFs::default()),
2150        Default::default(),
2151        &mut cx.to_async(),
2152    )
2153    .await
2154    .unwrap();
2155
2156    tree.flush_fs_events(cx).await;
2157    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2158        .await;
2159    cx.executor().run_until_parked();
2160
2161    // Check that the right git state is observed on startup
2162    tree.read_with(cx, |tree, _cx| {
2163        let snapshot = tree.snapshot();
2164        assert_eq!(snapshot.repositories().count(), 1);
2165        let (dir, repo_entry) = snapshot.repositories().next().unwrap();
2166        assert_eq!(dir.as_ref(), Path::new("project"));
2167        assert!(repo_entry.location_in_repo.is_none());
2168
2169        assert_eq!(
2170            snapshot.status_for_file(project_path.join(B_TXT)),
2171            Some(GitFileStatus::Added)
2172        );
2173        assert_eq!(
2174            snapshot.status_for_file(project_path.join(F_TXT)),
2175            Some(GitFileStatus::Added)
2176        );
2177    });
2178
2179    // Modify a file in the working copy.
2180    std::fs::write(work_dir.join(A_TXT), "aa").unwrap();
2181    tree.flush_fs_events(cx).await;
2182    cx.executor().run_until_parked();
2183
2184    // The worktree detects that the file's git status has changed.
2185    tree.read_with(cx, |tree, _cx| {
2186        let snapshot = tree.snapshot();
2187        assert_eq!(
2188            snapshot.status_for_file(project_path.join(A_TXT)),
2189            Some(GitFileStatus::Modified)
2190        );
2191    });
2192
2193    // Create a commit in the git repository.
2194    git_add(A_TXT, &repo);
2195    git_add(B_TXT, &repo);
2196    git_commit("Committing modified and added", &repo);
2197    tree.flush_fs_events(cx).await;
2198    cx.executor().run_until_parked();
2199
2200    // The worktree detects that the files' git status have changed.
2201    tree.read_with(cx, |tree, _cx| {
2202        let snapshot = tree.snapshot();
2203        assert_eq!(
2204            snapshot.status_for_file(project_path.join(F_TXT)),
2205            Some(GitFileStatus::Added)
2206        );
2207        assert_eq!(snapshot.status_for_file(project_path.join(B_TXT)), None);
2208        assert_eq!(snapshot.status_for_file(project_path.join(A_TXT)), None);
2209    });
2210
2211    // Modify files in the working copy and perform git operations on other files.
2212    git_reset(0, &repo);
2213    git_remove_index(Path::new(B_TXT), &repo);
2214    git_stash(&mut repo);
2215    std::fs::write(work_dir.join(E_TXT), "eeee").unwrap();
2216    std::fs::write(work_dir.join(BUILD_FILE), "this should be ignored").unwrap();
2217    tree.flush_fs_events(cx).await;
2218    cx.executor().run_until_parked();
2219
2220    // Check that more complex repo changes are tracked
2221    tree.read_with(cx, |tree, _cx| {
2222        let snapshot = tree.snapshot();
2223
2224        assert_eq!(snapshot.status_for_file(project_path.join(A_TXT)), None);
2225        assert_eq!(
2226            snapshot.status_for_file(project_path.join(B_TXT)),
2227            Some(GitFileStatus::Added)
2228        );
2229        assert_eq!(
2230            snapshot.status_for_file(project_path.join(E_TXT)),
2231            Some(GitFileStatus::Modified)
2232        );
2233    });
2234
2235    std::fs::remove_file(work_dir.join(B_TXT)).unwrap();
2236    std::fs::remove_dir_all(work_dir.join("c")).unwrap();
2237    std::fs::write(
2238        work_dir.join(DOTGITIGNORE),
2239        [IGNORE_RULE, "f.txt"].join("\n"),
2240    )
2241    .unwrap();
2242
2243    git_add(Path::new(DOTGITIGNORE), &repo);
2244    git_commit("Committing modified git ignore", &repo);
2245
2246    tree.flush_fs_events(cx).await;
2247    cx.executor().run_until_parked();
2248
2249    let mut renamed_dir_name = "first_directory/second_directory";
2250    const RENAMED_FILE: &str = "rf.txt";
2251
2252    std::fs::create_dir_all(work_dir.join(renamed_dir_name)).unwrap();
2253    std::fs::write(
2254        work_dir.join(renamed_dir_name).join(RENAMED_FILE),
2255        "new-contents",
2256    )
2257    .unwrap();
2258
2259    tree.flush_fs_events(cx).await;
2260    cx.executor().run_until_parked();
2261
2262    tree.read_with(cx, |tree, _cx| {
2263        let snapshot = tree.snapshot();
2264        assert_eq!(
2265            snapshot.status_for_file(project_path.join(renamed_dir_name).join(RENAMED_FILE)),
2266            Some(GitFileStatus::Added)
2267        );
2268    });
2269
2270    renamed_dir_name = "new_first_directory/second_directory";
2271
2272    std::fs::rename(
2273        work_dir.join("first_directory"),
2274        work_dir.join("new_first_directory"),
2275    )
2276    .unwrap();
2277
2278    tree.flush_fs_events(cx).await;
2279    cx.executor().run_until_parked();
2280
2281    tree.read_with(cx, |tree, _cx| {
2282        let snapshot = tree.snapshot();
2283
2284        assert_eq!(
2285            snapshot.status_for_file(
2286                project_path
2287                    .join(Path::new(renamed_dir_name))
2288                    .join(RENAMED_FILE)
2289            ),
2290            Some(GitFileStatus::Added)
2291        );
2292    });
2293}
2294
2295#[gpui::test]
2296async fn test_repository_subfolder_git_status(cx: &mut TestAppContext) {
2297    init_test(cx);
2298    cx.executor().allow_parking();
2299
2300    let root = temp_tree(json!({
2301        "my-repo": {
2302            // .git folder will go here
2303            "a.txt": "a",
2304            "sub-folder-1": {
2305                "sub-folder-2": {
2306                    "c.txt": "cc",
2307                    "d": {
2308                        "e.txt": "eee"
2309                    }
2310                },
2311            }
2312        },
2313
2314    }));
2315
2316    const C_TXT: &str = "sub-folder-1/sub-folder-2/c.txt";
2317    const E_TXT: &str = "sub-folder-1/sub-folder-2/d/e.txt";
2318
2319    // Set up git repository before creating the worktree.
2320    let git_repo_work_dir = root.path().join("my-repo");
2321    let repo = git_init(git_repo_work_dir.as_path());
2322    git_add(C_TXT, &repo);
2323    git_commit("Initial commit", &repo);
2324
2325    // Open the worktree in subfolder
2326    let project_root = Path::new("my-repo/sub-folder-1/sub-folder-2");
2327    let tree = Worktree::local(
2328        root.path().join(project_root),
2329        true,
2330        Arc::new(RealFs::default()),
2331        Default::default(),
2332        &mut cx.to_async(),
2333    )
2334    .await
2335    .unwrap();
2336
2337    tree.flush_fs_events(cx).await;
2338    tree.flush_fs_events_in_root_git_repository(cx).await;
2339    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2340        .await;
2341    cx.executor().run_until_parked();
2342
2343    // Ensure that the git status is loaded correctly
2344    tree.read_with(cx, |tree, _cx| {
2345        let snapshot = tree.snapshot();
2346        assert_eq!(snapshot.repositories().count(), 1);
2347        let (dir, repo_entry) = snapshot.repositories().next().unwrap();
2348        // Path is blank because the working directory of
2349        // the git repository is located at the root of the project
2350        assert_eq!(dir.as_ref(), Path::new(""));
2351
2352        // This is the missing path between the root of the project (sub-folder-2) and its
2353        // location relative to the root of the repository.
2354        assert_eq!(
2355            repo_entry.location_in_repo,
2356            Some(Arc::from(Path::new("sub-folder-1/sub-folder-2")))
2357        );
2358
2359        assert_eq!(snapshot.status_for_file("c.txt"), None);
2360        assert_eq!(
2361            snapshot.status_for_file("d/e.txt"),
2362            Some(GitFileStatus::Added)
2363        );
2364    });
2365
2366    // Now we simulate FS events, but ONLY in the .git folder that's outside
2367    // of out project root.
2368    // Meaning: we don't produce any FS events for files inside the project.
2369    git_add(E_TXT, &repo);
2370    git_commit("Second commit", &repo);
2371    tree.flush_fs_events_in_root_git_repository(cx).await;
2372    cx.executor().run_until_parked();
2373
2374    tree.read_with(cx, |tree, _cx| {
2375        let snapshot = tree.snapshot();
2376
2377        assert!(snapshot.repositories().next().is_some());
2378
2379        assert_eq!(snapshot.status_for_file("c.txt"), None);
2380        assert_eq!(snapshot.status_for_file("d/e.txt"), None);
2381    });
2382}
2383
2384#[gpui::test]
2385async fn test_propagate_git_statuses(cx: &mut TestAppContext) {
2386    init_test(cx);
2387    let fs = FakeFs::new(cx.background_executor.clone());
2388    fs.insert_tree(
2389        "/root",
2390        json!({
2391            ".git": {},
2392            "a": {
2393                "b": {
2394                    "c1.txt": "",
2395                    "c2.txt": "",
2396                },
2397                "d": {
2398                    "e1.txt": "",
2399                    "e2.txt": "",
2400                    "e3.txt": "",
2401                }
2402            },
2403            "f": {
2404                "no-status.txt": ""
2405            },
2406            "g": {
2407                "h1.txt": "",
2408                "h2.txt": ""
2409            },
2410
2411        }),
2412    )
2413    .await;
2414
2415    fs.set_status_for_repo_via_git_operation(
2416        Path::new("/root/.git"),
2417        &[
2418            (Path::new("a/b/c1.txt"), GitFileStatus::Added),
2419            (Path::new("a/d/e2.txt"), GitFileStatus::Modified),
2420            (Path::new("g/h2.txt"), GitFileStatus::Conflict),
2421        ],
2422    );
2423
2424    let tree = Worktree::local(
2425        Path::new("/root"),
2426        true,
2427        fs.clone(),
2428        Default::default(),
2429        &mut cx.to_async(),
2430    )
2431    .await
2432    .unwrap();
2433
2434    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2435        .await;
2436
2437    cx.executor().run_until_parked();
2438    let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
2439
2440    check_propagated_statuses(
2441        &snapshot,
2442        &[
2443            (Path::new(""), Some(GitFileStatus::Conflict)),
2444            (Path::new("a"), Some(GitFileStatus::Modified)),
2445            (Path::new("a/b"), Some(GitFileStatus::Added)),
2446            (Path::new("a/b/c1.txt"), Some(GitFileStatus::Added)),
2447            (Path::new("a/b/c2.txt"), None),
2448            (Path::new("a/d"), Some(GitFileStatus::Modified)),
2449            (Path::new("a/d/e2.txt"), Some(GitFileStatus::Modified)),
2450            (Path::new("f"), None),
2451            (Path::new("f/no-status.txt"), None),
2452            (Path::new("g"), Some(GitFileStatus::Conflict)),
2453            (Path::new("g/h2.txt"), Some(GitFileStatus::Conflict)),
2454        ],
2455    );
2456
2457    check_propagated_statuses(
2458        &snapshot,
2459        &[
2460            (Path::new("a/b"), Some(GitFileStatus::Added)),
2461            (Path::new("a/b/c1.txt"), Some(GitFileStatus::Added)),
2462            (Path::new("a/b/c2.txt"), None),
2463            (Path::new("a/d"), Some(GitFileStatus::Modified)),
2464            (Path::new("a/d/e1.txt"), None),
2465            (Path::new("a/d/e2.txt"), Some(GitFileStatus::Modified)),
2466            (Path::new("f"), None),
2467            (Path::new("f/no-status.txt"), None),
2468            (Path::new("g"), Some(GitFileStatus::Conflict)),
2469        ],
2470    );
2471
2472    check_propagated_statuses(
2473        &snapshot,
2474        &[
2475            (Path::new("a/b/c1.txt"), Some(GitFileStatus::Added)),
2476            (Path::new("a/b/c2.txt"), None),
2477            (Path::new("a/d/e1.txt"), None),
2478            (Path::new("a/d/e2.txt"), Some(GitFileStatus::Modified)),
2479            (Path::new("f/no-status.txt"), None),
2480        ],
2481    );
2482}
2483
2484#[track_caller]
2485fn check_propagated_statuses(
2486    snapshot: &Snapshot,
2487    expected_statuses: &[(&Path, Option<GitFileStatus>)],
2488) {
2489    let mut entries = expected_statuses
2490        .iter()
2491        .map(|(path, _)| snapshot.entry_for_path(path).unwrap().clone())
2492        .collect::<Vec<_>>();
2493    snapshot.propagate_git_statuses(&mut entries);
2494    assert_eq!(
2495        entries
2496            .iter()
2497            .map(|e| (e.path.as_ref(), e.git_status))
2498            .collect::<Vec<_>>(),
2499        expected_statuses
2500    );
2501}
2502
2503#[track_caller]
2504fn git_init(path: &Path) -> git2::Repository {
2505    git2::Repository::init(path).expect("Failed to initialize git repository")
2506}
2507
2508#[track_caller]
2509fn git_add<P: AsRef<Path>>(path: P, repo: &git2::Repository) {
2510    let path = path.as_ref();
2511    let mut index = repo.index().expect("Failed to get index");
2512    index.add_path(path).expect("Failed to add a.txt");
2513    index.write().expect("Failed to write index");
2514}
2515
2516#[track_caller]
2517fn git_remove_index(path: &Path, repo: &git2::Repository) {
2518    let mut index = repo.index().expect("Failed to get index");
2519    index.remove_path(path).expect("Failed to add a.txt");
2520    index.write().expect("Failed to write index");
2521}
2522
2523#[track_caller]
2524fn git_commit(msg: &'static str, repo: &git2::Repository) {
2525    use git2::Signature;
2526
2527    let signature = Signature::now("test", "test@zed.dev").unwrap();
2528    let oid = repo.index().unwrap().write_tree().unwrap();
2529    let tree = repo.find_tree(oid).unwrap();
2530    if let Ok(head) = repo.head() {
2531        let parent_obj = head.peel(git2::ObjectType::Commit).unwrap();
2532
2533        let parent_commit = parent_obj.as_commit().unwrap();
2534
2535        repo.commit(
2536            Some("HEAD"),
2537            &signature,
2538            &signature,
2539            msg,
2540            &tree,
2541            &[parent_commit],
2542        )
2543        .expect("Failed to commit with parent");
2544    } else {
2545        repo.commit(Some("HEAD"), &signature, &signature, msg, &tree, &[])
2546            .expect("Failed to commit");
2547    }
2548}
2549
2550#[track_caller]
2551fn git_stash(repo: &mut git2::Repository) {
2552    use git2::Signature;
2553
2554    let signature = Signature::now("test", "test@zed.dev").unwrap();
2555    repo.stash_save(&signature, "N/A", None)
2556        .expect("Failed to stash");
2557}
2558
2559#[track_caller]
2560fn git_reset(offset: usize, repo: &git2::Repository) {
2561    let head = repo.head().expect("Couldn't get repo head");
2562    let object = head.peel(git2::ObjectType::Commit).unwrap();
2563    let commit = object.as_commit().unwrap();
2564    let new_head = commit
2565        .parents()
2566        .inspect(|parnet| {
2567            parnet.message();
2568        })
2569        .nth(offset)
2570        .expect("Not enough history");
2571    repo.reset(new_head.as_object(), git2::ResetType::Soft, None)
2572        .expect("Could not reset");
2573}
2574
2575#[allow(dead_code)]
2576#[track_caller]
2577fn git_status(repo: &git2::Repository) -> collections::HashMap<String, git2::Status> {
2578    repo.statuses(None)
2579        .unwrap()
2580        .iter()
2581        .map(|status| (status.path().unwrap().to_string(), status.status()))
2582        .collect()
2583}
2584
2585#[track_caller]
2586fn check_worktree_entries(
2587    tree: &Worktree,
2588    expected_excluded_paths: &[&str],
2589    expected_ignored_paths: &[&str],
2590    expected_tracked_paths: &[&str],
2591) {
2592    for path in expected_excluded_paths {
2593        let entry = tree.entry_for_path(path);
2594        assert!(
2595            entry.is_none(),
2596            "expected path '{path}' to be excluded, but got entry: {entry:?}",
2597        );
2598    }
2599    for path in expected_ignored_paths {
2600        let entry = tree
2601            .entry_for_path(path)
2602            .unwrap_or_else(|| panic!("Missing entry for expected ignored path '{path}'"));
2603        assert!(
2604            entry.is_ignored,
2605            "expected path '{path}' to be ignored, but got entry: {entry:?}",
2606        );
2607    }
2608    for path in expected_tracked_paths {
2609        let entry = tree
2610            .entry_for_path(path)
2611            .unwrap_or_else(|| panic!("Missing entry for expected tracked path '{path}'"));
2612        assert!(
2613            !entry.is_ignored,
2614            "expected path '{path}' to be tracked, but got entry: {entry:?}",
2615        );
2616    }
2617}
2618
2619fn init_test(cx: &mut gpui::TestAppContext) {
2620    if std::env::var("RUST_LOG").is_ok() {
2621        env_logger::try_init().ok();
2622    }
2623
2624    cx.update(|cx| {
2625        let settings_store = SettingsStore::test(cx);
2626        cx.set_global(settings_store);
2627        WorktreeSettings::register(cx);
2628    });
2629}
2630
2631fn assert_entry_git_state(
2632    tree: &Worktree,
2633    path: &str,
2634    git_status: Option<GitFileStatus>,
2635    is_ignored: bool,
2636) {
2637    let entry = tree.entry_for_path(path).expect("entry {path} not found");
2638    assert_eq!(
2639        entry.git_status, git_status,
2640        "expected {path} to have git status: {git_status:?}"
2641    );
2642    assert_eq!(
2643        entry.is_ignored, is_ignored,
2644        "expected {path} to have is_ignored: {is_ignored}"
2645    );
2646}