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