worktree_tests.rs

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