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