worktree_tests.rs

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