worktree_tests.rs

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