worktree_tests.rs

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