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