Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T04:02:15.849Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

worktree_tests.rs

6181 lines · 185.1 KB · rust
1mod worktree_settings_tests;
2
3use anyhow::Result;
4use encoding_rs;
5use fs::{FakeFs, Fs, PathEventKind, RealFs, RemoveOptions};
6use git::{DOT_GIT, GITIGNORE, REPO_EXCLUDE};
7use gpui::{AppContext as _, BackgroundExecutor, BorrowAppContext, Context, Task, TestAppContext};
8use parking_lot::Mutex;
9use postage::stream::Stream;
10use pretty_assertions::assert_eq;
11use rand::prelude::*;
12use rpc::{AnyProtoClient, NoopProtoClient, proto};
13use worktree::{Entry, EntryKind, Event, PathChange, Worktree, WorktreeModelHandle};
14
15use serde_json::json;
16use settings::{SettingsStore, WorktreeId};
17use std::{
18    cell::Cell,
19    env,
20    fmt::Write,
21    mem,
22    path::{Path, PathBuf},
23    rc::Rc,
24    sync::Arc,
25};
26use util::{
27    ResultExt, path,
28    paths::PathStyle,
29    rel_path::{RelPath, rel_path},
30    test::TempTree,
31};
32
33#[gpui::test]
34async fn test_traversal(cx: &mut TestAppContext) {
35    init_test(cx);
36    let fs = FakeFs::new(cx.background_executor.clone());
37    fs.insert_tree(
38        "/root",
39        json!({
40           ".gitignore": "a/b\n",
41           "a": {
42               "b": "",
43               "c": "",
44           }
45        }),
46    )
47    .await;
48
49    let tree = Worktree::local(
50        Path::new("/root"),
51        true,
52        fs,
53        Default::default(),
54        true,
55        WorktreeId::from_proto(0),
56        &mut cx.to_async(),
57    )
58    .await
59    .unwrap();
60    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
61        .await;
62
63    tree.read_with(cx, |tree, _| {
64        assert_eq!(
65            tree.entries(false, 0)
66                .map(|entry| entry.path.as_ref())
67                .collect::<Vec<_>>(),
68            vec![
69                rel_path(""),
70                rel_path(".gitignore"),
71                rel_path("a"),
72                rel_path("a/c"),
73            ]
74        );
75        assert_eq!(
76            tree.entries(true, 0)
77                .map(|entry| entry.path.as_ref())
78                .collect::<Vec<_>>(),
79            vec![
80                rel_path(""),
81                rel_path(".gitignore"),
82                rel_path("a"),
83                rel_path("a/b"),
84                rel_path("a/c"),
85            ]
86        );
87    })
88}
89
90#[gpui::test(iterations = 10)]
91async fn test_circular_symlinks(cx: &mut TestAppContext) {
92    init_test(cx);
93    let fs = FakeFs::new(cx.background_executor.clone());
94    fs.insert_tree(
95        "/root",
96        json!({
97            "lib": {
98                "a": {
99                    "a.txt": ""
100                },
101                "b": {
102                    "b.txt": ""
103                }
104            }
105        }),
106    )
107    .await;
108    fs.create_symlink("/root/lib/a/lib".as_ref(), "..".into())
109        .await
110        .unwrap();
111    fs.create_symlink("/root/lib/b/lib".as_ref(), "..".into())
112        .await
113        .unwrap();
114
115    let tree = Worktree::local(
116        Path::new("/root"),
117        true,
118        fs.clone(),
119        Default::default(),
120        true,
121        WorktreeId::from_proto(0),
122        &mut cx.to_async(),
123    )
124    .await
125    .unwrap();
126
127    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
128        .await;
129
130    tree.read_with(cx, |tree, _| {
131        assert_eq!(
132            tree.entries(false, 0)
133                .map(|entry| entry.path.as_ref())
134                .collect::<Vec<_>>(),
135            vec![
136                rel_path(""),
137                rel_path("lib"),
138                rel_path("lib/a"),
139                rel_path("lib/a/a.txt"),
140                rel_path("lib/a/lib"),
141                rel_path("lib/b"),
142                rel_path("lib/b/b.txt"),
143                rel_path("lib/b/lib"),
144            ]
145        );
146    });
147
148    fs.rename(
149        Path::new("/root/lib/a/lib"),
150        Path::new("/root/lib/a/lib-2"),
151        Default::default(),
152    )
153    .await
154    .unwrap();
155    cx.executor().run_until_parked();
156    tree.read_with(cx, |tree, _| {
157        assert_eq!(
158            tree.entries(false, 0)
159                .map(|entry| entry.path.as_ref())
160                .collect::<Vec<_>>(),
161            vec![
162                rel_path(""),
163                rel_path("lib"),
164                rel_path("lib/a"),
165                rel_path("lib/a/a.txt"),
166                rel_path("lib/a/lib-2"),
167                rel_path("lib/b"),
168                rel_path("lib/b/b.txt"),
169                rel_path("lib/b/lib"),
170            ]
171        );
172    });
173}
174
175#[gpui::test]
176async fn test_symlinks_pointing_outside(cx: &mut TestAppContext) {
177    init_test(cx);
178    let fs = FakeFs::new(cx.background_executor.clone());
179    fs.insert_tree(
180        "/root",
181        json!({
182            "dir1": {
183                "deps": {
184                    // symlinks here
185                },
186                "src": {
187                    "a.rs": "",
188                    "b.rs": "",
189                },
190            },
191            "dir2": {
192                "src": {
193                    "c.rs": "",
194                    "d.rs": "",
195                }
196            },
197            "dir3": {
198                "deps": {},
199                "src": {
200                    "e.rs": "",
201                    "f.rs": "",
202                    "nested": {
203                        "deep.rs": ""
204                    }
205                },
206            }
207        }),
208    )
209    .await;
210
211    // These symlinks point to directories outside of the worktree's root, dir1.
212    fs.create_symlink("/root/dir1/deps/dep-dir2".as_ref(), "../../dir2".into())
213        .await
214        .unwrap();
215    fs.create_symlink("/root/dir1/deps/dep-dir3".as_ref(), "../../dir3".into())
216        .await
217        .unwrap();
218    fs.create_symlink(
219        "/root/dir1/deps/dep-dir3-alias".as_ref(),
220        "../../dir3".into(),
221    )
222    .await
223    .unwrap();
224    fs.create_symlink(
225        "/root/dir1/deps/dep-dir3-nested".as_ref(),
226        "../../dir3/src/nested".into(),
227    )
228    .await
229    .unwrap();
230
231    let tree = Worktree::local(
232        Path::new("/root/dir1"),
233        true,
234        fs.clone(),
235        Default::default(),
236        true,
237        WorktreeId::from_proto(0),
238        &mut cx.to_async(),
239    )
240    .await
241    .unwrap();
242
243    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
244        .await;
245
246    let tree_updates = Arc::new(Mutex::new(Vec::new()));
247    tree.update(cx, |_, cx| {
248        let tree_updates = tree_updates.clone();
249        cx.subscribe(&tree, move |_, _, event, _| {
250            if let Event::UpdatedEntries(update) = event {
251                tree_updates.lock().extend(
252                    update
253                        .iter()
254                        .map(|(path, _, change)| (path.clone(), *change)),
255                );
256            }
257        })
258        .detach();
259    });
260
261    // The symlinked directories are not scanned by default.
262    tree.read_with(cx, |tree, _| {
263        assert_eq!(
264            tree.entries(true, 0)
265                .map(|entry| (entry.path.as_ref(), entry.is_external))
266                .collect::<Vec<_>>(),
267            vec![
268                (rel_path(""), false),
269                (rel_path("deps"), false),
270                (rel_path("deps/dep-dir2"), true),
271                (rel_path("deps/dep-dir3"), true),
272                (rel_path("deps/dep-dir3-alias"), true),
273                (rel_path("deps/dep-dir3-nested"), true),
274                (rel_path("src"), false),
275                (rel_path("src/a.rs"), false),
276                (rel_path("src/b.rs"), false),
277            ]
278        );
279
280        assert_eq!(
281            tree.entry_for_path(rel_path("deps/dep-dir2")).unwrap().kind,
282            EntryKind::UnloadedDir
283        );
284    });
285
286    // Expand one of the symlinked directories.
287    tree.read_with(cx, |tree, _| {
288        tree.as_local()
289            .unwrap()
290            .refresh_entries_for_paths(vec![rel_path("deps/dep-dir3").into()])
291    })
292    .recv()
293    .await;
294
295    // The expanded directory's contents are loaded. Subdirectories are
296    // not scanned yet.
297    tree.read_with(cx, |tree, _| {
298        assert_eq!(
299            tree.entries(true, 0)
300                .map(|entry| (entry.path.as_ref(), entry.is_external))
301                .collect::<Vec<_>>(),
302            vec![
303                (rel_path(""), false),
304                (rel_path("deps"), false),
305                (rel_path("deps/dep-dir2"), true),
306                (rel_path("deps/dep-dir3"), true),
307                (rel_path("deps/dep-dir3/deps"), true),
308                (rel_path("deps/dep-dir3/src"), true),
309                (rel_path("deps/dep-dir3-alias"), true),
310                (rel_path("deps/dep-dir3-nested"), true),
311                (rel_path("src"), false),
312                (rel_path("src/a.rs"), false),
313                (rel_path("src/b.rs"), false),
314            ]
315        );
316    });
317    assert_eq!(
318        mem::take(&mut *tree_updates.lock()),
319        &[
320            (rel_path("deps/dep-dir3").into(), PathChange::Loaded),
321            (rel_path("deps/dep-dir3/deps").into(), PathChange::Loaded),
322            (rel_path("deps/dep-dir3/src").into(), PathChange::Loaded)
323        ]
324    );
325
326    // Expand a subdirectory of one of the symlinked directories.
327    tree.read_with(cx, |tree, _| {
328        tree.as_local()
329            .unwrap()
330            .refresh_entries_for_paths(vec![rel_path("deps/dep-dir3/src").into()])
331    })
332    .recv()
333    .await;
334
335    // The expanded subdirectory's contents are loaded.
336    tree.read_with(cx, |tree, _| {
337        assert_eq!(
338            tree.entries(true, 0)
339                .map(|entry| (entry.path.as_ref(), entry.is_external))
340                .collect::<Vec<_>>(),
341            vec![
342                (rel_path(""), false),
343                (rel_path("deps"), false),
344                (rel_path("deps/dep-dir2"), true),
345                (rel_path("deps/dep-dir3"), true),
346                (rel_path("deps/dep-dir3/deps"), true),
347                (rel_path("deps/dep-dir3/src"), true),
348                (rel_path("deps/dep-dir3/src/e.rs"), true),
349                (rel_path("deps/dep-dir3/src/f.rs"), true),
350                (rel_path("deps/dep-dir3/src/nested"), true),
351                (rel_path("deps/dep-dir3-alias"), true),
352                (rel_path("deps/dep-dir3-nested"), true),
353                (rel_path("src"), false),
354                (rel_path("src/a.rs"), false),
355                (rel_path("src/b.rs"), false),
356            ]
357        );
358    });
359
360    assert_eq!(
361        mem::take(&mut *tree_updates.lock()),
362        &[
363            (rel_path("deps/dep-dir3/src").into(), PathChange::Loaded),
364            (
365                rel_path("deps/dep-dir3/src/e.rs").into(),
366                PathChange::Loaded
367            ),
368            (
369                rel_path("deps/dep-dir3/src/f.rs").into(),
370                PathChange::Loaded
371            ),
372            (
373                rel_path("deps/dep-dir3/src/nested").into(),
374                PathChange::Loaded
375            )
376        ]
377    );
378
379    // After an external symlink subtree is loaded, changes in the target should be reflected.
380    fs.insert_file(Path::new("/root/dir3/src/new.rs"), b"".to_vec())
381        .await;
382
383    wait_for_condition(cx, |cx| {
384        tree.read_with(cx, |tree, _| {
385            tree.entry_for_path(rel_path("deps/dep-dir3/src/new.rs"))
386                .is_some()
387        })
388    })
389    .await;
390
391    tree.read_with(cx, |tree, _| {
392        assert!(
393            tree.entry_for_path(rel_path("deps/dep-dir3/src/new.rs"))
394                .is_some()
395        );
396    });
397
398    tree.read_with(cx, |tree, _| {
399        tree.as_local()
400            .unwrap()
401            .refresh_entries_for_paths(vec![rel_path("deps/dep-dir3-alias").into()])
402    })
403    .recv()
404    .await;
405
406    tree.read_with(cx, |tree, _| {
407        tree.as_local()
408            .unwrap()
409            .refresh_entries_for_paths(vec![rel_path("deps/dep-dir3-alias/src").into()])
410    })
411    .recv()
412    .await;
413
414    tree.read_with(cx, |tree, _| {
415        tree.as_local()
416            .unwrap()
417            .refresh_entries_for_paths(vec![rel_path("deps/dep-dir3-nested").into()])
418    })
419    .recv()
420    .await;
421    // Create a file in the shared target subtree. Because dep-dir3 and dep-dir3-alias both
422    // point to the same target, both logical paths should observe the new file.
423    fs.insert_file(Path::new("/root/dir3/src/shared-new.rs"), b"".to_vec())
424        .await;
425
426    wait_for_condition(cx, |cx| {
427        tree.read_with(cx, |tree, _| {
428            tree.entry_for_path(rel_path("deps/dep-dir3/src/shared-new.rs"))
429                .is_some()
430                && tree
431                    .entry_for_path(rel_path("deps/dep-dir3-alias/src/shared-new.rs"))
432                    .is_some()
433        })
434    })
435    .await;
436
437    tree.read_with(cx, |tree, _| {
438        assert!(
439            tree.entry_for_path(rel_path("deps/dep-dir3/src/shared-new.rs"))
440                .is_some()
441        );
442        assert!(
443            tree.entry_for_path(rel_path("deps/dep-dir3-alias/src/shared-new.rs"))
444                .is_some()
445        );
446    });
447
448    // Create a file under the more specific nested target. Longest-prefix matching means this should appear under dep-dir3-nested
449    fs.insert_file(
450        Path::new("/root/dir3/src/nested/longest-prefix.rs"),
451        b"".to_vec(),
452    )
453    .await;
454
455    wait_for_condition(cx, |cx| {
456        tree.read_with(cx, |tree, _| {
457            tree.entry_for_path(rel_path("deps/dep-dir3-nested/longest-prefix.rs"))
458                .is_some()
459        })
460    })
461    .await;
462
463    tree.read_with(cx, |tree, _| {
464        assert!(
465            tree.entry_for_path(rel_path("deps/dep-dir3-nested/longest-prefix.rs"))
466                .is_some()
467        );
468        assert!(
469            tree.entry_for_path(rel_path("deps/dep-dir3/src/nested/longest-prefix.rs"))
470                .is_none()
471        );
472        assert!(
473            tree.entry_for_path(rel_path("deps/dep-dir3-alias/src/nested/longest-prefix.rs"))
474                .is_none()
475        );
476    });
477}
478
479#[gpui::test]
480async fn test_renaming_subdir_under_symlinked_root_keeps_children(cx: &mut TestAppContext) {
481    init_test(cx);
482    let fs = FakeFs::new(cx.background_executor.clone());
483    fs.insert_tree(
484        "/target",
485        json!({
486            "file1.txt": "",
487            "file2.log": "",
488            "subdir-a": {
489                "config.ini": "",
490            },
491            "subdir-b": {
492                "nested": {
493                    "note.md": "",
494                },
495            },
496        }),
497    )
498    .await;
499    fs.create_symlink("/link".as_ref(), "/target".into())
500        .await
501        .unwrap();
502
503    let tree = Worktree::local(
504        Path::new("/link"),
505        true,
506        fs.clone(),
507        Default::default(),
508        true,
509        WorktreeId::from_proto(0),
510        &mut cx.to_async(),
511    )
512    .await
513    .unwrap();
514
515    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
516        .await;
517
518    fs.rename(
519        Path::new("/link/subdir-a"),
520        Path::new("/link/subdir-aa"),
521        Default::default(),
522    )
523    .await
524    .unwrap();
525
526    wait_for_condition(cx, |cx| {
527        tree.read_with(cx, |tree, _| {
528            tree.entry_for_path(rel_path("subdir-a")).is_none()
529                && tree
530                    .entry_for_path(rel_path("subdir-aa/config.ini"))
531                    .is_some()
532        })
533    })
534    .await;
535
536    tree.read_with(cx, |tree, _| {
537        assert_eq!(
538            tree.entries(true, 0)
539                .map(|entry| entry.path.as_ref())
540                .collect::<Vec<_>>(),
541            vec![
542                rel_path(""),
543                rel_path("file1.txt"),
544                rel_path("file2.log"),
545                rel_path("subdir-aa"),
546                rel_path("subdir-aa/config.ini"),
547                rel_path("subdir-b"),
548                rel_path("subdir-b/nested"),
549                rel_path("subdir-b/nested/note.md"),
550            ]
551        );
552    });
553}
554
555#[gpui::test]
556async fn test_symlinked_dir_inside_project(cx: &mut TestAppContext) {
557    init_test(cx);
558    let fs = FakeFs::new(cx.background_executor.clone());
559
560    fs.insert_tree(
561        "/root",
562        json!({
563            "project": {
564                "real-dir": {
565                    "existing.rs": "",
566                    "nested": {
567                        "deep.rs": ""
568                    }
569                },
570                "links": {}
571            }
572        }),
573    )
574    .await;
575
576    fs.create_symlink(
577        "/root/project/links/internal".as_ref(),
578        "../real-dir".into(),
579    )
580    .await
581    .unwrap();
582
583    let tree = Worktree::local(
584        Path::new("/root/project"),
585        true,
586        fs.clone(),
587        Default::default(),
588        true,
589        WorktreeId::from_proto(0),
590        &mut cx.to_async(),
591    )
592    .await
593    .unwrap();
594
595    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
596        .await;
597
598    tree.read_with(cx, |tree, _| {
599        assert_eq!(
600            tree.entries(true, 0)
601                .map(|entry| (entry.path.as_ref(), entry.is_external))
602                .collect::<Vec<_>>(),
603            vec![
604                (rel_path(""), false),
605                (rel_path("links"), false),
606                (rel_path("links/internal"), false),
607                (rel_path("links/internal/existing.rs"), false),
608                (rel_path("links/internal/nested"), false),
609                (rel_path("links/internal/nested/deep.rs"), false),
610                (rel_path("real-dir"), false),
611                (rel_path("real-dir/existing.rs"), false),
612                (rel_path("real-dir/nested"), false),
613                (rel_path("real-dir/nested/deep.rs"), false),
614            ]
615        );
616
617        assert_eq!(
618            tree.entry_for_path(rel_path("links/internal"))
619                .unwrap()
620                .kind,
621            EntryKind::Dir
622        );
623    });
624
625    fs.insert_file(Path::new("/root/project/real-dir/new.txt"), b"".to_vec())
626        .await;
627    wait_for_condition(cx, |cx| {
628        tree.read_with(cx, |tree, _| {
629            tree.entry_for_path(rel_path("links/internal/new.txt"))
630                .is_some()
631        })
632    })
633    .await;
634
635    tree.read_with(cx, |tree, _| {
636        assert!(
637            tree.entry_for_path(rel_path("links/internal/new.txt"))
638                .is_some()
639        );
640    });
641
642    fs.insert_file(
643        Path::new("/root/project/real-dir/nested/inner.txt"),
644        b"".to_vec(),
645    )
646    .await;
647    wait_for_condition(cx, |cx| {
648        tree.read_with(cx, |tree, _| {
649            tree.entry_for_path(rel_path("links/internal/nested/inner.txt"))
650                .is_some()
651        })
652    })
653    .await;
654
655    tree.read_with(cx, |tree, _| {
656        assert!(
657            tree.entry_for_path(rel_path("links/internal/nested/inner.txt"))
658                .is_some()
659        );
660    });
661}
662
663#[gpui::test]
664async fn test_scan_symlinks_always(cx: &mut TestAppContext) {
665    init_test(cx);
666
667    cx.update(|cx| {
668        cx.update_global::<SettingsStore, _>(|store, cx| {
669            store.update_user_settings(cx, |settings| {
670                settings.project.worktree.scan_symlinks =
671                    Some(settings::ScanSymlinksSetting::Always);
672            });
673        });
674    });
675
676    let fs = FakeFs::new(cx.background_executor.clone());
677    fs.insert_tree(
678        "/root",
679        json!({
680            "dir1": {
681                "deps": {
682                    // symlink target placed here by create_symlink below
683                },
684                "src": {
685                    "a.rs": "",
686                },
687            },
688            "dir2": {
689                "src": {
690                    "b.rs": "",
691                }
692            }
693        }),
694    )
695    .await;
696
697    fs.create_symlink("/root/dir1/deps/dep-dir2".as_ref(), "../../dir2".into())
698        .await
699        .unwrap();
700
701    let tree = Worktree::local(
702        Path::new("/root/dir1"),
703        true,
704        fs.clone(),
705        Default::default(),
706        true,
707        WorktreeId::from_proto(0),
708        &mut cx.to_async(),
709    )
710    .await
711    .unwrap();
712
713    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
714        .await;
715
716    // With scan_symlinks = Always, the symlinked directory's contents should be
717    // fully visible on the first scan without any manual expansion.
718    tree.read_with(cx, |tree, _| {
719        assert_eq!(
720            tree.entries(true, 0)
721                .map(|entry| (entry.path.as_ref(), entry.is_external))
722                .collect::<Vec<_>>(),
723            vec![
724                (rel_path(""), false),
725                (rel_path("deps"), false),
726                (rel_path("deps/dep-dir2"), true),
727                (rel_path("deps/dep-dir2/src"), true),
728                (rel_path("deps/dep-dir2/src/b.rs"), true),
729                (rel_path("src"), false),
730                (rel_path("src/a.rs"), false),
731            ]
732        );
733    });
734}
735
736#[gpui::test]
737async fn test_scan_symlinks_expanded(cx: &mut TestAppContext) {
738    init_test(cx);
739
740    // scan_symlinks defaults to Expanded — no settings change needed.
741
742    let fs = FakeFs::new(cx.background_executor.clone());
743    fs.insert_tree(
744        "/root",
745        json!({
746            "dir1": {
747                "deps": {
748                    // symlink target placed here by create_symlink below
749                },
750                "src": {
751                    "a.rs": "",
752                },
753            },
754            "dir2": {
755                "src": {
756                    "b.rs": "",
757                }
758            }
759        }),
760    )
761    .await;
762
763    fs.create_symlink("/root/dir1/deps/dep-dir2".as_ref(), "../../dir2".into())
764        .await
765        .unwrap();
766
767    let tree = Worktree::local(
768        Path::new("/root/dir1"),
769        true,
770        fs.clone(),
771        Default::default(),
772        true,
773        WorktreeId::from_proto(0),
774        &mut cx.to_async(),
775    )
776    .await
777    .unwrap();
778
779    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
780        .await;
781
782    // With the default scan_symlinks = Expanded, the symlinked directory
783    // should appear as an UnloadedDir entry with no children visible.
784    tree.read_with(cx, |tree, _| {
785        assert_eq!(
786            tree.entries(true, 0)
787                .map(|entry| (entry.path.as_ref(), entry.is_external))
788                .collect::<Vec<_>>(),
789            vec![
790                (rel_path(""), false),
791                (rel_path("deps"), false),
792                (rel_path("deps/dep-dir2"), true),
793                (rel_path("src"), false),
794                (rel_path("src/a.rs"), false),
795            ]
796        );
797
798        assert_eq!(
799            tree.entry_for_path(rel_path("deps/dep-dir2")).unwrap().kind,
800            EntryKind::UnloadedDir
801        );
802    });
803
804    // Manually expand the symlinked directory.
805    tree.read_with(cx, |tree, _| {
806        tree.as_local()
807            .unwrap()
808            .refresh_entries_for_paths(vec![rel_path("deps/dep-dir2").into()])
809    })
810    .recv()
811    .await;
812
813    // After expansion, dep-dir2's immediate children are visible. Subdirectories
814    // within it are present but not yet scanned.
815    tree.read_with(cx, |tree, _| {
816        assert_eq!(
817            tree.entries(true, 0)
818                .map(|entry| (entry.path.as_ref(), entry.is_external))
819                .collect::<Vec<_>>(),
820            vec![
821                (rel_path(""), false),
822                (rel_path("deps"), false),
823                (rel_path("deps/dep-dir2"), true),
824                (rel_path("deps/dep-dir2/src"), true),
825                (rel_path("src"), false),
826                (rel_path("src/a.rs"), false),
827            ]
828        );
829
830        assert_eq!(
831            tree.entry_for_path(rel_path("deps/dep-dir2/src"))
832                .unwrap()
833                .kind,
834            EntryKind::UnloadedDir
835        );
836    });
837
838    // Expand the subdirectory inside the symlinked directory.
839    tree.read_with(cx, |tree, _| {
840        tree.as_local()
841            .unwrap()
842            .refresh_entries_for_paths(vec![rel_path("deps/dep-dir2/src").into()])
843    })
844    .recv()
845    .await;
846
847    // After expanding the subdirectory, its files are visible.
848    tree.read_with(cx, |tree, _| {
849        assert_eq!(
850            tree.entries(true, 0)
851                .map(|entry| (entry.path.as_ref(), entry.is_external))
852                .collect::<Vec<_>>(),
853            vec![
854                (rel_path(""), false),
855                (rel_path("deps"), false),
856                (rel_path("deps/dep-dir2"), true),
857                (rel_path("deps/dep-dir2/src"), true),
858                (rel_path("deps/dep-dir2/src/b.rs"), true),
859                (rel_path("src"), false),
860                (rel_path("src/a.rs"), false),
861            ]
862        );
863    });
864}
865
866#[gpui::test(iterations = 10)]
867async fn test_circular_symlinks_always(cx: &mut TestAppContext) {
868    init_test(cx);
869
870    cx.update(|cx| {
871        cx.update_global::<SettingsStore, _>(|store, cx| {
872            store.update_user_settings(cx, |settings| {
873                settings.project.worktree.scan_symlinks =
874                    Some(settings::ScanSymlinksSetting::Always);
875            });
876        });
877    });
878
879    let fs = FakeFs::new(cx.background_executor.clone());
880    fs.insert_tree(
881        "/root",
882        json!({
883            "project": {
884                "lib": {
885                    "a": {
886                        "a.txt": ""
887                    }
888                },
889                "deps": {}
890            },
891            "outside": {
892                "data.txt": ""
893            }
894        }),
895    )
896    .await;
897
898    fs.create_symlink("/root/project/deps/ext".as_ref(), "../../outside".into())
899        .await
900        .unwrap();
901    fs.create_symlink("/root/outside/back".as_ref(), "../../project".into())
902        .await
903        .unwrap();
904
905    let tree = Worktree::local(
906        Path::new("/root/project"),
907        true,
908        fs.clone(),
909        Default::default(),
910        true,
911        WorktreeId::from_proto(0),
912        &mut cx.to_async(),
913    )
914    .await
915    .unwrap();
916
917    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
918        .await;
919
920    tree.read_with(cx, |tree, _| {
921        let entries: Vec<_> = tree
922            .entries(true, 0)
923            .map(|entry| (entry.path.as_ref(), entry.is_external))
924            .collect();
925
926        assert_eq!(
927            entries,
928            vec![
929                (rel_path(""), false),
930                (rel_path("deps"), false),
931                (rel_path("deps/ext"), true),
932                (rel_path("deps/ext/data.txt"), true),
933                (rel_path("lib"), false),
934                (rel_path("lib/a"), false),
935                (rel_path("lib/a/a.txt"), false),
936            ]
937        );
938    });
939}
940
941#[gpui::test]
942async fn test_scan_symlinks_always_respects_gitignore(cx: &mut TestAppContext) {
943    init_test(cx);
944
945    cx.update(|cx| {
946        cx.update_global::<SettingsStore, _>(|store, cx| {
947            store.update_user_settings(cx, |settings| {
948                settings.project.worktree.scan_symlinks =
949                    Some(settings::ScanSymlinksSetting::Always);
950            });
951        });
952    });
953
954    let fs = FakeFs::new(cx.background_executor.clone());
955    fs.insert_tree(
956        "/root",
957        json!({
958            "project": {
959                ".gitignore": "ignored-dep\n",
960                "deps": {}
961            },
962            "external-included": {
963                "src": {
964                    "included.rs": ""
965                }
966            },
967            "external-ignored": {
968                "src": {
969                    "ignored.rs": ""
970                }
971            }
972        }),
973    )
974    .await;
975
976    fs.create_symlink(
977        "/root/project/deps/included-dep".as_ref(),
978        "../../external-included".into(),
979    )
980    .await
981    .unwrap();
982    fs.create_symlink(
983        "/root/project/deps/ignored-dep".as_ref(),
984        "../../external-ignored".into(),
985    )
986    .await
987    .unwrap();
988
989    let tree = Worktree::local(
990        Path::new("/root/project"),
991        true,
992        fs.clone(),
993        Default::default(),
994        true,
995        WorktreeId::from_proto(0),
996        &mut cx.to_async(),
997    )
998    .await
999    .unwrap();
1000
1001    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1002        .await;
1003
1004    tree.read_with(cx, |tree, _| {
1005        assert_eq!(
1006            tree.entries(true, 0)
1007                .map(|entry| (entry.path.as_ref(), entry.is_external, entry.is_ignored))
1008                .collect::<Vec<_>>(),
1009            vec![
1010                (rel_path(""), false, false),
1011                (rel_path(".gitignore"), false, false),
1012                (rel_path("deps"), false, false),
1013                (rel_path("deps/ignored-dep"), true, true),
1014                (rel_path("deps/included-dep"), true, false),
1015                (rel_path("deps/included-dep/src"), true, false),
1016                (rel_path("deps/included-dep/src/included.rs"), true, false),
1017            ]
1018        );
1019
1020        assert_eq!(
1021            tree.entry_for_path(rel_path("deps/ignored-dep"))
1022                .unwrap()
1023                .kind,
1024            EntryKind::UnloadedDir
1025        );
1026    });
1027}
1028
1029// Real-fs counterparts to the FakeFs scan_symlinks tests above. FakeFs does not
1030// model `fs::canonicalize` against a real filesystem, so platform-specific
1031// canonicalization or readdir behavior is not covered by the FakeFs tests.
1032// These tests use a real temp directory and a real symlink to exercise the
1033// production path on the host platform.
1034#[cfg(unix)]
1035#[gpui::test]
1036async fn test_real_fs_scan_symlinks_always(cx: &mut TestAppContext) {
1037    cx.executor().allow_parking();
1038    init_test(cx);
1039
1040    cx.update(|cx| {
1041        cx.update_global::<SettingsStore, _>(|store, cx| {
1042            store.update_user_settings(cx, |settings| {
1043                settings.project.worktree.scan_symlinks =
1044                    Some(settings::ScanSymlinksSetting::Always);
1045            });
1046        });
1047    });
1048
1049    let temp_root = TempTree::new(json!({
1050        "project": {
1051            "deps": {},
1052            "src": {
1053                "a.rs": "",
1054            },
1055        },
1056        "external": {
1057            "src": {
1058                "b.rs": "",
1059            },
1060        },
1061    }));
1062
1063    // Relative symlink: from temp_root/project/deps/, `../../external` resolves
1064    // to temp_root/external — outside the worktree root at temp_root/project.
1065    std::os::unix::fs::symlink(
1066        "../../external",
1067        temp_root.path().join("project/deps/dep-external"),
1068    )
1069    .unwrap();
1070
1071    let project_root = temp_root.path().join("project");
1072    let tree = Worktree::local(
1073        project_root.as_path(),
1074        true,
1075        Arc::new(RealFs::new(None, cx.executor())),
1076        Default::default(),
1077        true,
1078        WorktreeId::from_proto(0),
1079        &mut cx.to_async(),
1080    )
1081    .await
1082    .unwrap();
1083
1084    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1085        .await;
1086
1087    tree.read_with(cx, |tree, _| {
1088        assert_eq!(
1089            tree.entries(true, 0)
1090                .map(|entry| (entry.path.as_ref(), entry.is_external))
1091                .collect::<Vec<_>>(),
1092            vec![
1093                (rel_path(""), false),
1094                (rel_path("deps"), false),
1095                (rel_path("deps/dep-external"), true),
1096                (rel_path("deps/dep-external/src"), true),
1097                (rel_path("deps/dep-external/src/b.rs"), true),
1098                (rel_path("src"), false),
1099                (rel_path("src/a.rs"), false),
1100            ]
1101        );
1102    });
1103}
1104
1105#[cfg(unix)]
1106#[gpui::test]
1107async fn test_real_fs_scan_symlinks_expanded(cx: &mut TestAppContext) {
1108    cx.executor().allow_parking();
1109    init_test(cx);
1110
1111    // scan_symlinks defaults to Expanded — no settings change needed.
1112
1113    let temp_root = TempTree::new(json!({
1114        "project": {
1115            "deps": {},
1116            "src": {
1117                "a.rs": "",
1118            },
1119        },
1120        "external": {
1121            "src": {
1122                "b.rs": "",
1123            },
1124        },
1125    }));
1126
1127    std::os::unix::fs::symlink(
1128        "../../external",
1129        temp_root.path().join("project/deps/dep-external"),
1130    )
1131    .unwrap();
1132
1133    let project_root = temp_root.path().join("project");
1134    let tree = Worktree::local(
1135        project_root.as_path(),
1136        true,
1137        Arc::new(RealFs::new(None, cx.executor())),
1138        Default::default(),
1139        true,
1140        WorktreeId::from_proto(0),
1141        &mut cx.to_async(),
1142    )
1143    .await
1144    .unwrap();
1145
1146    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1147        .await;
1148
1149    // Before expansion, the symlinked directory should appear as an UnloadedDir
1150    // with no children visible.
1151    tree.read_with(cx, |tree, _| {
1152        assert_eq!(
1153            tree.entries(true, 0)
1154                .map(|entry| (entry.path.as_ref(), entry.is_external))
1155                .collect::<Vec<_>>(),
1156            vec![
1157                (rel_path(""), false),
1158                (rel_path("deps"), false),
1159                (rel_path("deps/dep-external"), true),
1160                (rel_path("src"), false),
1161                (rel_path("src/a.rs"), false),
1162            ]
1163        );
1164
1165        assert_eq!(
1166            tree.entry_for_path(rel_path("deps/dep-external"))
1167                .unwrap()
1168                .kind,
1169            EntryKind::UnloadedDir
1170        );
1171    });
1172
1173    // Manually expand the symlinked directory. This is the case #51382 was
1174    // added to fix; if this assertion fails it's a regression of that fix on
1175    // real filesystems.
1176    tree.read_with(cx, |tree, _| {
1177        tree.as_local()
1178            .unwrap()
1179            .refresh_entries_for_paths(vec![rel_path("deps/dep-external").into()])
1180    })
1181    .recv()
1182    .await;
1183
1184    tree.read_with(cx, |tree, _| {
1185        assert_eq!(
1186            tree.entries(true, 0)
1187                .map(|entry| (entry.path.as_ref(), entry.is_external))
1188                .collect::<Vec<_>>(),
1189            vec![
1190                (rel_path(""), false),
1191                (rel_path("deps"), false),
1192                (rel_path("deps/dep-external"), true),
1193                (rel_path("deps/dep-external/src"), true),
1194                (rel_path("src"), false),
1195                (rel_path("src/a.rs"), false),
1196            ]
1197        );
1198    });
1199}
1200
1201#[gpui::test]
1202async fn test_internal_symlink_updates_preserve_entry_ids(cx: &mut TestAppContext) {
1203    init_test(cx);
1204    let fs = FakeFs::new(cx.background_executor.clone());
1205
1206    fs.insert_tree(
1207        "/root",
1208        json!({
1209            "project": {
1210                "real-dir": {
1211                    "existing.rs": "old",
1212                },
1213                "links": {}
1214            }
1215        }),
1216    )
1217    .await;
1218
1219    fs.create_symlink(
1220        "/root/project/links/internal".as_ref(),
1221        "../real-dir".into(),
1222    )
1223    .await
1224    .unwrap();
1225
1226    let tree = Worktree::local(
1227        Path::new("/root/project"),
1228        true,
1229        fs.clone(),
1230        Default::default(),
1231        true,
1232        WorktreeId::from_proto(0),
1233        &mut cx.to_async(),
1234    )
1235    .await
1236    .unwrap();
1237
1238    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1239        .await;
1240
1241    let (real_entry_id, symlink_entry_id, old_mtime) = tree.read_with(cx, |tree, _| {
1242        let real_entry = tree
1243            .entry_for_path(rel_path("real-dir/existing.rs"))
1244            .unwrap();
1245        let symlink_entry = tree
1246            .entry_for_path(rel_path("links/internal/existing.rs"))
1247            .unwrap();
1248        assert_eq!(real_entry.inode, symlink_entry.inode);
1249        assert_eq!(real_entry.mtime, symlink_entry.mtime);
1250        assert_ne!(real_entry.id, symlink_entry.id);
1251        (real_entry.id, symlink_entry.id, real_entry.mtime)
1252    });
1253
1254    fs.write(Path::new("/root/project/real-dir/existing.rs"), b"new")
1255        .await
1256        .unwrap();
1257
1258    wait_for_condition(cx, |cx| {
1259        tree.read_with(cx, |tree, _| {
1260            let real_entry = tree
1261                .entry_for_path(rel_path("real-dir/existing.rs"))
1262                .unwrap();
1263            let symlink_entry = tree
1264                .entry_for_path(rel_path("links/internal/existing.rs"))
1265                .unwrap();
1266            real_entry.mtime != old_mtime && symlink_entry.mtime != old_mtime
1267        })
1268    })
1269    .await;
1270
1271    tree.read_with(cx, |tree, _| {
1272        let real_entry = tree
1273            .entry_for_path(rel_path("real-dir/existing.rs"))
1274            .unwrap();
1275        let symlink_entry = tree
1276            .entry_for_path(rel_path("links/internal/existing.rs"))
1277            .unwrap();
1278
1279        assert_eq!(real_entry.inode, symlink_entry.inode);
1280        assert_eq!(real_entry.id, real_entry_id);
1281        assert_eq!(
1282            tree.entry_for_id(real_entry_id).unwrap().path.as_ref(),
1283            rel_path("real-dir/existing.rs")
1284        );
1285        assert_eq!(symlink_entry.id, symlink_entry_id);
1286        assert_eq!(
1287            tree.entry_for_id(symlink_entry_id).unwrap().path.as_ref(),
1288            rel_path("links/internal/existing.rs")
1289        );
1290    });
1291}
1292
1293#[cfg(target_os = "macos")]
1294#[gpui::test]
1295async fn test_renaming_case_only(cx: &mut TestAppContext) {
1296    cx.executor().allow_parking();
1297    init_test(cx);
1298
1299    const OLD_NAME: &str = "aaa.rs";
1300    const NEW_NAME: &str = "AAA.rs";
1301
1302    let fs = Arc::new(RealFs::new(None, cx.executor()));
1303    let temp_root = TempTree::new(json!({
1304        OLD_NAME: "",
1305    }));
1306
1307    let tree = Worktree::local(
1308        temp_root.path(),
1309        true,
1310        fs.clone(),
1311        Default::default(),
1312        true,
1313        WorktreeId::from_proto(0),
1314        &mut cx.to_async(),
1315    )
1316    .await
1317    .unwrap();
1318
1319    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1320        .await;
1321    tree.read_with(cx, |tree, _| {
1322        assert_eq!(
1323            tree.entries(true, 0)
1324                .map(|entry| entry.path.as_ref())
1325                .collect::<Vec<_>>(),
1326            vec![rel_path(""), rel_path(OLD_NAME)]
1327        );
1328    });
1329
1330    fs.rename(
1331        &temp_root.path().join(OLD_NAME),
1332        &temp_root.path().join(NEW_NAME),
1333        fs::RenameOptions {
1334            overwrite: true,
1335            ignore_if_exists: true,
1336            create_parents: false,
1337        },
1338    )
1339    .await
1340    .unwrap();
1341
1342    tree.flush_fs_events(cx).await;
1343
1344    tree.read_with(cx, |tree, _| {
1345        assert_eq!(
1346            tree.entries(true, 0)
1347                .map(|entry| entry.path.as_ref())
1348                .collect::<Vec<_>>(),
1349            vec![rel_path(""), rel_path(NEW_NAME)]
1350        );
1351    });
1352}
1353
1354#[gpui::test]
1355async fn test_root_rescan_reconciles_stale_state(cx: &mut TestAppContext) {
1356    init_test(cx);
1357    let fs = FakeFs::new(cx.background_executor.clone());
1358    fs.insert_tree(
1359        "/root",
1360        json!({
1361            "old.txt": "",
1362        }),
1363    )
1364    .await;
1365
1366    let tree = Worktree::local(
1367        Path::new("/root"),
1368        true,
1369        fs.clone(),
1370        Default::default(),
1371        true,
1372        WorktreeId::from_proto(0),
1373        &mut cx.to_async(),
1374    )
1375    .await
1376    .unwrap();
1377
1378    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1379        .await;
1380
1381    tree.read_with(cx, |tree, _| {
1382        assert_eq!(
1383            tree.entries(true, 0)
1384                .map(|entry| entry.path.as_ref())
1385                .collect::<Vec<_>>(),
1386            vec![rel_path(""), rel_path("old.txt")]
1387        );
1388    });
1389
1390    fs.pause_events();
1391    fs.remove_file(Path::new("/root/old.txt"), RemoveOptions::default())
1392        .await
1393        .unwrap();
1394    fs.insert_file(Path::new("/root/new.txt"), Vec::new()).await;
1395    assert_eq!(fs.buffered_event_count(), 2);
1396    fs.clear_buffered_events();
1397
1398    tree.read_with(cx, |tree, _| {
1399        assert!(tree.entry_for_path(rel_path("old.txt")).is_some());
1400        assert!(tree.entry_for_path(rel_path("new.txt")).is_none());
1401    });
1402
1403    fs.emit_fs_event("/root", Some(fs::PathEventKind::Rescan));
1404    fs.unpause_events_and_flush();
1405    tree.flush_fs_events(cx).await;
1406
1407    tree.read_with(cx, |tree, _| {
1408        assert!(tree.entry_for_path(rel_path("old.txt")).is_none());
1409        assert!(tree.entry_for_path(rel_path("new.txt")).is_some());
1410        assert_eq!(
1411            tree.entries(true, 0)
1412                .map(|entry| entry.path.as_ref())
1413                .collect::<Vec<_>>(),
1414            vec![rel_path(""), rel_path("new.txt")]
1415        );
1416    });
1417}
1418
1419#[gpui::test]
1420async fn test_root_rescan_does_not_miss_event_before_readding_root_watcher(
1421    cx: &mut TestAppContext,
1422) {
1423    init_test(cx);
1424    let fs = FakeFs::new(cx.background_executor.clone());
1425    fs.insert_tree("/root", json!({})).await;
1426
1427    let tree = Worktree::local(
1428        Path::new("/root"),
1429        true,
1430        fs.clone(),
1431        Default::default(),
1432        true,
1433        WorktreeId::from_proto(0),
1434        &mut cx.to_async(),
1435    )
1436    .await
1437    .unwrap();
1438
1439    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1440        .await;
1441
1442    fs.create_file_before_next_watch_add("/root", "/root/created-before-root-readd.txt");
1443    fs.emit_fs_event("/root", Some(PathEventKind::Rescan));
1444
1445    wait_for_condition(cx, |cx| {
1446        tree.read_with(cx, |tree, _| {
1447            tree.entry_for_path(rel_path("created-before-root-readd.txt"))
1448                .is_some()
1449        })
1450    })
1451    .await;
1452}
1453
1454#[gpui::test]
1455async fn test_subtree_rescan_reports_unchanged_descendants_as_updated(cx: &mut TestAppContext) {
1456    init_test(cx);
1457    let fs = FakeFs::new(cx.background_executor.clone());
1458    fs.insert_tree(
1459        "/root",
1460        json!({
1461            "dir": {
1462                "child.txt": "",
1463                "nested": {
1464                    "grandchild.txt": "",
1465                },
1466                "remove": {
1467                    "removed.txt": "",
1468                }
1469            },
1470            "other.txt": "",
1471        }),
1472    )
1473    .await;
1474
1475    let tree = Worktree::local(
1476        Path::new("/root"),
1477        true,
1478        fs.clone(),
1479        Default::default(),
1480        true,
1481        WorktreeId::from_proto(0),
1482        &mut cx.to_async(),
1483    )
1484    .await
1485    .unwrap();
1486
1487    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1488        .await;
1489
1490    let tree_updates = Arc::new(Mutex::new(Vec::new()));
1491    tree.update(cx, |_, cx| {
1492        let tree_updates = tree_updates.clone();
1493        cx.subscribe(&tree, move |_, _, event, _| {
1494            if let Event::UpdatedEntries(update) = event {
1495                tree_updates.lock().extend(
1496                    update
1497                        .iter()
1498                        .filter(|(path, _, _)| path.as_ref() != rel_path("fs-event-sentinel"))
1499                        .map(|(path, _, change)| (path.clone(), *change)),
1500                );
1501            }
1502        })
1503        .detach();
1504    });
1505    fs.pause_events();
1506    fs.insert_file("/root/dir/new.txt", b"new content".to_vec())
1507        .await;
1508    fs.remove_dir(
1509        "/root/dir/remove".as_ref(),
1510        RemoveOptions {
1511            recursive: true,
1512            ignore_if_not_exists: false,
1513        },
1514    )
1515    .await
1516    .unwrap();
1517    fs.clear_buffered_events();
1518    fs.unpause_events_and_flush();
1519
1520    fs.emit_fs_event("/root/dir", Some(fs::PathEventKind::Rescan));
1521    tree.flush_fs_events(cx).await;
1522
1523    assert_eq!(
1524        mem::take(&mut *tree_updates.lock()),
1525        &[
1526            (rel_path("dir").into(), PathChange::Updated),
1527            (rel_path("dir/child.txt").into(), PathChange::Updated),
1528            (rel_path("dir/nested").into(), PathChange::Updated),
1529            (
1530                rel_path("dir/nested/grandchild.txt").into(),
1531                PathChange::Updated
1532            ),
1533            (rel_path("dir/new.txt").into(), PathChange::Added),
1534            (rel_path("dir/remove").into(), PathChange::Removed),
1535            (
1536                rel_path("dir/remove/removed.txt").into(),
1537                PathChange::Removed
1538            ),
1539        ]
1540    );
1541
1542    tree.read_with(cx, |tree, _| {
1543        assert!(tree.entry_for_path(rel_path("other.txt")).is_some());
1544    });
1545}
1546
1547#[gpui::test]
1548async fn test_open_gitignored_files(cx: &mut TestAppContext) {
1549    init_test(cx);
1550    let fs = FakeFs::new(cx.background_executor.clone());
1551    fs.insert_tree(
1552        "/root",
1553        json!({
1554            ".gitignore": "node_modules\n",
1555            "one": {
1556                "node_modules": {
1557                    "a": {
1558                        "a1.js": "a1",
1559                        "a2.js": "a2",
1560                    },
1561                    "b": {
1562                        "b1.js": "b1",
1563                        "b2.js": "b2",
1564                    },
1565                    "c": {
1566                        "c1.js": "c1",
1567                        "c2.js": "c2",
1568                    }
1569                },
1570            },
1571            "two": {
1572                "x.js": "",
1573                "y.js": "",
1574            },
1575        }),
1576    )
1577    .await;
1578
1579    let tree = Worktree::local(
1580        Path::new("/root"),
1581        true,
1582        fs.clone(),
1583        Default::default(),
1584        true,
1585        WorktreeId::from_proto(0),
1586        &mut cx.to_async(),
1587    )
1588    .await
1589    .unwrap();
1590
1591    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1592        .await;
1593
1594    tree.read_with(cx, |tree, _| {
1595        assert_eq!(
1596            tree.entries(true, 0)
1597                .map(|entry| (entry.path.as_ref(), entry.is_ignored))
1598                .collect::<Vec<_>>(),
1599            vec![
1600                (rel_path(""), false),
1601                (rel_path(".gitignore"), false),
1602                (rel_path("one"), false),
1603                (rel_path("one/node_modules"), true),
1604                (rel_path("two"), false),
1605                (rel_path("two/x.js"), false),
1606                (rel_path("two/y.js"), false),
1607            ]
1608        );
1609    });
1610
1611    // Open a file that is nested inside of a gitignored directory that
1612    // has not yet been expanded.
1613    let prev_read_dir_count = fs.read_dir_call_count();
1614    let loaded = tree
1615        .update(cx, |tree, cx| {
1616            tree.load_file(rel_path("one/node_modules/b/b1.js"), cx)
1617        })
1618        .await
1619        .unwrap();
1620
1621    tree.read_with(cx, |tree, _| {
1622        assert_eq!(
1623            tree.entries(true, 0)
1624                .map(|entry| (entry.path.as_ref(), entry.is_ignored))
1625                .collect::<Vec<_>>(),
1626            vec![
1627                (rel_path(""), false),
1628                (rel_path(".gitignore"), false),
1629                (rel_path("one"), false),
1630                (rel_path("one/node_modules"), true),
1631                (rel_path("one/node_modules/a"), true),
1632                (rel_path("one/node_modules/b"), true),
1633                (rel_path("one/node_modules/b/b1.js"), true),
1634                (rel_path("one/node_modules/b/b2.js"), true),
1635                (rel_path("one/node_modules/c"), true),
1636                (rel_path("two"), false),
1637                (rel_path("two/x.js"), false),
1638                (rel_path("two/y.js"), false),
1639            ]
1640        );
1641
1642        assert_eq!(
1643            loaded.file.path.as_ref(),
1644            rel_path("one/node_modules/b/b1.js")
1645        );
1646
1647        // Only the newly-expanded directories are scanned.
1648        assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 2);
1649    });
1650
1651    // Open another file in a different subdirectory of the same
1652    // gitignored directory.
1653    let prev_read_dir_count = fs.read_dir_call_count();
1654    let loaded = tree
1655        .update(cx, |tree, cx| {
1656            tree.load_file(rel_path("one/node_modules/a/a2.js"), cx)
1657        })
1658        .await
1659        .unwrap();
1660
1661    tree.read_with(cx, |tree, _| {
1662        assert_eq!(
1663            tree.entries(true, 0)
1664                .map(|entry| (entry.path.as_ref(), entry.is_ignored))
1665                .collect::<Vec<_>>(),
1666            vec![
1667                (rel_path(""), false),
1668                (rel_path(".gitignore"), false),
1669                (rel_path("one"), false),
1670                (rel_path("one/node_modules"), true),
1671                (rel_path("one/node_modules/a"), true),
1672                (rel_path("one/node_modules/a/a1.js"), true),
1673                (rel_path("one/node_modules/a/a2.js"), true),
1674                (rel_path("one/node_modules/b"), true),
1675                (rel_path("one/node_modules/b/b1.js"), true),
1676                (rel_path("one/node_modules/b/b2.js"), true),
1677                (rel_path("one/node_modules/c"), true),
1678                (rel_path("two"), false),
1679                (rel_path("two/x.js"), false),
1680                (rel_path("two/y.js"), false),
1681            ]
1682        );
1683
1684        assert_eq!(
1685            loaded.file.path.as_ref(),
1686            rel_path("one/node_modules/a/a2.js")
1687        );
1688
1689        // Only the newly-expanded directory is scanned.
1690        assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 1);
1691    });
1692
1693    let path = PathBuf::from("/root/one/node_modules/c/lib");
1694
1695    // No work happens when files and directories change within an unloaded directory.
1696    let prev_fs_call_count = fs.read_dir_call_count() + fs.metadata_call_count();
1697    // When we open a directory, we check each ancestor whether it's a git
1698    // repository. That means we have an fs.metadata call per ancestor that we
1699    // need to subtract here.
1700    let ancestors = path.ancestors().count();
1701
1702    fs.create_dir(path.as_ref()).await.unwrap();
1703    cx.executor().run_until_parked();
1704
1705    assert_eq!(
1706        fs.read_dir_call_count() + fs.metadata_call_count() - prev_fs_call_count - ancestors,
1707        0
1708    );
1709}
1710
1711#[gpui::test]
1712async fn test_dirs_no_longer_ignored(cx: &mut TestAppContext) {
1713    init_test(cx);
1714    let fs = FakeFs::new(cx.background_executor.clone());
1715    fs.insert_tree(
1716        "/root",
1717        json!({
1718            ".gitignore": "node_modules\n",
1719            "a": {
1720                "a.js": "",
1721            },
1722            "b": {
1723                "b.js": "",
1724            },
1725            "node_modules": {
1726                "c": {
1727                    "c.js": "",
1728                },
1729                "d": {
1730                    "d.js": "",
1731                    "e": {
1732                        "e1.js": "",
1733                        "e2.js": "",
1734                    },
1735                    "f": {
1736                        "f1.js": "",
1737                        "f2.js": "",
1738                    }
1739                },
1740            },
1741        }),
1742    )
1743    .await;
1744
1745    let tree = Worktree::local(
1746        Path::new("/root"),
1747        true,
1748        fs.clone(),
1749        Default::default(),
1750        true,
1751        WorktreeId::from_proto(0),
1752        &mut cx.to_async(),
1753    )
1754    .await
1755    .unwrap();
1756
1757    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1758        .await;
1759
1760    // Open a file within the gitignored directory, forcing some of its
1761    // subdirectories to be read, but not all.
1762    let read_dir_count_1 = fs.read_dir_call_count();
1763    tree.read_with(cx, |tree, _| {
1764        tree.as_local()
1765            .unwrap()
1766            .refresh_entries_for_paths(vec![rel_path("node_modules/d/d.js").into()])
1767    })
1768    .recv()
1769    .await;
1770
1771    // Those subdirectories are now loaded.
1772    tree.read_with(cx, |tree, _| {
1773        assert_eq!(
1774            tree.entries(true, 0)
1775                .map(|e| (e.path.as_ref(), e.is_ignored))
1776                .collect::<Vec<_>>(),
1777            &[
1778                (rel_path(""), false),
1779                (rel_path(".gitignore"), false),
1780                (rel_path("a"), false),
1781                (rel_path("a/a.js"), false),
1782                (rel_path("b"), false),
1783                (rel_path("b/b.js"), false),
1784                (rel_path("node_modules"), true),
1785                (rel_path("node_modules/c"), true),
1786                (rel_path("node_modules/d"), true),
1787                (rel_path("node_modules/d/d.js"), true),
1788                (rel_path("node_modules/d/e"), true),
1789                (rel_path("node_modules/d/f"), true),
1790            ]
1791        );
1792    });
1793    let read_dir_count_2 = fs.read_dir_call_count();
1794    assert_eq!(read_dir_count_2 - read_dir_count_1, 2);
1795
1796    // Update the gitignore so that node_modules is no longer ignored,
1797    // but a subdirectory is ignored
1798    fs.save("/root/.gitignore".as_ref(), &"e".into(), Default::default())
1799        .await
1800        .unwrap();
1801    cx.executor().run_until_parked();
1802
1803    // All of the directories that are no longer ignored are now loaded.
1804    tree.read_with(cx, |tree, _| {
1805        assert_eq!(
1806            tree.entries(true, 0)
1807                .map(|e| (e.path.as_ref(), e.is_ignored))
1808                .collect::<Vec<_>>(),
1809            &[
1810                (rel_path(""), false),
1811                (rel_path(".gitignore"), false),
1812                (rel_path("a"), false),
1813                (rel_path("a/a.js"), false),
1814                (rel_path("b"), false),
1815                (rel_path("b/b.js"), false),
1816                // This directory is no longer ignored
1817                (rel_path("node_modules"), false),
1818                (rel_path("node_modules/c"), false),
1819                (rel_path("node_modules/c/c.js"), false),
1820                (rel_path("node_modules/d"), false),
1821                (rel_path("node_modules/d/d.js"), false),
1822                // This subdirectory is now ignored
1823                (rel_path("node_modules/d/e"), true),
1824                (rel_path("node_modules/d/f"), false),
1825                (rel_path("node_modules/d/f/f1.js"), false),
1826                (rel_path("node_modules/d/f/f2.js"), false),
1827            ]
1828        );
1829    });
1830
1831    // Each of the newly-loaded directories is scanned only once.
1832    let read_dir_count_3 = fs.read_dir_call_count();
1833    assert_eq!(read_dir_count_3 - read_dir_count_2, 2);
1834}
1835
1836#[gpui::test]
1837async fn test_write_file(cx: &mut TestAppContext) {
1838    init_test(cx);
1839    cx.executor().allow_parking();
1840    let dir = TempTree::new(json!({
1841        ".git": {},
1842        ".gitignore": "ignored-dir\n",
1843        "tracked-dir": {},
1844        "ignored-dir": {}
1845    }));
1846
1847    let worktree = Worktree::local(
1848        dir.path(),
1849        true,
1850        Arc::new(RealFs::new(None, cx.executor())),
1851        Default::default(),
1852        true,
1853        WorktreeId::from_proto(0),
1854        &mut cx.to_async(),
1855    )
1856    .await
1857    .unwrap();
1858
1859    #[cfg(not(target_os = "macos"))]
1860    fs::fs_watcher::global(|_| {}).unwrap();
1861
1862    cx.read(|cx| worktree.read(cx).as_local().unwrap().scan_complete())
1863        .await;
1864    worktree.flush_fs_events(cx).await;
1865
1866    worktree
1867        .update(cx, |tree, cx| {
1868            tree.write_file(
1869                rel_path("tracked-dir/file.txt").into(),
1870                "hello".into(),
1871                Default::default(),
1872                encoding_rs::UTF_8,
1873                false,
1874                cx,
1875            )
1876        })
1877        .await
1878        .unwrap();
1879    worktree
1880        .update(cx, |tree, cx| {
1881            tree.write_file(
1882                rel_path("ignored-dir/file.txt").into(),
1883                "world".into(),
1884                Default::default(),
1885                encoding_rs::UTF_8,
1886                false,
1887                cx,
1888            )
1889        })
1890        .await
1891        .unwrap();
1892    worktree.read_with(cx, |tree, _| {
1893        let tracked = tree
1894            .entry_for_path(rel_path("tracked-dir/file.txt"))
1895            .unwrap();
1896        let ignored = tree
1897            .entry_for_path(rel_path("ignored-dir/file.txt"))
1898            .unwrap();
1899        assert!(!tracked.is_ignored);
1900        assert!(ignored.is_ignored);
1901    });
1902}
1903
1904#[gpui::test]
1905async fn test_file_scan_inclusions(cx: &mut TestAppContext) {
1906    init_test(cx);
1907    cx.executor().allow_parking();
1908    let dir = TempTree::new(json!({
1909        ".gitignore": "**/target\n/node_modules\ntop_level.txt\n",
1910        "target": {
1911            "index": "blah2"
1912        },
1913        "node_modules": {
1914            ".DS_Store": "",
1915            "prettier": {
1916                "package.json": "{}",
1917            },
1918            "package.json": "//package.json"
1919        },
1920        "src": {
1921            ".DS_Store": "",
1922            "foo": {
1923                "foo.rs": "mod another;\n",
1924                "another.rs": "// another",
1925            },
1926            "bar": {
1927                "bar.rs": "// bar",
1928            },
1929            "lib.rs": "mod foo;\nmod bar;\n",
1930        },
1931        "top_level.txt": "top level file",
1932        ".DS_Store": "",
1933    }));
1934    cx.update(|cx| {
1935        cx.update_global::<SettingsStore, _>(|store, cx| {
1936            store.update_user_settings(cx, |settings| {
1937                settings.project.worktree.file_scan_exclusions = Some(vec![]);
1938                settings.project.worktree.file_scan_inclusions = Some(vec![
1939                    "node_modules/**/package.json".to_string(),
1940                    "**/.DS_Store".to_string(),
1941                ]);
1942            });
1943        });
1944    });
1945
1946    let tree = Worktree::local(
1947        dir.path(),
1948        true,
1949        Arc::new(RealFs::new(None, cx.executor())),
1950        Default::default(),
1951        true,
1952        WorktreeId::from_proto(0),
1953        &mut cx.to_async(),
1954    )
1955    .await
1956    .unwrap();
1957    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1958        .await;
1959    tree.flush_fs_events(cx).await;
1960    tree.read_with(cx, |tree, _| {
1961        // Assert that file_scan_inclusions overrides  file_scan_exclusions.
1962        check_worktree_entries(
1963            tree,
1964            WorktreeExpectations {
1965                excluded_paths: &[],
1966                ignored_paths: &["target", "node_modules"],
1967                tracked_paths: &["src/lib.rs", "src/bar/bar.rs", ".gitignore"],
1968                included_paths: &[
1969                    "node_modules/prettier/package.json",
1970                    ".DS_Store",
1971                    "node_modules/.DS_Store",
1972                    "src/.DS_Store",
1973                ],
1974            },
1975        )
1976    });
1977}
1978
1979#[gpui::test]
1980async fn test_file_scan_exclusions_overrules_inclusions(cx: &mut TestAppContext) {
1981    init_test(cx);
1982    cx.executor().allow_parking();
1983    let dir = TempTree::new(json!({
1984        ".gitignore": "**/target\n/node_modules\n",
1985        "target": {
1986            "index": "blah2"
1987        },
1988        "node_modules": {
1989            ".DS_Store": "",
1990            "prettier": {
1991                "package.json": "{}",
1992            },
1993        },
1994        "src": {
1995            ".DS_Store": "",
1996            "foo": {
1997                "foo.rs": "mod another;\n",
1998                "another.rs": "// another",
1999            },
2000        },
2001        ".DS_Store": "",
2002    }));
2003
2004    cx.update(|cx| {
2005        cx.update_global::<SettingsStore, _>(|store, cx| {
2006            store.update_user_settings(cx, |settings| {
2007                settings.project.worktree.file_scan_exclusions =
2008                    Some(vec!["**/.DS_Store".to_string()]);
2009                settings.project.worktree.file_scan_inclusions =
2010                    Some(vec!["**/.DS_Store".to_string()]);
2011            });
2012        });
2013    });
2014
2015    let tree = Worktree::local(
2016        dir.path(),
2017        true,
2018        Arc::new(RealFs::new(None, cx.executor())),
2019        Default::default(),
2020        true,
2021        WorktreeId::from_proto(0),
2022        &mut cx.to_async(),
2023    )
2024    .await
2025    .unwrap();
2026    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2027        .await;
2028    tree.flush_fs_events(cx).await;
2029    tree.read_with(cx, |tree, _| {
2030        // Assert that file_scan_inclusions overrides  file_scan_exclusions.
2031        check_worktree_entries(
2032            tree,
2033            WorktreeExpectations {
2034                excluded_paths: &[".DS_Store, src/.DS_Store"],
2035                ignored_paths: &["target", "node_modules"],
2036                tracked_paths: &["src/foo/another.rs", "src/foo/foo.rs", ".gitignore"],
2037                included_paths: &[],
2038            },
2039        )
2040    });
2041}
2042
2043#[gpui::test]
2044async fn test_file_scan_inclusions_reindexes_on_setting_change(cx: &mut TestAppContext) {
2045    init_test(cx);
2046    cx.executor().allow_parking();
2047    let dir = TempTree::new(json!({
2048        ".gitignore": "**/target\n/node_modules/\n",
2049        "target": {
2050            "index": "blah2"
2051        },
2052        "node_modules": {
2053            ".DS_Store": "",
2054            "prettier": {
2055                "package.json": "{}",
2056            },
2057        },
2058        "src": {
2059            ".DS_Store": "",
2060            "foo": {
2061                "foo.rs": "mod another;\n",
2062                "another.rs": "// another",
2063            },
2064        },
2065        ".DS_Store": "",
2066    }));
2067
2068    cx.update(|cx| {
2069        cx.update_global::<SettingsStore, _>(|store, cx| {
2070            store.update_user_settings(cx, |settings| {
2071                settings.project.worktree.file_scan_exclusions = Some(vec![]);
2072                settings.project.worktree.file_scan_inclusions =
2073                    Some(vec!["node_modules/**".to_string()]);
2074            });
2075        });
2076    });
2077    let tree = Worktree::local(
2078        dir.path(),
2079        true,
2080        Arc::new(RealFs::new(None, cx.executor())),
2081        Default::default(),
2082        true,
2083        WorktreeId::from_proto(0),
2084        &mut cx.to_async(),
2085    )
2086    .await
2087    .unwrap();
2088    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2089        .await;
2090    tree.flush_fs_events(cx).await;
2091
2092    tree.read_with(cx, |tree, _| {
2093        assert!(
2094            tree.entry_for_path(rel_path("node_modules"))
2095                .is_some_and(|f| f.is_always_included)
2096        );
2097        assert!(
2098            tree.entry_for_path(rel_path("node_modules/prettier/package.json"))
2099                .is_some_and(|f| f.is_always_included)
2100        );
2101    });
2102
2103    cx.update(|cx| {
2104        cx.update_global::<SettingsStore, _>(|store, cx| {
2105            store.update_user_settings(cx, |settings| {
2106                settings.project.worktree.file_scan_exclusions = Some(vec![]);
2107                settings.project.worktree.file_scan_inclusions = Some(vec![]);
2108            });
2109        });
2110    });
2111    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2112        .await;
2113    tree.flush_fs_events(cx).await;
2114
2115    tree.read_with(cx, |tree, _| {
2116        assert!(
2117            tree.entry_for_path(rel_path("node_modules"))
2118                .is_some_and(|f| !f.is_always_included)
2119        );
2120        assert!(
2121            tree.entry_for_path(rel_path("node_modules/prettier/package.json"))
2122                .is_some_and(|f| !f.is_always_included)
2123        );
2124    });
2125}
2126
2127#[gpui::test]
2128async fn test_file_scan_exclusions(cx: &mut TestAppContext) {
2129    init_test(cx);
2130    cx.executor().allow_parking();
2131    let dir = TempTree::new(json!({
2132        ".gitignore": "**/target\n/node_modules\n",
2133        "target": {
2134            "index": "blah2"
2135        },
2136        "node_modules": {
2137            ".DS_Store": "",
2138            "prettier": {
2139                "package.json": "{}",
2140            },
2141        },
2142        "src": {
2143            ".DS_Store": "",
2144            "foo": {
2145                "foo.rs": "mod another;\n",
2146                "another.rs": "// another",
2147            },
2148            "bar": {
2149                "bar.rs": "// bar",
2150            },
2151            "lib.rs": "mod foo;\nmod bar;\n",
2152        },
2153        ".DS_Store": "",
2154    }));
2155    cx.update(|cx| {
2156        cx.update_global::<SettingsStore, _>(|store, cx| {
2157            store.update_user_settings(cx, |settings| {
2158                settings.project.worktree.file_scan_exclusions =
2159                    Some(vec!["**/foo/**".to_string(), "**/.DS_Store".to_string()]);
2160            });
2161        });
2162    });
2163
2164    let tree = Worktree::local(
2165        dir.path(),
2166        true,
2167        Arc::new(RealFs::new(None, cx.executor())),
2168        Default::default(),
2169        true,
2170        WorktreeId::from_proto(0),
2171        &mut cx.to_async(),
2172    )
2173    .await
2174    .unwrap();
2175    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2176        .await;
2177    tree.flush_fs_events(cx).await;
2178    tree.read_with(cx, |tree, _| {
2179        check_worktree_entries(
2180            tree,
2181            WorktreeExpectations {
2182                excluded_paths: &[
2183                    "src/foo/foo.rs",
2184                    "src/foo/another.rs",
2185                    "node_modules/.DS_Store",
2186                    "src/.DS_Store",
2187                    ".DS_Store",
2188                ],
2189                ignored_paths: &["target", "node_modules"],
2190                tracked_paths: &["src/lib.rs", "src/bar/bar.rs", ".gitignore"],
2191                included_paths: &[],
2192            },
2193        )
2194    });
2195
2196    cx.update(|cx| {
2197        cx.update_global::<SettingsStore, _>(|store, cx| {
2198            store.update_user_settings(cx, |settings| {
2199                settings.project.worktree.file_scan_exclusions =
2200                    Some(vec!["**/node_modules/**".to_string()]);
2201            });
2202        });
2203    });
2204    tree.flush_fs_events(cx).await;
2205    cx.executor().run_until_parked();
2206    tree.read_with(cx, |tree, _| {
2207        check_worktree_entries(
2208            tree,
2209            WorktreeExpectations {
2210                excluded_paths: &[
2211                    "node_modules/prettier/package.json",
2212                    "node_modules/.DS_Store",
2213                    "node_modules",
2214                ],
2215                ignored_paths: &["target"],
2216                tracked_paths: &[
2217                    ".gitignore",
2218                    "src/lib.rs",
2219                    "src/bar/bar.rs",
2220                    "src/foo/foo.rs",
2221                    "src/foo/another.rs",
2222                    "src/.DS_Store",
2223                    ".DS_Store",
2224                ],
2225                included_paths: &[],
2226            },
2227        )
2228    });
2229}
2230
2231#[gpui::test]
2232async fn test_hidden_files(cx: &mut TestAppContext) {
2233    init_test(cx);
2234    cx.executor().allow_parking();
2235    let dir = TempTree::new(json!({
2236        ".gitignore": "**/target\n",
2237        ".hidden_file": "content",
2238        ".hidden_dir": {
2239            "nested.rs": "code",
2240        },
2241        "src": {
2242            "visible.rs": "code",
2243        },
2244        "logs": {
2245            "app.log": "logs",
2246            "debug.log": "logs",
2247        },
2248        "visible.txt": "content",
2249    }));
2250
2251    let tree = Worktree::local(
2252        dir.path(),
2253        true,
2254        Arc::new(RealFs::new(None, cx.executor())),
2255        Default::default(),
2256        true,
2257        WorktreeId::from_proto(0),
2258        &mut cx.to_async(),
2259    )
2260    .await
2261    .unwrap();
2262    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2263        .await;
2264    tree.flush_fs_events(cx).await;
2265
2266    tree.read_with(cx, |tree, _| {
2267        assert_eq!(
2268            tree.entries(true, 0)
2269                .map(|entry| (entry.path.as_ref(), entry.is_hidden))
2270                .collect::<Vec<_>>(),
2271            vec![
2272                (rel_path(""), false),
2273                (rel_path(".gitignore"), true),
2274                (rel_path(".hidden_dir"), true),
2275                (rel_path(".hidden_dir/nested.rs"), true),
2276                (rel_path(".hidden_file"), true),
2277                (rel_path("logs"), false),
2278                (rel_path("logs/app.log"), false),
2279                (rel_path("logs/debug.log"), false),
2280                (rel_path("src"), false),
2281                (rel_path("src/visible.rs"), false),
2282                (rel_path("visible.txt"), false),
2283            ]
2284        );
2285    });
2286
2287    cx.update(|cx| {
2288        cx.update_global::<SettingsStore, _>(|store, cx| {
2289            store.update_user_settings(cx, |settings| {
2290                settings.project.worktree.hidden_files = Some(vec!["**/*.log".to_string()]);
2291            });
2292        });
2293    });
2294    tree.flush_fs_events(cx).await;
2295    cx.executor().run_until_parked();
2296
2297    tree.read_with(cx, |tree, _| {
2298        assert_eq!(
2299            tree.entries(true, 0)
2300                .map(|entry| (entry.path.as_ref(), entry.is_hidden))
2301                .collect::<Vec<_>>(),
2302            vec![
2303                (rel_path(""), false),
2304                (rel_path(".gitignore"), false),
2305                (rel_path(".hidden_dir"), false),
2306                (rel_path(".hidden_dir/nested.rs"), false),
2307                (rel_path(".hidden_file"), false),
2308                (rel_path("logs"), false),
2309                (rel_path("logs/app.log"), true),
2310                (rel_path("logs/debug.log"), true),
2311                (rel_path("src"), false),
2312                (rel_path("src/visible.rs"), false),
2313                (rel_path("visible.txt"), false),
2314            ]
2315        );
2316    });
2317}
2318
2319#[gpui::test]
2320async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) {
2321    init_test(cx);
2322    cx.executor().allow_parking();
2323    let dir = TempTree::new(json!({
2324        ".git": {
2325            "HEAD": "ref: refs/heads/main\n",
2326            "foo": "bar",
2327        },
2328        ".gitignore": "**/target\n/node_modules\ntest_output\n",
2329        "target": {
2330            "index": "blah2"
2331        },
2332        "node_modules": {
2333            ".DS_Store": "",
2334            "prettier": {
2335                "package.json": "{}",
2336            },
2337        },
2338        "src": {
2339            ".DS_Store": "",
2340            "foo": {
2341                "foo.rs": "mod another;\n",
2342                "another.rs": "// another",
2343            },
2344            "bar": {
2345                "bar.rs": "// bar",
2346            },
2347            "lib.rs": "mod foo;\nmod bar;\n",
2348        },
2349        ".DS_Store": "",
2350    }));
2351    cx.update(|cx| {
2352        cx.update_global::<SettingsStore, _>(|store, cx| {
2353            store.update_user_settings(cx, |settings| {
2354                settings.project.worktree.file_scan_exclusions = Some(vec![
2355                    "**/.git".to_string(),
2356                    "node_modules/".to_string(),
2357                    "build_output".to_string(),
2358                ]);
2359            });
2360        });
2361    });
2362
2363    let tree = Worktree::local(
2364        dir.path(),
2365        true,
2366        Arc::new(RealFs::new(None, cx.executor())),
2367        Default::default(),
2368        true,
2369        WorktreeId::from_proto(0),
2370        &mut cx.to_async(),
2371    )
2372    .await
2373    .unwrap();
2374    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2375        .await;
2376    tree.flush_fs_events(cx).await;
2377    tree.read_with(cx, |tree, _| {
2378        check_worktree_entries(
2379            tree,
2380            WorktreeExpectations {
2381                excluded_paths: &[
2382                    ".git/HEAD",
2383                    ".git/foo",
2384                    "node_modules",
2385                    "node_modules/.DS_Store",
2386                    "node_modules/prettier",
2387                    "node_modules/prettier/package.json",
2388                ],
2389                ignored_paths: &["target"],
2390                tracked_paths: &[
2391                    ".DS_Store",
2392                    "src/.DS_Store",
2393                    "src/lib.rs",
2394                    "src/foo/foo.rs",
2395                    "src/foo/another.rs",
2396                    "src/bar/bar.rs",
2397                    ".gitignore",
2398                ],
2399                included_paths: &[],
2400            },
2401        )
2402    });
2403
2404    let new_excluded_dir = dir.path().join("build_output");
2405    let new_ignored_dir = dir.path().join("test_output");
2406    std::fs::create_dir_all(&new_excluded_dir)
2407        .unwrap_or_else(|e| panic!("Failed to create a {new_excluded_dir:?} directory: {e}"));
2408    std::fs::create_dir_all(&new_ignored_dir)
2409        .unwrap_or_else(|e| panic!("Failed to create a {new_ignored_dir:?} directory: {e}"));
2410    let node_modules_dir = dir.path().join("node_modules");
2411    let dot_git_dir = dir.path().join(".git");
2412    let src_dir = dir.path().join("src");
2413    for existing_dir in [&node_modules_dir, &dot_git_dir, &src_dir] {
2414        assert!(
2415            existing_dir.is_dir(),
2416            "Expect {existing_dir:?} to be present in the FS already"
2417        );
2418    }
2419
2420    for directory_for_new_file in [
2421        new_excluded_dir,
2422        new_ignored_dir,
2423        node_modules_dir,
2424        dot_git_dir,
2425        src_dir,
2426    ] {
2427        std::fs::write(directory_for_new_file.join("new_file"), "new file contents")
2428            .unwrap_or_else(|e| {
2429                panic!("Failed to create in {directory_for_new_file:?} a new file: {e}")
2430            });
2431    }
2432    tree.flush_fs_events(cx).await;
2433
2434    tree.read_with(cx, |tree, _| {
2435        check_worktree_entries(
2436            tree,
2437            WorktreeExpectations {
2438                excluded_paths: &[
2439                    ".git/HEAD",
2440                    ".git/foo",
2441                    ".git/new_file",
2442                    "node_modules",
2443                    "node_modules/.DS_Store",
2444                    "node_modules/prettier",
2445                    "node_modules/prettier/package.json",
2446                    "node_modules/new_file",
2447                    "build_output",
2448                    "build_output/new_file",
2449                    "test_output/new_file",
2450                ],
2451                ignored_paths: &["target", "test_output"],
2452                tracked_paths: &[
2453                    ".DS_Store",
2454                    "src/.DS_Store",
2455                    "src/lib.rs",
2456                    "src/foo/foo.rs",
2457                    "src/foo/another.rs",
2458                    "src/bar/bar.rs",
2459                    "src/new_file",
2460                    ".gitignore",
2461                ],
2462                included_paths: &[],
2463            },
2464        )
2465    });
2466}
2467
2468#[gpui::test]
2469async fn test_fs_events_in_dot_git_worktree(cx: &mut TestAppContext) {
2470    init_test(cx);
2471    cx.executor().allow_parking();
2472    let dir = TempTree::new(json!({
2473        ".git": {
2474            "HEAD": "ref: refs/heads/main\n",
2475            "foo": "foo contents",
2476        },
2477    }));
2478    let dot_git_worktree_dir = dir.path().join(".git");
2479
2480    let tree = Worktree::local(
2481        dot_git_worktree_dir.clone(),
2482        true,
2483        Arc::new(RealFs::new(None, cx.executor())),
2484        Default::default(),
2485        true,
2486        WorktreeId::from_proto(0),
2487        &mut cx.to_async(),
2488    )
2489    .await
2490    .unwrap();
2491    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2492        .await;
2493    tree.flush_fs_events(cx).await;
2494    tree.read_with(cx, |tree, _| {
2495        check_worktree_entries(
2496            tree,
2497            WorktreeExpectations {
2498                ignored_paths: &["HEAD", "foo"],
2499                ..Default::default()
2500            },
2501        )
2502    });
2503
2504    std::fs::write(dot_git_worktree_dir.join("new_file"), "new file contents")
2505        .unwrap_or_else(|e| panic!("Failed to create in {dot_git_worktree_dir:?} a new file: {e}"));
2506    tree.flush_fs_events(cx).await;
2507    tree.read_with(cx, |tree, _| {
2508        check_worktree_entries(
2509            tree,
2510            WorktreeExpectations {
2511                ignored_paths: &["HEAD", "foo", "new_file"],
2512                ..Default::default()
2513            },
2514        )
2515    });
2516}
2517
2518#[gpui::test(iterations = 30)]
2519async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
2520    init_test(cx);
2521    let fs = FakeFs::new(cx.background_executor.clone());
2522    fs.insert_tree(
2523        "/root",
2524        json!({
2525            "b": {},
2526            "c": {},
2527            "d": {},
2528        }),
2529    )
2530    .await;
2531
2532    let tree = Worktree::local(
2533        "/root".as_ref(),
2534        true,
2535        fs,
2536        Default::default(),
2537        true,
2538        WorktreeId::from_proto(0),
2539        &mut cx.to_async(),
2540    )
2541    .await
2542    .unwrap();
2543
2544    let snapshot1 = tree.update(cx, |tree, cx| {
2545        let tree = tree.as_local_mut().unwrap();
2546        let snapshot = Arc::new(Mutex::new(tree.snapshot()));
2547        tree.observe_updates(0, cx, {
2548            let snapshot = snapshot.clone();
2549            let settings = tree.settings();
2550            move |update| {
2551                snapshot
2552                    .lock()
2553                    .apply_remote_update(update, &settings.file_scan_inclusions);
2554                async { true }
2555            }
2556        });
2557        snapshot
2558    });
2559
2560    let entry = tree
2561        .update(cx, |tree, cx| {
2562            tree.as_local_mut()
2563                .unwrap()
2564                .create_entry(rel_path("a/e").into(), true, None, cx)
2565        })
2566        .await
2567        .unwrap()
2568        .into_included()
2569        .unwrap();
2570    assert!(entry.is_dir());
2571
2572    cx.executor().run_until_parked();
2573    tree.read_with(cx, |tree, _| {
2574        assert_eq!(
2575            tree.entry_for_path(rel_path("a/e")).unwrap().kind,
2576            EntryKind::Dir
2577        );
2578    });
2579
2580    let snapshot2 = tree.update(cx, |tree, _| tree.as_local().unwrap().snapshot());
2581    assert_eq!(
2582        snapshot1.lock().entries(true, 0).collect::<Vec<_>>(),
2583        snapshot2.entries(true, 0).collect::<Vec<_>>()
2584    );
2585}
2586
2587#[gpui::test]
2588async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
2589    init_test(cx);
2590    cx.executor().allow_parking();
2591
2592    let fs_fake = FakeFs::new(cx.background_executor.clone());
2593    fs_fake
2594        .insert_tree(
2595            "/root",
2596            json!({
2597                "a": {},
2598            }),
2599        )
2600        .await;
2601
2602    let tree_fake = Worktree::local(
2603        "/root".as_ref(),
2604        true,
2605        fs_fake,
2606        Default::default(),
2607        true,
2608        WorktreeId::from_proto(0),
2609        &mut cx.to_async(),
2610    )
2611    .await
2612    .unwrap();
2613
2614    let entry = tree_fake
2615        .update(cx, |tree, cx| {
2616            tree.as_local_mut().unwrap().create_entry(
2617                rel_path("a/b/c/d.txt").into(),
2618                false,
2619                None,
2620                cx,
2621            )
2622        })
2623        .await
2624        .unwrap()
2625        .into_included()
2626        .unwrap();
2627    assert!(entry.is_file());
2628
2629    cx.executor().run_until_parked();
2630    tree_fake.read_with(cx, |tree, _| {
2631        assert!(
2632            tree.entry_for_path(rel_path("a/b/c/d.txt"))
2633                .unwrap()
2634                .is_file()
2635        );
2636        assert!(tree.entry_for_path(rel_path("a/b/c")).unwrap().is_dir());
2637        assert!(tree.entry_for_path(rel_path("a/b")).unwrap().is_dir());
2638    });
2639
2640    let fs_real = Arc::new(RealFs::new(None, cx.executor()));
2641    let temp_root = TempTree::new(json!({
2642        "a": {}
2643    }));
2644
2645    let tree_real = Worktree::local(
2646        temp_root.path(),
2647        true,
2648        fs_real,
2649        Default::default(),
2650        true,
2651        WorktreeId::from_proto(0),
2652        &mut cx.to_async(),
2653    )
2654    .await
2655    .unwrap();
2656
2657    let entry = tree_real
2658        .update(cx, |tree, cx| {
2659            tree.as_local_mut().unwrap().create_entry(
2660                rel_path("a/b/c/d.txt").into(),
2661                false,
2662                None,
2663                cx,
2664            )
2665        })
2666        .await
2667        .unwrap()
2668        .into_included()
2669        .unwrap();
2670    assert!(entry.is_file());
2671
2672    cx.executor().run_until_parked();
2673    tree_real.read_with(cx, |tree, _| {
2674        assert!(
2675            tree.entry_for_path(rel_path("a/b/c/d.txt"))
2676                .unwrap()
2677                .is_file()
2678        );
2679        assert!(tree.entry_for_path(rel_path("a/b/c")).unwrap().is_dir());
2680        assert!(tree.entry_for_path(rel_path("a/b")).unwrap().is_dir());
2681    });
2682
2683    // Test smallest change
2684    let entry = tree_real
2685        .update(cx, |tree, cx| {
2686            tree.as_local_mut().unwrap().create_entry(
2687                rel_path("a/b/c/e.txt").into(),
2688                false,
2689                None,
2690                cx,
2691            )
2692        })
2693        .await
2694        .unwrap()
2695        .into_included()
2696        .unwrap();
2697    assert!(entry.is_file());
2698
2699    cx.executor().run_until_parked();
2700    tree_real.read_with(cx, |tree, _| {
2701        assert!(
2702            tree.entry_for_path(rel_path("a/b/c/e.txt"))
2703                .unwrap()
2704                .is_file()
2705        );
2706    });
2707
2708    // Test largest change
2709    let entry = tree_real
2710        .update(cx, |tree, cx| {
2711            tree.as_local_mut().unwrap().create_entry(
2712                rel_path("d/e/f/g.txt").into(),
2713                false,
2714                None,
2715                cx,
2716            )
2717        })
2718        .await
2719        .unwrap()
2720        .into_included()
2721        .unwrap();
2722    assert!(entry.is_file());
2723
2724    cx.executor().run_until_parked();
2725    tree_real.read_with(cx, |tree, _| {
2726        assert!(
2727            tree.entry_for_path(rel_path("d/e/f/g.txt"))
2728                .unwrap()
2729                .is_file()
2730        );
2731        assert!(tree.entry_for_path(rel_path("d/e/f")).unwrap().is_dir());
2732        assert!(tree.entry_for_path(rel_path("d/e")).unwrap().is_dir());
2733        assert!(tree.entry_for_path(rel_path("d")).unwrap().is_dir());
2734    });
2735}
2736
2737#[gpui::test]
2738async fn test_create_file_in_expanded_gitignored_dir(cx: &mut TestAppContext) {
2739    // Tests the behavior of our worktree refresh when a file in a gitignored directory
2740    // is created.
2741    init_test(cx);
2742    let fs = FakeFs::new(cx.background_executor.clone());
2743    fs.insert_tree(
2744        "/root",
2745        json!({
2746            ".gitignore": "ignored_dir\n",
2747            "ignored_dir": {
2748                "existing_file.txt": "existing content",
2749                "another_file.txt": "another content",
2750            },
2751        }),
2752    )
2753    .await;
2754
2755    let tree = Worktree::local(
2756        Path::new("/root"),
2757        true,
2758        fs.clone(),
2759        Default::default(),
2760        true,
2761        WorktreeId::from_proto(0),
2762        &mut cx.to_async(),
2763    )
2764    .await
2765    .unwrap();
2766
2767    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2768        .await;
2769
2770    tree.read_with(cx, |tree, _| {
2771        let ignored_dir = tree.entry_for_path(rel_path("ignored_dir")).unwrap();
2772        assert!(ignored_dir.is_ignored);
2773        assert_eq!(ignored_dir.kind, EntryKind::UnloadedDir);
2774    });
2775
2776    tree.update(cx, |tree, cx| {
2777        tree.load_file(rel_path("ignored_dir/existing_file.txt"), cx)
2778    })
2779    .await
2780    .unwrap();
2781
2782    tree.read_with(cx, |tree, _| {
2783        let ignored_dir = tree.entry_for_path(rel_path("ignored_dir")).unwrap();
2784        assert!(ignored_dir.is_ignored);
2785        assert_eq!(ignored_dir.kind, EntryKind::Dir);
2786
2787        assert!(
2788            tree.entry_for_path(rel_path("ignored_dir/existing_file.txt"))
2789                .is_some()
2790        );
2791        assert!(
2792            tree.entry_for_path(rel_path("ignored_dir/another_file.txt"))
2793                .is_some()
2794        );
2795    });
2796
2797    let entry = tree
2798        .update(cx, |tree, cx| {
2799            tree.create_entry(rel_path("ignored_dir/new_file.txt").into(), false, None, cx)
2800        })
2801        .await
2802        .unwrap();
2803    assert!(entry.into_included().is_some());
2804
2805    cx.executor().run_until_parked();
2806
2807    tree.read_with(cx, |tree, _| {
2808        let ignored_dir = tree.entry_for_path(rel_path("ignored_dir")).unwrap();
2809        assert!(ignored_dir.is_ignored);
2810        assert_eq!(
2811            ignored_dir.kind,
2812            EntryKind::Dir,
2813            "ignored_dir should still be loaded, not UnloadedDir"
2814        );
2815
2816        assert!(
2817            tree.entry_for_path(rel_path("ignored_dir/existing_file.txt"))
2818                .is_some(),
2819            "existing_file.txt should still be visible"
2820        );
2821        assert!(
2822            tree.entry_for_path(rel_path("ignored_dir/another_file.txt"))
2823                .is_some(),
2824            "another_file.txt should still be visible"
2825        );
2826        assert!(
2827            tree.entry_for_path(rel_path("ignored_dir/new_file.txt"))
2828                .is_some(),
2829            "new_file.txt should be visible"
2830        );
2831    });
2832}
2833
2834#[gpui::test]
2835async fn test_fs_event_for_gitignored_dir_does_not_lose_contents(cx: &mut TestAppContext) {
2836    // Tests the behavior of our worktree refresh when a directory modification for a gitignored directory
2837    // is triggered.
2838    init_test(cx);
2839    let fs = FakeFs::new(cx.background_executor.clone());
2840    fs.insert_tree(
2841        "/root",
2842        json!({
2843            ".gitignore": "ignored_dir\n",
2844            "ignored_dir": {
2845                "file1.txt": "content1",
2846                "file2.txt": "content2",
2847            },
2848        }),
2849    )
2850    .await;
2851
2852    let tree = Worktree::local(
2853        Path::new("/root"),
2854        true,
2855        fs.clone(),
2856        Default::default(),
2857        true,
2858        WorktreeId::from_proto(0),
2859        &mut cx.to_async(),
2860    )
2861    .await
2862    .unwrap();
2863
2864    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2865        .await;
2866
2867    // Load a file to expand the ignored directory
2868    tree.update(cx, |tree, cx| {
2869        tree.load_file(rel_path("ignored_dir/file1.txt"), cx)
2870    })
2871    .await
2872    .unwrap();
2873
2874    tree.read_with(cx, |tree, _| {
2875        let ignored_dir = tree.entry_for_path(rel_path("ignored_dir")).unwrap();
2876        assert_eq!(ignored_dir.kind, EntryKind::Dir);
2877        assert!(
2878            tree.entry_for_path(rel_path("ignored_dir/file1.txt"))
2879                .is_some()
2880        );
2881        assert!(
2882            tree.entry_for_path(rel_path("ignored_dir/file2.txt"))
2883                .is_some()
2884        );
2885    });
2886
2887    fs.emit_fs_event("/root/ignored_dir", Some(fs::PathEventKind::Changed));
2888    tree.flush_fs_events(cx).await;
2889
2890    tree.read_with(cx, |tree, _| {
2891        let ignored_dir = tree.entry_for_path(rel_path("ignored_dir")).unwrap();
2892        assert_eq!(
2893            ignored_dir.kind,
2894            EntryKind::Dir,
2895            "ignored_dir should still be loaded (Dir), not UnloadedDir"
2896        );
2897        assert!(
2898            tree.entry_for_path(rel_path("ignored_dir/file1.txt"))
2899                .is_some(),
2900            "file1.txt should still be visible after directory fs event"
2901        );
2902        assert!(
2903            tree.entry_for_path(rel_path("ignored_dir/file2.txt"))
2904                .is_some(),
2905            "file2.txt should still be visible after directory fs event"
2906        );
2907    });
2908}
2909
2910#[gpui::test(iterations = 100)]
2911async fn test_random_worktree_operations_during_initial_scan(
2912    cx: &mut TestAppContext,
2913    mut rng: StdRng,
2914) {
2915    init_test(cx);
2916    let operations = env::var("OPERATIONS")
2917        .map(|o| o.parse().unwrap())
2918        .unwrap_or(5);
2919    let initial_entries = env::var("INITIAL_ENTRIES")
2920        .map(|o| o.parse().unwrap())
2921        .unwrap_or(20);
2922
2923    let root_dir = Path::new(path!("/test"));
2924    let fs = FakeFs::new(cx.background_executor.clone()) as Arc<dyn Fs>;
2925    fs.as_fake().insert_tree(root_dir, json!({})).await;
2926    for _ in 0..initial_entries {
2927        randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
2928    }
2929    log::info!("generated initial tree");
2930
2931    let worktree = Worktree::local(
2932        root_dir,
2933        true,
2934        fs.clone(),
2935        Default::default(),
2936        true,
2937        WorktreeId::from_proto(0),
2938        &mut cx.to_async(),
2939    )
2940    .await
2941    .unwrap();
2942
2943    let mut snapshots = vec![worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot())];
2944    let updates = Arc::new(Mutex::new(Vec::new()));
2945    worktree.update(cx, |tree, cx| {
2946        check_worktree_change_events(tree, cx);
2947
2948        tree.as_local_mut().unwrap().observe_updates(0, cx, {
2949            let updates = updates.clone();
2950            move |update| {
2951                updates.lock().push(update);
2952                async { true }
2953            }
2954        });
2955    });
2956
2957    for _ in 0..operations {
2958        worktree
2959            .update(cx, |worktree, cx| {
2960                randomly_mutate_worktree(worktree, &mut rng, cx)
2961            })
2962            .await
2963            .log_err();
2964        worktree.read_with(cx, |tree, _| {
2965            tree.as_local().unwrap().snapshot().check_invariants(true)
2966        });
2967
2968        if rng.random_bool(0.6) {
2969            snapshots.push(worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot()));
2970        }
2971    }
2972
2973    worktree
2974        .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
2975        .await;
2976
2977    cx.executor().run_until_parked();
2978
2979    let final_snapshot = worktree.read_with(cx, |tree, _| {
2980        let tree = tree.as_local().unwrap();
2981        let snapshot = tree.snapshot();
2982        snapshot.check_invariants(true);
2983        snapshot
2984    });
2985
2986    let settings = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().settings());
2987
2988    for (i, snapshot) in snapshots.into_iter().enumerate().rev() {
2989        let mut updated_snapshot = snapshot.clone();
2990        for update in updates.lock().iter() {
2991            if update.scan_id >= updated_snapshot.scan_id() as u64 {
2992                updated_snapshot
2993                    .apply_remote_update(update.clone(), &settings.file_scan_inclusions);
2994            }
2995        }
2996
2997        assert_eq!(
2998            updated_snapshot.entries(true, 0).collect::<Vec<_>>(),
2999            final_snapshot.entries(true, 0).collect::<Vec<_>>(),
3000            "wrong updates after snapshot {i}: {updates:#?}",
3001        );
3002    }
3003}
3004
3005#[gpui::test(iterations = 100)]
3006async fn test_random_worktree_changes(cx: &mut TestAppContext, mut rng: StdRng) {
3007    init_test(cx);
3008    let operations = env::var("OPERATIONS")
3009        .map(|o| o.parse().unwrap())
3010        .unwrap_or(40);
3011    let initial_entries = env::var("INITIAL_ENTRIES")
3012        .map(|o| o.parse().unwrap())
3013        .unwrap_or(20);
3014
3015    let root_dir = Path::new(path!("/test"));
3016    let fs = FakeFs::new(cx.background_executor.clone()) as Arc<dyn Fs>;
3017    fs.as_fake().insert_tree(root_dir, json!({})).await;
3018    for _ in 0..initial_entries {
3019        randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
3020    }
3021    log::info!("generated initial tree");
3022
3023    let worktree = Worktree::local(
3024        root_dir,
3025        true,
3026        fs.clone(),
3027        Default::default(),
3028        true,
3029        WorktreeId::from_proto(0),
3030        &mut cx.to_async(),
3031    )
3032    .await
3033    .unwrap();
3034
3035    let updates = Arc::new(Mutex::new(Vec::new()));
3036    worktree.update(cx, |tree, cx| {
3037        check_worktree_change_events(tree, cx);
3038
3039        tree.as_local_mut().unwrap().observe_updates(0, cx, {
3040            let updates = updates.clone();
3041            move |update| {
3042                updates.lock().push(update);
3043                async { true }
3044            }
3045        });
3046    });
3047
3048    worktree
3049        .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
3050        .await;
3051
3052    fs.as_fake().pause_events();
3053    let mut snapshots = Vec::new();
3054    let mut mutations_len = operations;
3055    while mutations_len > 1 {
3056        if rng.random_bool(0.2) {
3057            worktree
3058                .update(cx, |worktree, cx| {
3059                    randomly_mutate_worktree(worktree, &mut rng, cx)
3060                })
3061                .await
3062                .log_err();
3063        } else {
3064            randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
3065        }
3066
3067        let buffered_event_count = fs.as_fake().buffered_event_count();
3068        if buffered_event_count > 0 && rng.random_bool(0.3) {
3069            if rng.random_bool(0.2) {
3070                log::info!(
3071                    "simulating watcher overflow, losing {} events",
3072                    buffered_event_count
3073                );
3074                fs.as_fake().simulate_watcher_overflow(root_dir);
3075            } else {
3076                let len = rng.random_range(0..=buffered_event_count);
3077                log::info!("flushing {} events", len);
3078                fs.as_fake().flush_events(len);
3079            }
3080        } else {
3081            randomly_mutate_fs(&fs, root_dir, 0.6, &mut rng).await;
3082            mutations_len -= 1;
3083        }
3084
3085        cx.executor().run_until_parked();
3086        if rng.random_bool(0.2) {
3087            log::info!("storing snapshot {}", snapshots.len());
3088            let snapshot = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
3089            snapshots.push(snapshot);
3090        }
3091    }
3092
3093    log::info!("quiescing");
3094    fs.as_fake().flush_events(usize::MAX);
3095    cx.executor().run_until_parked();
3096
3097    let snapshot = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
3098    snapshot.check_invariants(true);
3099    let expanded_paths = snapshot
3100        .expanded_entries()
3101        .map(|e| e.path.clone())
3102        .collect::<Vec<_>>();
3103
3104    {
3105        let new_worktree = Worktree::local(
3106            root_dir,
3107            true,
3108            fs.clone(),
3109            Default::default(),
3110            true,
3111            WorktreeId::from_proto(0),
3112            &mut cx.to_async(),
3113        )
3114        .await
3115        .unwrap();
3116        new_worktree
3117            .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
3118            .await;
3119        new_worktree
3120            .update(cx, |tree, _| {
3121                tree.as_local_mut()
3122                    .unwrap()
3123                    .refresh_entries_for_paths(expanded_paths)
3124            })
3125            .recv()
3126            .await;
3127        let new_snapshot =
3128            new_worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
3129        assert_eq!(
3130            snapshot.entries_without_ids(true),
3131            new_snapshot.entries_without_ids(true)
3132        );
3133    }
3134
3135    let settings = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().settings());
3136
3137    for (i, mut prev_snapshot) in snapshots.into_iter().enumerate().rev() {
3138        for update in updates.lock().iter() {
3139            if update.scan_id >= prev_snapshot.scan_id() as u64 {
3140                prev_snapshot.apply_remote_update(update.clone(), &settings.file_scan_inclusions);
3141            }
3142        }
3143
3144        assert_eq!(
3145            prev_snapshot
3146                .entries(true, 0)
3147                .map(ignore_pending_dir)
3148                .collect::<Vec<_>>(),
3149            snapshot
3150                .entries(true, 0)
3151                .map(ignore_pending_dir)
3152                .collect::<Vec<_>>(),
3153            "wrong updates after snapshot {i}: {updates:#?}",
3154        );
3155    }
3156
3157    fn ignore_pending_dir(entry: &Entry) -> Entry {
3158        let mut entry = entry.clone();
3159        if entry.kind.is_dir() {
3160            entry.kind = EntryKind::Dir
3161        }
3162        entry
3163    }
3164}
3165
3166#[gpui::test(iterations = 100)]
3167async fn test_random_git_updates_with_watcher_overflows(cx: &mut TestAppContext, mut rng: StdRng) {
3168    // Property: every git state change is eventually signaled via
3169    // `UpdatedGitRepositories`, no matter how the events reporting it are
3170    // batched, delayed, or lost to watcher overflows.
3171    init_test(cx);
3172    let operations = env::var("OPERATIONS")
3173        .map(|o| o.parse().unwrap())
3174        .unwrap_or(40);
3175
3176    let root_dir = Path::new(path!("/test"));
3177    let dot_git = root_dir.join(".git");
3178    // Random fs mutations are confined to this subdirectory so that they
3179    // cannot rename or delete `.git` itself.
3180    let src_dir = root_dir.join("src");
3181    let fs = FakeFs::new(cx.background_executor.clone()) as Arc<dyn Fs>;
3182    fs.as_fake()
3183        .insert_tree(
3184            root_dir,
3185            json!({
3186                ".git": {},
3187                "src": {
3188                    "main.rs": "fn main() {}",
3189                },
3190            }),
3191        )
3192        .await;
3193
3194    let worktree = Worktree::local(
3195        root_dir,
3196        true,
3197        fs.clone(),
3198        Default::default(),
3199        true,
3200        WorktreeId::from_proto(0),
3201        &mut cx.to_async(),
3202    )
3203    .await
3204    .unwrap();
3205    worktree
3206        .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
3207        .await;
3208    cx.executor().run_until_parked();
3209
3210    // Set when the fake repository's state is mutated, cleared when the
3211    // worktree signals a git update. A signal observed after a mutation
3212    // prompts downstream consumers (the GitStore) to re-read the repository,
3213    // at which point they see that mutation's state, so clearing on any
3214    // subsequent signal is sound.
3215    let pending_git_update: Rc<Cell<bool>> = Rc::new(Cell::new(false));
3216    worktree.update(cx, {
3217        let pending_git_update = pending_git_update.clone();
3218        |_, cx| {
3219            cx.subscribe(&cx.entity(), move |_, _, event, _| {
3220                if matches!(event, Event::UpdatedGitRepositories(_)) {
3221                    pending_git_update.set(false);
3222                }
3223            })
3224            .detach();
3225        }
3226    });
3227
3228    fs.as_fake().pause_events();
3229    let mut commit_count = 0;
3230    for _ in 0..operations {
3231        match rng.random_range(0_u32..100) {
3232            // Change the repository's git state, e.g. a commit moving HEAD.
3233            0..25 => {
3234                commit_count += 1;
3235                log::info!("setting HEAD to commit {commit_count}");
3236                fs.as_fake().set_head_for_repo(
3237                    &dot_git,
3238                    &[("src/main.rs", format!("fn main() {{}} // {commit_count}"))],
3239                    format!("sha-{commit_count}"),
3240                );
3241                pending_git_update.set(true);
3242            }
3243            // The watch queue overflows: all undelivered events are lost and
3244            // only a rescan for the root is reported.
3245            25..40 => {
3246                log::info!(
3247                    "simulating watcher overflow, losing {} events",
3248                    fs.as_fake().buffered_event_count()
3249                );
3250                fs.as_fake().simulate_watcher_overflow(root_dir);
3251            }
3252            // Deliver a prefix of the queued events.
3253            40..65 => {
3254                let buffered_event_count = fs.as_fake().buffered_event_count();
3255                let len = rng.random_range(0..=buffered_event_count);
3256                log::info!("flushing {len} of {buffered_event_count} events");
3257                fs.as_fake().flush_events(len);
3258            }
3259            // Unrelated churn in the working tree.
3260            _ => {
3261                randomly_mutate_fs(&fs, &src_dir, 1.0, &mut rng).await;
3262            }
3263        }
3264        cx.executor().run_until_parked();
3265    }
3266
3267    log::info!("quiescing");
3268    fs.as_fake().unpause_events_and_flush();
3269    cx.executor().run_until_parked();
3270
3271    worktree.read_with(cx, |tree, _| {
3272        tree.as_local().unwrap().snapshot().check_invariants(true)
3273    });
3274    assert!(
3275        !pending_git_update.get(),
3276        "a git state change was never signaled via UpdatedGitRepositories"
3277    );
3278}
3279
3280// The worktree's `UpdatedEntries` event can be used to follow along with
3281// all changes to the worktree's snapshot.
3282fn check_worktree_change_events(tree: &mut Worktree, cx: &mut Context<Worktree>) {
3283    let mut entries = tree.entries(true, 0).cloned().collect::<Vec<_>>();
3284    cx.subscribe(&cx.entity(), move |tree, _, event, _| {
3285        if let Event::UpdatedEntries(changes) = event {
3286            for (path, _, change_type) in changes.iter() {
3287                let entry = tree.entry_for_path(path).cloned();
3288                let ix = match entries.binary_search_by_key(&path, |e| &e.path) {
3289                    Ok(ix) | Err(ix) => ix,
3290                };
3291                match change_type {
3292                    PathChange::Added => entries.insert(ix, entry.unwrap()),
3293                    PathChange::Removed => drop(entries.remove(ix)),
3294                    PathChange::Updated => {
3295                        let entry = entry.unwrap();
3296                        let existing_entry = entries.get_mut(ix).unwrap();
3297                        assert_eq!(existing_entry.path, entry.path);
3298                        *existing_entry = entry;
3299                    }
3300                    PathChange::AddedOrUpdated | PathChange::Loaded => {
3301                        let entry = entry.unwrap();
3302                        if entries.get(ix).map(|e| &e.path) == Some(&entry.path) {
3303                            *entries.get_mut(ix).unwrap() = entry;
3304                        } else {
3305                            entries.insert(ix, entry);
3306                        }
3307                    }
3308                }
3309            }
3310
3311            let new_entries = tree.entries(true, 0).cloned().collect::<Vec<_>>();
3312            assert_eq!(entries, new_entries, "incorrect changes: {:?}", changes);
3313        }
3314    })
3315    .detach();
3316}
3317
3318fn randomly_mutate_worktree(
3319    worktree: &mut Worktree,
3320    rng: &mut impl Rng,
3321    cx: &mut Context<Worktree>,
3322) -> Task<Result<()>> {
3323    log::info!("mutating worktree");
3324    let worktree = worktree.as_local_mut().unwrap();
3325    let snapshot = worktree.snapshot();
3326    let entry = snapshot.entries(false, 0).choose(rng).unwrap();
3327
3328    match rng.random_range(0_u32..100) {
3329        0..=33 if entry.path.as_ref() != RelPath::empty() => {
3330            log::info!("deleting entry {:?} ({})", entry.path, entry.id.to_usize());
3331            let task = worktree.delete_entry(entry.clone(), cx);
3332
3333            cx.background_spawn(async move {
3334                task.await?;
3335                Ok(())
3336            })
3337        }
3338        _ => {
3339            if entry.is_dir() {
3340                let child_path = entry.path.join(rel_path(&random_filename(rng)));
3341                let is_dir = rng.random_bool(0.3);
3342                log::info!(
3343                    "creating {} at {:?}",
3344                    if is_dir { "dir" } else { "file" },
3345                    child_path,
3346                );
3347                let task = worktree.create_entry(child_path.into(), is_dir, None, cx);
3348                cx.background_spawn(async move {
3349                    task.await?;
3350                    Ok(())
3351                })
3352            } else {
3353                log::info!(
3354                    "overwriting file {:?} ({})",
3355                    &entry.path,
3356                    entry.id.to_usize()
3357                );
3358                let task = worktree.write_file(
3359                    entry.path.clone(),
3360                    "".into(),
3361                    Default::default(),
3362                    encoding_rs::UTF_8,
3363                    false,
3364                    cx,
3365                );
3366                cx.background_spawn(async move {
3367                    task.await?;
3368                    Ok(())
3369                })
3370            }
3371        }
3372    }
3373}
3374
3375async fn randomly_mutate_fs(
3376    fs: &Arc<dyn Fs>,
3377    root_path: &Path,
3378    insertion_probability: f64,
3379    rng: &mut impl Rng,
3380) {
3381    log::info!("mutating fs");
3382    let mut files = Vec::new();
3383    let mut dirs = Vec::new();
3384    for path in fs.as_fake().paths(false) {
3385        if path.starts_with(root_path) {
3386            if fs.is_file(&path).await {
3387                files.push(path);
3388            } else {
3389                dirs.push(path);
3390            }
3391        }
3392    }
3393
3394    if (files.is_empty() && dirs.len() == 1) || rng.random_bool(insertion_probability) {
3395        let path = dirs.choose(rng).unwrap();
3396        let new_path = path.join(random_filename(rng));
3397
3398        if rng.random() {
3399            log::info!(
3400                "creating dir {:?}",
3401                new_path.strip_prefix(root_path).unwrap()
3402            );
3403            fs.create_dir(&new_path).await.unwrap();
3404        } else {
3405            log::info!(
3406                "creating file {:?}",
3407                new_path.strip_prefix(root_path).unwrap()
3408            );
3409            fs.create_file(&new_path, Default::default()).await.unwrap();
3410        }
3411    } else if rng.random_bool(0.05) {
3412        let ignore_dir_path = dirs.choose(rng).unwrap();
3413        let ignore_path = ignore_dir_path.join(GITIGNORE);
3414
3415        let subdirs = dirs
3416            .iter()
3417            .filter(|d| d.starts_with(ignore_dir_path))
3418            .cloned()
3419            .collect::<Vec<_>>();
3420        let subfiles = files
3421            .iter()
3422            .filter(|d| d.starts_with(ignore_dir_path))
3423            .cloned()
3424            .collect::<Vec<_>>();
3425        let files_to_ignore = {
3426            let len = rng.random_range(0..=subfiles.len());
3427            subfiles.choose_multiple(rng, len)
3428        };
3429        let dirs_to_ignore = {
3430            let len = rng.random_range(0..subdirs.len());
3431            subdirs.choose_multiple(rng, len)
3432        };
3433
3434        let mut ignore_contents = String::new();
3435        for path_to_ignore in files_to_ignore.chain(dirs_to_ignore) {
3436            writeln!(
3437                ignore_contents,
3438                "{}",
3439                path_to_ignore
3440                    .strip_prefix(ignore_dir_path)
3441                    .unwrap()
3442                    .to_str()
3443                    .unwrap()
3444            )
3445            .unwrap();
3446        }
3447        log::info!(
3448            "creating gitignore {:?} with contents:\n{}",
3449            ignore_path.strip_prefix(root_path).unwrap(),
3450            ignore_contents
3451        );
3452        fs.save(
3453            &ignore_path,
3454            &ignore_contents.as_str().into(),
3455            Default::default(),
3456        )
3457        .await
3458        .unwrap();
3459    } else {
3460        let old_path = {
3461            let file_path = files.choose(rng);
3462            let dir_path = dirs[1..].choose(rng);
3463            file_path.into_iter().chain(dir_path).choose(rng).unwrap()
3464        };
3465
3466        let is_rename = rng.random();
3467        if is_rename {
3468            let new_path_parent = dirs
3469                .iter()
3470                .filter(|d| !d.starts_with(old_path))
3471                .choose(rng)
3472                .unwrap();
3473
3474            let overwrite_existing_dir =
3475                !old_path.starts_with(new_path_parent) && rng.random_bool(0.3);
3476            let new_path = if overwrite_existing_dir {
3477                fs.remove_dir(
3478                    new_path_parent,
3479                    RemoveOptions {
3480                        recursive: true,
3481                        ignore_if_not_exists: true,
3482                    },
3483                )
3484                .await
3485                .unwrap();
3486                new_path_parent.to_path_buf()
3487            } else {
3488                new_path_parent.join(random_filename(rng))
3489            };
3490
3491            log::info!(
3492                "renaming {:?} to {}{:?}",
3493                old_path.strip_prefix(root_path).unwrap(),
3494                if overwrite_existing_dir {
3495                    "overwrite "
3496                } else {
3497                    ""
3498                },
3499                new_path.strip_prefix(root_path).unwrap()
3500            );
3501            fs.rename(
3502                old_path,
3503                &new_path,
3504                fs::RenameOptions {
3505                    overwrite: true,
3506                    ignore_if_exists: true,
3507                    create_parents: false,
3508                },
3509            )
3510            .await
3511            .unwrap();
3512        } else if fs.is_file(old_path).await {
3513            log::info!(
3514                "deleting file {:?}",
3515                old_path.strip_prefix(root_path).unwrap()
3516            );
3517            fs.remove_file(old_path, Default::default()).await.unwrap();
3518        } else {
3519            log::info!(
3520                "deleting dir {:?}",
3521                old_path.strip_prefix(root_path).unwrap()
3522            );
3523            fs.remove_dir(
3524                old_path,
3525                RemoveOptions {
3526                    recursive: true,
3527                    ignore_if_not_exists: true,
3528                },
3529            )
3530            .await
3531            .unwrap();
3532        }
3533    }
3534}
3535
3536fn random_filename(rng: &mut impl Rng) -> String {
3537    (0..6)
3538        .map(|_| rng.sample(rand::distr::Alphanumeric))
3539        .map(char::from)
3540        .collect()
3541}
3542
3543#[gpui::test]
3544async fn test_private_single_file_worktree(cx: &mut TestAppContext) {
3545    init_test(cx);
3546    let fs = FakeFs::new(cx.background_executor.clone());
3547    fs.insert_tree("/", json!({".env": "PRIVATE=secret\n"}))
3548        .await;
3549    let tree = Worktree::local(
3550        Path::new("/.env"),
3551        true,
3552        fs.clone(),
3553        Default::default(),
3554        true,
3555        WorktreeId::from_proto(0),
3556        &mut cx.to_async(),
3557    )
3558    .await
3559    .unwrap();
3560    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
3561        .await;
3562    tree.read_with(cx, |tree, _| {
3563        let entry = tree.entry_for_path(rel_path("")).unwrap();
3564        assert!(entry.is_private);
3565    });
3566}
3567
3568#[gpui::test]
3569async fn test_repository_above_root(executor: BackgroundExecutor, cx: &mut TestAppContext) {
3570    init_test(cx);
3571
3572    let fs = FakeFs::new(executor);
3573    fs.insert_tree(
3574        path!("/root"),
3575        json!({
3576            ".git": {},
3577            "subproject": {
3578                "a.txt": "A"
3579            }
3580        }),
3581    )
3582    .await;
3583    let worktree = Worktree::local(
3584        path!("/root/subproject").as_ref(),
3585        true,
3586        fs.clone(),
3587        Arc::default(),
3588        true,
3589        WorktreeId::from_proto(0),
3590        &mut cx.to_async(),
3591    )
3592    .await
3593    .unwrap();
3594    worktree
3595        .update(cx, |worktree, _| {
3596            worktree.as_local().unwrap().scan_complete()
3597        })
3598        .await;
3599    cx.run_until_parked();
3600    let repos = worktree.update(cx, |worktree, _| {
3601        worktree.as_local().unwrap().repositories()
3602    });
3603    pretty_assertions::assert_eq!(repos, [Path::new(path!("/root")).into()]);
3604
3605    fs.touch_path(path!("/root/subproject")).await;
3606    worktree
3607        .update(cx, |worktree, _| {
3608            worktree.as_local().unwrap().scan_complete()
3609        })
3610        .await;
3611    cx.run_until_parked();
3612
3613    let repos = worktree.update(cx, |worktree, _| {
3614        worktree.as_local().unwrap().repositories()
3615    });
3616    pretty_assertions::assert_eq!(repos, [Path::new(path!("/root")).into()]);
3617}
3618
3619#[gpui::test]
3620async fn test_global_gitignore(executor: BackgroundExecutor, cx: &mut TestAppContext) {
3621    init_test(cx);
3622
3623    let home = paths::home_dir();
3624    let fs = FakeFs::new(executor);
3625    fs.insert_tree(
3626        home,
3627        json!({
3628            ".config": {
3629                "git": {
3630                    "ignore": "foo\n/bar\nbaz\n"
3631                }
3632            },
3633            "project": {
3634                ".git": {},
3635                ".gitignore": "!baz",
3636                "foo": "",
3637                "bar": "",
3638                "sub": {
3639                    "bar": "",
3640                },
3641                "subrepo": {
3642                    ".git": {},
3643                    "bar": ""
3644                },
3645                "baz": ""
3646            }
3647        }),
3648    )
3649    .await;
3650    let worktree = Worktree::local(
3651        home.join("project"),
3652        true,
3653        fs.clone(),
3654        Arc::default(),
3655        true,
3656        WorktreeId::from_proto(0),
3657        &mut cx.to_async(),
3658    )
3659    .await
3660    .unwrap();
3661    worktree
3662        .update(cx, |worktree, _| {
3663            worktree.as_local().unwrap().scan_complete()
3664        })
3665        .await;
3666    cx.run_until_parked();
3667
3668    // .gitignore overrides excludesFile, and anchored paths in excludesFile are resolved
3669    // relative to the nearest containing repository
3670    worktree.update(cx, |worktree, _cx| {
3671        check_worktree_entries(
3672            worktree,
3673            WorktreeExpectations {
3674                ignored_paths: &["foo", "bar", "subrepo/bar"],
3675                tracked_paths: &["sub/bar", "baz"],
3676                ..Default::default()
3677            },
3678        );
3679    });
3680
3681    // Ignore statuses are updated when excludesFile changes
3682    fs.write(
3683        &home.join(".config").join("git").join("ignore"),
3684        "/bar\nbaz\n".as_bytes(),
3685    )
3686    .await
3687    .unwrap();
3688    worktree
3689        .update(cx, |worktree, _| {
3690            worktree.as_local().unwrap().scan_complete()
3691        })
3692        .await;
3693    cx.run_until_parked();
3694
3695    worktree.update(cx, |worktree, _cx| {
3696        check_worktree_entries(
3697            worktree,
3698            WorktreeExpectations {
3699                ignored_paths: &["bar", "subrepo/bar"],
3700                tracked_paths: &["foo", "sub/bar", "baz"],
3701                ..Default::default()
3702            },
3703        );
3704    });
3705
3706    // Statuses are updated when .git added/removed
3707    fs.remove_dir(
3708        &home.join("project").join("subrepo").join(".git"),
3709        RemoveOptions {
3710            recursive: true,
3711            ..Default::default()
3712        },
3713    )
3714    .await
3715    .unwrap();
3716    worktree
3717        .update(cx, |worktree, _| {
3718            worktree.as_local().unwrap().scan_complete()
3719        })
3720        .await;
3721    cx.run_until_parked();
3722
3723    worktree.update(cx, |worktree, _cx| {
3724        check_worktree_entries(
3725            worktree,
3726            WorktreeExpectations {
3727                ignored_paths: &["bar"],
3728                tracked_paths: &["foo", "sub/bar", "baz", "subrepo/bar"],
3729                ..Default::default()
3730            },
3731        );
3732    });
3733}
3734
3735#[gpui::test]
3736async fn test_repo_exclude_in_worktree(executor: BackgroundExecutor, cx: &mut TestAppContext) {
3737    init_test(cx);
3738
3739    let fs = FakeFs::new(executor);
3740
3741    fs.insert_tree(
3742        path!("/repo"),
3743        json!({
3744            ".git": {
3745                "info": {
3746                    "exclude": ".env.*"
3747                },
3748                "worktrees": {
3749                    "my-worktree": {
3750                        "commondir": "../.."
3751                    }
3752                }
3753            }
3754        }),
3755    )
3756    .await;
3757
3758    fs.insert_tree(
3759        path!("/worktree"),
3760        json!({
3761            // .git is pointing to the repo
3762            ".git": "gitdir: /repo/.git/worktrees/my-worktree",
3763            ".env.local": "secret=1234",
3764            "not-ignored.txt": "",
3765        }),
3766    )
3767    .await;
3768
3769    let worktree = Worktree::local(
3770        path!("/worktree").as_ref(),
3771        true,
3772        fs.clone(),
3773        Default::default(),
3774        true,
3775        WorktreeId::from_proto(0),
3776        &mut cx.to_async(),
3777    )
3778    .await
3779    .unwrap();
3780
3781    worktree
3782        .update(cx, |worktree, _| {
3783            worktree.as_local().unwrap().scan_complete()
3784        })
3785        .await;
3786    cx.run_until_parked();
3787
3788    // .env.local should be ignored via info/exclude from the repo's exclude
3789    worktree.update(cx, |worktree, _cx| {
3790        check_worktree_entries(
3791            worktree,
3792            WorktreeExpectations {
3793                ignored_paths: &[".env.local"],
3794                tracked_paths: &["not-ignored.txt"],
3795                ..Default::default()
3796            },
3797        );
3798    });
3799}
3800
3801#[gpui::test]
3802async fn test_repo_exclude(executor: BackgroundExecutor, cx: &mut TestAppContext) {
3803    init_test(cx);
3804
3805    let fs = FakeFs::new(executor);
3806    let project_dir = Path::new(path!("/project"));
3807    fs.insert_tree(
3808        project_dir,
3809        json!({
3810            ".git": {
3811                "info": {
3812                    "exclude": ".env.*"
3813                }
3814            },
3815            ".env.example": "secret=xxxx",
3816            ".env.local": "secret=1234",
3817            ".gitignore": "!.env.example",
3818            "README.md": "# Repo Exclude",
3819            "src": {
3820                "main.rs": "fn main() {}",
3821            },
3822        }),
3823    )
3824    .await;
3825
3826    let worktree = Worktree::local(
3827        project_dir,
3828        true,
3829        fs.clone(),
3830        Default::default(),
3831        true,
3832        WorktreeId::from_proto(0),
3833        &mut cx.to_async(),
3834    )
3835    .await
3836    .unwrap();
3837    worktree
3838        .update(cx, |worktree, _| {
3839            worktree.as_local().unwrap().scan_complete()
3840        })
3841        .await;
3842    cx.run_until_parked();
3843
3844    // .gitignore overrides .git/info/exclude
3845    worktree.update(cx, |worktree, _cx| {
3846        check_worktree_entries(
3847            worktree,
3848            WorktreeExpectations {
3849                ignored_paths: &[".env.local"],
3850                tracked_paths: &[".env.example", "README.md", "src/main.rs"],
3851                ..Default::default()
3852            },
3853        );
3854    });
3855
3856    // Ignore statuses are updated when .git/info/exclude file changes
3857    fs.write(
3858        &project_dir.join(DOT_GIT).join(REPO_EXCLUDE),
3859        ".env.example".as_bytes(),
3860    )
3861    .await
3862    .unwrap();
3863    worktree
3864        .update(cx, |worktree, _| {
3865            worktree.as_local().unwrap().scan_complete()
3866        })
3867        .await;
3868    cx.run_until_parked();
3869
3870    worktree.update(cx, |worktree, _cx| {
3871        check_worktree_entries(
3872            worktree,
3873            WorktreeExpectations {
3874                tracked_paths: &[".env.example", ".env.local", "README.md", "src/main.rs"],
3875                ..Default::default()
3876            },
3877        );
3878    });
3879}
3880
3881#[gpui::test]
3882async fn test_repo_exclude_anchored_pattern(executor: BackgroundExecutor, cx: &mut TestAppContext) {
3883    init_test(cx);
3884
3885    let fs = FakeFs::new(executor);
3886    let project_dir = Path::new(path!("/project"));
3887    fs.insert_tree(
3888        project_dir,
3889        json!({
3890            ".git": {
3891                "info": {
3892                    "exclude": "vendor/cache"
3893                }
3894            },
3895            "vendor": {
3896                "cache": {
3897                    "blob.bin": "",
3898                },
3899                "keep.txt": "",
3900            },
3901            "elsewhere": {
3902                "vendor": {
3903                    "cache": {
3904                        "blob.bin": "",
3905                    },
3906                },
3907            },
3908        }),
3909    )
3910    .await;
3911
3912    let worktree = Worktree::local(
3913        project_dir,
3914        true,
3915        fs.clone(),
3916        Default::default(),
3917        true,
3918        WorktreeId::from_proto(0),
3919        &mut cx.to_async(),
3920    )
3921    .await
3922    .unwrap();
3923    worktree
3924        .update(cx, |worktree, _| {
3925            worktree.as_local().unwrap().scan_complete()
3926        })
3927        .await;
3928    cx.run_until_parked();
3929
3930    // An anchored pattern (containing a `/`) is matched relative to the work
3931    // tree root, so only the top-level `vendor/cache` is ignored.
3932    worktree.update(cx, |worktree, _cx| {
3933        check_worktree_entries(
3934            worktree,
3935            WorktreeExpectations {
3936                ignored_paths: &["vendor/cache"],
3937                tracked_paths: &["vendor/keep.txt", "elsewhere/vendor/cache"],
3938                ..Default::default()
3939            },
3940        );
3941    });
3942}
3943
3944#[gpui::test]
3945async fn test_repo_exclude_applies_within_nested_repos(
3946    executor: BackgroundExecutor,
3947    cx: &mut TestAppContext,
3948) {
3949    init_test(cx);
3950
3951    let fs = FakeFs::new(executor);
3952    let project_dir = Path::new(path!("/project"));
3953
3954    // Mirrors the layout used by tools that keep working copies of the
3955    // repository inside the repository itself: a bare clone in
3956    // `.scratch/clones` and a linked worktree of that clone in
3957    // `.scratch/worktrees`, both hidden via the outer repository's
3958    // `.git/info/exclude` rather than a `.gitignore`.
3959    fs.insert_tree(
3960        project_dir,
3961        json!({
3962            ".git": {
3963                "info": {
3964                    "exclude": "/.scratch/worktrees/\n/.scratch/clones/\n"
3965                }
3966            },
3967            "src": {
3968                "main.rs": "fn main() {}",
3969            },
3970            ".scratch": {
3971                "clones": {
3972                    "abc": {
3973                        "project.git": {
3974                            "HEAD": "ref: refs/heads/main",
3975                            "worktrees": {
3976                                "project": {
3977                                    "HEAD": "ref: refs/heads/feature",
3978                                    "commondir": "../..",
3979                                }
3980                            }
3981                        }
3982                    }
3983                },
3984                "worktrees": {
3985                    "abc": {
3986                        "project": {
3987                            ".git": "gitdir: ../../../clones/abc/project.git/worktrees/project",
3988                            "src": {
3989                                "main.rs": "fn main() {}",
3990                            }
3991                        }
3992                    }
3993                }
3994            }
3995        }),
3996    )
3997    .await;
3998
3999    let worktree = Worktree::local(
4000        project_dir,
4001        true,
4002        fs.clone(),
4003        Default::default(),
4004        true,
4005        WorktreeId::from_proto(0),
4006        &mut cx.to_async(),
4007    )
4008    .await
4009    .unwrap();
4010    worktree
4011        .update(cx, |worktree, _| {
4012            worktree.as_local().unwrap().scan_complete()
4013        })
4014        .await;
4015    cx.run_until_parked();
4016
4017    // After the initial scan, both excluded directories are ignored.
4018    worktree.update(cx, |worktree, _cx| {
4019        check_worktree_entries(
4020            worktree,
4021            WorktreeExpectations {
4022                ignored_paths: &[".scratch/clones", ".scratch/worktrees"],
4023                tracked_paths: &["src/main.rs"],
4024                ..Default::default()
4025            },
4026        );
4027    });
4028
4029    // Load a file within the excluded nested repository, as happens when a
4030    // search that includes ignored files runs or when the file is opened.
4031    worktree
4032        .update(cx, |worktree, _| {
4033            worktree.as_local().unwrap().refresh_entries_for_paths(vec![
4034                rel_path(".scratch/worktrees/abc/project/src/main.rs").into(),
4035            ])
4036        })
4037        .recv()
4038        .await;
4039    cx.run_until_parked();
4040
4041    // The nested repository's own `.git` must not cause the outer
4042    // repository's `info/exclude` rules to be dropped.
4043    worktree.update(cx, |worktree, _cx| {
4044        check_worktree_entries(
4045            worktree,
4046            WorktreeExpectations {
4047                ignored_paths: &[
4048                    ".scratch/worktrees/abc",
4049                    ".scratch/worktrees/abc/project",
4050                    ".scratch/worktrees/abc/project/src",
4051                    ".scratch/worktrees/abc/project/src/main.rs",
4052                ],
4053                tracked_paths: &["src/main.rs"],
4054                ..Default::default()
4055            },
4056        );
4057    });
4058
4059    // A file written inside the loaded nested repository (e.g. by a tool
4060    // working in the clone) must also be ignored.
4061    fs.save(
4062        path!("/project/.scratch/worktrees/abc/project/src/generated.rs").as_ref(),
4063        &"fn generated() {}".into(),
4064        Default::default(),
4065    )
4066    .await
4067    .unwrap();
4068    cx.run_until_parked();
4069
4070    worktree.update(cx, |worktree, _cx| {
4071        check_worktree_entries(
4072            worktree,
4073            WorktreeExpectations {
4074                ignored_paths: &[".scratch/worktrees/abc/project/src/generated.rs"],
4075                ..Default::default()
4076            },
4077        );
4078    });
4079
4080    // Nothing under the excluded directories is visible to a traversal that
4081    // skips ignored entries, which is what project search uses.
4082    worktree.update(cx, |worktree, _cx| {
4083        let unignored_entries = worktree
4084            .entries(false, 0)
4085            .filter(|entry| {
4086                entry.path.starts_with(rel_path(".scratch"))
4087                    && entry.path.as_ref() != rel_path(".scratch")
4088            })
4089            .map(|entry| entry.path.clone())
4090            .collect::<Vec<_>>();
4091        assert_eq!(
4092            unignored_entries,
4093            Vec::<Arc<RelPath>>::new(),
4094            "entries under the excluded .scratch directories leaked into the unignored traversal",
4095        );
4096    });
4097}
4098
4099#[derive(Default)]
4100struct WorktreeExpectations {
4101    excluded_paths: &'static [&'static str],
4102    ignored_paths: &'static [&'static str],
4103    tracked_paths: &'static [&'static str],
4104    included_paths: &'static [&'static str],
4105}
4106
4107#[track_caller]
4108fn check_worktree_entries(tree: &Worktree, expectations: WorktreeExpectations) {
4109    for path in expectations.excluded_paths {
4110        let entry = tree.entry_for_path(rel_path(path));
4111        assert!(
4112            entry.is_none(),
4113            "expected path '{path}' to be excluded, but got entry: {entry:?}",
4114        );
4115    }
4116    for path in expectations.ignored_paths {
4117        let entry = tree
4118            .entry_for_path(rel_path(path))
4119            .unwrap_or_else(|| panic!("Missing entry for expected ignored path '{path}'"));
4120        assert!(
4121            entry.is_ignored,
4122            "expected path '{path}' to be ignored, but got entry: {entry:?}",
4123        );
4124    }
4125    for path in expectations.tracked_paths {
4126        let entry = tree
4127            .entry_for_path(rel_path(path))
4128            .unwrap_or_else(|| panic!("Missing entry for expected tracked path '{path}'"));
4129        assert!(
4130            !entry.is_ignored || entry.is_always_included,
4131            "expected path '{path}' to be tracked, but got entry: {entry:?}",
4132        );
4133    }
4134    for path in expectations.included_paths {
4135        let entry = tree
4136            .entry_for_path(rel_path(path))
4137            .unwrap_or_else(|| panic!("Missing entry for expected included path '{path}'"));
4138        assert!(
4139            entry.is_always_included,
4140            "expected path '{path}' to always be included, but got entry: {entry:?}",
4141        );
4142    }
4143}
4144
4145#[gpui::test]
4146async fn test_root_repo_common_dir_for_relative_gitdir(
4147    executor: BackgroundExecutor,
4148    cx: &mut TestAppContext,
4149) {
4150    init_test(cx);
4151
4152    let fs = FakeFs::new(executor);
4153    fs.insert_tree(
4154        path!("/repo"),
4155        json!({
4156            ".git": {
4157                "HEAD": "ref: refs/heads/main",
4158                "config": "[core]\n\tbare = false\n",
4159                "info": {
4160                    "exclude": "ignored.txt\n",
4161                },
4162                "worktrees": {
4163                    "feature-a": {
4164                        "HEAD": "ref: refs/heads/feature-a",
4165                        "commondir": "../..",
4166                        "gitdir": "/repo/feature-a/.git",
4167                    },
4168                },
4169            },
4170            "feature-a": {
4171                ".git": "gitdir: ../.git/worktrees/feature-a",
4172                "file.txt": "content",
4173                "ignored.txt": "ignored",
4174                "subdir": {
4175                    "file.txt": "content",
4176                    "ignored.txt": "ignored",
4177                },
4178            },
4179        }),
4180    )
4181    .await;
4182
4183    let feature_tree = Worktree::local(
4184        path!("/repo/feature-a").as_ref(),
4185        true,
4186        fs.clone(),
4187        Arc::default(),
4188        true,
4189        WorktreeId::from_proto(0),
4190        &mut cx.to_async(),
4191    )
4192    .await
4193    .unwrap();
4194    feature_tree
4195        .update(cx, |tree, _| tree.as_local().unwrap().scan_complete())
4196        .await;
4197    cx.run_until_parked();
4198
4199    feature_tree.read_with(cx, |tree, _| {
4200        assert_eq!(
4201            tree.snapshot()
4202                .root_repo_common_dir()
4203                .map(|path| path.as_ref()),
4204            Some(Path::new(path!("/repo/.git"))),
4205        );
4206        check_worktree_entries(
4207            tree,
4208            WorktreeExpectations {
4209                ignored_paths: &["ignored.txt"],
4210                tracked_paths: &["file.txt"],
4211                ..Default::default()
4212            },
4213        );
4214    });
4215
4216    let nested_tree = Worktree::local(
4217        path!("/repo/feature-a/subdir").as_ref(),
4218        true,
4219        fs.clone(),
4220        Arc::default(),
4221        true,
4222        WorktreeId::from_proto(1),
4223        &mut cx.to_async(),
4224    )
4225    .await
4226    .unwrap();
4227    nested_tree
4228        .update(cx, |tree, _| tree.as_local().unwrap().scan_complete())
4229        .await;
4230    cx.run_until_parked();
4231
4232    nested_tree.read_with(cx, |tree, _| {
4233        check_worktree_entries(
4234            tree,
4235            WorktreeExpectations {
4236                ignored_paths: &["ignored.txt"],
4237                tracked_paths: &["file.txt"],
4238                ..Default::default()
4239            },
4240        );
4241    });
4242
4243    fs.write(
4244        Path::new(path!("/repo/.git")).join(REPO_EXCLUDE).as_ref(),
4245        "file.txt\n".as_bytes(),
4246    )
4247    .await
4248    .unwrap();
4249    cx.run_until_parked();
4250
4251    feature_tree.read_with(cx, |tree, _| {
4252        check_worktree_entries(
4253            tree,
4254            WorktreeExpectations {
4255                ignored_paths: &["file.txt", "subdir/file.txt"],
4256                tracked_paths: &["ignored.txt", "subdir/ignored.txt"],
4257                ..Default::default()
4258            },
4259        );
4260    });
4261    nested_tree.read_with(cx, |tree, _| {
4262        check_worktree_entries(
4263            tree,
4264            WorktreeExpectations {
4265                ignored_paths: &["file.txt"],
4266                tracked_paths: &["ignored.txt"],
4267                ..Default::default()
4268            },
4269        );
4270    });
4271}
4272
4273#[gpui::test]
4274async fn test_root_repo_common_dir(executor: BackgroundExecutor, cx: &mut TestAppContext) {
4275    init_test(cx);
4276
4277    use git::repository::Worktree as GitWorktree;
4278
4279    let fs = FakeFs::new(executor);
4280
4281    // Set up a main repo and a linked worktree pointing back to it.
4282    fs.insert_tree(
4283        path!("/main_repo"),
4284        json!({
4285            ".git": {},
4286            "file.txt": "content",
4287        }),
4288    )
4289    .await;
4290    fs.add_linked_worktree_for_repo(
4291        Path::new(path!("/main_repo/.git")),
4292        false,
4293        GitWorktree {
4294            path: PathBuf::from(path!("/linked_worktree")),
4295            ref_name: Some("refs/heads/feature".into()),
4296            sha: "abc123".into(),
4297            is_main: false,
4298            is_bare: false,
4299        },
4300    )
4301    .await;
4302    fs.write(
4303        path!("/linked_worktree/file.txt").as_ref(),
4304        "content".as_bytes(),
4305    )
4306    .await
4307    .unwrap();
4308
4309    let tree = Worktree::local(
4310        path!("/linked_worktree").as_ref(),
4311        true,
4312        fs.clone(),
4313        Arc::default(),
4314        true,
4315        WorktreeId::from_proto(0),
4316        &mut cx.to_async(),
4317    )
4318    .await
4319    .unwrap();
4320    tree.update(cx, |tree, _| tree.as_local().unwrap().scan_complete())
4321        .await;
4322    cx.run_until_parked();
4323
4324    // For a linked worktree, root_repo_common_dir should point to the
4325    // main repo's .git, not the worktree-specific git directory.
4326    tree.read_with(cx, |tree, _| {
4327        assert_eq!(
4328            tree.snapshot().root_repo_common_dir().map(|p| p.as_ref()),
4329            Some(Path::new(path!("/main_repo/.git"))),
4330        );
4331    });
4332
4333    let event_count: Rc<Cell<usize>> = Rc::new(Cell::new(0));
4334    tree.update(cx, {
4335        let event_count = event_count.clone();
4336        |_, cx| {
4337            cx.subscribe(&cx.entity(), move |_, _, event, _| {
4338                if matches!(event, Event::UpdatedRootRepoCommonDir { .. }) {
4339                    event_count.set(event_count.get() + 1);
4340                }
4341            })
4342            .detach();
4343        }
4344    });
4345
4346    // Remove .git — root_repo_common_dir should become None.
4347    fs.remove_file(
4348        &PathBuf::from(path!("/linked_worktree/.git")),
4349        Default::default(),
4350    )
4351    .await
4352    .unwrap();
4353    tree.flush_fs_events(cx).await;
4354
4355    tree.read_with(cx, |tree, _| {
4356        assert_eq!(tree.snapshot().root_repo_common_dir(), None);
4357    });
4358    assert_eq!(
4359        event_count.get(),
4360        1,
4361        "should have emitted UpdatedRootRepoCommonDir on removal"
4362    );
4363}
4364
4365#[gpui::test]
4366async fn test_invisible_worktree_does_not_track_ancestor_git_repository(
4367    executor: BackgroundExecutor,
4368    cx: &mut TestAppContext,
4369) {
4370    init_test(cx);
4371
4372    let fs = FakeFs::new(executor);
4373    fs.insert_tree(
4374        path!("/repo"),
4375        json!({
4376            ".git": {},
4377            "project": {
4378                "file.txt": "content",
4379            },
4380        }),
4381    )
4382    .await;
4383
4384    let worktree = Worktree::local(
4385        path!("/repo/project").as_ref(),
4386        false,
4387        fs.clone(),
4388        Arc::default(),
4389        true,
4390        WorktreeId::from_proto(0),
4391        &mut cx.to_async(),
4392    )
4393    .await
4394    .unwrap();
4395    worktree
4396        .update(cx, |worktree, _| {
4397            worktree.as_local().unwrap().scan_complete()
4398        })
4399        .await;
4400    cx.run_until_parked();
4401
4402    worktree.read_with(cx, |worktree, _| {
4403        let local_worktree = worktree.as_local().unwrap();
4404        assert!(local_worktree.repositories().is_empty());
4405        assert_eq!(local_worktree.root_repo_common_dir(), None);
4406    });
4407}
4408
4409#[gpui::test]
4410async fn test_linked_worktree_gitfile_event_preserves_repo(
4411    executor: BackgroundExecutor,
4412    cx: &mut TestAppContext,
4413) {
4414    // Regression test: in a linked worktree, `.git` is a file (containing
4415    // "gitdir: ..."), not a directory. When the background scanner receives
4416    // a filesystem event for a path inside the main repo's `.git` directory
4417    // (which it watches via the commondir), the ancestor-walking code in
4418    // `process_events` calls `is_git_dir` on each ancestor. If `is_git_dir`
4419    // treats `.git` files the same as `.git` directories, it incorrectly
4420    // identifies the gitfile as a git dir, adds it to `dot_git_abs_paths`,
4421    // and `update_git_repositories` panics because the path is outside the
4422    // worktree root.
4423    init_test(cx);
4424    use git::repository::Worktree as GitWorktree;
4425
4426    let fs = FakeFs::new(executor);
4427    fs.insert_tree(path!("/main_repo"), json!({ ".git": {}, "file.txt": "" }))
4428        .await;
4429    fs.add_linked_worktree_for_repo(
4430        Path::new(path!("/main_repo/.git")),
4431        false,
4432        GitWorktree {
4433            path: PathBuf::from(path!("/linked_worktree")),
4434            ref_name: Some("refs/heads/feature".into()),
4435            sha: "abc123".into(),
4436            is_main: false,
4437            is_bare: false,
4438        },
4439    )
4440    .await;
4441    fs.write(path!("/linked_worktree/file.txt").as_ref(), b"content")
4442        .await
4443        .unwrap();
4444
4445    let tree = Worktree::local(
4446        path!("/linked_worktree").as_ref(),
4447        true,
4448        fs.clone(),
4449        Arc::default(),
4450        true,
4451        WorktreeId::from_proto(0),
4452        &mut cx.to_async(),
4453    )
4454    .await
4455    .unwrap();
4456    tree.update(cx, |tree, _| tree.as_local().unwrap().scan_complete())
4457        .await;
4458    cx.run_until_parked();
4459
4460    // Overwrite the .git gitfile with garbage to trigger an event for the
4461    // gitfile path itself, which only matches `dot_git_abs_path`.
4462    fs.write(path!("/linked_worktree/.git").as_ref(), b"garbage")
4463        .await
4464        .unwrap();
4465    tree.flush_fs_events(cx).await;
4466
4467    // The worktree should still be intact.
4468    tree.read_with(cx, |tree, _| {
4469        assert_eq!(
4470            tree.snapshot().root_repo_common_dir().map(|p| p.as_ref()),
4471            Some(Path::new(path!("/main_repo/.git"))),
4472            "linked worktree repo should survive a gitfile change event"
4473        );
4474    });
4475}
4476
4477#[gpui::test]
4478async fn test_shared_common_dir_event_updates_all_repositories(
4479    executor: BackgroundExecutor,
4480    cx: &mut TestAppContext,
4481) {
4482    // A main checkout and one of its linked worktrees can both live inside the
4483    // same project worktree, sharing a common git directory. An event in that
4484    // common directory (e.g. a ref update) must refresh every repository that
4485    // reads from it, not just the first match.
4486    init_test(cx);
4487
4488    use git::repository::Worktree as GitWorktree;
4489
4490    let fs = FakeFs::new(executor.clone());
4491    fs.insert_tree(
4492        path!("/project"),
4493        json!({
4494            "main_repo": {
4495                ".git": {},
4496                "file.txt": "content",
4497            },
4498        }),
4499    )
4500    .await;
4501    fs.add_linked_worktree_for_repo(
4502        Path::new(path!("/project/main_repo/.git")),
4503        false,
4504        GitWorktree {
4505            path: PathBuf::from(path!("/project/linked")),
4506            ref_name: Some("refs/heads/feature".into()),
4507            sha: "abc123".into(),
4508            is_main: false,
4509            is_bare: false,
4510        },
4511    )
4512    .await;
4513
4514    let tree = Worktree::local(
4515        path!("/project").as_ref(),
4516        true,
4517        fs.clone(),
4518        Arc::default(),
4519        true,
4520        WorktreeId::from_proto(0),
4521        &mut cx.to_async(),
4522    )
4523    .await
4524    .unwrap();
4525    tree.update(cx, |tree, _| tree.as_local().unwrap().scan_complete())
4526        .await;
4527    cx.run_until_parked();
4528
4529    let mut events = cx.events(&tree);
4530    fs.emit_fs_event(
4531        path!("/project/main_repo/.git/refs/heads/main"),
4532        Some(PathEventKind::Changed),
4533    );
4534    executor.run_until_parked();
4535
4536    let mut updated_work_dirs = Vec::new();
4537    while let Ok(event) = events.try_recv() {
4538        if let Event::UpdatedGitRepositories(updates) = event {
4539            updated_work_dirs.extend(
4540                updates
4541                    .iter()
4542                    .filter_map(|update| update.new_work_directory_abs_path.clone()),
4543            );
4544        }
4545    }
4546    updated_work_dirs.sort();
4547    assert_eq!(
4548        updated_work_dirs,
4549        [
4550            Arc::from(Path::new(path!("/project/linked"))),
4551            Arc::from(Path::new(path!("/project/main_repo"))),
4552        ],
4553        "a ref update in the shared common dir should refresh both repositories"
4554    );
4555}
4556
4557#[gpui::test]
4558async fn test_noisy_dot_git_events_do_not_emit_git_repo_update(
4559    executor: BackgroundExecutor,
4560    cx: &mut TestAppContext,
4561) {
4562    // Events for object database writes, hook files, lock files, and the
4563    // reflogs of HEAD/branches/remote-tracking branches carry no git state
4564    // changes that Zed cares about beyond what the accompanying ref or index
4565    // events already convey, so they must not trigger a git metadata rescan.
4566    // The stash reflog and ref updates themselves must still trigger one.
4567    //
4568    init_test(cx);
4569
4570    use git::repository::Worktree as GitWorktree;
4571
4572    let fs = FakeFs::new(executor);
4573
4574    fs.insert_tree(
4575        path!("/main_repo"),
4576        json!({
4577            ".git": {},
4578            "file.txt": "content",
4579        }),
4580    )
4581    .await;
4582    fs.add_linked_worktree_for_repo(
4583        Path::new(path!("/main_repo/.git")),
4584        false,
4585        GitWorktree {
4586            path: PathBuf::from(path!("/linked_worktree")),
4587            ref_name: Some("refs/heads/feature".into()),
4588            sha: "abc123".into(),
4589            is_main: false,
4590            is_bare: false,
4591        },
4592    )
4593    .await;
4594    fs.write(
4595        path!("/linked_worktree/file.txt").as_ref(),
4596        "content".as_bytes(),
4597    )
4598    .await
4599    .unwrap();
4600
4601    let tree = Worktree::local(
4602        path!("/linked_worktree").as_ref(),
4603        true,
4604        fs.clone(),
4605        Arc::default(),
4606        true,
4607        WorktreeId::from_proto(0),
4608        &mut cx.to_async(),
4609    )
4610    .await
4611    .unwrap();
4612    tree.update(cx, |tree, _| tree.as_local().unwrap().scan_complete())
4613        .await;
4614    cx.run_until_parked();
4615
4616    let repo_update_count: Rc<Cell<usize>> = Rc::new(Cell::new(0));
4617    tree.update(cx, {
4618        let repo_update_count = repo_update_count.clone();
4619        |_, cx| {
4620            cx.subscribe(&cx.entity(), move |_, _, event, _| {
4621                if matches!(event, Event::UpdatedGitRepositories(_)) {
4622                    repo_update_count.set(repo_update_count.get() + 1);
4623                }
4624            })
4625            .detach();
4626        }
4627    });
4628
4629    let skipped_paths = [
4630        // Standard common git dir skipped paths
4631        path!("/main_repo/.git/objects/aa/bbccddee"),
4632        path!("/main_repo/.git/objects/pack/pack-1234.pack"),
4633        path!("/main_repo/.git/hooks/pre-commit"),
4634        path!("/main_repo/.git/logs/HEAD"),
4635        path!("/main_repo/.git/logs/refs/heads/main"),
4636        path!("/main_repo/.git/logs/refs/remotes/origin/main"),
4637        path!("/main_repo/.git/logs/refs/tags/v1.0"),
4638        path!("/main_repo/.git/rebase-merge/done"),
4639        path!("/main_repo/.git/rebase-apply/onto"),
4640        path!("/main_repo/.git/sequencer/todo"),
4641        path!("/main_repo/.git/index.lock"),
4642        path!("/main_repo/.git/refs/heads/main.lock"),
4643        path!("/main_repo/.git/COMMIT_EDITMSG"),
4644        path!("/main_repo/.git/packed-refs.new"),
4645        path!("/main_repo/.git/config.new"),
4646        path!("/main_repo/.git/index.new"),
4647        path!("/main_repo/.git/index-abc123.tmp"),
4648        path!("/main_repo/.git/FETCH_HEAD"),
4649        path!("/main_repo/.git/ORIG_HEAD"),
4650        path!("/main_repo/.git/BISECT_LOG"),
4651        path!("/main_repo/.git/info/refs"),
4652        path!("/main_repo/.git/info/refs_lzOf51"),
4653        path!("/main_repo/.git/gc.pid"),
4654        // Linked-worktree specific skipped paths
4655        path!("/main_repo/.git/worktrees/feature/index.lock"),
4656    ];
4657    for path in skipped_paths {
4658        fs.emit_fs_event(path, Some(PathEventKind::Changed));
4659        cx.run_until_parked();
4660        assert_eq!(
4661            repo_update_count.get(),
4662            0,
4663            "event for {path} should not emit UpdatedGitRepositories"
4664        );
4665    }
4666
4667    let rescan_paths = [
4668        // Standard common git dir rescan paths
4669        path!("/main_repo/.git/logs/refs/stash"),
4670        path!("/main_repo/.git/refs/heads/main"),
4671        path!("/main_repo/.git/info/exclude"),
4672        path!("/main_repo/.git/refs/heads/branch.new"),
4673        path!("/main_repo/.git/refs/heads/branch.tmp"),
4674        // Linked-worktree worktree-specific rescan paths
4675        path!("/main_repo/.git/worktrees/feature/index"),
4676        path!("/main_repo/.git/worktrees/feature/HEAD"),
4677    ];
4678    for path in rescan_paths {
4679        let count_before = repo_update_count.get();
4680        fs.emit_fs_event(path, Some(PathEventKind::Changed));
4681        cx.run_until_parked();
4682        assert!(
4683            repo_update_count.get() > count_before,
4684            "event for {path} should emit UpdatedGitRepositories"
4685        );
4686    }
4687}
4688
4689#[gpui::test]
4690async fn test_watcher_overflow_rescan_reloads_git_state(cx: &mut TestAppContext) {
4691    // When the OS watch queue overflows, pending events are dropped and the
4692    // watcher reports only a `Rescan` event for the worktree root. The dropped
4693    // events may have included changes inside `.git`, so the rescan must
4694    // trigger a git state reload even though no `.git` event is ever seen.
4695    init_test(cx);
4696    let fs = FakeFs::new(cx.background_executor.clone());
4697    fs.insert_tree(
4698        path!("/root"),
4699        json!({
4700            ".git": {},
4701            "file.txt": "content",
4702        }),
4703    )
4704    .await;
4705
4706    let tree = Worktree::local(
4707        path!("/root").as_ref(),
4708        true,
4709        fs.clone(),
4710        Default::default(),
4711        true,
4712        WorktreeId::from_proto(0),
4713        &mut cx.to_async(),
4714    )
4715    .await
4716    .unwrap();
4717    tree.update(cx, |tree, _| tree.as_local().unwrap().scan_complete())
4718        .await;
4719    cx.run_until_parked();
4720
4721    let repo_update_count: Rc<Cell<usize>> = Rc::new(Cell::new(0));
4722    tree.update(cx, {
4723        let repo_update_count = repo_update_count.clone();
4724        |_, cx| {
4725            cx.subscribe(&cx.entity(), move |_, _, event, _| {
4726                if matches!(event, Event::UpdatedGitRepositories(_)) {
4727                    repo_update_count.set(repo_update_count.get() + 1);
4728                }
4729            })
4730            .detach();
4731        }
4732    });
4733
4734    // A git state change occurs while events are queued but undelivered, and
4735    // is then lost to a watcher overflow: the only event the worktree ever
4736    // receives is the root rescan.
4737    fs.pause_events();
4738    fs.set_head_for_repo(
4739        path!("/root/.git").as_ref(),
4740        &[("file.txt", "content".into())],
4741        "sha-after-overflow",
4742    );
4743    fs.simulate_watcher_overflow(path!("/root"));
4744    fs.unpause_events_and_flush();
4745    cx.run_until_parked();
4746
4747    assert!(
4748        repo_update_count.get() > 0,
4749        "a watcher overflow rescan should reload git state, since the dropped \
4750         events may have included .git changes"
4751    );
4752}
4753
4754#[gpui::test]
4755async fn test_git_update_in_same_batch_as_rescan_is_not_lost(cx: &mut TestAppContext) {
4756    // When a `.git` event and a watcher rescan arrive in the same batch,
4757    // `update_git_repositories` stamps the repository's `git_dir_scan_id`, but
4758    // the rescan then re-inserts the repository entry. If the re-insertion
4759    // resets `git_dir_scan_id`, the stamp is lost before the snapshot diff can
4760    // observe it. The git update must still be signaled via
4761    // `UpdatedGitRepositories`.
4762    init_test(cx);
4763    let fs = FakeFs::new(cx.background_executor.clone());
4764    fs.insert_tree(
4765        path!("/root"),
4766        json!({
4767            ".git": {},
4768            "file.txt": "content",
4769        }),
4770    )
4771    .await;
4772
4773    let tree = Worktree::local(
4774        path!("/root").as_ref(),
4775        true,
4776        fs.clone(),
4777        Default::default(),
4778        true,
4779        WorktreeId::from_proto(0),
4780        &mut cx.to_async(),
4781    )
4782    .await
4783    .unwrap();
4784    tree.update(cx, |tree, _| tree.as_local().unwrap().scan_complete())
4785        .await;
4786    cx.run_until_parked();
4787
4788    let repo_update_count: Rc<Cell<usize>> = Rc::new(Cell::new(0));
4789    tree.update(cx, {
4790        let repo_update_count = repo_update_count.clone();
4791        |_, cx| {
4792            cx.subscribe(&cx.entity(), move |_, _, event, _| {
4793                if matches!(event, Event::UpdatedGitRepositories(_)) {
4794                    repo_update_count.set(repo_update_count.get() + 1);
4795                }
4796            })
4797            .detach();
4798        }
4799    });
4800
4801    // Deliver the git change and the rescan in a single batch, as happens when
4802    // a rescan arrives while other events are still queued.
4803    fs.pause_events();
4804    fs.set_head_for_repo(
4805        path!("/root/.git").as_ref(),
4806        &[("file.txt", "content".into())],
4807        "sha-with-rescan",
4808    );
4809    fs.emit_fs_event(path!("/root"), Some(PathEventKind::Rescan));
4810    fs.unpause_events_and_flush();
4811    cx.run_until_parked();
4812
4813    assert!(
4814        repo_update_count.get() > 0,
4815        "a git update processed in the same batch as a rescan should still be \
4816         signaled via UpdatedGitRepositories"
4817    );
4818}
4819
4820#[gpui::test]
4821async fn test_linked_worktree_event_in_unregistered_common_git_dir_does_not_panic(
4822    executor: BackgroundExecutor,
4823    cx: &mut TestAppContext,
4824) {
4825    // Regression test: a rescan event on a linked worktree's commondir
4826    // must not panic when the worktree's repository has already been
4827    // unregistered from `git_repositories`.
4828    init_test(cx);
4829
4830    use git::repository::Worktree as GitWorktree;
4831
4832    let fs = FakeFs::new(executor);
4833
4834    fs.insert_tree(
4835        path!("/main_repo"),
4836        json!({
4837            ".git": {},
4838            "file.txt": "content",
4839        }),
4840    )
4841    .await;
4842    fs.add_linked_worktree_for_repo(
4843        Path::new(path!("/main_repo/.git")),
4844        false,
4845        GitWorktree {
4846            path: PathBuf::from(path!("/linked_worktree")),
4847            ref_name: Some("refs/heads/feature".into()),
4848            sha: "abc123".into(),
4849            is_main: false,
4850            is_bare: false,
4851        },
4852    )
4853    .await;
4854    fs.write(
4855        path!("/linked_worktree/file.txt").as_ref(),
4856        "content".as_bytes(),
4857    )
4858    .await
4859    .unwrap();
4860
4861    let tree = Worktree::local(
4862        path!("/linked_worktree").as_ref(),
4863        true,
4864        fs.clone(),
4865        Arc::default(),
4866        true,
4867        WorktreeId::from_proto(0),
4868        &mut cx.to_async(),
4869    )
4870    .await
4871    .unwrap();
4872    tree.update(cx, |tree, _| tree.as_local().unwrap().scan_complete())
4873        .await;
4874    cx.run_until_parked();
4875
4876    // Unregister the linked worktree's repository by removing its gitfile.
4877    fs.remove_file(
4878        Path::new(path!("/linked_worktree/.git")),
4879        Default::default(),
4880    )
4881    .await
4882    .unwrap();
4883    tree.flush_fs_events(cx).await;
4884
4885    // Deliver the kind of Rescan event `FsWatcher` emits when the kernel
4886    // signals `need_rescan` for the commondir.
4887    fs.emit_fs_event(path!("/main_repo/.git"), Some(fs::PathEventKind::Rescan));
4888    cx.run_until_parked();
4889    tree.flush_fs_events(cx).await;
4890}
4891
4892#[gpui::test]
4893async fn test_dot_git_dir_event_does_not_suppress_children(
4894    executor: BackgroundExecutor,
4895    cx: &mut TestAppContext,
4896) {
4897    // On Windows, modifying a file inside .git causes ReadDirectoryChangesW to also emit
4898    // a Modify event for the .git directory itself (because its last-write timestamp changes).
4899    // When these events arrive in the same batch, a naive ancestor-based dedup would collapse
4900    // all child events into the .git directory event, losing the information about which
4901    // specific files changed. This test verifies that the git-related event processing happens
4902    // before the dedup, so that meaningful .git child events still trigger UpdatedGitRepositories.
4903    init_test(cx);
4904
4905    let fs = FakeFs::new(executor.clone());
4906    let project_dir = Path::new(path!("/project"));
4907    fs.insert_tree(
4908        project_dir,
4909        json!({
4910            ".git": {},
4911            "src": {
4912                "main.rs": "fn main() {}",
4913            },
4914        }),
4915    )
4916    .await;
4917
4918    let worktree = Worktree::local(
4919        project_dir,
4920        true,
4921        fs.clone(),
4922        Default::default(),
4923        true,
4924        WorktreeId::from_proto(0),
4925        &mut cx.to_async(),
4926    )
4927    .await
4928    .unwrap();
4929    worktree
4930        .update(cx, |worktree, _| {
4931            worktree.as_local().unwrap().scan_complete()
4932        })
4933        .await;
4934    cx.run_until_parked();
4935
4936    let dot_git = project_dir.join(DOT_GIT);
4937
4938    // Case 1: Event for .git/index.lock only should NOT emit UpdatedGitRepositories
4939    // (index.lock is in the skipped files list)
4940    {
4941        let mut events = cx.events(&worktree);
4942        fs.pause_events();
4943        fs.emit_fs_event(dot_git.join("index.lock"), Some(PathEventKind::Created));
4944        fs.unpause_events_and_flush();
4945        executor.run_until_parked();
4946
4947        let got_git_update = drain_git_repo_updates(&mut events);
4948        assert!(
4949            !got_git_update,
4950            "should NOT emit UpdatedGitRepositories when .git batch only contains index.lock"
4951        );
4952    }
4953
4954    // Case 2: Event for just .git (bare directory event) should emit UpdatedGitRepositories
4955    {
4956        let mut events = cx.events(&worktree);
4957        fs.pause_events();
4958        fs.emit_fs_event(dot_git.clone(), Some(PathEventKind::Changed));
4959        fs.unpause_events_and_flush();
4960        executor.run_until_parked();
4961
4962        let got_git_update = drain_git_repo_updates(&mut events);
4963        assert!(
4964            got_git_update,
4965            "should emit UpdatedGitRepositories for a bare .git directory event"
4966        );
4967    }
4968
4969    // Case 3: Events for .git AND .git/index should emit UpdatedGitRepositories
4970    {
4971        let mut events = cx.events(&worktree);
4972        fs.pause_events();
4973        fs.emit_fs_event(dot_git.clone(), Some(PathEventKind::Changed));
4974        fs.emit_fs_event(dot_git.join("index"), Some(PathEventKind::Changed));
4975        fs.unpause_events_and_flush();
4976        executor.run_until_parked();
4977
4978        let got_git_update = drain_git_repo_updates(&mut events);
4979        assert!(
4980            got_git_update,
4981            "should emit UpdatedGitRepositories when .git batch contains index"
4982        );
4983    }
4984
4985    // Case 4: Event for .git/index only should emit UpdatedGitRepositories
4986    {
4987        let mut events = cx.events(&worktree);
4988        fs.pause_events();
4989        fs.emit_fs_event(dot_git.join("index"), Some(PathEventKind::Changed));
4990        fs.unpause_events_and_flush();
4991        executor.run_until_parked();
4992
4993        let got_git_update = drain_git_repo_updates(&mut events);
4994        assert!(
4995            got_git_update,
4996            "should emit UpdatedGitRepositories for a .git/index event"
4997        );
4998    }
4999
5000    {
5001        let mut events = cx.events(&worktree);
5002        fs.pause_events();
5003        fs.emit_fs_event(dot_git, Some(PathEventKind::Rescan));
5004        fs.unpause_events_and_flush();
5005        executor.run_until_parked();
5006
5007        let got_git_update = drain_git_repo_updates(&mut events);
5008        assert!(
5009            got_git_update,
5010            "should emit UpdatedGitRepositories for a .git rescan event"
5011        );
5012    }
5013
5014    {
5015        let mut events = cx.events(&worktree);
5016        fs.pause_events();
5017        fs.emit_fs_event(project_dir, Some(PathEventKind::Rescan));
5018        fs.unpause_events_and_flush();
5019        executor.run_until_parked();
5020
5021        let got_git_update = drain_git_repo_updates(&mut events);
5022        assert!(
5023            got_git_update,
5024            "should emit UpdatedGitRepositories for a .git rescan event"
5025        );
5026    }
5027}
5028
5029#[gpui::test]
5030async fn test_ref_updates_in_dot_git_subdirectories_are_detected(cx: &mut TestAppContext) {
5031    // On Linux and FreeBSD the native file watcher is non-recursive: watching `.git`
5032    // does not deliver events for files nested below it, like the loose refs that git
5033    // updates on commit, fetch, and branch operations. The worktree must watch the
5034    // `refs` tree explicitly, including directories created after the initial scan.
5035    init_test(cx);
5036    cx.executor().allow_parking();
5037
5038    let dir = TempTree::new(json!({
5039        ".git": {},
5040        "a.txt": "a-contents",
5041    }));
5042    std::fs::write(
5043        dir.path().join(".git/refs/heads/main"),
5044        "0000000000000000000000000000000000000000\n",
5045    )
5046    .unwrap();
5047
5048    let tree = Worktree::local(
5049        dir.path(),
5050        true,
5051        Arc::new(RealFs::new(None, cx.executor())),
5052        Default::default(),
5053        true,
5054        WorktreeId::from_proto(0),
5055        &mut cx.to_async(),
5056    )
5057    .await
5058    .unwrap();
5059    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
5060        .await;
5061    tree.flush_fs_events(cx).await;
5062
5063    let mut events = cx.events(&tree);
5064    std::fs::write(
5065        dir.path().join(".git/refs/heads/main"),
5066        "1111111111111111111111111111111111111111\n",
5067    )
5068    .unwrap();
5069    expect_git_repo_update(&mut events, cx, "updating a loose ref").await;
5070
5071    std::fs::create_dir_all(dir.path().join(".git/refs/remotes/origin")).unwrap();
5072    expect_git_repo_update(&mut events, cx, "creating a directory under refs").await;
5073    tree.flush_fs_events(cx).await;
5074    drain_git_repo_updates(&mut events);
5075
5076    std::fs::write(
5077        dir.path().join(".git/refs/remotes/origin/main"),
5078        "2222222222222222222222222222222222222222\n",
5079    )
5080    .unwrap();
5081    expect_git_repo_update(
5082        &mut events,
5083        cx,
5084        "updating a ref in a directory created after the initial scan",
5085    )
5086    .await;
5087}
5088
5089async fn expect_git_repo_update(
5090    events: &mut futures::channel::mpsc::UnboundedReceiver<Event>,
5091    cx: &mut TestAppContext,
5092    description: &str,
5093) {
5094    let mut elapsed = std::time::Duration::ZERO;
5095    let timeout = std::time::Duration::from_secs(10);
5096    let poll_interval = std::time::Duration::from_millis(50);
5097    loop {
5098        match events.try_recv() {
5099            Ok(Event::UpdatedGitRepositories(_)) => return,
5100            Ok(_) => continue,
5101            Err(_) => {}
5102        }
5103        assert!(
5104            elapsed < timeout,
5105            "timed out waiting for UpdatedGitRepositories after {description}"
5106        );
5107        cx.background_executor.timer(poll_interval).await;
5108        elapsed += poll_interval;
5109    }
5110}
5111
5112fn drain_git_repo_updates(events: &mut futures::channel::mpsc::UnboundedReceiver<Event>) -> bool {
5113    let mut found = false;
5114    while let Ok(event) = events.try_recv() {
5115        if matches!(event, Event::UpdatedGitRepositories(_)) {
5116            found = true;
5117        }
5118    }
5119    found
5120}
5121
5122fn init_test(cx: &mut gpui::TestAppContext) {
5123    zlog::init_test();
5124
5125    cx.update(|cx| {
5126        let settings_store = SettingsStore::test(cx);
5127        cx.set_global(settings_store);
5128    });
5129}
5130
5131async fn wait_for_condition(
5132    cx: &mut TestAppContext,
5133    mut condition: impl FnMut(&mut TestAppContext) -> bool,
5134) {
5135    for _ in 0..50 {
5136        if condition(cx) {
5137            return;
5138        }
5139        cx.executor().run_until_parked();
5140        cx.background_executor
5141            .timer(std::time::Duration::from_millis(10))
5142            .await;
5143    }
5144    panic!("timed out waiting for test condition");
5145}
5146
5147#[gpui::test]
5148async fn test_load_file_encoding(cx: &mut TestAppContext) {
5149    init_test(cx);
5150
5151    struct TestCase {
5152        name: &'static str,
5153        bytes: Vec<u8>,
5154        expected_text: &'static str,
5155    }
5156
5157    // --- Success Cases ---
5158    let success_cases = vec![
5159        TestCase {
5160            name: "utf8.txt",
5161            bytes: "こんにちは".as_bytes().to_vec(),
5162            expected_text: "こんにちは",
5163        },
5164        TestCase {
5165            name: "sjis.txt",
5166            bytes: vec![0x82, 0xb1, 0x82, 0xf1, 0x82, 0xc9, 0x82, 0xbf, 0x82, 0xcd],
5167            expected_text: "こんにちは",
5168        },
5169        TestCase {
5170            name: "eucjp.txt",
5171            bytes: vec![0xa4, 0xb3, 0xa4, 0xf3, 0xa4, 0xcb, 0xa4, 0xc1, 0xa4, 0xcf],
5172            expected_text: "こんにちは",
5173        },
5174        TestCase {
5175            name: "iso2022jp.txt",
5176            bytes: vec![
5177                0x1b, 0x24, 0x42, 0x24, 0x33, 0x24, 0x73, 0x24, 0x4b, 0x24, 0x41, 0x24, 0x4f, 0x1b,
5178                0x28, 0x42,
5179            ],
5180            expected_text: "こんにちは",
5181        },
5182        TestCase {
5183            name: "win1252.txt",
5184            bytes: vec![0x43, 0x61, 0x66, 0xe9],
5185            expected_text: "Café",
5186        },
5187        TestCase {
5188            name: "gbk.txt",
5189            bytes: vec![
5190                0xbd, 0xf1, 0xcc, 0xec, 0xcc, 0xec, 0xc6, 0xf8, 0xb2, 0xbb, 0xb4, 0xed,
5191            ],
5192            expected_text: "今天天气不错",
5193        },
5194        // UTF-16LE with BOM
5195        TestCase {
5196            name: "utf16le_bom.txt",
5197            bytes: vec![
5198                0xFF, 0xFE, // BOM
5199                0x53, 0x30, 0x93, 0x30, 0x6B, 0x30, 0x61, 0x30, 0x6F, 0x30,
5200            ],
5201            expected_text: "こんにちは",
5202        },
5203        // UTF-16BE with BOM
5204        TestCase {
5205            name: "utf16be_bom.txt",
5206            bytes: vec![
5207                0xFE, 0xFF, // BOM
5208                0x30, 0x53, 0x30, 0x93, 0x30, 0x6B, 0x30, 0x61, 0x30, 0x6F,
5209            ],
5210            expected_text: "こんにちは",
5211        },
5212        // UTF-16LE without BOM (ASCII only)
5213        // This relies on the "null byte heuristic" we implemented.
5214        // "ABC" -> 41 00 42 00 43 00
5215        TestCase {
5216            name: "utf16le_ascii_no_bom.txt",
5217            bytes: vec![0x41, 0x00, 0x42, 0x00, 0x43, 0x00],
5218            expected_text: "ABC",
5219        },
5220    ];
5221
5222    // --- Failure Cases ---
5223    let failure_cases = vec![
5224        // Binary File (Should be detected by heuristic and return Error)
5225        // Contains random bytes and mixed nulls that don't match UTF-16 patterns
5226        TestCase {
5227            name: "binary.bin",
5228            bytes: vec![0x00, 0xFF, 0x12, 0x00, 0x99, 0x88, 0x77, 0x66, 0x00],
5229            expected_text: "", // Not used
5230        },
5231    ];
5232
5233    let root_path = if cfg!(windows) {
5234        Path::new("C:\\root")
5235    } else {
5236        Path::new("/root")
5237    };
5238
5239    let fs = FakeFs::new(cx.background_executor.clone());
5240    fs.create_dir(root_path).await.unwrap();
5241
5242    for case in success_cases.iter().chain(failure_cases.iter()) {
5243        let path = root_path.join(case.name);
5244        fs.write(&path, &case.bytes).await.unwrap();
5245    }
5246
5247    let tree = Worktree::local(
5248        root_path,
5249        true,
5250        fs,
5251        Default::default(),
5252        true,
5253        WorktreeId::from_proto(0),
5254        &mut cx.to_async(),
5255    )
5256    .await
5257    .unwrap();
5258
5259    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
5260        .await;
5261
5262    let rel_path = |name: &str| {
5263        RelPath::new(&Path::new(name), PathStyle::local())
5264            .unwrap()
5265            .into_arc()
5266    };
5267
5268    // Run Success Tests
5269    for case in success_cases {
5270        let loaded = tree
5271            .update(cx, |tree, cx| tree.load_file(&rel_path(case.name), cx))
5272            .await;
5273        if let Err(e) = &loaded {
5274            panic!("Failed to load success case '{}': {:?}", case.name, e);
5275        }
5276        let loaded = loaded.unwrap();
5277        assert_eq!(
5278            loaded.text, case.expected_text,
5279            "Encoding mismatch for file: {}",
5280            case.name
5281        );
5282    }
5283
5284    // Run Failure Tests
5285    for case in failure_cases {
5286        let loaded = tree
5287            .update(cx, |tree, cx| tree.load_file(&rel_path(case.name), cx))
5288            .await;
5289        assert!(
5290            loaded.is_err(),
5291            "Failure case '{}' unexpectedly succeeded! It should have been detected as binary.",
5292            case.name
5293        );
5294        let err_msg = loaded.unwrap_err().to_string();
5295        println!("Got expected error for {}: {}", case.name, err_msg);
5296    }
5297}
5298
5299#[gpui::test]
5300async fn test_write_file_encoding(cx: &mut gpui::TestAppContext) {
5301    init_test(cx);
5302    let fs = FakeFs::new(cx.executor());
5303
5304    let root_path = if cfg!(windows) {
5305        Path::new("C:\\root")
5306    } else {
5307        Path::new("/root")
5308    };
5309    fs.create_dir(root_path).await.unwrap();
5310
5311    let worktree = Worktree::local(
5312        root_path,
5313        true,
5314        fs.clone(),
5315        Default::default(),
5316        true,
5317        WorktreeId::from_proto(0),
5318        &mut cx.to_async(),
5319    )
5320    .await
5321    .unwrap();
5322
5323    // Define test case structure
5324    struct TestCase {
5325        name: &'static str,
5326        text: &'static str,
5327        encoding: &'static encoding_rs::Encoding,
5328        has_bom: bool,
5329        expected_bytes: Vec<u8>,
5330    }
5331
5332    let cases = vec![
5333        // Shift_JIS with Japanese
5334        TestCase {
5335            name: "Shift_JIS with Japanese",
5336            text: "こんにちは",
5337            encoding: encoding_rs::SHIFT_JIS,
5338            has_bom: false,
5339            expected_bytes: vec![0x82, 0xb1, 0x82, 0xf1, 0x82, 0xc9, 0x82, 0xbf, 0x82, 0xcd],
5340        },
5341        // UTF-8 No BOM
5342        TestCase {
5343            name: "UTF-8 No BOM",
5344            text: "AB",
5345            encoding: encoding_rs::UTF_8,
5346            has_bom: false,
5347            expected_bytes: vec![0x41, 0x42],
5348        },
5349        // UTF-8 with BOM
5350        TestCase {
5351            name: "UTF-8 with BOM",
5352            text: "AB",
5353            encoding: encoding_rs::UTF_8,
5354            has_bom: true,
5355            expected_bytes: vec![0xEF, 0xBB, 0xBF, 0x41, 0x42],
5356        },
5357        // UTF-16LE No BOM with Japanese
5358        // NOTE: This passes thanks to the manual encoding fix implemented in `write_file`.
5359        TestCase {
5360            name: "UTF-16LE No BOM with Japanese",
5361            text: "こんにちは",
5362            encoding: encoding_rs::UTF_16LE,
5363            has_bom: false,
5364            expected_bytes: vec![0x53, 0x30, 0x93, 0x30, 0x6b, 0x30, 0x61, 0x30, 0x6f, 0x30],
5365        },
5366        // UTF-16LE with BOM
5367        TestCase {
5368            name: "UTF-16LE with BOM",
5369            text: "A",
5370            encoding: encoding_rs::UTF_16LE,
5371            has_bom: true,
5372            expected_bytes: vec![0xFF, 0xFE, 0x41, 0x00],
5373        },
5374        // UTF-16BE No BOM with Japanese
5375        // NOTE: This passes thanks to the manual encoding fix.
5376        TestCase {
5377            name: "UTF-16BE No BOM with Japanese",
5378            text: "こんにちは",
5379            encoding: encoding_rs::UTF_16BE,
5380            has_bom: false,
5381            expected_bytes: vec![0x30, 0x53, 0x30, 0x93, 0x30, 0x6b, 0x30, 0x61, 0x30, 0x6f],
5382        },
5383        // UTF-16BE with BOM
5384        TestCase {
5385            name: "UTF-16BE with BOM",
5386            text: "A",
5387            encoding: encoding_rs::UTF_16BE,
5388            has_bom: true,
5389            expected_bytes: vec![0xFE, 0xFF, 0x00, 0x41],
5390        },
5391    ];
5392
5393    for (i, case) in cases.into_iter().enumerate() {
5394        let file_name = format!("test_{}.txt", i);
5395        let path: Arc<Path> = Path::new(&file_name).into();
5396        let file_path = root_path.join(&file_name);
5397
5398        fs.insert_file(&file_path, "".into()).await;
5399
5400        let rel_path = RelPath::new(&path, PathStyle::local()).unwrap().into_arc();
5401        let text = text::Rope::from(case.text);
5402
5403        let task = worktree.update(cx, |wt, cx| {
5404            wt.write_file(
5405                rel_path,
5406                text,
5407                text::LineEnding::Unix,
5408                case.encoding,
5409                case.has_bom,
5410                cx,
5411            )
5412        });
5413
5414        if let Err(e) = task.await {
5415            panic!("Unexpected error in case '{}': {:?}", case.name, e);
5416        }
5417
5418        let bytes = fs.load_bytes(&file_path).await.unwrap();
5419
5420        assert_eq!(
5421            bytes, case.expected_bytes,
5422            "case '{}' mismatch. Expected {:?}, but got {:?}",
5423            case.name, case.expected_bytes, bytes
5424        );
5425    }
5426}
5427
5428#[gpui::test]
5429async fn test_refresh_entries_for_paths_creates_ancestors(cx: &mut TestAppContext) {
5430    init_test(cx);
5431    let fs = FakeFs::new(cx.background_executor.clone());
5432    fs.insert_tree(
5433        "/root",
5434        json!({
5435            "a": {
5436                "b": {
5437                    "c": {
5438                        "deep_file.txt": "content",
5439                        "sibling.txt": "content"
5440                    },
5441                    "d": {
5442                        "under_sibling_dir.txt": "content"
5443                    }
5444                }
5445            }
5446        }),
5447    )
5448    .await;
5449
5450    let tree = Worktree::local(
5451        Path::new("/root"),
5452        true,
5453        fs.clone(),
5454        Default::default(),
5455        false, // Disable scanning so the initial scan doesn't discover any entries
5456        WorktreeId::from_proto(0),
5457        &mut cx.to_async(),
5458    )
5459    .await
5460    .unwrap();
5461
5462    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
5463        .await;
5464
5465    tree.read_with(cx, |tree, _| {
5466        assert_eq!(
5467            tree.entries(true, 0)
5468                .map(|e| e.path.as_ref())
5469                .collect::<Vec<_>>(),
5470            &[rel_path("")],
5471            "Only root entry should exist when scanning is disabled"
5472        );
5473
5474        assert!(tree.entry_for_path(rel_path("a")).is_none());
5475        assert!(tree.entry_for_path(rel_path("a/b")).is_none());
5476        assert!(tree.entry_for_path(rel_path("a/b/c")).is_none());
5477        assert!(
5478            tree.entry_for_path(rel_path("a/b/c/deep_file.txt"))
5479                .is_none()
5480        );
5481    });
5482
5483    tree.read_with(cx, |tree, _| {
5484        tree.as_local()
5485            .unwrap()
5486            .refresh_entries_for_paths(vec![rel_path("a/b/c/deep_file.txt").into()])
5487    })
5488    .recv()
5489    .await;
5490
5491    tree.read_with(cx, |tree, _| {
5492        assert_eq!(
5493            tree.entries(true, 0)
5494                .map(|e| e.path.as_ref())
5495                .collect::<Vec<_>>(),
5496            &[
5497                rel_path(""),
5498                rel_path("a"),
5499                rel_path("a/b"),
5500                rel_path("a/b/c"),
5501                rel_path("a/b/c/deep_file.txt"),
5502                rel_path("a/b/c/sibling.txt"),
5503                rel_path("a/b/d"),
5504            ],
5505            "All ancestors should be created when refreshing a deeply nested path"
5506        );
5507    });
5508}
5509
5510#[gpui::test]
5511async fn test_single_file_worktree_deleted(cx: &mut TestAppContext) {
5512    init_test(cx);
5513    let fs = FakeFs::new(cx.background_executor.clone());
5514
5515    fs.insert_tree(
5516        "/root",
5517        json!({
5518            "test.txt": "content",
5519        }),
5520    )
5521    .await;
5522
5523    let tree = Worktree::local(
5524        Path::new("/root/test.txt"),
5525        true,
5526        fs.clone(),
5527        Default::default(),
5528        true,
5529        WorktreeId::from_proto(0),
5530        &mut cx.to_async(),
5531    )
5532    .await
5533    .unwrap();
5534
5535    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
5536        .await;
5537
5538    tree.read_with(cx, |tree, _| {
5539        assert!(tree.is_single_file(), "Should be a single-file worktree");
5540        assert_eq!(tree.abs_path().as_ref(), Path::new("/root/test.txt"));
5541    });
5542
5543    // Delete the file
5544    fs.remove_file(Path::new("/root/test.txt"), Default::default())
5545        .await
5546        .unwrap();
5547
5548    // Subscribe to worktree events
5549    let deleted_event_received = Rc::new(Cell::new(false));
5550    let _subscription = cx.update({
5551        let deleted_event_received = deleted_event_received.clone();
5552        |cx| {
5553            cx.subscribe(&tree, move |_, event, _| {
5554                if matches!(event, Event::Deleted) {
5555                    deleted_event_received.set(true);
5556                }
5557            })
5558        }
5559    });
5560
5561    // Trigger filesystem events - the scanner should detect the file is gone immediately
5562    // and emit a Deleted event
5563    cx.background_executor.run_until_parked();
5564    cx.background_executor
5565        .advance_clock(std::time::Duration::from_secs(1));
5566    cx.background_executor.run_until_parked();
5567
5568    assert!(
5569        deleted_event_received.get(),
5570        "Should receive Deleted event when single-file worktree root is deleted"
5571    );
5572}
5573
5574#[gpui::test]
5575async fn test_remote_worktree_without_git_emits_root_repo_event_after_first_update(
5576    cx: &mut TestAppContext,
5577) {
5578    cx.update(|cx| {
5579        let store = SettingsStore::test(cx);
5580        cx.set_global(store);
5581    });
5582
5583    let client = AnyProtoClient::new(NoopProtoClient::new());
5584
5585    let worktree = cx.update(|cx| {
5586        Worktree::remote(
5587            1,
5588            clock::ReplicaId::new(1),
5589            proto::WorktreeMetadata {
5590                id: 1,
5591                root_name: "project".to_string(),
5592                visible: true,
5593                abs_path: "/home/user/project".to_string(),
5594                root_repo_common_dir: None,
5595                root_repo_is_linked_worktree: false,
5596            },
5597            client,
5598            PathStyle::Unix,
5599            cx,
5600        )
5601    });
5602
5603    let events: Arc<std::sync::Mutex<Vec<&'static str>>> =
5604        Arc::new(std::sync::Mutex::new(Vec::new()));
5605    let events_clone = events.clone();
5606    cx.update(|cx| {
5607        cx.subscribe(&worktree, move |_, event, _cx| {
5608            if matches!(event, Event::UpdatedRootRepoCommonDir { .. }) {
5609                events_clone
5610                    .lock()
5611                    .unwrap()
5612                    .push("UpdatedRootRepoCommonDir");
5613            }
5614            if matches!(event, Event::UpdatedEntries(_)) {
5615                events_clone.lock().unwrap().push("UpdatedEntries");
5616            }
5617        })
5618        .detach();
5619    });
5620
5621    // Send an update with entries but no repo info (plain directory).
5622    worktree.update(cx, |worktree, _cx| {
5623        worktree
5624            .as_remote()
5625            .unwrap()
5626            .update_from_remote(proto::UpdateWorktree {
5627                project_id: 1,
5628                worktree_id: 1,
5629                abs_path: "/home/user/project".to_string(),
5630                root_name: "project".to_string(),
5631                updated_entries: vec![proto::Entry {
5632                    id: 1,
5633                    is_dir: true,
5634                    path: "".to_string(),
5635                    inode: 1,
5636                    mtime: Some(proto::Timestamp {
5637                        seconds: 0,
5638                        nanos: 0,
5639                    }),
5640                    is_ignored: false,
5641                    is_hidden: false,
5642                    is_external: false,
5643                    is_fifo: false,
5644                    size: None,
5645                    canonical_path: None,
5646                }],
5647                removed_entries: vec![],
5648                scan_id: 1,
5649                is_last_update: true,
5650                updated_repositories: vec![],
5651                removed_repositories: vec![],
5652                root_repo_common_dir: None,
5653                root_repo_is_linked_worktree: false,
5654            });
5655    });
5656
5657    cx.run_until_parked();
5658
5659    let fired = events.lock().unwrap();
5660    assert!(
5661        fired.contains(&"UpdatedEntries"),
5662        "UpdatedEntries should fire after remote update"
5663    );
5664    assert!(
5665        fired.contains(&"UpdatedRootRepoCommonDir"),
5666        "UpdatedRootRepoCommonDir should fire after first remote update even when \
5667         root_repo_common_dir is None, to signal that repo state is now known"
5668    );
5669}
5670
5671#[gpui::test]
5672async fn test_remote_worktree_with_git_emits_root_repo_event_when_repo_info_arrives(
5673    cx: &mut TestAppContext,
5674) {
5675    cx.update(|cx| {
5676        let store = SettingsStore::test(cx);
5677        cx.set_global(store);
5678    });
5679
5680    let client = AnyProtoClient::new(NoopProtoClient::new());
5681
5682    let worktree = cx.update(|cx| {
5683        Worktree::remote(
5684            1,
5685            clock::ReplicaId::new(1),
5686            proto::WorktreeMetadata {
5687                id: 1,
5688                root_name: "project".to_string(),
5689                visible: true,
5690                abs_path: "/home/user/project".to_string(),
5691                root_repo_common_dir: None,
5692                root_repo_is_linked_worktree: false,
5693            },
5694            client,
5695            PathStyle::Unix,
5696            cx,
5697        )
5698    });
5699
5700    let events: Arc<std::sync::Mutex<Vec<&'static str>>> =
5701        Arc::new(std::sync::Mutex::new(Vec::new()));
5702    let events_clone = events.clone();
5703    cx.update(|cx| {
5704        cx.subscribe(&worktree, move |_, event, _cx| {
5705            if matches!(event, Event::UpdatedRootRepoCommonDir { .. }) {
5706                events_clone
5707                    .lock()
5708                    .unwrap()
5709                    .push("UpdatedRootRepoCommonDir");
5710            }
5711        })
5712        .detach();
5713    });
5714
5715    // Send an update where repo info arrives (None -> Some).
5716    worktree.update(cx, |worktree, _cx| {
5717        worktree
5718            .as_remote()
5719            .unwrap()
5720            .update_from_remote(proto::UpdateWorktree {
5721                project_id: 1,
5722                worktree_id: 1,
5723                abs_path: "/home/user/project".to_string(),
5724                root_name: "project".to_string(),
5725                updated_entries: vec![proto::Entry {
5726                    id: 1,
5727                    is_dir: true,
5728                    path: "".to_string(),
5729                    inode: 1,
5730                    mtime: Some(proto::Timestamp {
5731                        seconds: 0,
5732                        nanos: 0,
5733                    }),
5734                    is_ignored: false,
5735                    is_hidden: false,
5736                    is_external: false,
5737                    is_fifo: false,
5738                    size: None,
5739                    canonical_path: None,
5740                }],
5741                removed_entries: vec![],
5742                scan_id: 1,
5743                is_last_update: true,
5744                updated_repositories: vec![],
5745                removed_repositories: vec![],
5746                root_repo_common_dir: Some("/home/user/project/.git".to_string()),
5747                root_repo_is_linked_worktree: false,
5748            });
5749    });
5750
5751    cx.run_until_parked();
5752
5753    let fired = events.lock().unwrap();
5754    assert!(
5755        fired.contains(&"UpdatedRootRepoCommonDir"),
5756        "UpdatedRootRepoCommonDir should fire when repo info arrives (None -> Some)"
5757    );
5758    assert_eq!(
5759        fired
5760            .iter()
5761            .filter(|e| **e == "UpdatedRootRepoCommonDir")
5762            .count(),
5763        1,
5764        "should fire exactly once, not duplicate"
5765    );
5766}
5767
5768#[gpui::test]
5769async fn test_remote_worktree_root_repo_metadata_cleared_only_by_completed_scan(
5770    cx: &mut TestAppContext,
5771) {
5772    cx.update(|cx| {
5773        let store = SettingsStore::test(cx);
5774        cx.set_global(store);
5775    });
5776
5777    let client = AnyProtoClient::new(NoopProtoClient::new());
5778
5779    // Metadata eagerly seeds the root repo info, as `AddWorktreeResponse` /
5780    // `WorktreeMetadata` do before the host's scan completes.
5781    let worktree = cx.update(|cx| {
5782        Worktree::remote(
5783            1,
5784            clock::ReplicaId::new(1),
5785            proto::WorktreeMetadata {
5786                id: 1,
5787                root_name: "feature-a".to_string(),
5788                visible: true,
5789                abs_path: "/home/user/monty/feature-a".to_string(),
5790                root_repo_common_dir: Some("/home/user/monty/.bare".to_string()),
5791                root_repo_is_linked_worktree: true,
5792            },
5793            client,
5794            PathStyle::Unix,
5795            cx,
5796        )
5797    });
5798
5799    let root_repo_metadata = |cx: &mut TestAppContext| {
5800        worktree.read_with(cx, |worktree, _| {
5801            let snapshot = worktree.snapshot();
5802            (
5803                snapshot.root_repo_common_dir().cloned(),
5804                snapshot.root_repo_is_linked_worktree(),
5805            )
5806        })
5807    };
5808
5809    let update = |scan_id: u64, is_last_update: bool| proto::UpdateWorktree {
5810        project_id: 1,
5811        worktree_id: 1,
5812        abs_path: "/home/user/monty/feature-a".to_string(),
5813        root_name: "feature-a".to_string(),
5814        updated_entries: vec![],
5815        removed_entries: vec![],
5816        scan_id,
5817        is_last_update,
5818        updated_repositories: vec![],
5819        removed_repositories: vec![],
5820        root_repo_common_dir: None,
5821        root_repo_is_linked_worktree: false,
5822    };
5823
5824    // A mid-scan update without repo info must not clobber the seeded
5825    // metadata: the sender's scanner may not have registered the repo yet.
5826    worktree.update(cx, |worktree, _cx| {
5827        worktree
5828            .as_remote()
5829            .unwrap()
5830            .update_from_remote(update(2, false));
5831    });
5832    cx.run_until_parked();
5833
5834    assert_eq!(
5835        root_repo_metadata(cx),
5836        (Some(Arc::from(Path::new("/home/user/monty/.bare"))), true,),
5837        "mid-scan update without repo info should not clear seeded metadata"
5838    );
5839
5840    // A completed scan without repo info is authoritative: the repo is gone.
5841    worktree.update(cx, |worktree, _cx| {
5842        worktree
5843            .as_remote()
5844            .unwrap()
5845            .update_from_remote(update(3, true));
5846    });
5847    cx.run_until_parked();
5848
5849    assert_eq!(
5850        root_repo_metadata(cx),
5851        (None, false),
5852        "completed scan without repo info should clear root repo metadata"
5853    );
5854}
5855
5856// Regression test: a remote worktree used to emit `UpdatedEntries` with an
5857// empty changeset (`Arc::default()`), discarding the changed paths. Consumers
5858// that key off those paths - notably the agent's `.agents/skills` refresh -
5859// therefore never fired on remote projects, so skills pasted into an already
5860// open project were never picked up. The changeset must carry the real paths.
5861//
5862// This drives the real host -> remote pipeline: a `FakeFs`-backed local
5863// worktree scans the filesystem and produces `UpdateWorktree` messages via
5864// `observe_updates`, which we relay into a remote worktree exactly as the
5865// collab server does.
5866#[gpui::test]
5867async fn test_remote_worktree_update_entries_carry_changed_paths(cx: &mut TestAppContext) {
5868    init_test(cx);
5869
5870    let fs = FakeFs::new(cx.background_executor.clone());
5871    fs.insert_tree(
5872        path!("/root"),
5873        json!({
5874            ".agents": {
5875                "skills": {}
5876            }
5877        }),
5878    )
5879    .await;
5880
5881    // The host worktree scans the fake filesystem and broadcasts updates.
5882    let host = Worktree::local(
5883        path!("/root").as_ref(),
5884        true,
5885        fs.clone(),
5886        Default::default(),
5887        true,
5888        WorktreeId::from_proto(1),
5889        &mut cx.to_async(),
5890    )
5891    .await
5892    .unwrap();
5893    cx.read(|cx| host.read(cx).as_local().unwrap().scan_complete())
5894        .await;
5895
5896    // The remote worktree receives those updates over a simulated connection.
5897    let remote = cx.update(|cx| {
5898        Worktree::remote(
5899            1,
5900            clock::ReplicaId::new(1),
5901            proto::WorktreeMetadata {
5902                id: 1,
5903                root_name: "root".to_string(),
5904                visible: true,
5905                abs_path: path!("/root").to_string(),
5906                root_repo_common_dir: None,
5907                root_repo_is_linked_worktree: false,
5908            },
5909            AnyProtoClient::new(NoopProtoClient::new()),
5910            PathStyle::local(),
5911            cx,
5912        )
5913    });
5914
5915    // Relay every `UpdateWorktree` the host emits into the remote worktree,
5916    // mirroring how the collab server forwards them. The callback only buffers
5917    // the messages; we apply them on the foreground via `relay`.
5918    let pending: Arc<Mutex<Vec<proto::UpdateWorktree>>> = Arc::new(Mutex::new(Vec::new()));
5919    host.update(cx, |host, cx| {
5920        let pending = pending.clone();
5921        host.as_local_mut()
5922            .unwrap()
5923            .observe_updates(1, cx, move |update| {
5924                pending.lock().push(update);
5925                async { true }
5926            });
5927    });
5928    let relay = {
5929        let remote = remote.clone();
5930        move |cx: &mut TestAppContext| {
5931            let updates = std::mem::take(&mut *pending.lock());
5932            remote.update(cx, |remote, _| {
5933                let remote = remote.as_remote().unwrap();
5934                for update in updates {
5935                    remote.update_from_remote(update);
5936                }
5937            });
5938        }
5939    };
5940
5941    // Record the (path, change) pairs from every `UpdatedEntries` event the
5942    // remote worktree emits.
5943    let changes: Arc<Mutex<Vec<(String, PathChange)>>> = Arc::new(Mutex::new(Vec::new()));
5944    cx.update(|cx| {
5945        let changes = changes.clone();
5946        cx.subscribe(&remote, move |_, event, _cx| {
5947            if let Event::UpdatedEntries(updated) = event {
5948                changes.lock().extend(
5949                    updated
5950                        .iter()
5951                        .map(|(path, _, change)| (path.as_unix_str().to_string(), *change)),
5952                );
5953            }
5954        })
5955        .detach();
5956    });
5957
5958    // Flush the initial sync (root + existing dirs) and ignore those paths.
5959    cx.run_until_parked();
5960    relay(cx);
5961    cx.run_until_parked();
5962    changes.lock().clear();
5963
5964    // Paste a skill folder into `.agents/skills` on the host.
5965    fs.insert_tree(
5966        path!("/root/.agents/skills/skill-1"),
5967        json!({ "SKILL.md": "skill" }),
5968    )
5969    .await;
5970    cx.run_until_parked();
5971    relay(cx);
5972    cx.run_until_parked();
5973
5974    {
5975        let changes = changes.lock();
5976        assert!(
5977            changes
5978                .iter()
5979                .any(|(path, change)| path == ".agents/skills/skill-1/SKILL.md"
5980                    && *change == PathChange::AddedOrUpdated),
5981            "remote UpdatedEntries should carry the added skill path, got {:?}",
5982            changes
5983        );
5984    }
5985    changes.lock().clear();
5986
5987    // Remove the skill folder. The wire format only carries entry ids for
5988    // removals, so the remote worktree must resolve their paths against the
5989    // previous snapshot before it is replaced.
5990    fs.remove_dir(
5991        path!("/root/.agents/skills/skill-1").as_ref(),
5992        RemoveOptions {
5993            recursive: true,
5994            ignore_if_not_exists: false,
5995        },
5996    )
5997    .await
5998    .unwrap();
5999    cx.run_until_parked();
6000    relay(cx);
6001    cx.run_until_parked();
6002
6003    let changes = changes.lock();
6004    assert!(
6005        changes
6006            .iter()
6007            .any(|(path, change)| path == ".agents/skills/skill-1/SKILL.md"
6008                && *change == PathChange::Removed),
6009        "remote UpdatedEntries should carry removed paths resolved from the \
6010         previous snapshot, got {:?}",
6011        changes
6012    );
6013}
6014
6015#[gpui::test]
6016async fn test_deferred_watch_repository_above_root(
6017    executor: BackgroundExecutor,
6018    cx: &mut TestAppContext,
6019) {
6020    init_test(cx);
6021
6022    let fs = FakeFs::new(executor);
6023    fs.insert_tree(
6024        path!("/root"),
6025        json!({
6026            ".git": {},
6027            "subproject": {
6028                "a.txt": "A"
6029            }
6030        }),
6031    )
6032    .await;
6033    let worktree = Worktree::local(
6034        path!("/root/subproject").as_ref(),
6035        true,
6036        fs.clone(),
6037        Arc::default(),
6038        true,
6039        WorktreeId::from_proto(0),
6040        &mut cx.to_async(),
6041    )
6042    .await
6043    .unwrap();
6044    worktree
6045        .update(cx, |worktree, _| {
6046            worktree.as_local().unwrap().scan_complete()
6047        })
6048        .await;
6049    cx.run_until_parked();
6050
6051    worktree.update(cx, |worktree, cx| {
6052        worktree.as_local_mut().unwrap().set_defer_watch(true, cx);
6053    });
6054    worktree
6055        .update(cx, |worktree, _| {
6056            worktree.as_local().unwrap().scan_complete()
6057        })
6058        .await;
6059    cx.run_until_parked();
6060
6061    let repos = worktree.update(cx, |worktree, _| {
6062        worktree.as_local().unwrap().repositories()
6063    });
6064    pretty_assertions::assert_eq!(repos, [Path::new(path!("/root")).into()]);
6065}
6066
6067#[gpui::test]
6068async fn test_deferred_watch_symlinks_pointing_outside(cx: &mut TestAppContext) {
6069    init_test(cx);
6070    let fs = FakeFs::new(cx.background_executor.clone());
6071    fs.insert_tree(
6072        "/root",
6073        json!({
6074            "dir1": {
6075                "deps": {},
6076                "src": {
6077                    "a.rs": "",
6078                },
6079            },
6080            "dir2": {
6081                "src": {
6082                    "c.rs": "",
6083                }
6084            },
6085        }),
6086    )
6087    .await;
6088
6089    fs.create_symlink("/root/dir1/deps/dep-dir2".as_ref(), "../../dir2".into())
6090        .await
6091        .unwrap();
6092
6093    let tree = Worktree::local(
6094        Path::new("/root/dir1"),
6095        true,
6096        fs.clone(),
6097        Default::default(),
6098        true,
6099        WorktreeId::from_proto(0),
6100        &mut cx.to_async(),
6101    )
6102    .await
6103    .unwrap();
6104
6105    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
6106        .await;
6107    cx.run_until_parked();
6108
6109    tree.update(cx, |tree, cx| {
6110        tree.as_local_mut().unwrap().set_defer_watch(true, cx);
6111    });
6112    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
6113        .await;
6114    cx.run_until_parked();
6115
6116    tree.read_with(cx, |tree, _| {
6117        assert_eq!(
6118            tree.entries(true, 0)
6119                .map(|entry| (entry.path.as_ref(), entry.is_external))
6120                .collect::<Vec<_>>(),
6121            vec![
6122                (rel_path(""), false),
6123                (rel_path("deps"), false),
6124                (rel_path("deps/dep-dir2"), true),
6125                (rel_path("src"), false),
6126                (rel_path("src/a.rs"), false),
6127            ]
6128        );
6129    });
6130
6131    tree.read_with(cx, |tree, _| {
6132        tree.as_local()
6133            .unwrap()
6134            .refresh_entries_for_paths(vec![rel_path("deps/dep-dir2").into()])
6135    })
6136    .recv()
6137    .await;
6138
6139    tree.read_with(cx, |tree, _| {
6140        assert_eq!(
6141            tree.entries(true, 0)
6142                .map(|entry| (entry.path.as_ref(), entry.is_external))
6143                .collect::<Vec<_>>(),
6144            vec![
6145                (rel_path(""), false),
6146                (rel_path("deps"), false),
6147                (rel_path("deps/dep-dir2"), true),
6148                (rel_path("deps/dep-dir2/src"), true),
6149                (rel_path("src"), false),
6150                (rel_path("src/a.rs"), false),
6151            ]
6152        );
6153    });
6154
6155    tree.read_with(cx, |tree, _| {
6156        tree.as_local()
6157            .unwrap()
6158            .refresh_entries_for_paths(vec![rel_path("deps/dep-dir2/src").into()])
6159    })
6160    .recv()
6161    .await;
6162
6163    tree.read_with(cx, |tree, _| {
6164        assert!(
6165            tree.entry_for_path(rel_path("deps/dep-dir2/src/c.rs"))
6166                .is_some()
6167        );
6168    });
6169
6170    fs.insert_file(Path::new("/root/dir2/src/new.rs"), b"".to_vec())
6171        .await;
6172
6173    wait_for_condition(cx, |cx| {
6174        tree.read_with(cx, |tree, _| {
6175            tree.entry_for_path(rel_path("deps/dep-dir2/src/new.rs"))
6176                .is_some()
6177        })
6178    })
6179    .await;
6180}
6181
Served at tenant.openagents/omega Member data and write actions are omitted.