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