worktree_tests.rs

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