worktree_tests.rs

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