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