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