worktree_tests.rs

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