worktree_tests.rs

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