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