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