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 "one/node_modules/b/b1.js".as_ref(),
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 "one/node_modules/a/a2.js".as_ref(),
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_rename_file_to_new_directory(cx: &mut TestAppContext) {
1971 init_test(cx);
1972 let fs = FakeFs::new(cx.background_executor.clone());
1973 let expected_contents = "content";
1974 fs.as_fake()
1975 .insert_tree(
1976 "/root",
1977 json!({
1978 "test.txt": expected_contents
1979 }),
1980 )
1981 .await;
1982 let worktree = Worktree::local(
1983 Path::new("/root"),
1984 true,
1985 fs.clone(),
1986 Arc::default(),
1987 &mut cx.to_async(),
1988 )
1989 .await
1990 .unwrap();
1991 cx.read(|cx| worktree.read(cx).as_local().unwrap().scan_complete())
1992 .await;
1993
1994 let entry_id = worktree.read_with(cx, |worktree, _| {
1995 worktree.entry_for_path("test.txt").unwrap().id
1996 });
1997 let _result = worktree
1998 .update(cx, |worktree, cx| {
1999 worktree.rename_entry(entry_id, Path::new("dir1/dir2/dir3/test.txt"), cx)
2000 })
2001 .await
2002 .unwrap();
2003 worktree.read_with(cx, |worktree, _| {
2004 assert!(
2005 worktree.entry_for_path("test.txt").is_none(),
2006 "Old file should have been removed"
2007 );
2008 assert!(
2009 worktree.entry_for_path("dir1/dir2/dir3/test.txt").is_some(),
2010 "Whole directory hierarchy and the new file should have been created"
2011 );
2012 });
2013 assert_eq!(
2014 worktree
2015 .update(cx, |worktree, cx| {
2016 worktree.load_file("dir1/dir2/dir3/test.txt".as_ref(), None, cx)
2017 })
2018 .await
2019 .unwrap()
2020 .text,
2021 expected_contents,
2022 "Moved file's contents should be preserved"
2023 );
2024
2025 let entry_id = worktree.read_with(cx, |worktree, _| {
2026 worktree
2027 .entry_for_path("dir1/dir2/dir3/test.txt")
2028 .unwrap()
2029 .id
2030 });
2031 let _result = worktree
2032 .update(cx, |worktree, cx| {
2033 worktree.rename_entry(entry_id, Path::new("dir1/dir2/test.txt"), cx)
2034 })
2035 .await
2036 .unwrap();
2037 worktree.read_with(cx, |worktree, _| {
2038 assert!(
2039 worktree.entry_for_path("test.txt").is_none(),
2040 "First file should not reappear"
2041 );
2042 assert!(
2043 worktree.entry_for_path("dir1/dir2/dir3/test.txt").is_none(),
2044 "Old file should have been removed"
2045 );
2046 assert!(
2047 worktree.entry_for_path("dir1/dir2/test.txt").is_some(),
2048 "No error should have occurred after moving into existing directory"
2049 );
2050 });
2051 assert_eq!(
2052 worktree
2053 .update(cx, |worktree, cx| {
2054 worktree.load_file("dir1/dir2/test.txt".as_ref(), None, cx)
2055 })
2056 .await
2057 .unwrap()
2058 .text,
2059 expected_contents,
2060 "Moved file's contents should be preserved"
2061 );
2062}
2063
2064#[gpui::test]
2065async fn test_private_single_file_worktree(cx: &mut TestAppContext) {
2066 init_test(cx);
2067 let fs = FakeFs::new(cx.background_executor.clone());
2068 fs.insert_tree("/", json!({".env": "PRIVATE=secret\n"}))
2069 .await;
2070 let tree = Worktree::local(
2071 Path::new("/.env"),
2072 true,
2073 fs.clone(),
2074 Default::default(),
2075 &mut cx.to_async(),
2076 )
2077 .await
2078 .unwrap();
2079 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2080 .await;
2081 tree.read_with(cx, |tree, _| {
2082 let entry = tree.entry_for_path(rel_path("")).unwrap();
2083 assert!(entry.is_private);
2084 });
2085}
2086
2087#[gpui::test]
2088async fn test_repository_above_root(executor: BackgroundExecutor, cx: &mut TestAppContext) {
2089 init_test(cx);
2090
2091 let fs = FakeFs::new(executor);
2092 fs.insert_tree(
2093 path!("/root"),
2094 json!({
2095 ".git": {},
2096 "subproject": {
2097 "a.txt": "A"
2098 }
2099 }),
2100 )
2101 .await;
2102 let worktree = Worktree::local(
2103 path!("/root/subproject").as_ref(),
2104 true,
2105 fs.clone(),
2106 Arc::default(),
2107 &mut cx.to_async(),
2108 )
2109 .await
2110 .unwrap();
2111 worktree
2112 .update(cx, |worktree, _| {
2113 worktree.as_local().unwrap().scan_complete()
2114 })
2115 .await;
2116 cx.run_until_parked();
2117 let repos = worktree.update(cx, |worktree, _| {
2118 worktree
2119 .as_local()
2120 .unwrap()
2121 .git_repositories
2122 .values()
2123 .map(|entry| entry.work_directory_abs_path.clone())
2124 .collect::<Vec<_>>()
2125 });
2126 pretty_assertions::assert_eq!(repos, [Path::new(path!("/root")).into()]);
2127
2128 fs.touch_path(path!("/root/subproject")).await;
2129 worktree
2130 .update(cx, |worktree, _| {
2131 worktree.as_local().unwrap().scan_complete()
2132 })
2133 .await;
2134 cx.run_until_parked();
2135
2136 let repos = worktree.update(cx, |worktree, _| {
2137 worktree
2138 .as_local()
2139 .unwrap()
2140 .git_repositories
2141 .values()
2142 .map(|entry| entry.work_directory_abs_path.clone())
2143 .collect::<Vec<_>>()
2144 });
2145 pretty_assertions::assert_eq!(repos, [Path::new(path!("/root")).into()]);
2146}
2147
2148#[gpui::test]
2149async fn test_global_gitignore(executor: BackgroundExecutor, cx: &mut TestAppContext) {
2150 init_test(cx);
2151
2152 let home = paths::home_dir();
2153 let fs = FakeFs::new(executor);
2154 fs.insert_tree(
2155 home,
2156 json!({
2157 ".config": {
2158 "git": {
2159 "ignore": "foo\n/bar\nbaz\n"
2160 }
2161 },
2162 "project": {
2163 ".git": {},
2164 ".gitignore": "!baz",
2165 "foo": "",
2166 "bar": "",
2167 "sub": {
2168 "bar": "",
2169 },
2170 "subrepo": {
2171 ".git": {},
2172 "bar": ""
2173 },
2174 "baz": ""
2175 }
2176 }),
2177 )
2178 .await;
2179 let worktree = Worktree::local(
2180 home.join("project"),
2181 true,
2182 fs.clone(),
2183 Arc::default(),
2184 &mut cx.to_async(),
2185 )
2186 .await
2187 .unwrap();
2188 worktree
2189 .update(cx, |worktree, _| {
2190 worktree.as_local().unwrap().scan_complete()
2191 })
2192 .await;
2193 cx.run_until_parked();
2194
2195 // .gitignore overrides excludesFile, and anchored paths in excludesFile are resolved
2196 // relative to the nearest containing repository
2197 worktree.update(cx, |worktree, _cx| {
2198 check_worktree_entries(
2199 worktree,
2200 &[],
2201 &["foo", "bar", "subrepo/bar"],
2202 &["sub/bar", "baz"],
2203 &[],
2204 );
2205 });
2206
2207 // Ignore statuses are updated when excludesFile changes
2208 fs.write(
2209 &home.join(".config").join("git").join("ignore"),
2210 "/bar\nbaz\n".as_bytes(),
2211 )
2212 .await
2213 .unwrap();
2214 worktree
2215 .update(cx, |worktree, _| {
2216 worktree.as_local().unwrap().scan_complete()
2217 })
2218 .await;
2219 cx.run_until_parked();
2220
2221 worktree.update(cx, |worktree, _cx| {
2222 check_worktree_entries(
2223 worktree,
2224 &[],
2225 &["bar", "subrepo/bar"],
2226 &["foo", "sub/bar", "baz"],
2227 &[],
2228 );
2229 });
2230
2231 // Statuses are updated when .git added/removed
2232 fs.remove_dir(
2233 &home.join("project").join("subrepo").join(".git"),
2234 RemoveOptions {
2235 recursive: true,
2236 ..Default::default()
2237 },
2238 )
2239 .await
2240 .unwrap();
2241 worktree
2242 .update(cx, |worktree, _| {
2243 worktree.as_local().unwrap().scan_complete()
2244 })
2245 .await;
2246 cx.run_until_parked();
2247
2248 worktree.update(cx, |worktree, _cx| {
2249 check_worktree_entries(
2250 worktree,
2251 &[],
2252 &["bar"],
2253 &["foo", "sub/bar", "baz", "subrepo/bar"],
2254 &[],
2255 );
2256 });
2257}
2258
2259#[track_caller]
2260fn check_worktree_entries(
2261 tree: &Worktree,
2262 expected_excluded_paths: &[&str],
2263 expected_ignored_paths: &[&str],
2264 expected_tracked_paths: &[&str],
2265 expected_included_paths: &[&str],
2266) {
2267 for path in expected_excluded_paths {
2268 let entry = tree.entry_for_path(rel_path(path));
2269 assert!(
2270 entry.is_none(),
2271 "expected path '{path}' to be excluded, but got entry: {entry:?}",
2272 );
2273 }
2274 for path in expected_ignored_paths {
2275 let entry = tree
2276 .entry_for_path(rel_path(path))
2277 .unwrap_or_else(|| panic!("Missing entry for expected ignored path '{path}'"));
2278 assert!(
2279 entry.is_ignored,
2280 "expected path '{path}' to be ignored, but got entry: {entry:?}",
2281 );
2282 }
2283 for path in expected_tracked_paths {
2284 let entry = tree
2285 .entry_for_path(rel_path(path))
2286 .unwrap_or_else(|| panic!("Missing entry for expected tracked path '{path}'"));
2287 assert!(
2288 !entry.is_ignored || entry.is_always_included,
2289 "expected path '{path}' to be tracked, but got entry: {entry:?}",
2290 );
2291 }
2292 for path in expected_included_paths {
2293 let entry = tree
2294 .entry_for_path(rel_path(path))
2295 .unwrap_or_else(|| panic!("Missing entry for expected included path '{path}'"));
2296 assert!(
2297 entry.is_always_included,
2298 "expected path '{path}' to always be included, but got entry: {entry:?}",
2299 );
2300 }
2301}
2302
2303fn init_test(cx: &mut gpui::TestAppContext) {
2304 zlog::init_test();
2305
2306 cx.update(|cx| {
2307 let settings_store = SettingsStore::test(cx);
2308 cx.set_global(settings_store);
2309 WorktreeSettings::register(cx);
2310 });
2311}