worktree_tests.rs

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