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