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