worktree_tests.rs

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