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