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