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 "package.json": "//package.json"
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_hidden_files(cx: &mut TestAppContext) {
1062 init_test(cx);
1063 cx.executor().allow_parking();
1064 let dir = TempTree::new(json!({
1065 ".gitignore": "**/target\n",
1066 ".hidden_file": "content",
1067 ".hidden_dir": {
1068 "nested.rs": "code",
1069 },
1070 "src": {
1071 "visible.rs": "code",
1072 },
1073 "logs": {
1074 "app.log": "logs",
1075 "debug.log": "logs",
1076 },
1077 "visible.txt": "content",
1078 }));
1079
1080 let tree = Worktree::local(
1081 dir.path(),
1082 true,
1083 Arc::new(RealFs::new(None, cx.executor())),
1084 Default::default(),
1085 &mut cx.to_async(),
1086 )
1087 .await
1088 .unwrap();
1089 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1090 .await;
1091 tree.flush_fs_events(cx).await;
1092
1093 tree.read_with(cx, |tree, _| {
1094 assert_eq!(
1095 tree.entries(true, 0)
1096 .map(|entry| (entry.path.as_ref(), entry.is_hidden))
1097 .collect::<Vec<_>>(),
1098 vec![
1099 (rel_path(""), false),
1100 (rel_path(".gitignore"), true),
1101 (rel_path(".hidden_dir"), true),
1102 (rel_path(".hidden_dir/nested.rs"), true),
1103 (rel_path(".hidden_file"), true),
1104 (rel_path("logs"), false),
1105 (rel_path("logs/app.log"), false),
1106 (rel_path("logs/debug.log"), false),
1107 (rel_path("src"), false),
1108 (rel_path("src/visible.rs"), false),
1109 (rel_path("visible.txt"), false),
1110 ]
1111 );
1112 });
1113
1114 cx.update(|cx| {
1115 cx.update_global::<SettingsStore, _>(|store, cx| {
1116 store.update_user_settings(cx, |settings| {
1117 settings.project.worktree.hidden_files = Some(vec!["**/*.log".to_string()]);
1118 });
1119 });
1120 });
1121 tree.flush_fs_events(cx).await;
1122 cx.executor().run_until_parked();
1123
1124 tree.read_with(cx, |tree, _| {
1125 assert_eq!(
1126 tree.entries(true, 0)
1127 .map(|entry| (entry.path.as_ref(), entry.is_hidden))
1128 .collect::<Vec<_>>(),
1129 vec![
1130 (rel_path(""), false),
1131 (rel_path(".gitignore"), false),
1132 (rel_path(".hidden_dir"), false),
1133 (rel_path(".hidden_dir/nested.rs"), false),
1134 (rel_path(".hidden_file"), false),
1135 (rel_path("logs"), false),
1136 (rel_path("logs/app.log"), true),
1137 (rel_path("logs/debug.log"), true),
1138 (rel_path("src"), false),
1139 (rel_path("src/visible.rs"), false),
1140 (rel_path("visible.txt"), false),
1141 ]
1142 );
1143 });
1144}
1145
1146#[gpui::test]
1147async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) {
1148 init_test(cx);
1149 cx.executor().allow_parking();
1150 let dir = TempTree::new(json!({
1151 ".git": {
1152 "HEAD": "ref: refs/heads/main\n",
1153 "foo": "bar",
1154 },
1155 ".gitignore": "**/target\n/node_modules\ntest_output\n",
1156 "target": {
1157 "index": "blah2"
1158 },
1159 "node_modules": {
1160 ".DS_Store": "",
1161 "prettier": {
1162 "package.json": "{}",
1163 },
1164 },
1165 "src": {
1166 ".DS_Store": "",
1167 "foo": {
1168 "foo.rs": "mod another;\n",
1169 "another.rs": "// another",
1170 },
1171 "bar": {
1172 "bar.rs": "// bar",
1173 },
1174 "lib.rs": "mod foo;\nmod bar;\n",
1175 },
1176 ".DS_Store": "",
1177 }));
1178 cx.update(|cx| {
1179 cx.update_global::<SettingsStore, _>(|store, cx| {
1180 store.update_user_settings(cx, |settings| {
1181 settings.project.worktree.file_scan_exclusions = Some(vec![
1182 "**/.git".to_string(),
1183 "node_modules/".to_string(),
1184 "build_output".to_string(),
1185 ]);
1186 });
1187 });
1188 });
1189
1190 let tree = Worktree::local(
1191 dir.path(),
1192 true,
1193 Arc::new(RealFs::new(None, cx.executor())),
1194 Default::default(),
1195 &mut cx.to_async(),
1196 )
1197 .await
1198 .unwrap();
1199 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1200 .await;
1201 tree.flush_fs_events(cx).await;
1202 tree.read_with(cx, |tree, _| {
1203 check_worktree_entries(
1204 tree,
1205 &[
1206 ".git/HEAD",
1207 ".git/foo",
1208 "node_modules",
1209 "node_modules/.DS_Store",
1210 "node_modules/prettier",
1211 "node_modules/prettier/package.json",
1212 ],
1213 &["target"],
1214 &[
1215 ".DS_Store",
1216 "src/.DS_Store",
1217 "src/lib.rs",
1218 "src/foo/foo.rs",
1219 "src/foo/another.rs",
1220 "src/bar/bar.rs",
1221 ".gitignore",
1222 ],
1223 &[],
1224 )
1225 });
1226
1227 let new_excluded_dir = dir.path().join("build_output");
1228 let new_ignored_dir = dir.path().join("test_output");
1229 std::fs::create_dir_all(&new_excluded_dir)
1230 .unwrap_or_else(|e| panic!("Failed to create a {new_excluded_dir:?} directory: {e}"));
1231 std::fs::create_dir_all(&new_ignored_dir)
1232 .unwrap_or_else(|e| panic!("Failed to create a {new_ignored_dir:?} directory: {e}"));
1233 let node_modules_dir = dir.path().join("node_modules");
1234 let dot_git_dir = dir.path().join(".git");
1235 let src_dir = dir.path().join("src");
1236 for existing_dir in [&node_modules_dir, &dot_git_dir, &src_dir] {
1237 assert!(
1238 existing_dir.is_dir(),
1239 "Expect {existing_dir:?} to be present in the FS already"
1240 );
1241 }
1242
1243 for directory_for_new_file in [
1244 new_excluded_dir,
1245 new_ignored_dir,
1246 node_modules_dir,
1247 dot_git_dir,
1248 src_dir,
1249 ] {
1250 std::fs::write(directory_for_new_file.join("new_file"), "new file contents")
1251 .unwrap_or_else(|e| {
1252 panic!("Failed to create in {directory_for_new_file:?} a new file: {e}")
1253 });
1254 }
1255 tree.flush_fs_events(cx).await;
1256
1257 tree.read_with(cx, |tree, _| {
1258 check_worktree_entries(
1259 tree,
1260 &[
1261 ".git/HEAD",
1262 ".git/foo",
1263 ".git/new_file",
1264 "node_modules",
1265 "node_modules/.DS_Store",
1266 "node_modules/prettier",
1267 "node_modules/prettier/package.json",
1268 "node_modules/new_file",
1269 "build_output",
1270 "build_output/new_file",
1271 "test_output/new_file",
1272 ],
1273 &["target", "test_output"],
1274 &[
1275 ".DS_Store",
1276 "src/.DS_Store",
1277 "src/lib.rs",
1278 "src/foo/foo.rs",
1279 "src/foo/another.rs",
1280 "src/bar/bar.rs",
1281 "src/new_file",
1282 ".gitignore",
1283 ],
1284 &[],
1285 )
1286 });
1287}
1288
1289#[gpui::test]
1290async fn test_fs_events_in_dot_git_worktree(cx: &mut TestAppContext) {
1291 init_test(cx);
1292 cx.executor().allow_parking();
1293 let dir = TempTree::new(json!({
1294 ".git": {
1295 "HEAD": "ref: refs/heads/main\n",
1296 "foo": "foo contents",
1297 },
1298 }));
1299 let dot_git_worktree_dir = dir.path().join(".git");
1300
1301 let tree = Worktree::local(
1302 dot_git_worktree_dir.clone(),
1303 true,
1304 Arc::new(RealFs::new(None, cx.executor())),
1305 Default::default(),
1306 &mut cx.to_async(),
1307 )
1308 .await
1309 .unwrap();
1310 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1311 .await;
1312 tree.flush_fs_events(cx).await;
1313 tree.read_with(cx, |tree, _| {
1314 check_worktree_entries(tree, &[], &["HEAD", "foo"], &[], &[])
1315 });
1316
1317 std::fs::write(dot_git_worktree_dir.join("new_file"), "new file contents")
1318 .unwrap_or_else(|e| panic!("Failed to create in {dot_git_worktree_dir:?} a new file: {e}"));
1319 tree.flush_fs_events(cx).await;
1320 tree.read_with(cx, |tree, _| {
1321 check_worktree_entries(tree, &[], &["HEAD", "foo", "new_file"], &[], &[])
1322 });
1323}
1324
1325#[gpui::test(iterations = 30)]
1326async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
1327 init_test(cx);
1328 let fs = FakeFs::new(cx.background_executor.clone());
1329 fs.insert_tree(
1330 "/root",
1331 json!({
1332 "b": {},
1333 "c": {},
1334 "d": {},
1335 }),
1336 )
1337 .await;
1338
1339 let tree = Worktree::local(
1340 "/root".as_ref(),
1341 true,
1342 fs,
1343 Default::default(),
1344 &mut cx.to_async(),
1345 )
1346 .await
1347 .unwrap();
1348
1349 let snapshot1 = tree.update(cx, |tree, cx| {
1350 let tree = tree.as_local_mut().unwrap();
1351 let snapshot = Arc::new(Mutex::new(tree.snapshot()));
1352 tree.observe_updates(0, cx, {
1353 let snapshot = snapshot.clone();
1354 let settings = tree.settings();
1355 move |update| {
1356 snapshot
1357 .lock()
1358 .apply_remote_update(update, &settings.file_scan_inclusions);
1359 async { true }
1360 }
1361 });
1362 snapshot
1363 });
1364
1365 let entry = tree
1366 .update(cx, |tree, cx| {
1367 tree.as_local_mut()
1368 .unwrap()
1369 .create_entry(rel_path("a/e").into(), true, None, cx)
1370 })
1371 .await
1372 .unwrap()
1373 .into_included()
1374 .unwrap();
1375 assert!(entry.is_dir());
1376
1377 cx.executor().run_until_parked();
1378 tree.read_with(cx, |tree, _| {
1379 assert_eq!(
1380 tree.entry_for_path(rel_path("a/e")).unwrap().kind,
1381 EntryKind::Dir
1382 );
1383 });
1384
1385 let snapshot2 = tree.update(cx, |tree, _| tree.as_local().unwrap().snapshot());
1386 assert_eq!(
1387 snapshot1.lock().entries(true, 0).collect::<Vec<_>>(),
1388 snapshot2.entries(true, 0).collect::<Vec<_>>()
1389 );
1390}
1391
1392#[gpui::test]
1393async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
1394 init_test(cx);
1395 cx.executor().allow_parking();
1396
1397 let fs_fake = FakeFs::new(cx.background_executor.clone());
1398 fs_fake
1399 .insert_tree(
1400 "/root",
1401 json!({
1402 "a": {},
1403 }),
1404 )
1405 .await;
1406
1407 let tree_fake = Worktree::local(
1408 "/root".as_ref(),
1409 true,
1410 fs_fake,
1411 Default::default(),
1412 &mut cx.to_async(),
1413 )
1414 .await
1415 .unwrap();
1416
1417 let entry = tree_fake
1418 .update(cx, |tree, cx| {
1419 tree.as_local_mut().unwrap().create_entry(
1420 rel_path("a/b/c/d.txt").into(),
1421 false,
1422 None,
1423 cx,
1424 )
1425 })
1426 .await
1427 .unwrap()
1428 .into_included()
1429 .unwrap();
1430 assert!(entry.is_file());
1431
1432 cx.executor().run_until_parked();
1433 tree_fake.read_with(cx, |tree, _| {
1434 assert!(
1435 tree.entry_for_path(rel_path("a/b/c/d.txt"))
1436 .unwrap()
1437 .is_file()
1438 );
1439 assert!(tree.entry_for_path(rel_path("a/b/c")).unwrap().is_dir());
1440 assert!(tree.entry_for_path(rel_path("a/b")).unwrap().is_dir());
1441 });
1442
1443 let fs_real = Arc::new(RealFs::new(None, cx.executor()));
1444 let temp_root = TempTree::new(json!({
1445 "a": {}
1446 }));
1447
1448 let tree_real = Worktree::local(
1449 temp_root.path(),
1450 true,
1451 fs_real,
1452 Default::default(),
1453 &mut cx.to_async(),
1454 )
1455 .await
1456 .unwrap();
1457
1458 let entry = tree_real
1459 .update(cx, |tree, cx| {
1460 tree.as_local_mut().unwrap().create_entry(
1461 rel_path("a/b/c/d.txt").into(),
1462 false,
1463 None,
1464 cx,
1465 )
1466 })
1467 .await
1468 .unwrap()
1469 .into_included()
1470 .unwrap();
1471 assert!(entry.is_file());
1472
1473 cx.executor().run_until_parked();
1474 tree_real.read_with(cx, |tree, _| {
1475 assert!(
1476 tree.entry_for_path(rel_path("a/b/c/d.txt"))
1477 .unwrap()
1478 .is_file()
1479 );
1480 assert!(tree.entry_for_path(rel_path("a/b/c")).unwrap().is_dir());
1481 assert!(tree.entry_for_path(rel_path("a/b")).unwrap().is_dir());
1482 });
1483
1484 // Test smallest change
1485 let entry = tree_real
1486 .update(cx, |tree, cx| {
1487 tree.as_local_mut().unwrap().create_entry(
1488 rel_path("a/b/c/e.txt").into(),
1489 false,
1490 None,
1491 cx,
1492 )
1493 })
1494 .await
1495 .unwrap()
1496 .into_included()
1497 .unwrap();
1498 assert!(entry.is_file());
1499
1500 cx.executor().run_until_parked();
1501 tree_real.read_with(cx, |tree, _| {
1502 assert!(
1503 tree.entry_for_path(rel_path("a/b/c/e.txt"))
1504 .unwrap()
1505 .is_file()
1506 );
1507 });
1508
1509 // Test largest change
1510 let entry = tree_real
1511 .update(cx, |tree, cx| {
1512 tree.as_local_mut().unwrap().create_entry(
1513 rel_path("d/e/f/g.txt").into(),
1514 false,
1515 None,
1516 cx,
1517 )
1518 })
1519 .await
1520 .unwrap()
1521 .into_included()
1522 .unwrap();
1523 assert!(entry.is_file());
1524
1525 cx.executor().run_until_parked();
1526 tree_real.read_with(cx, |tree, _| {
1527 assert!(
1528 tree.entry_for_path(rel_path("d/e/f/g.txt"))
1529 .unwrap()
1530 .is_file()
1531 );
1532 assert!(tree.entry_for_path(rel_path("d/e/f")).unwrap().is_dir());
1533 assert!(tree.entry_for_path(rel_path("d/e")).unwrap().is_dir());
1534 assert!(tree.entry_for_path(rel_path("d")).unwrap().is_dir());
1535 });
1536}
1537
1538#[gpui::test(iterations = 100)]
1539async fn test_random_worktree_operations_during_initial_scan(
1540 cx: &mut TestAppContext,
1541 mut rng: StdRng,
1542) {
1543 init_test(cx);
1544 let operations = env::var("OPERATIONS")
1545 .map(|o| o.parse().unwrap())
1546 .unwrap_or(5);
1547 let initial_entries = env::var("INITIAL_ENTRIES")
1548 .map(|o| o.parse().unwrap())
1549 .unwrap_or(20);
1550
1551 let root_dir = Path::new(path!("/test"));
1552 let fs = FakeFs::new(cx.background_executor.clone()) as Arc<dyn Fs>;
1553 fs.as_fake().insert_tree(root_dir, json!({})).await;
1554 for _ in 0..initial_entries {
1555 randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
1556 }
1557 log::info!("generated initial tree");
1558
1559 let worktree = Worktree::local(
1560 root_dir,
1561 true,
1562 fs.clone(),
1563 Default::default(),
1564 &mut cx.to_async(),
1565 )
1566 .await
1567 .unwrap();
1568
1569 let mut snapshots = vec![worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot())];
1570 let updates = Arc::new(Mutex::new(Vec::new()));
1571 worktree.update(cx, |tree, cx| {
1572 check_worktree_change_events(tree, cx);
1573
1574 tree.as_local_mut().unwrap().observe_updates(0, cx, {
1575 let updates = updates.clone();
1576 move |update| {
1577 updates.lock().push(update);
1578 async { true }
1579 }
1580 });
1581 });
1582
1583 for _ in 0..operations {
1584 worktree
1585 .update(cx, |worktree, cx| {
1586 randomly_mutate_worktree(worktree, &mut rng, cx)
1587 })
1588 .await
1589 .log_err();
1590 worktree.read_with(cx, |tree, _| {
1591 tree.as_local().unwrap().snapshot().check_invariants(true)
1592 });
1593
1594 if rng.random_bool(0.6) {
1595 snapshots.push(worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot()));
1596 }
1597 }
1598
1599 worktree
1600 .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
1601 .await;
1602
1603 cx.executor().run_until_parked();
1604
1605 let final_snapshot = worktree.read_with(cx, |tree, _| {
1606 let tree = tree.as_local().unwrap();
1607 let snapshot = tree.snapshot();
1608 snapshot.check_invariants(true);
1609 snapshot
1610 });
1611
1612 let settings = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().settings());
1613
1614 for (i, snapshot) in snapshots.into_iter().enumerate().rev() {
1615 let mut updated_snapshot = snapshot.clone();
1616 for update in updates.lock().iter() {
1617 if update.scan_id >= updated_snapshot.scan_id() as u64 {
1618 updated_snapshot
1619 .apply_remote_update(update.clone(), &settings.file_scan_inclusions);
1620 }
1621 }
1622
1623 assert_eq!(
1624 updated_snapshot.entries(true, 0).collect::<Vec<_>>(),
1625 final_snapshot.entries(true, 0).collect::<Vec<_>>(),
1626 "wrong updates after snapshot {i}: {updates:#?}",
1627 );
1628 }
1629}
1630
1631#[gpui::test(iterations = 100)]
1632async fn test_random_worktree_changes(cx: &mut TestAppContext, mut rng: StdRng) {
1633 init_test(cx);
1634 let operations = env::var("OPERATIONS")
1635 .map(|o| o.parse().unwrap())
1636 .unwrap_or(40);
1637 let initial_entries = env::var("INITIAL_ENTRIES")
1638 .map(|o| o.parse().unwrap())
1639 .unwrap_or(20);
1640
1641 let root_dir = Path::new(path!("/test"));
1642 let fs = FakeFs::new(cx.background_executor.clone()) as Arc<dyn Fs>;
1643 fs.as_fake().insert_tree(root_dir, json!({})).await;
1644 for _ in 0..initial_entries {
1645 randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
1646 }
1647 log::info!("generated initial tree");
1648
1649 let worktree = Worktree::local(
1650 root_dir,
1651 true,
1652 fs.clone(),
1653 Default::default(),
1654 &mut cx.to_async(),
1655 )
1656 .await
1657 .unwrap();
1658
1659 let updates = Arc::new(Mutex::new(Vec::new()));
1660 worktree.update(cx, |tree, cx| {
1661 check_worktree_change_events(tree, cx);
1662
1663 tree.as_local_mut().unwrap().observe_updates(0, cx, {
1664 let updates = updates.clone();
1665 move |update| {
1666 updates.lock().push(update);
1667 async { true }
1668 }
1669 });
1670 });
1671
1672 worktree
1673 .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
1674 .await;
1675
1676 fs.as_fake().pause_events();
1677 let mut snapshots = Vec::new();
1678 let mut mutations_len = operations;
1679 while mutations_len > 1 {
1680 if rng.random_bool(0.2) {
1681 worktree
1682 .update(cx, |worktree, cx| {
1683 randomly_mutate_worktree(worktree, &mut rng, cx)
1684 })
1685 .await
1686 .log_err();
1687 } else {
1688 randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
1689 }
1690
1691 let buffered_event_count = fs.as_fake().buffered_event_count();
1692 if buffered_event_count > 0 && rng.random_bool(0.3) {
1693 let len = rng.random_range(0..=buffered_event_count);
1694 log::info!("flushing {} events", len);
1695 fs.as_fake().flush_events(len);
1696 } else {
1697 randomly_mutate_fs(&fs, root_dir, 0.6, &mut rng).await;
1698 mutations_len -= 1;
1699 }
1700
1701 cx.executor().run_until_parked();
1702 if rng.random_bool(0.2) {
1703 log::info!("storing snapshot {}", snapshots.len());
1704 let snapshot = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
1705 snapshots.push(snapshot);
1706 }
1707 }
1708
1709 log::info!("quiescing");
1710 fs.as_fake().flush_events(usize::MAX);
1711 cx.executor().run_until_parked();
1712
1713 let snapshot = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
1714 snapshot.check_invariants(true);
1715 let expanded_paths = snapshot
1716 .expanded_entries()
1717 .map(|e| e.path.clone())
1718 .collect::<Vec<_>>();
1719
1720 {
1721 let new_worktree = Worktree::local(
1722 root_dir,
1723 true,
1724 fs.clone(),
1725 Default::default(),
1726 &mut cx.to_async(),
1727 )
1728 .await
1729 .unwrap();
1730 new_worktree
1731 .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
1732 .await;
1733 new_worktree
1734 .update(cx, |tree, _| {
1735 tree.as_local_mut()
1736 .unwrap()
1737 .refresh_entries_for_paths(expanded_paths)
1738 })
1739 .recv()
1740 .await;
1741 let new_snapshot =
1742 new_worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
1743 assert_eq!(
1744 snapshot.entries_without_ids(true),
1745 new_snapshot.entries_without_ids(true)
1746 );
1747 }
1748
1749 let settings = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().settings());
1750
1751 for (i, mut prev_snapshot) in snapshots.into_iter().enumerate().rev() {
1752 for update in updates.lock().iter() {
1753 if update.scan_id >= prev_snapshot.scan_id() as u64 {
1754 prev_snapshot.apply_remote_update(update.clone(), &settings.file_scan_inclusions);
1755 }
1756 }
1757
1758 assert_eq!(
1759 prev_snapshot
1760 .entries(true, 0)
1761 .map(ignore_pending_dir)
1762 .collect::<Vec<_>>(),
1763 snapshot
1764 .entries(true, 0)
1765 .map(ignore_pending_dir)
1766 .collect::<Vec<_>>(),
1767 "wrong updates after snapshot {i}: {updates:#?}",
1768 );
1769 }
1770
1771 fn ignore_pending_dir(entry: &Entry) -> Entry {
1772 let mut entry = entry.clone();
1773 if entry.kind.is_dir() {
1774 entry.kind = EntryKind::Dir
1775 }
1776 entry
1777 }
1778}
1779
1780// The worktree's `UpdatedEntries` event can be used to follow along with
1781// all changes to the worktree's snapshot.
1782fn check_worktree_change_events(tree: &mut Worktree, cx: &mut Context<Worktree>) {
1783 let mut entries = tree.entries(true, 0).cloned().collect::<Vec<_>>();
1784 cx.subscribe(&cx.entity(), move |tree, _, event, _| {
1785 if let Event::UpdatedEntries(changes) = event {
1786 for (path, _, change_type) in changes.iter() {
1787 let entry = tree.entry_for_path(path).cloned();
1788 let ix = match entries.binary_search_by_key(&path, |e| &e.path) {
1789 Ok(ix) | Err(ix) => ix,
1790 };
1791 match change_type {
1792 PathChange::Added => entries.insert(ix, entry.unwrap()),
1793 PathChange::Removed => drop(entries.remove(ix)),
1794 PathChange::Updated => {
1795 let entry = entry.unwrap();
1796 let existing_entry = entries.get_mut(ix).unwrap();
1797 assert_eq!(existing_entry.path, entry.path);
1798 *existing_entry = entry;
1799 }
1800 PathChange::AddedOrUpdated | PathChange::Loaded => {
1801 let entry = entry.unwrap();
1802 if entries.get(ix).map(|e| &e.path) == Some(&entry.path) {
1803 *entries.get_mut(ix).unwrap() = entry;
1804 } else {
1805 entries.insert(ix, entry);
1806 }
1807 }
1808 }
1809 }
1810
1811 let new_entries = tree.entries(true, 0).cloned().collect::<Vec<_>>();
1812 assert_eq!(entries, new_entries, "incorrect changes: {:?}", changes);
1813 }
1814 })
1815 .detach();
1816}
1817
1818fn randomly_mutate_worktree(
1819 worktree: &mut Worktree,
1820 rng: &mut impl Rng,
1821 cx: &mut Context<Worktree>,
1822) -> Task<Result<()>> {
1823 log::info!("mutating worktree");
1824 let worktree = worktree.as_local_mut().unwrap();
1825 let snapshot = worktree.snapshot();
1826 let entry = snapshot.entries(false, 0).choose(rng).unwrap();
1827
1828 match rng.random_range(0_u32..100) {
1829 0..=33 if entry.path.as_ref() != RelPath::empty() => {
1830 log::info!("deleting entry {:?} ({})", entry.path, entry.id.0);
1831 worktree.delete_entry(entry.id, false, cx).unwrap()
1832 }
1833 _ => {
1834 if entry.is_dir() {
1835 let child_path = entry.path.join(rel_path(&random_filename(rng)));
1836 let is_dir = rng.random_bool(0.3);
1837 log::info!(
1838 "creating {} at {:?}",
1839 if is_dir { "dir" } else { "file" },
1840 child_path,
1841 );
1842 let task = worktree.create_entry(child_path, is_dir, None, cx);
1843 cx.background_spawn(async move {
1844 task.await?;
1845 Ok(())
1846 })
1847 } else {
1848 log::info!("overwriting file {:?} ({})", &entry.path, entry.id.0);
1849 let task =
1850 worktree.write_file(entry.path.clone(), "".into(), Default::default(), cx);
1851 cx.background_spawn(async move {
1852 task.await?;
1853 Ok(())
1854 })
1855 }
1856 }
1857 }
1858}
1859
1860async fn randomly_mutate_fs(
1861 fs: &Arc<dyn Fs>,
1862 root_path: &Path,
1863 insertion_probability: f64,
1864 rng: &mut impl Rng,
1865) {
1866 log::info!("mutating fs");
1867 let mut files = Vec::new();
1868 let mut dirs = Vec::new();
1869 for path in fs.as_fake().paths(false) {
1870 if path.starts_with(root_path) {
1871 if fs.is_file(&path).await {
1872 files.push(path);
1873 } else {
1874 dirs.push(path);
1875 }
1876 }
1877 }
1878
1879 if (files.is_empty() && dirs.len() == 1) || rng.random_bool(insertion_probability) {
1880 let path = dirs.choose(rng).unwrap();
1881 let new_path = path.join(random_filename(rng));
1882
1883 if rng.random() {
1884 log::info!(
1885 "creating dir {:?}",
1886 new_path.strip_prefix(root_path).unwrap()
1887 );
1888 fs.create_dir(&new_path).await.unwrap();
1889 } else {
1890 log::info!(
1891 "creating file {:?}",
1892 new_path.strip_prefix(root_path).unwrap()
1893 );
1894 fs.create_file(&new_path, Default::default()).await.unwrap();
1895 }
1896 } else if rng.random_bool(0.05) {
1897 let ignore_dir_path = dirs.choose(rng).unwrap();
1898 let ignore_path = ignore_dir_path.join(GITIGNORE);
1899
1900 let subdirs = dirs
1901 .iter()
1902 .filter(|d| d.starts_with(ignore_dir_path))
1903 .cloned()
1904 .collect::<Vec<_>>();
1905 let subfiles = files
1906 .iter()
1907 .filter(|d| d.starts_with(ignore_dir_path))
1908 .cloned()
1909 .collect::<Vec<_>>();
1910 let files_to_ignore = {
1911 let len = rng.random_range(0..=subfiles.len());
1912 subfiles.choose_multiple(rng, len)
1913 };
1914 let dirs_to_ignore = {
1915 let len = rng.random_range(0..subdirs.len());
1916 subdirs.choose_multiple(rng, len)
1917 };
1918
1919 let mut ignore_contents = String::new();
1920 for path_to_ignore in files_to_ignore.chain(dirs_to_ignore) {
1921 writeln!(
1922 ignore_contents,
1923 "{}",
1924 path_to_ignore
1925 .strip_prefix(ignore_dir_path)
1926 .unwrap()
1927 .to_str()
1928 .unwrap()
1929 )
1930 .unwrap();
1931 }
1932 log::info!(
1933 "creating gitignore {:?} with contents:\n{}",
1934 ignore_path.strip_prefix(root_path).unwrap(),
1935 ignore_contents
1936 );
1937 fs.save(
1938 &ignore_path,
1939 &ignore_contents.as_str().into(),
1940 Default::default(),
1941 )
1942 .await
1943 .unwrap();
1944 } else {
1945 let old_path = {
1946 let file_path = files.choose(rng);
1947 let dir_path = dirs[1..].choose(rng);
1948 file_path.into_iter().chain(dir_path).choose(rng).unwrap()
1949 };
1950
1951 let is_rename = rng.random();
1952 if is_rename {
1953 let new_path_parent = dirs
1954 .iter()
1955 .filter(|d| !d.starts_with(old_path))
1956 .choose(rng)
1957 .unwrap();
1958
1959 let overwrite_existing_dir =
1960 !old_path.starts_with(new_path_parent) && rng.random_bool(0.3);
1961 let new_path = if overwrite_existing_dir {
1962 fs.remove_dir(
1963 new_path_parent,
1964 RemoveOptions {
1965 recursive: true,
1966 ignore_if_not_exists: true,
1967 },
1968 )
1969 .await
1970 .unwrap();
1971 new_path_parent.to_path_buf()
1972 } else {
1973 new_path_parent.join(random_filename(rng))
1974 };
1975
1976 log::info!(
1977 "renaming {:?} to {}{:?}",
1978 old_path.strip_prefix(root_path).unwrap(),
1979 if overwrite_existing_dir {
1980 "overwrite "
1981 } else {
1982 ""
1983 },
1984 new_path.strip_prefix(root_path).unwrap()
1985 );
1986 fs.rename(
1987 old_path,
1988 &new_path,
1989 fs::RenameOptions {
1990 overwrite: true,
1991 ignore_if_exists: true,
1992 },
1993 )
1994 .await
1995 .unwrap();
1996 } else if fs.is_file(old_path).await {
1997 log::info!(
1998 "deleting file {:?}",
1999 old_path.strip_prefix(root_path).unwrap()
2000 );
2001 fs.remove_file(old_path, Default::default()).await.unwrap();
2002 } else {
2003 log::info!(
2004 "deleting dir {:?}",
2005 old_path.strip_prefix(root_path).unwrap()
2006 );
2007 fs.remove_dir(
2008 old_path,
2009 RemoveOptions {
2010 recursive: true,
2011 ignore_if_not_exists: true,
2012 },
2013 )
2014 .await
2015 .unwrap();
2016 }
2017 }
2018}
2019
2020fn random_filename(rng: &mut impl Rng) -> String {
2021 (0..6)
2022 .map(|_| rng.sample(rand::distr::Alphanumeric))
2023 .map(char::from)
2024 .collect()
2025}
2026
2027#[gpui::test]
2028async fn test_private_single_file_worktree(cx: &mut TestAppContext) {
2029 init_test(cx);
2030 let fs = FakeFs::new(cx.background_executor.clone());
2031 fs.insert_tree("/", json!({".env": "PRIVATE=secret\n"}))
2032 .await;
2033 let tree = Worktree::local(
2034 Path::new("/.env"),
2035 true,
2036 fs.clone(),
2037 Default::default(),
2038 &mut cx.to_async(),
2039 )
2040 .await
2041 .unwrap();
2042 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2043 .await;
2044 tree.read_with(cx, |tree, _| {
2045 let entry = tree.entry_for_path(rel_path("")).unwrap();
2046 assert!(entry.is_private);
2047 });
2048}
2049
2050#[gpui::test]
2051async fn test_repository_above_root(executor: BackgroundExecutor, cx: &mut TestAppContext) {
2052 init_test(cx);
2053
2054 let fs = FakeFs::new(executor);
2055 fs.insert_tree(
2056 path!("/root"),
2057 json!({
2058 ".git": {},
2059 "subproject": {
2060 "a.txt": "A"
2061 }
2062 }),
2063 )
2064 .await;
2065 let worktree = Worktree::local(
2066 path!("/root/subproject").as_ref(),
2067 true,
2068 fs.clone(),
2069 Arc::default(),
2070 &mut cx.to_async(),
2071 )
2072 .await
2073 .unwrap();
2074 worktree
2075 .update(cx, |worktree, _| {
2076 worktree.as_local().unwrap().scan_complete()
2077 })
2078 .await;
2079 cx.run_until_parked();
2080 let repos = worktree.update(cx, |worktree, _| {
2081 worktree
2082 .as_local()
2083 .unwrap()
2084 .git_repositories
2085 .values()
2086 .map(|entry| entry.work_directory_abs_path.clone())
2087 .collect::<Vec<_>>()
2088 });
2089 pretty_assertions::assert_eq!(repos, [Path::new(path!("/root")).into()]);
2090
2091 fs.touch_path(path!("/root/subproject")).await;
2092 worktree
2093 .update(cx, |worktree, _| {
2094 worktree.as_local().unwrap().scan_complete()
2095 })
2096 .await;
2097 cx.run_until_parked();
2098
2099 let repos = worktree.update(cx, |worktree, _| {
2100 worktree
2101 .as_local()
2102 .unwrap()
2103 .git_repositories
2104 .values()
2105 .map(|entry| entry.work_directory_abs_path.clone())
2106 .collect::<Vec<_>>()
2107 });
2108 pretty_assertions::assert_eq!(repos, [Path::new(path!("/root")).into()]);
2109}
2110
2111#[gpui::test]
2112async fn test_global_gitignore(executor: BackgroundExecutor, cx: &mut TestAppContext) {
2113 init_test(cx);
2114
2115 let home = paths::home_dir();
2116 let fs = FakeFs::new(executor);
2117 fs.insert_tree(
2118 home,
2119 json!({
2120 ".config": {
2121 "git": {
2122 "ignore": "foo\n/bar\nbaz\n"
2123 }
2124 },
2125 "project": {
2126 ".git": {},
2127 ".gitignore": "!baz",
2128 "foo": "",
2129 "bar": "",
2130 "sub": {
2131 "bar": "",
2132 },
2133 "subrepo": {
2134 ".git": {},
2135 "bar": ""
2136 },
2137 "baz": ""
2138 }
2139 }),
2140 )
2141 .await;
2142 let worktree = Worktree::local(
2143 home.join("project"),
2144 true,
2145 fs.clone(),
2146 Arc::default(),
2147 &mut cx.to_async(),
2148 )
2149 .await
2150 .unwrap();
2151 worktree
2152 .update(cx, |worktree, _| {
2153 worktree.as_local().unwrap().scan_complete()
2154 })
2155 .await;
2156 cx.run_until_parked();
2157
2158 // .gitignore overrides excludesFile, and anchored paths in excludesFile are resolved
2159 // relative to the nearest containing repository
2160 worktree.update(cx, |worktree, _cx| {
2161 check_worktree_entries(
2162 worktree,
2163 &[],
2164 &["foo", "bar", "subrepo/bar"],
2165 &["sub/bar", "baz"],
2166 &[],
2167 );
2168 });
2169
2170 // Ignore statuses are updated when excludesFile changes
2171 fs.write(
2172 &home.join(".config").join("git").join("ignore"),
2173 "/bar\nbaz\n".as_bytes(),
2174 )
2175 .await
2176 .unwrap();
2177 worktree
2178 .update(cx, |worktree, _| {
2179 worktree.as_local().unwrap().scan_complete()
2180 })
2181 .await;
2182 cx.run_until_parked();
2183
2184 worktree.update(cx, |worktree, _cx| {
2185 check_worktree_entries(
2186 worktree,
2187 &[],
2188 &["bar", "subrepo/bar"],
2189 &["foo", "sub/bar", "baz"],
2190 &[],
2191 );
2192 });
2193
2194 // Statuses are updated when .git added/removed
2195 fs.remove_dir(
2196 &home.join("project").join("subrepo").join(".git"),
2197 RemoveOptions {
2198 recursive: true,
2199 ..Default::default()
2200 },
2201 )
2202 .await
2203 .unwrap();
2204 worktree
2205 .update(cx, |worktree, _| {
2206 worktree.as_local().unwrap().scan_complete()
2207 })
2208 .await;
2209 cx.run_until_parked();
2210
2211 worktree.update(cx, |worktree, _cx| {
2212 check_worktree_entries(
2213 worktree,
2214 &[],
2215 &["bar"],
2216 &["foo", "sub/bar", "baz", "subrepo/bar"],
2217 &[],
2218 );
2219 });
2220}
2221
2222#[track_caller]
2223fn check_worktree_entries(
2224 tree: &Worktree,
2225 expected_excluded_paths: &[&str],
2226 expected_ignored_paths: &[&str],
2227 expected_tracked_paths: &[&str],
2228 expected_included_paths: &[&str],
2229) {
2230 for path in expected_excluded_paths {
2231 let entry = tree.entry_for_path(rel_path(path));
2232 assert!(
2233 entry.is_none(),
2234 "expected path '{path}' to be excluded, but got entry: {entry:?}",
2235 );
2236 }
2237 for path in expected_ignored_paths {
2238 let entry = tree
2239 .entry_for_path(rel_path(path))
2240 .unwrap_or_else(|| panic!("Missing entry for expected ignored path '{path}'"));
2241 assert!(
2242 entry.is_ignored,
2243 "expected path '{path}' to be ignored, but got entry: {entry:?}",
2244 );
2245 }
2246 for path in expected_tracked_paths {
2247 let entry = tree
2248 .entry_for_path(rel_path(path))
2249 .unwrap_or_else(|| panic!("Missing entry for expected tracked path '{path}'"));
2250 assert!(
2251 !entry.is_ignored || entry.is_always_included,
2252 "expected path '{path}' to be tracked, but got entry: {entry:?}",
2253 );
2254 }
2255 for path in expected_included_paths {
2256 let entry = tree
2257 .entry_for_path(rel_path(path))
2258 .unwrap_or_else(|| panic!("Missing entry for expected included path '{path}'"));
2259 assert!(
2260 entry.is_always_included,
2261 "expected path '{path}' to always be included, but got entry: {entry:?}",
2262 );
2263 }
2264}
2265
2266fn init_test(cx: &mut gpui::TestAppContext) {
2267 zlog::init_test();
2268
2269 cx.update(|cx| {
2270 let settings_store = SettingsStore::test(cx);
2271 cx.set_global(settings_store);
2272 WorktreeSettings::register(cx);
2273 });
2274}