worktree_tests.rs

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