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