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