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