worktree_tests.rs

   1use crate::{
   2    worktree::{Event, Snapshot, WorktreeHandle},
   3    EntryKind, PathChange, Worktree,
   4};
   5use anyhow::Result;
   6use client::Client;
   7use fs::{repository::GitFileStatus, FakeFs, Fs, RealFs, RemoveOptions};
   8use git::GITIGNORE;
   9use gpui::{executor::Deterministic, ModelContext, Task, TestAppContext};
  10use parking_lot::Mutex;
  11use postage::stream::Stream;
  12use pretty_assertions::assert_eq;
  13use rand::prelude::*;
  14use serde_json::json;
  15use std::{
  16    env,
  17    fmt::Write,
  18    path::{Path, PathBuf},
  19    sync::Arc,
  20};
  21use util::{http::FakeHttpClient, test::temp_tree, ResultExt};
  22
  23#[gpui::test]
  24async fn test_traversal(cx: &mut TestAppContext) {
  25    let fs = FakeFs::new(cx.background());
  26    fs.insert_tree(
  27        "/root",
  28        json!({
  29           ".gitignore": "a/b\n",
  30           "a": {
  31               "b": "",
  32               "c": "",
  33           }
  34        }),
  35    )
  36    .await;
  37
  38    let http_client = FakeHttpClient::with_404_response();
  39    let client = cx.read(|cx| Client::new(http_client, cx));
  40
  41    let tree = Worktree::local(
  42        client,
  43        Path::new("/root"),
  44        true,
  45        fs,
  46        Default::default(),
  47        &mut cx.to_async(),
  48    )
  49    .await
  50    .unwrap();
  51    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
  52        .await;
  53
  54    tree.read_with(cx, |tree, _| {
  55        assert_eq!(
  56            tree.entries(false)
  57                .map(|entry| entry.path.as_ref())
  58                .collect::<Vec<_>>(),
  59            vec![
  60                Path::new(""),
  61                Path::new(".gitignore"),
  62                Path::new("a"),
  63                Path::new("a/c"),
  64            ]
  65        );
  66        assert_eq!(
  67            tree.entries(true)
  68                .map(|entry| entry.path.as_ref())
  69                .collect::<Vec<_>>(),
  70            vec![
  71                Path::new(""),
  72                Path::new(".gitignore"),
  73                Path::new("a"),
  74                Path::new("a/b"),
  75                Path::new("a/c"),
  76            ]
  77        );
  78    })
  79}
  80
  81#[gpui::test]
  82async fn test_descendent_entries(cx: &mut TestAppContext) {
  83    let fs = FakeFs::new(cx.background());
  84    fs.insert_tree(
  85        "/root",
  86        json!({
  87            "a": "",
  88            "b": {
  89               "c": {
  90                   "d": ""
  91               },
  92               "e": {}
  93            },
  94            "f": "",
  95            "g": {
  96                "h": {}
  97            },
  98            "i": {
  99                "j": {
 100                    "k": ""
 101                },
 102                "l": {
 103
 104                }
 105            },
 106            ".gitignore": "i/j\n",
 107        }),
 108    )
 109    .await;
 110
 111    let http_client = FakeHttpClient::with_404_response();
 112    let client = cx.read(|cx| Client::new(http_client, cx));
 113
 114    let tree = Worktree::local(
 115        client,
 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.update(cx, |tree, cx| {
 162        let tree = tree.as_local_mut().unwrap();
 163        tree.expand_dir(tree.entry_for_path("i/j").unwrap().id, cx)
 164    })
 165    .recv()
 166    .await;
 167
 168    tree.read_with(cx, |tree, _| {
 169        assert_eq!(
 170            tree.descendent_entries(false, false, Path::new("i"))
 171                .map(|entry| entry.path.as_ref())
 172                .collect::<Vec<_>>(),
 173            Vec::<PathBuf>::new()
 174        );
 175        assert_eq!(
 176            tree.descendent_entries(false, true, Path::new("i"))
 177                .map(|entry| entry.path.as_ref())
 178                .collect::<Vec<_>>(),
 179            vec![Path::new("i/j/k")]
 180        );
 181        assert_eq!(
 182            tree.descendent_entries(true, false, Path::new("i"))
 183                .map(|entry| entry.path.as_ref())
 184                .collect::<Vec<_>>(),
 185            vec![Path::new("i"), Path::new("i/l"),]
 186        );
 187    })
 188}
 189
 190#[gpui::test(iterations = 10)]
 191async fn test_circular_symlinks(executor: Arc<Deterministic>, cx: &mut TestAppContext) {
 192    let fs = FakeFs::new(cx.background());
 193    fs.insert_tree(
 194        "/root",
 195        json!({
 196            "lib": {
 197                "a": {
 198                    "a.txt": ""
 199                },
 200                "b": {
 201                    "b.txt": ""
 202                }
 203            }
 204        }),
 205    )
 206    .await;
 207    fs.insert_symlink("/root/lib/a/lib", "..".into()).await;
 208    fs.insert_symlink("/root/lib/b/lib", "..".into()).await;
 209
 210    let client = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
 211    let tree = Worktree::local(
 212        client,
 213        Path::new("/root"),
 214        true,
 215        fs.clone(),
 216        Default::default(),
 217        &mut cx.to_async(),
 218    )
 219    .await
 220    .unwrap();
 221
 222    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
 223        .await;
 224
 225    tree.read_with(cx, |tree, _| {
 226        assert_eq!(
 227            tree.entries(false)
 228                .map(|entry| entry.path.as_ref())
 229                .collect::<Vec<_>>(),
 230            vec![
 231                Path::new(""),
 232                Path::new("lib"),
 233                Path::new("lib/a"),
 234                Path::new("lib/a/a.txt"),
 235                Path::new("lib/a/lib"),
 236                Path::new("lib/b"),
 237                Path::new("lib/b/b.txt"),
 238                Path::new("lib/b/lib"),
 239            ]
 240        );
 241    });
 242
 243    fs.rename(
 244        Path::new("/root/lib/a/lib"),
 245        Path::new("/root/lib/a/lib-2"),
 246        Default::default(),
 247    )
 248    .await
 249    .unwrap();
 250    executor.run_until_parked();
 251    tree.read_with(cx, |tree, _| {
 252        assert_eq!(
 253            tree.entries(false)
 254                .map(|entry| entry.path.as_ref())
 255                .collect::<Vec<_>>(),
 256            vec![
 257                Path::new(""),
 258                Path::new("lib"),
 259                Path::new("lib/a"),
 260                Path::new("lib/a/a.txt"),
 261                Path::new("lib/a/lib-2"),
 262                Path::new("lib/b"),
 263                Path::new("lib/b/b.txt"),
 264                Path::new("lib/b/lib"),
 265            ]
 266        );
 267    });
 268}
 269
 270#[gpui::test]
 271async fn test_symlinks_pointing_outside(cx: &mut TestAppContext) {
 272    let fs = FakeFs::new(cx.background());
 273    fs.insert_tree(
 274        "/root",
 275        json!({
 276            "dir1": {
 277                "deps": {
 278                    // symlinks here
 279                },
 280                "src": {
 281                    "a.rs": "",
 282                    "b.rs": "",
 283                },
 284            },
 285            "dir2": {
 286                "src": {
 287                    "c.rs": "",
 288                    "d.rs": "",
 289                }
 290            },
 291            "dir3": {
 292                "deps": {},
 293                "src": {
 294                    "e.rs": "",
 295                    "f.rs": "",
 296                },
 297            }
 298        }),
 299    )
 300    .await;
 301
 302    // These symlinks point to directories outside of the worktree's root, dir1.
 303    fs.insert_symlink("/root/dir1/deps/dep-dir2", "../../dir2".into())
 304        .await;
 305    fs.insert_symlink("/root/dir1/deps/dep-dir3", "../../dir3".into())
 306        .await;
 307
 308    let client = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
 309    let tree = Worktree::local(
 310        client,
 311        Path::new("/root/dir1"),
 312        true,
 313        fs.clone(),
 314        Default::default(),
 315        &mut cx.to_async(),
 316    )
 317    .await
 318    .unwrap();
 319
 320    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
 321        .await;
 322
 323    // The symlinked directories are not scanned by default.
 324    tree.read_with(cx, |tree, _| {
 325        assert_eq!(
 326            tree.entries(false)
 327                .map(|entry| (entry.path.as_ref(), entry.is_external))
 328                .collect::<Vec<_>>(),
 329            vec![
 330                (Path::new(""), false),
 331                (Path::new("deps"), false),
 332                (Path::new("deps/dep-dir2"), true),
 333                (Path::new("deps/dep-dir3"), true),
 334                (Path::new("src"), false),
 335                (Path::new("src/a.rs"), false),
 336                (Path::new("src/b.rs"), false),
 337            ]
 338        );
 339    });
 340
 341    // Expand one of the symlinked directories.
 342    tree.update(cx, |tree, cx| {
 343        let tree = tree.as_local_mut().unwrap();
 344        tree.expand_dir(tree.entry_for_path("deps/dep-dir3").unwrap().id, cx)
 345    })
 346    .recv()
 347    .await;
 348
 349    // The expanded directory's contents are loaded. Subdirectories are
 350    // not scanned yet.
 351    tree.read_with(cx, |tree, _| {
 352        assert_eq!(
 353            tree.entries(false)
 354                .map(|entry| (entry.path.as_ref(), entry.is_external))
 355                .collect::<Vec<_>>(),
 356            vec![
 357                (Path::new(""), false),
 358                (Path::new("deps"), false),
 359                (Path::new("deps/dep-dir2"), true),
 360                (Path::new("deps/dep-dir3"), true),
 361                (Path::new("deps/dep-dir3/deps"), true),
 362                (Path::new("deps/dep-dir3/src"), true),
 363                (Path::new("src"), false),
 364                (Path::new("src/a.rs"), false),
 365                (Path::new("src/b.rs"), false),
 366            ]
 367        );
 368    });
 369
 370    // Expand a subdirectory of one of the symlinked directories.
 371    tree.update(cx, |tree, cx| {
 372        let tree = tree.as_local_mut().unwrap();
 373        tree.expand_dir(tree.entry_for_path("deps/dep-dir3/src").unwrap().id, cx)
 374    })
 375    .recv()
 376    .await;
 377
 378    // The expanded subdirectory's contents are loaded.
 379    tree.read_with(cx, |tree, _| {
 380        assert_eq!(
 381            tree.entries(false)
 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("deps/dep-dir3/src/e.rs"), true),
 392                (Path::new("deps/dep-dir3/src/f.rs"), true),
 393                (Path::new("src"), false),
 394                (Path::new("src/a.rs"), false),
 395                (Path::new("src/b.rs"), false),
 396            ]
 397        );
 398    });
 399}
 400
 401#[gpui::test]
 402async fn test_rescan_with_gitignore(cx: &mut TestAppContext) {
 403    // .gitignores are handled explicitly by Zed and do not use the git
 404    // machinery that the git_tests module checks
 405    let parent_dir = temp_tree(json!({
 406        ".gitignore": "ancestor-ignored-file1\nancestor-ignored-file2\n",
 407        "tree": {
 408            ".git": {},
 409            ".gitignore": "ignored-dir\n",
 410            "tracked-dir": {
 411                "tracked-file1": "",
 412                "ancestor-ignored-file1": "",
 413            },
 414            "ignored-dir": {
 415                "ignored-file1": ""
 416            }
 417        }
 418    }));
 419    let dir = parent_dir.path().join("tree");
 420
 421    let client = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
 422
 423    let tree = Worktree::local(
 424        client,
 425        dir.as_path(),
 426        true,
 427        Arc::new(RealFs),
 428        Default::default(),
 429        &mut cx.to_async(),
 430    )
 431    .await
 432    .unwrap();
 433    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
 434        .await;
 435
 436    tree.update(cx, |tree, cx| {
 437        let tree = tree.as_local_mut().unwrap();
 438        tree.expand_dir(tree.entry_for_path("ignored-dir").unwrap().id, cx)
 439    })
 440    .recv()
 441    .await;
 442
 443    cx.read(|cx| {
 444        let tree = tree.read(cx);
 445        assert!(
 446            !tree
 447                .entry_for_path("tracked-dir/tracked-file1")
 448                .unwrap()
 449                .is_ignored
 450        );
 451        assert!(
 452            tree.entry_for_path("tracked-dir/ancestor-ignored-file1")
 453                .unwrap()
 454                .is_ignored
 455        );
 456        assert!(
 457            tree.entry_for_path("ignored-dir/ignored-file1")
 458                .unwrap()
 459                .is_ignored
 460        );
 461    });
 462
 463    std::fs::write(dir.join("tracked-dir/tracked-file2"), "").unwrap();
 464    std::fs::write(dir.join("tracked-dir/ancestor-ignored-file2"), "").unwrap();
 465    std::fs::write(dir.join("ignored-dir/ignored-file2"), "").unwrap();
 466    tree.flush_fs_events(cx).await;
 467    cx.read(|cx| {
 468        let tree = tree.read(cx);
 469        assert!(
 470            !tree
 471                .entry_for_path("tracked-dir/tracked-file2")
 472                .unwrap()
 473                .is_ignored
 474        );
 475        assert!(
 476            tree.entry_for_path("tracked-dir/ancestor-ignored-file2")
 477                .unwrap()
 478                .is_ignored
 479        );
 480        assert!(
 481            tree.entry_for_path("ignored-dir/ignored-file2")
 482                .unwrap()
 483                .is_ignored
 484        );
 485        assert!(tree.entry_for_path(".git").unwrap().is_ignored);
 486    });
 487}
 488
 489#[gpui::test]
 490async fn test_write_file(cx: &mut TestAppContext) {
 491    let dir = temp_tree(json!({
 492        ".git": {},
 493        ".gitignore": "ignored-dir\n",
 494        "tracked-dir": {},
 495        "ignored-dir": {}
 496    }));
 497
 498    let client = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
 499
 500    let tree = Worktree::local(
 501        client,
 502        dir.path(),
 503        true,
 504        Arc::new(RealFs),
 505        Default::default(),
 506        &mut cx.to_async(),
 507    )
 508    .await
 509    .unwrap();
 510    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
 511        .await;
 512    tree.flush_fs_events(cx).await;
 513
 514    tree.update(cx, |tree, cx| {
 515        tree.as_local().unwrap().write_file(
 516            Path::new("tracked-dir/file.txt"),
 517            "hello".into(),
 518            Default::default(),
 519            cx,
 520        )
 521    })
 522    .await
 523    .unwrap();
 524    tree.update(cx, |tree, cx| {
 525        tree.as_local().unwrap().write_file(
 526            Path::new("ignored-dir/file.txt"),
 527            "world".into(),
 528            Default::default(),
 529            cx,
 530        )
 531    })
 532    .await
 533    .unwrap();
 534
 535    tree.read_with(cx, |tree, _| {
 536        let tracked = tree.entry_for_path("tracked-dir/file.txt").unwrap();
 537        let ignored = tree.entry_for_path("ignored-dir/file.txt").unwrap();
 538        assert!(!tracked.is_ignored);
 539        assert!(ignored.is_ignored);
 540    });
 541}
 542
 543#[gpui::test(iterations = 30)]
 544async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
 545    let client = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
 546
 547    let fs = FakeFs::new(cx.background());
 548    fs.insert_tree(
 549        "/root",
 550        json!({
 551            "b": {},
 552            "c": {},
 553            "d": {},
 554        }),
 555    )
 556    .await;
 557
 558    let tree = Worktree::local(
 559        client,
 560        "/root".as_ref(),
 561        true,
 562        fs,
 563        Default::default(),
 564        &mut cx.to_async(),
 565    )
 566    .await
 567    .unwrap();
 568
 569    let snapshot1 = tree.update(cx, |tree, cx| {
 570        let tree = tree.as_local_mut().unwrap();
 571        let snapshot = Arc::new(Mutex::new(tree.snapshot()));
 572        let _ = tree.observe_updates(0, cx, {
 573            let snapshot = snapshot.clone();
 574            move |update| {
 575                snapshot.lock().apply_remote_update(update).unwrap();
 576                async { true }
 577            }
 578        });
 579        snapshot
 580    });
 581
 582    let entry = tree
 583        .update(cx, |tree, cx| {
 584            tree.as_local_mut()
 585                .unwrap()
 586                .create_entry("a/e".as_ref(), true, cx)
 587        })
 588        .await
 589        .unwrap();
 590    assert!(entry.is_dir());
 591
 592    cx.foreground().run_until_parked();
 593    tree.read_with(cx, |tree, _| {
 594        assert_eq!(tree.entry_for_path("a/e").unwrap().kind, EntryKind::Dir);
 595    });
 596
 597    let snapshot2 = tree.update(cx, |tree, _| tree.as_local().unwrap().snapshot());
 598    assert_eq!(
 599        snapshot1.lock().entries(true).collect::<Vec<_>>(),
 600        snapshot2.entries(true).collect::<Vec<_>>()
 601    );
 602}
 603
 604#[gpui::test(iterations = 100)]
 605async fn test_random_worktree_operations_during_initial_scan(
 606    cx: &mut TestAppContext,
 607    mut rng: StdRng,
 608) {
 609    let operations = env::var("OPERATIONS")
 610        .map(|o| o.parse().unwrap())
 611        .unwrap_or(5);
 612    let initial_entries = env::var("INITIAL_ENTRIES")
 613        .map(|o| o.parse().unwrap())
 614        .unwrap_or(20);
 615
 616    let root_dir = Path::new("/test");
 617    let fs = FakeFs::new(cx.background()) as Arc<dyn Fs>;
 618    fs.as_fake().insert_tree(root_dir, json!({})).await;
 619    for _ in 0..initial_entries {
 620        randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
 621    }
 622    log::info!("generated initial tree");
 623
 624    let client = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
 625    let worktree = Worktree::local(
 626        client.clone(),
 627        root_dir,
 628        true,
 629        fs.clone(),
 630        Default::default(),
 631        &mut cx.to_async(),
 632    )
 633    .await
 634    .unwrap();
 635
 636    let mut snapshots = vec![worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot())];
 637    let updates = Arc::new(Mutex::new(Vec::new()));
 638    worktree.update(cx, |tree, cx| {
 639        check_worktree_change_events(tree, cx);
 640
 641        let _ = tree.as_local_mut().unwrap().observe_updates(0, cx, {
 642            let updates = updates.clone();
 643            move |update| {
 644                updates.lock().push(update);
 645                async { true }
 646            }
 647        });
 648    });
 649
 650    for _ in 0..operations {
 651        worktree
 652            .update(cx, |worktree, cx| {
 653                randomly_mutate_worktree(worktree, &mut rng, cx)
 654            })
 655            .await
 656            .log_err();
 657        worktree.read_with(cx, |tree, _| {
 658            tree.as_local().unwrap().snapshot().check_invariants()
 659        });
 660
 661        if rng.gen_bool(0.6) {
 662            snapshots.push(worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot()));
 663        }
 664    }
 665
 666    worktree
 667        .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
 668        .await;
 669
 670    cx.foreground().run_until_parked();
 671
 672    let final_snapshot = worktree.read_with(cx, |tree, _| {
 673        let tree = tree.as_local().unwrap();
 674        let snapshot = tree.snapshot();
 675        snapshot.check_invariants();
 676        snapshot
 677    });
 678
 679    for (i, snapshot) in snapshots.into_iter().enumerate().rev() {
 680        let mut updated_snapshot = snapshot.clone();
 681        for update in updates.lock().iter() {
 682            if update.scan_id >= updated_snapshot.scan_id() as u64 {
 683                updated_snapshot
 684                    .apply_remote_update(update.clone())
 685                    .unwrap();
 686            }
 687        }
 688
 689        assert_eq!(
 690            updated_snapshot.entries(true).collect::<Vec<_>>(),
 691            final_snapshot.entries(true).collect::<Vec<_>>(),
 692            "wrong updates after snapshot {i}: {snapshot:#?} {updates:#?}",
 693        );
 694    }
 695}
 696
 697#[gpui::test(iterations = 100)]
 698async fn test_random_worktree_changes(cx: &mut TestAppContext, mut rng: StdRng) {
 699    let operations = env::var("OPERATIONS")
 700        .map(|o| o.parse().unwrap())
 701        .unwrap_or(40);
 702    let initial_entries = env::var("INITIAL_ENTRIES")
 703        .map(|o| o.parse().unwrap())
 704        .unwrap_or(20);
 705
 706    let root_dir = Path::new("/test");
 707    let fs = FakeFs::new(cx.background()) as Arc<dyn Fs>;
 708    fs.as_fake().insert_tree(root_dir, json!({})).await;
 709    for _ in 0..initial_entries {
 710        randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
 711    }
 712    log::info!("generated initial tree");
 713
 714    let client = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
 715    let worktree = Worktree::local(
 716        client.clone(),
 717        root_dir,
 718        true,
 719        fs.clone(),
 720        Default::default(),
 721        &mut cx.to_async(),
 722    )
 723    .await
 724    .unwrap();
 725
 726    let updates = Arc::new(Mutex::new(Vec::new()));
 727    worktree.update(cx, |tree, cx| {
 728        check_worktree_change_events(tree, cx);
 729
 730        let _ = tree.as_local_mut().unwrap().observe_updates(0, cx, {
 731            let updates = updates.clone();
 732            move |update| {
 733                updates.lock().push(update);
 734                async { true }
 735            }
 736        });
 737    });
 738
 739    worktree
 740        .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
 741        .await;
 742
 743    fs.as_fake().pause_events();
 744    let mut snapshots = Vec::new();
 745    let mut mutations_len = operations;
 746    while mutations_len > 1 {
 747        if rng.gen_bool(0.2) {
 748            worktree
 749                .update(cx, |worktree, cx| {
 750                    randomly_mutate_worktree(worktree, &mut rng, cx)
 751                })
 752                .await
 753                .log_err();
 754        } else {
 755            randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
 756        }
 757
 758        let buffered_event_count = fs.as_fake().buffered_event_count();
 759        if buffered_event_count > 0 && rng.gen_bool(0.3) {
 760            let len = rng.gen_range(0..=buffered_event_count);
 761            log::info!("flushing {} events", len);
 762            fs.as_fake().flush_events(len);
 763        } else {
 764            randomly_mutate_fs(&fs, root_dir, 0.6, &mut rng).await;
 765            mutations_len -= 1;
 766        }
 767
 768        cx.foreground().run_until_parked();
 769        if rng.gen_bool(0.2) {
 770            log::info!("storing snapshot {}", snapshots.len());
 771            let snapshot = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
 772            snapshots.push(snapshot);
 773        }
 774    }
 775
 776    log::info!("quiescing");
 777    fs.as_fake().flush_events(usize::MAX);
 778    cx.foreground().run_until_parked();
 779    let snapshot = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
 780    snapshot.check_invariants();
 781
 782    {
 783        let new_worktree = Worktree::local(
 784            client.clone(),
 785            root_dir,
 786            true,
 787            fs.clone(),
 788            Default::default(),
 789            &mut cx.to_async(),
 790        )
 791        .await
 792        .unwrap();
 793        new_worktree
 794            .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
 795            .await;
 796        let new_snapshot =
 797            new_worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
 798        assert_eq!(
 799            snapshot.entries_without_ids(true),
 800            new_snapshot.entries_without_ids(true)
 801        );
 802    }
 803
 804    for (i, mut prev_snapshot) in snapshots.into_iter().enumerate().rev() {
 805        for update in updates.lock().iter() {
 806            if update.scan_id >= prev_snapshot.scan_id() as u64 {
 807                prev_snapshot.apply_remote_update(update.clone()).unwrap();
 808            }
 809        }
 810
 811        assert_eq!(
 812            prev_snapshot.entries(true).collect::<Vec<_>>(),
 813            snapshot.entries(true).collect::<Vec<_>>(),
 814            "wrong updates after snapshot {i}: {updates:#?}",
 815        );
 816    }
 817}
 818
 819// The worktree's `UpdatedEntries` event can be used to follow along with
 820// all changes to the worktree's snapshot.
 821fn check_worktree_change_events(tree: &mut Worktree, cx: &mut ModelContext<Worktree>) {
 822    let mut entries = tree.entries(true).cloned().collect::<Vec<_>>();
 823    cx.subscribe(&cx.handle(), move |tree, _, event, _| {
 824        if let Event::UpdatedEntries(changes) = event {
 825            for (path, _, change_type) in changes.iter() {
 826                let entry = tree.entry_for_path(&path).cloned();
 827                let ix = match entries.binary_search_by_key(&path, |e| &e.path) {
 828                    Ok(ix) | Err(ix) => ix,
 829                };
 830                match change_type {
 831                    PathChange::Loaded => entries.insert(ix, entry.unwrap()),
 832                    PathChange::Added => entries.insert(ix, entry.unwrap()),
 833                    PathChange::Removed => drop(entries.remove(ix)),
 834                    PathChange::Updated => {
 835                        let entry = entry.unwrap();
 836                        let existing_entry = entries.get_mut(ix).unwrap();
 837                        assert_eq!(existing_entry.path, entry.path);
 838                        *existing_entry = entry;
 839                    }
 840                    PathChange::AddedOrUpdated => {
 841                        let entry = entry.unwrap();
 842                        if entries.get(ix).map(|e| &e.path) == Some(&entry.path) {
 843                            *entries.get_mut(ix).unwrap() = entry;
 844                        } else {
 845                            entries.insert(ix, entry);
 846                        }
 847                    }
 848                }
 849            }
 850
 851            let new_entries = tree.entries(true).cloned().collect::<Vec<_>>();
 852            assert_eq!(entries, new_entries, "incorrect changes: {:?}", changes);
 853        }
 854    })
 855    .detach();
 856}
 857
 858fn randomly_mutate_worktree(
 859    worktree: &mut Worktree,
 860    rng: &mut impl Rng,
 861    cx: &mut ModelContext<Worktree>,
 862) -> Task<Result<()>> {
 863    log::info!("mutating worktree");
 864    let worktree = worktree.as_local_mut().unwrap();
 865    let snapshot = worktree.snapshot();
 866    let entry = snapshot.entries(false).choose(rng).unwrap();
 867
 868    match rng.gen_range(0_u32..100) {
 869        0..=33 if entry.path.as_ref() != Path::new("") => {
 870            log::info!("deleting entry {:?} ({})", entry.path, entry.id.0);
 871            worktree.delete_entry(entry.id, cx).unwrap()
 872        }
 873        ..=66 if entry.path.as_ref() != Path::new("") => {
 874            let other_entry = snapshot.entries(false).choose(rng).unwrap();
 875            let new_parent_path = if other_entry.is_dir() {
 876                other_entry.path.clone()
 877            } else {
 878                other_entry.path.parent().unwrap().into()
 879            };
 880            let mut new_path = new_parent_path.join(random_filename(rng));
 881            if new_path.starts_with(&entry.path) {
 882                new_path = random_filename(rng).into();
 883            }
 884
 885            log::info!(
 886                "renaming entry {:?} ({}) to {:?}",
 887                entry.path,
 888                entry.id.0,
 889                new_path
 890            );
 891            let task = worktree.rename_entry(entry.id, new_path, cx).unwrap();
 892            cx.foreground().spawn(async move {
 893                task.await?;
 894                Ok(())
 895            })
 896        }
 897        _ => {
 898            let task = if entry.is_dir() {
 899                let child_path = entry.path.join(random_filename(rng));
 900                let is_dir = rng.gen_bool(0.3);
 901                log::info!(
 902                    "creating {} at {:?}",
 903                    if is_dir { "dir" } else { "file" },
 904                    child_path,
 905                );
 906                worktree.create_entry(child_path, is_dir, cx)
 907            } else {
 908                log::info!("overwriting file {:?} ({})", entry.path, entry.id.0);
 909                worktree.write_file(entry.path.clone(), "".into(), Default::default(), cx)
 910            };
 911            cx.foreground().spawn(async move {
 912                task.await?;
 913                Ok(())
 914            })
 915        }
 916    }
 917}
 918
 919async fn randomly_mutate_fs(
 920    fs: &Arc<dyn Fs>,
 921    root_path: &Path,
 922    insertion_probability: f64,
 923    rng: &mut impl Rng,
 924) {
 925    log::info!("mutating fs");
 926    let mut files = Vec::new();
 927    let mut dirs = Vec::new();
 928    for path in fs.as_fake().paths(false) {
 929        if path.starts_with(root_path) {
 930            if fs.is_file(&path).await {
 931                files.push(path);
 932            } else {
 933                dirs.push(path);
 934            }
 935        }
 936    }
 937
 938    if (files.is_empty() && dirs.len() == 1) || rng.gen_bool(insertion_probability) {
 939        let path = dirs.choose(rng).unwrap();
 940        let new_path = path.join(random_filename(rng));
 941
 942        if rng.gen() {
 943            log::info!(
 944                "creating dir {:?}",
 945                new_path.strip_prefix(root_path).unwrap()
 946            );
 947            fs.create_dir(&new_path).await.unwrap();
 948        } else {
 949            log::info!(
 950                "creating file {:?}",
 951                new_path.strip_prefix(root_path).unwrap()
 952            );
 953            fs.create_file(&new_path, Default::default()).await.unwrap();
 954        }
 955    } else if rng.gen_bool(0.05) {
 956        let ignore_dir_path = dirs.choose(rng).unwrap();
 957        let ignore_path = ignore_dir_path.join(&*GITIGNORE);
 958
 959        let subdirs = dirs
 960            .iter()
 961            .filter(|d| d.starts_with(&ignore_dir_path))
 962            .cloned()
 963            .collect::<Vec<_>>();
 964        let subfiles = files
 965            .iter()
 966            .filter(|d| d.starts_with(&ignore_dir_path))
 967            .cloned()
 968            .collect::<Vec<_>>();
 969        let files_to_ignore = {
 970            let len = rng.gen_range(0..=subfiles.len());
 971            subfiles.choose_multiple(rng, len)
 972        };
 973        let dirs_to_ignore = {
 974            let len = rng.gen_range(0..subdirs.len());
 975            subdirs.choose_multiple(rng, len)
 976        };
 977
 978        let mut ignore_contents = String::new();
 979        for path_to_ignore in files_to_ignore.chain(dirs_to_ignore) {
 980            writeln!(
 981                ignore_contents,
 982                "{}",
 983                path_to_ignore
 984                    .strip_prefix(&ignore_dir_path)
 985                    .unwrap()
 986                    .to_str()
 987                    .unwrap()
 988            )
 989            .unwrap();
 990        }
 991        log::info!(
 992            "creating gitignore {:?} with contents:\n{}",
 993            ignore_path.strip_prefix(&root_path).unwrap(),
 994            ignore_contents
 995        );
 996        fs.save(
 997            &ignore_path,
 998            &ignore_contents.as_str().into(),
 999            Default::default(),
1000        )
1001        .await
1002        .unwrap();
1003    } else {
1004        let old_path = {
1005            let file_path = files.choose(rng);
1006            let dir_path = dirs[1..].choose(rng);
1007            file_path.into_iter().chain(dir_path).choose(rng).unwrap()
1008        };
1009
1010        let is_rename = rng.gen();
1011        if is_rename {
1012            let new_path_parent = dirs
1013                .iter()
1014                .filter(|d| !d.starts_with(old_path))
1015                .choose(rng)
1016                .unwrap();
1017
1018            let overwrite_existing_dir =
1019                !old_path.starts_with(&new_path_parent) && rng.gen_bool(0.3);
1020            let new_path = if overwrite_existing_dir {
1021                fs.remove_dir(
1022                    &new_path_parent,
1023                    RemoveOptions {
1024                        recursive: true,
1025                        ignore_if_not_exists: true,
1026                    },
1027                )
1028                .await
1029                .unwrap();
1030                new_path_parent.to_path_buf()
1031            } else {
1032                new_path_parent.join(random_filename(rng))
1033            };
1034
1035            log::info!(
1036                "renaming {:?} to {}{:?}",
1037                old_path.strip_prefix(&root_path).unwrap(),
1038                if overwrite_existing_dir {
1039                    "overwrite "
1040                } else {
1041                    ""
1042                },
1043                new_path.strip_prefix(&root_path).unwrap()
1044            );
1045            fs.rename(
1046                &old_path,
1047                &new_path,
1048                fs::RenameOptions {
1049                    overwrite: true,
1050                    ignore_if_exists: true,
1051                },
1052            )
1053            .await
1054            .unwrap();
1055        } else if fs.is_file(&old_path).await {
1056            log::info!(
1057                "deleting file {:?}",
1058                old_path.strip_prefix(&root_path).unwrap()
1059            );
1060            fs.remove_file(old_path, Default::default()).await.unwrap();
1061        } else {
1062            log::info!(
1063                "deleting dir {:?}",
1064                old_path.strip_prefix(&root_path).unwrap()
1065            );
1066            fs.remove_dir(
1067                &old_path,
1068                RemoveOptions {
1069                    recursive: true,
1070                    ignore_if_not_exists: true,
1071                },
1072            )
1073            .await
1074            .unwrap();
1075        }
1076    }
1077}
1078
1079fn random_filename(rng: &mut impl Rng) -> String {
1080    (0..6)
1081        .map(|_| rng.sample(rand::distributions::Alphanumeric))
1082        .map(char::from)
1083        .collect()
1084}
1085
1086#[gpui::test]
1087async fn test_rename_work_directory(cx: &mut TestAppContext) {
1088    let root = temp_tree(json!({
1089        "projects": {
1090            "project1": {
1091                "a": "",
1092                "b": "",
1093            }
1094        },
1095
1096    }));
1097    let root_path = root.path();
1098
1099    let http_client = FakeHttpClient::with_404_response();
1100    let client = cx.read(|cx| Client::new(http_client, cx));
1101    let tree = Worktree::local(
1102        client,
1103        root_path,
1104        true,
1105        Arc::new(RealFs),
1106        Default::default(),
1107        &mut cx.to_async(),
1108    )
1109    .await
1110    .unwrap();
1111
1112    let repo = git_init(&root_path.join("projects/project1"));
1113    git_add("a", &repo);
1114    git_commit("init", &repo);
1115    std::fs::write(root_path.join("projects/project1/a"), "aa").ok();
1116
1117    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1118        .await;
1119
1120    tree.flush_fs_events(cx).await;
1121
1122    cx.read(|cx| {
1123        let tree = tree.read(cx);
1124        let (work_dir, _) = tree.repositories().next().unwrap();
1125        assert_eq!(work_dir.as_ref(), Path::new("projects/project1"));
1126        assert_eq!(
1127            tree.status_for_file(Path::new("projects/project1/a")),
1128            Some(GitFileStatus::Modified)
1129        );
1130        assert_eq!(
1131            tree.status_for_file(Path::new("projects/project1/b")),
1132            Some(GitFileStatus::Added)
1133        );
1134    });
1135
1136    std::fs::rename(
1137        root_path.join("projects/project1"),
1138        root_path.join("projects/project2"),
1139    )
1140    .ok();
1141    tree.flush_fs_events(cx).await;
1142
1143    cx.read(|cx| {
1144        let tree = tree.read(cx);
1145        let (work_dir, _) = tree.repositories().next().unwrap();
1146        assert_eq!(work_dir.as_ref(), Path::new("projects/project2"));
1147        assert_eq!(
1148            tree.status_for_file(Path::new("projects/project2/a")),
1149            Some(GitFileStatus::Modified)
1150        );
1151        assert_eq!(
1152            tree.status_for_file(Path::new("projects/project2/b")),
1153            Some(GitFileStatus::Added)
1154        );
1155    });
1156}
1157
1158#[gpui::test]
1159async fn test_git_repository_for_path(cx: &mut TestAppContext) {
1160    let root = temp_tree(json!({
1161        "c.txt": "",
1162        "dir1": {
1163            ".git": {},
1164            "deps": {
1165                "dep1": {
1166                    ".git": {},
1167                    "src": {
1168                        "a.txt": ""
1169                    }
1170                }
1171            },
1172            "src": {
1173                "b.txt": ""
1174            }
1175        },
1176    }));
1177
1178    let http_client = FakeHttpClient::with_404_response();
1179    let client = cx.read(|cx| Client::new(http_client, cx));
1180    let tree = Worktree::local(
1181        client,
1182        root.path(),
1183        true,
1184        Arc::new(RealFs),
1185        Default::default(),
1186        &mut cx.to_async(),
1187    )
1188    .await
1189    .unwrap();
1190
1191    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1192        .await;
1193    tree.flush_fs_events(cx).await;
1194
1195    tree.read_with(cx, |tree, _cx| {
1196        let tree = tree.as_local().unwrap();
1197
1198        assert!(tree.repository_for_path("c.txt".as_ref()).is_none());
1199
1200        let entry = tree.repository_for_path("dir1/src/b.txt".as_ref()).unwrap();
1201        assert_eq!(
1202            entry
1203                .work_directory(tree)
1204                .map(|directory| directory.as_ref().to_owned()),
1205            Some(Path::new("dir1").to_owned())
1206        );
1207
1208        let entry = tree
1209            .repository_for_path("dir1/deps/dep1/src/a.txt".as_ref())
1210            .unwrap();
1211        assert_eq!(
1212            entry
1213                .work_directory(tree)
1214                .map(|directory| directory.as_ref().to_owned()),
1215            Some(Path::new("dir1/deps/dep1").to_owned())
1216        );
1217
1218        let entries = tree.files(false, 0);
1219
1220        let paths_with_repos = tree
1221            .entries_with_repositories(entries)
1222            .map(|(entry, repo)| {
1223                (
1224                    entry.path.as_ref(),
1225                    repo.and_then(|repo| {
1226                        repo.work_directory(&tree)
1227                            .map(|work_directory| work_directory.0.to_path_buf())
1228                    }),
1229                )
1230            })
1231            .collect::<Vec<_>>();
1232
1233        assert_eq!(
1234            paths_with_repos,
1235            &[
1236                (Path::new("c.txt"), None),
1237                (
1238                    Path::new("dir1/deps/dep1/src/a.txt"),
1239                    Some(Path::new("dir1/deps/dep1").into())
1240                ),
1241                (Path::new("dir1/src/b.txt"), Some(Path::new("dir1").into())),
1242            ]
1243        );
1244    });
1245
1246    let repo_update_events = Arc::new(Mutex::new(vec![]));
1247    tree.update(cx, |_, cx| {
1248        let repo_update_events = repo_update_events.clone();
1249        cx.subscribe(&tree, move |_, _, event, _| {
1250            if let Event::UpdatedGitRepositories(update) = event {
1251                repo_update_events.lock().push(update.clone());
1252            }
1253        })
1254        .detach();
1255    });
1256
1257    std::fs::write(root.path().join("dir1/.git/random_new_file"), "hello").unwrap();
1258    tree.flush_fs_events(cx).await;
1259
1260    assert_eq!(
1261        repo_update_events.lock()[0]
1262            .iter()
1263            .map(|e| e.0.clone())
1264            .collect::<Vec<Arc<Path>>>(),
1265        vec![Path::new("dir1").into()]
1266    );
1267
1268    std::fs::remove_dir_all(root.path().join("dir1/.git")).unwrap();
1269    tree.flush_fs_events(cx).await;
1270
1271    tree.read_with(cx, |tree, _cx| {
1272        let tree = tree.as_local().unwrap();
1273
1274        assert!(tree
1275            .repository_for_path("dir1/src/b.txt".as_ref())
1276            .is_none());
1277    });
1278}
1279
1280#[gpui::test]
1281async fn test_git_status(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
1282    const IGNORE_RULE: &'static str = "**/target";
1283
1284    let root = temp_tree(json!({
1285        "project": {
1286            "a.txt": "a",
1287            "b.txt": "bb",
1288            "c": {
1289                "d": {
1290                    "e.txt": "eee"
1291                }
1292            },
1293            "f.txt": "ffff",
1294            "target": {
1295                "build_file": "???"
1296            },
1297            ".gitignore": IGNORE_RULE
1298        },
1299
1300    }));
1301
1302    let http_client = FakeHttpClient::with_404_response();
1303    let client = cx.read(|cx| Client::new(http_client, cx));
1304    let tree = Worktree::local(
1305        client,
1306        root.path(),
1307        true,
1308        Arc::new(RealFs),
1309        Default::default(),
1310        &mut cx.to_async(),
1311    )
1312    .await
1313    .unwrap();
1314
1315    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1316        .await;
1317
1318    const A_TXT: &'static str = "a.txt";
1319    const B_TXT: &'static str = "b.txt";
1320    const E_TXT: &'static str = "c/d/e.txt";
1321    const F_TXT: &'static str = "f.txt";
1322    const DOTGITIGNORE: &'static str = ".gitignore";
1323    const BUILD_FILE: &'static str = "target/build_file";
1324    let project_path: &Path = &Path::new("project");
1325
1326    let work_dir = root.path().join("project");
1327    let mut repo = git_init(work_dir.as_path());
1328    repo.add_ignore_rule(IGNORE_RULE).unwrap();
1329    git_add(Path::new(A_TXT), &repo);
1330    git_add(Path::new(E_TXT), &repo);
1331    git_add(Path::new(DOTGITIGNORE), &repo);
1332    git_commit("Initial commit", &repo);
1333
1334    tree.flush_fs_events(cx).await;
1335    deterministic.run_until_parked();
1336
1337    // Check that the right git state is observed on startup
1338    tree.read_with(cx, |tree, _cx| {
1339        let snapshot = tree.snapshot();
1340        assert_eq!(snapshot.repositories().count(), 1);
1341        let (dir, _) = snapshot.repositories().next().unwrap();
1342        assert_eq!(dir.as_ref(), Path::new("project"));
1343
1344        assert_eq!(
1345            snapshot.status_for_file(project_path.join(B_TXT)),
1346            Some(GitFileStatus::Added)
1347        );
1348        assert_eq!(
1349            snapshot.status_for_file(project_path.join(F_TXT)),
1350            Some(GitFileStatus::Added)
1351        );
1352    });
1353
1354    std::fs::write(work_dir.join(A_TXT), "aa").unwrap();
1355
1356    tree.flush_fs_events(cx).await;
1357    deterministic.run_until_parked();
1358
1359    tree.read_with(cx, |tree, _cx| {
1360        let snapshot = tree.snapshot();
1361
1362        assert_eq!(
1363            snapshot.status_for_file(project_path.join(A_TXT)),
1364            Some(GitFileStatus::Modified)
1365        );
1366    });
1367
1368    git_add(Path::new(A_TXT), &repo);
1369    git_add(Path::new(B_TXT), &repo);
1370    git_commit("Committing modified and added", &repo);
1371    tree.flush_fs_events(cx).await;
1372    deterministic.run_until_parked();
1373
1374    // Check that repo only changes are tracked
1375    tree.read_with(cx, |tree, _cx| {
1376        let snapshot = tree.snapshot();
1377
1378        assert_eq!(
1379            snapshot.status_for_file(project_path.join(F_TXT)),
1380            Some(GitFileStatus::Added)
1381        );
1382
1383        assert_eq!(snapshot.status_for_file(project_path.join(B_TXT)), None);
1384        assert_eq!(snapshot.status_for_file(project_path.join(A_TXT)), None);
1385    });
1386
1387    git_reset(0, &repo);
1388    git_remove_index(Path::new(B_TXT), &repo);
1389    git_stash(&mut repo);
1390    std::fs::write(work_dir.join(E_TXT), "eeee").unwrap();
1391    std::fs::write(work_dir.join(BUILD_FILE), "this should be ignored").unwrap();
1392    tree.flush_fs_events(cx).await;
1393    deterministic.run_until_parked();
1394
1395    // Check that more complex repo changes are tracked
1396    tree.read_with(cx, |tree, _cx| {
1397        let snapshot = tree.snapshot();
1398
1399        assert_eq!(snapshot.status_for_file(project_path.join(A_TXT)), None);
1400        assert_eq!(
1401            snapshot.status_for_file(project_path.join(B_TXT)),
1402            Some(GitFileStatus::Added)
1403        );
1404        assert_eq!(
1405            snapshot.status_for_file(project_path.join(E_TXT)),
1406            Some(GitFileStatus::Modified)
1407        );
1408    });
1409
1410    std::fs::remove_file(work_dir.join(B_TXT)).unwrap();
1411    std::fs::remove_dir_all(work_dir.join("c")).unwrap();
1412    std::fs::write(
1413        work_dir.join(DOTGITIGNORE),
1414        [IGNORE_RULE, "f.txt"].join("\n"),
1415    )
1416    .unwrap();
1417
1418    git_add(Path::new(DOTGITIGNORE), &repo);
1419    git_commit("Committing modified git ignore", &repo);
1420
1421    tree.flush_fs_events(cx).await;
1422    deterministic.run_until_parked();
1423
1424    let mut renamed_dir_name = "first_directory/second_directory";
1425    const RENAMED_FILE: &'static str = "rf.txt";
1426
1427    std::fs::create_dir_all(work_dir.join(renamed_dir_name)).unwrap();
1428    std::fs::write(
1429        work_dir.join(renamed_dir_name).join(RENAMED_FILE),
1430        "new-contents",
1431    )
1432    .unwrap();
1433
1434    tree.flush_fs_events(cx).await;
1435    deterministic.run_until_parked();
1436
1437    tree.read_with(cx, |tree, _cx| {
1438        let snapshot = tree.snapshot();
1439        assert_eq!(
1440            snapshot.status_for_file(&project_path.join(renamed_dir_name).join(RENAMED_FILE)),
1441            Some(GitFileStatus::Added)
1442        );
1443    });
1444
1445    renamed_dir_name = "new_first_directory/second_directory";
1446
1447    std::fs::rename(
1448        work_dir.join("first_directory"),
1449        work_dir.join("new_first_directory"),
1450    )
1451    .unwrap();
1452
1453    tree.flush_fs_events(cx).await;
1454    deterministic.run_until_parked();
1455
1456    tree.read_with(cx, |tree, _cx| {
1457        let snapshot = tree.snapshot();
1458
1459        assert_eq!(
1460            snapshot.status_for_file(
1461                project_path
1462                    .join(Path::new(renamed_dir_name))
1463                    .join(RENAMED_FILE)
1464            ),
1465            Some(GitFileStatus::Added)
1466        );
1467    });
1468}
1469
1470#[gpui::test]
1471async fn test_propagate_git_statuses(cx: &mut TestAppContext) {
1472    let fs = FakeFs::new(cx.background());
1473    fs.insert_tree(
1474        "/root",
1475        json!({
1476            ".git": {},
1477            "a": {
1478                "b": {
1479                    "c1.txt": "",
1480                    "c2.txt": "",
1481                },
1482                "d": {
1483                    "e1.txt": "",
1484                    "e2.txt": "",
1485                    "e3.txt": "",
1486                }
1487            },
1488            "f": {
1489                "no-status.txt": ""
1490            },
1491            "g": {
1492                "h1.txt": "",
1493                "h2.txt": ""
1494            },
1495
1496        }),
1497    )
1498    .await;
1499
1500    fs.set_status_for_repo_via_git_operation(
1501        &Path::new("/root/.git"),
1502        &[
1503            (Path::new("a/b/c1.txt"), GitFileStatus::Added),
1504            (Path::new("a/d/e2.txt"), GitFileStatus::Modified),
1505            (Path::new("g/h2.txt"), GitFileStatus::Conflict),
1506        ],
1507    );
1508
1509    let http_client = FakeHttpClient::with_404_response();
1510    let client = cx.read(|cx| Client::new(http_client, cx));
1511    let tree = Worktree::local(
1512        client,
1513        Path::new("/root"),
1514        true,
1515        fs.clone(),
1516        Default::default(),
1517        &mut cx.to_async(),
1518    )
1519    .await
1520    .unwrap();
1521
1522    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1523        .await;
1524
1525    cx.foreground().run_until_parked();
1526    let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
1527
1528    check_propagated_statuses(
1529        &snapshot,
1530        &[
1531            (Path::new(""), Some(GitFileStatus::Conflict)),
1532            (Path::new("a"), Some(GitFileStatus::Modified)),
1533            (Path::new("a/b"), Some(GitFileStatus::Added)),
1534            (Path::new("a/b/c1.txt"), Some(GitFileStatus::Added)),
1535            (Path::new("a/b/c2.txt"), None),
1536            (Path::new("a/d"), Some(GitFileStatus::Modified)),
1537            (Path::new("a/d/e2.txt"), Some(GitFileStatus::Modified)),
1538            (Path::new("f"), None),
1539            (Path::new("f/no-status.txt"), None),
1540            (Path::new("g"), Some(GitFileStatus::Conflict)),
1541            (Path::new("g/h2.txt"), Some(GitFileStatus::Conflict)),
1542        ],
1543    );
1544
1545    check_propagated_statuses(
1546        &snapshot,
1547        &[
1548            (Path::new("a/b"), Some(GitFileStatus::Added)),
1549            (Path::new("a/b/c1.txt"), Some(GitFileStatus::Added)),
1550            (Path::new("a/b/c2.txt"), None),
1551            (Path::new("a/d"), Some(GitFileStatus::Modified)),
1552            (Path::new("a/d/e1.txt"), None),
1553            (Path::new("a/d/e2.txt"), Some(GitFileStatus::Modified)),
1554            (Path::new("f"), None),
1555            (Path::new("f/no-status.txt"), None),
1556            (Path::new("g"), Some(GitFileStatus::Conflict)),
1557        ],
1558    );
1559
1560    check_propagated_statuses(
1561        &snapshot,
1562        &[
1563            (Path::new("a/b/c1.txt"), Some(GitFileStatus::Added)),
1564            (Path::new("a/b/c2.txt"), None),
1565            (Path::new("a/d/e1.txt"), None),
1566            (Path::new("a/d/e2.txt"), Some(GitFileStatus::Modified)),
1567            (Path::new("f/no-status.txt"), None),
1568        ],
1569    );
1570
1571    #[track_caller]
1572    fn check_propagated_statuses(
1573        snapshot: &Snapshot,
1574        expected_statuses: &[(&Path, Option<GitFileStatus>)],
1575    ) {
1576        let mut entries = expected_statuses
1577            .iter()
1578            .map(|(path, _)| snapshot.entry_for_path(path).unwrap().clone())
1579            .collect::<Vec<_>>();
1580        snapshot.propagate_git_statuses(&mut entries);
1581        assert_eq!(
1582            entries
1583                .iter()
1584                .map(|e| (e.path.as_ref(), e.git_status))
1585                .collect::<Vec<_>>(),
1586            expected_statuses
1587        );
1588    }
1589}
1590
1591#[track_caller]
1592fn git_init(path: &Path) -> git2::Repository {
1593    git2::Repository::init(path).expect("Failed to initialize git repository")
1594}
1595
1596#[track_caller]
1597fn git_add<P: AsRef<Path>>(path: P, repo: &git2::Repository) {
1598    let path = path.as_ref();
1599    let mut index = repo.index().expect("Failed to get index");
1600    index.add_path(path).expect("Failed to add a.txt");
1601    index.write().expect("Failed to write index");
1602}
1603
1604#[track_caller]
1605fn git_remove_index(path: &Path, repo: &git2::Repository) {
1606    let mut index = repo.index().expect("Failed to get index");
1607    index.remove_path(path).expect("Failed to add a.txt");
1608    index.write().expect("Failed to write index");
1609}
1610
1611#[track_caller]
1612fn git_commit(msg: &'static str, repo: &git2::Repository) {
1613    use git2::Signature;
1614
1615    let signature = Signature::now("test", "test@zed.dev").unwrap();
1616    let oid = repo.index().unwrap().write_tree().unwrap();
1617    let tree = repo.find_tree(oid).unwrap();
1618    if let Some(head) = repo.head().ok() {
1619        let parent_obj = head.peel(git2::ObjectType::Commit).unwrap();
1620
1621        let parent_commit = parent_obj.as_commit().unwrap();
1622
1623        repo.commit(
1624            Some("HEAD"),
1625            &signature,
1626            &signature,
1627            msg,
1628            &tree,
1629            &[parent_commit],
1630        )
1631        .expect("Failed to commit with parent");
1632    } else {
1633        repo.commit(Some("HEAD"), &signature, &signature, msg, &tree, &[])
1634            .expect("Failed to commit");
1635    }
1636}
1637
1638#[track_caller]
1639fn git_stash(repo: &mut git2::Repository) {
1640    use git2::Signature;
1641
1642    let signature = Signature::now("test", "test@zed.dev").unwrap();
1643    repo.stash_save(&signature, "N/A", None)
1644        .expect("Failed to stash");
1645}
1646
1647#[track_caller]
1648fn git_reset(offset: usize, repo: &git2::Repository) {
1649    let head = repo.head().expect("Couldn't get repo head");
1650    let object = head.peel(git2::ObjectType::Commit).unwrap();
1651    let commit = object.as_commit().unwrap();
1652    let new_head = commit
1653        .parents()
1654        .inspect(|parnet| {
1655            parnet.message();
1656        })
1657        .skip(offset)
1658        .next()
1659        .expect("Not enough history");
1660    repo.reset(&new_head.as_object(), git2::ResetType::Soft, None)
1661        .expect("Could not reset");
1662}
1663
1664#[allow(dead_code)]
1665#[track_caller]
1666fn git_status(repo: &git2::Repository) -> collections::HashMap<String, git2::Status> {
1667    repo.statuses(None)
1668        .unwrap()
1669        .iter()
1670        .map(|status| (status.path().unwrap().to_string(), status.status()))
1671        .collect()
1672}