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