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(cx, |settings| {
774 settings.project.worktree.file_scan_exclusions = Some(vec![]);
775 settings.project.worktree.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(cx, |settings| {
840 settings.project.worktree.file_scan_exclusions =
841 Some(vec!["**/.DS_Store".to_string()]);
842 settings.project.worktree.file_scan_inclusions =
843 Some(vec!["**/.DS_Store".to_string()]);
844 });
845 });
846 });
847
848 let tree = Worktree::local(
849 dir.path(),
850 true,
851 Arc::new(RealFs::new(None, cx.executor())),
852 Default::default(),
853 &mut cx.to_async(),
854 )
855 .await
856 .unwrap();
857 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
858 .await;
859 tree.flush_fs_events(cx).await;
860 tree.read_with(cx, |tree, _| {
861 // Assert that file_scan_inclusions overrides file_scan_exclusions.
862 check_worktree_entries(
863 tree,
864 &[".DS_Store, src/.DS_Store"],
865 &["target", "node_modules"],
866 &["src/foo/another.rs", "src/foo/foo.rs", ".gitignore"],
867 &[],
868 )
869 });
870}
871
872#[gpui::test]
873async fn test_file_scan_inclusions_reindexes_on_setting_change(cx: &mut TestAppContext) {
874 init_test(cx);
875 cx.executor().allow_parking();
876 let dir = TempTree::new(json!({
877 ".gitignore": "**/target\n/node_modules/\n",
878 "target": {
879 "index": "blah2"
880 },
881 "node_modules": {
882 ".DS_Store": "",
883 "prettier": {
884 "package.json": "{}",
885 },
886 },
887 "src": {
888 ".DS_Store": "",
889 "foo": {
890 "foo.rs": "mod another;\n",
891 "another.rs": "// another",
892 },
893 },
894 ".DS_Store": "",
895 }));
896
897 cx.update(|cx| {
898 cx.update_global::<SettingsStore, _>(|store, cx| {
899 store.update_user_settings(cx, |settings| {
900 settings.project.worktree.file_scan_exclusions = Some(vec![]);
901 settings.project.worktree.file_scan_inclusions =
902 Some(vec!["node_modules/**".to_string()]);
903 });
904 });
905 });
906 let tree = Worktree::local(
907 dir.path(),
908 true,
909 Arc::new(RealFs::new(None, cx.executor())),
910 Default::default(),
911 &mut cx.to_async(),
912 )
913 .await
914 .unwrap();
915 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
916 .await;
917 tree.flush_fs_events(cx).await;
918
919 tree.read_with(cx, |tree, _| {
920 assert!(
921 tree.entry_for_path("node_modules")
922 .is_some_and(|f| f.is_always_included)
923 );
924 assert!(
925 tree.entry_for_path("node_modules/prettier/package.json")
926 .is_some_and(|f| f.is_always_included)
927 );
928 });
929
930 cx.update(|cx| {
931 cx.update_global::<SettingsStore, _>(|store, cx| {
932 store.update_user_settings(cx, |settings| {
933 settings.project.worktree.file_scan_exclusions = Some(vec![]);
934 settings.project.worktree.file_scan_inclusions = Some(vec![]);
935 });
936 });
937 });
938 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
939 .await;
940 tree.flush_fs_events(cx).await;
941
942 tree.read_with(cx, |tree, _| {
943 assert!(
944 tree.entry_for_path("node_modules")
945 .is_some_and(|f| !f.is_always_included)
946 );
947 assert!(
948 tree.entry_for_path("node_modules/prettier/package.json")
949 .is_some_and(|f| !f.is_always_included)
950 );
951 });
952}
953
954#[gpui::test]
955async fn test_file_scan_exclusions(cx: &mut TestAppContext) {
956 init_test(cx);
957 cx.executor().allow_parking();
958 let dir = TempTree::new(json!({
959 ".gitignore": "**/target\n/node_modules\n",
960 "target": {
961 "index": "blah2"
962 },
963 "node_modules": {
964 ".DS_Store": "",
965 "prettier": {
966 "package.json": "{}",
967 },
968 },
969 "src": {
970 ".DS_Store": "",
971 "foo": {
972 "foo.rs": "mod another;\n",
973 "another.rs": "// another",
974 },
975 "bar": {
976 "bar.rs": "// bar",
977 },
978 "lib.rs": "mod foo;\nmod bar;\n",
979 },
980 ".DS_Store": "",
981 }));
982 cx.update(|cx| {
983 cx.update_global::<SettingsStore, _>(|store, cx| {
984 store.update_user_settings(cx, |settings| {
985 settings.project.worktree.file_scan_exclusions =
986 Some(vec!["**/foo/**".to_string(), "**/.DS_Store".to_string()]);
987 });
988 });
989 });
990
991 let tree = Worktree::local(
992 dir.path(),
993 true,
994 Arc::new(RealFs::new(None, cx.executor())),
995 Default::default(),
996 &mut cx.to_async(),
997 )
998 .await
999 .unwrap();
1000 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1001 .await;
1002 tree.flush_fs_events(cx).await;
1003 tree.read_with(cx, |tree, _| {
1004 check_worktree_entries(
1005 tree,
1006 &[
1007 "src/foo/foo.rs",
1008 "src/foo/another.rs",
1009 "node_modules/.DS_Store",
1010 "src/.DS_Store",
1011 ".DS_Store",
1012 ],
1013 &["target", "node_modules"],
1014 &["src/lib.rs", "src/bar/bar.rs", ".gitignore"],
1015 &[],
1016 )
1017 });
1018
1019 cx.update(|cx| {
1020 cx.update_global::<SettingsStore, _>(|store, cx| {
1021 store.update_user_settings(cx, |settings| {
1022 settings.project.worktree.file_scan_exclusions =
1023 Some(vec!["**/node_modules/**".to_string()]);
1024 });
1025 });
1026 });
1027 tree.flush_fs_events(cx).await;
1028 cx.executor().run_until_parked();
1029 tree.read_with(cx, |tree, _| {
1030 check_worktree_entries(
1031 tree,
1032 &[
1033 "node_modules/prettier/package.json",
1034 "node_modules/.DS_Store",
1035 "node_modules",
1036 ],
1037 &["target"],
1038 &[
1039 ".gitignore",
1040 "src/lib.rs",
1041 "src/bar/bar.rs",
1042 "src/foo/foo.rs",
1043 "src/foo/another.rs",
1044 "src/.DS_Store",
1045 ".DS_Store",
1046 ],
1047 &[],
1048 )
1049 });
1050}
1051
1052#[gpui::test]
1053async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) {
1054 init_test(cx);
1055 cx.executor().allow_parking();
1056 let dir = TempTree::new(json!({
1057 ".git": {
1058 "HEAD": "ref: refs/heads/main\n",
1059 "foo": "bar",
1060 },
1061 ".gitignore": "**/target\n/node_modules\ntest_output\n",
1062 "target": {
1063 "index": "blah2"
1064 },
1065 "node_modules": {
1066 ".DS_Store": "",
1067 "prettier": {
1068 "package.json": "{}",
1069 },
1070 },
1071 "src": {
1072 ".DS_Store": "",
1073 "foo": {
1074 "foo.rs": "mod another;\n",
1075 "another.rs": "// another",
1076 },
1077 "bar": {
1078 "bar.rs": "// bar",
1079 },
1080 "lib.rs": "mod foo;\nmod bar;\n",
1081 },
1082 ".DS_Store": "",
1083 }));
1084 cx.update(|cx| {
1085 cx.update_global::<SettingsStore, _>(|store, cx| {
1086 store.update_user_settings(cx, |settings| {
1087 settings.project.worktree.file_scan_exclusions = Some(vec![
1088 "**/.git".to_string(),
1089 "node_modules/".to_string(),
1090 "build_output".to_string(),
1091 ]);
1092 });
1093 });
1094 });
1095
1096 let tree = Worktree::local(
1097 dir.path(),
1098 true,
1099 Arc::new(RealFs::new(None, cx.executor())),
1100 Default::default(),
1101 &mut cx.to_async(),
1102 )
1103 .await
1104 .unwrap();
1105 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1106 .await;
1107 tree.flush_fs_events(cx).await;
1108 tree.read_with(cx, |tree, _| {
1109 check_worktree_entries(
1110 tree,
1111 &[
1112 ".git/HEAD",
1113 ".git/foo",
1114 "node_modules",
1115 "node_modules/.DS_Store",
1116 "node_modules/prettier",
1117 "node_modules/prettier/package.json",
1118 ],
1119 &["target"],
1120 &[
1121 ".DS_Store",
1122 "src/.DS_Store",
1123 "src/lib.rs",
1124 "src/foo/foo.rs",
1125 "src/foo/another.rs",
1126 "src/bar/bar.rs",
1127 ".gitignore",
1128 ],
1129 &[],
1130 )
1131 });
1132
1133 let new_excluded_dir = dir.path().join("build_output");
1134 let new_ignored_dir = dir.path().join("test_output");
1135 std::fs::create_dir_all(&new_excluded_dir)
1136 .unwrap_or_else(|e| panic!("Failed to create a {new_excluded_dir:?} directory: {e}"));
1137 std::fs::create_dir_all(&new_ignored_dir)
1138 .unwrap_or_else(|e| panic!("Failed to create a {new_ignored_dir:?} directory: {e}"));
1139 let node_modules_dir = dir.path().join("node_modules");
1140 let dot_git_dir = dir.path().join(".git");
1141 let src_dir = dir.path().join("src");
1142 for existing_dir in [&node_modules_dir, &dot_git_dir, &src_dir] {
1143 assert!(
1144 existing_dir.is_dir(),
1145 "Expect {existing_dir:?} to be present in the FS already"
1146 );
1147 }
1148
1149 for directory_for_new_file in [
1150 new_excluded_dir,
1151 new_ignored_dir,
1152 node_modules_dir,
1153 dot_git_dir,
1154 src_dir,
1155 ] {
1156 std::fs::write(directory_for_new_file.join("new_file"), "new file contents")
1157 .unwrap_or_else(|e| {
1158 panic!("Failed to create in {directory_for_new_file:?} a new file: {e}")
1159 });
1160 }
1161 tree.flush_fs_events(cx).await;
1162
1163 tree.read_with(cx, |tree, _| {
1164 check_worktree_entries(
1165 tree,
1166 &[
1167 ".git/HEAD",
1168 ".git/foo",
1169 ".git/new_file",
1170 "node_modules",
1171 "node_modules/.DS_Store",
1172 "node_modules/prettier",
1173 "node_modules/prettier/package.json",
1174 "node_modules/new_file",
1175 "build_output",
1176 "build_output/new_file",
1177 "test_output/new_file",
1178 ],
1179 &["target", "test_output"],
1180 &[
1181 ".DS_Store",
1182 "src/.DS_Store",
1183 "src/lib.rs",
1184 "src/foo/foo.rs",
1185 "src/foo/another.rs",
1186 "src/bar/bar.rs",
1187 "src/new_file",
1188 ".gitignore",
1189 ],
1190 &[],
1191 )
1192 });
1193}
1194
1195#[gpui::test]
1196async fn test_fs_events_in_dot_git_worktree(cx: &mut TestAppContext) {
1197 init_test(cx);
1198 cx.executor().allow_parking();
1199 let dir = TempTree::new(json!({
1200 ".git": {
1201 "HEAD": "ref: refs/heads/main\n",
1202 "foo": "foo contents",
1203 },
1204 }));
1205 let dot_git_worktree_dir = dir.path().join(".git");
1206
1207 let tree = Worktree::local(
1208 dot_git_worktree_dir.clone(),
1209 true,
1210 Arc::new(RealFs::new(None, cx.executor())),
1211 Default::default(),
1212 &mut cx.to_async(),
1213 )
1214 .await
1215 .unwrap();
1216 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1217 .await;
1218 tree.flush_fs_events(cx).await;
1219 tree.read_with(cx, |tree, _| {
1220 check_worktree_entries(tree, &[], &["HEAD", "foo"], &[], &[])
1221 });
1222
1223 std::fs::write(dot_git_worktree_dir.join("new_file"), "new file contents")
1224 .unwrap_or_else(|e| panic!("Failed to create in {dot_git_worktree_dir:?} a new file: {e}"));
1225 tree.flush_fs_events(cx).await;
1226 tree.read_with(cx, |tree, _| {
1227 check_worktree_entries(tree, &[], &["HEAD", "foo", "new_file"], &[], &[])
1228 });
1229}
1230
1231#[gpui::test(iterations = 30)]
1232async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
1233 init_test(cx);
1234 let fs = FakeFs::new(cx.background_executor.clone());
1235 fs.insert_tree(
1236 "/root",
1237 json!({
1238 "b": {},
1239 "c": {},
1240 "d": {},
1241 }),
1242 )
1243 .await;
1244
1245 let tree = Worktree::local(
1246 "/root".as_ref(),
1247 true,
1248 fs,
1249 Default::default(),
1250 &mut cx.to_async(),
1251 )
1252 .await
1253 .unwrap();
1254
1255 let snapshot1 = tree.update(cx, |tree, cx| {
1256 let tree = tree.as_local_mut().unwrap();
1257 let snapshot = Arc::new(Mutex::new(tree.snapshot()));
1258 tree.observe_updates(0, cx, {
1259 let snapshot = snapshot.clone();
1260 let settings = tree.settings();
1261 move |update| {
1262 snapshot
1263 .lock()
1264 .apply_remote_update(update, &settings.file_scan_inclusions)
1265 .unwrap();
1266 async { true }
1267 }
1268 });
1269 snapshot
1270 });
1271
1272 let entry = tree
1273 .update(cx, |tree, cx| {
1274 tree.as_local_mut()
1275 .unwrap()
1276 .create_entry("a/e".as_ref(), true, None, cx)
1277 })
1278 .await
1279 .unwrap()
1280 .into_included()
1281 .unwrap();
1282 assert!(entry.is_dir());
1283
1284 cx.executor().run_until_parked();
1285 tree.read_with(cx, |tree, _| {
1286 assert_eq!(tree.entry_for_path("a/e").unwrap().kind, EntryKind::Dir);
1287 });
1288
1289 let snapshot2 = tree.update(cx, |tree, _| tree.as_local().unwrap().snapshot());
1290 assert_eq!(
1291 snapshot1.lock().entries(true, 0).collect::<Vec<_>>(),
1292 snapshot2.entries(true, 0).collect::<Vec<_>>()
1293 );
1294}
1295
1296#[gpui::test]
1297async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
1298 init_test(cx);
1299 cx.executor().allow_parking();
1300
1301 let fs_fake = FakeFs::new(cx.background_executor.clone());
1302 fs_fake
1303 .insert_tree(
1304 "/root",
1305 json!({
1306 "a": {},
1307 }),
1308 )
1309 .await;
1310
1311 let tree_fake = Worktree::local(
1312 "/root".as_ref(),
1313 true,
1314 fs_fake,
1315 Default::default(),
1316 &mut cx.to_async(),
1317 )
1318 .await
1319 .unwrap();
1320
1321 let entry = tree_fake
1322 .update(cx, |tree, cx| {
1323 tree.as_local_mut()
1324 .unwrap()
1325 .create_entry("a/b/c/d.txt".as_ref(), false, None, cx)
1326 })
1327 .await
1328 .unwrap()
1329 .into_included()
1330 .unwrap();
1331 assert!(entry.is_file());
1332
1333 cx.executor().run_until_parked();
1334 tree_fake.read_with(cx, |tree, _| {
1335 assert!(tree.entry_for_path("a/b/c/d.txt").unwrap().is_file());
1336 assert!(tree.entry_for_path("a/b/c/").unwrap().is_dir());
1337 assert!(tree.entry_for_path("a/b/").unwrap().is_dir());
1338 });
1339
1340 let fs_real = Arc::new(RealFs::new(None, cx.executor()));
1341 let temp_root = TempTree::new(json!({
1342 "a": {}
1343 }));
1344
1345 let tree_real = Worktree::local(
1346 temp_root.path(),
1347 true,
1348 fs_real,
1349 Default::default(),
1350 &mut cx.to_async(),
1351 )
1352 .await
1353 .unwrap();
1354
1355 let entry = tree_real
1356 .update(cx, |tree, cx| {
1357 tree.as_local_mut()
1358 .unwrap()
1359 .create_entry("a/b/c/d.txt".as_ref(), false, None, cx)
1360 })
1361 .await
1362 .unwrap()
1363 .into_included()
1364 .unwrap();
1365 assert!(entry.is_file());
1366
1367 cx.executor().run_until_parked();
1368 tree_real.read_with(cx, |tree, _| {
1369 assert!(tree.entry_for_path("a/b/c/d.txt").unwrap().is_file());
1370 assert!(tree.entry_for_path("a/b/c/").unwrap().is_dir());
1371 assert!(tree.entry_for_path("a/b/").unwrap().is_dir());
1372 });
1373
1374 // Test smallest change
1375 let entry = tree_real
1376 .update(cx, |tree, cx| {
1377 tree.as_local_mut()
1378 .unwrap()
1379 .create_entry("a/b/c/e.txt".as_ref(), false, None, cx)
1380 })
1381 .await
1382 .unwrap()
1383 .into_included()
1384 .unwrap();
1385 assert!(entry.is_file());
1386
1387 cx.executor().run_until_parked();
1388 tree_real.read_with(cx, |tree, _| {
1389 assert!(tree.entry_for_path("a/b/c/e.txt").unwrap().is_file());
1390 });
1391
1392 // Test largest change
1393 let entry = tree_real
1394 .update(cx, |tree, cx| {
1395 tree.as_local_mut()
1396 .unwrap()
1397 .create_entry("d/e/f/g.txt".as_ref(), false, None, cx)
1398 })
1399 .await
1400 .unwrap()
1401 .into_included()
1402 .unwrap();
1403 assert!(entry.is_file());
1404
1405 cx.executor().run_until_parked();
1406 tree_real.read_with(cx, |tree, _| {
1407 assert!(tree.entry_for_path("d/e/f/g.txt").unwrap().is_file());
1408 assert!(tree.entry_for_path("d/e/f").unwrap().is_dir());
1409 assert!(tree.entry_for_path("d/e/").unwrap().is_dir());
1410 assert!(tree.entry_for_path("d/").unwrap().is_dir());
1411 });
1412}
1413
1414#[gpui::test(iterations = 100)]
1415async fn test_random_worktree_operations_during_initial_scan(
1416 cx: &mut TestAppContext,
1417 mut rng: StdRng,
1418) {
1419 init_test(cx);
1420 let operations = env::var("OPERATIONS")
1421 .map(|o| o.parse().unwrap())
1422 .unwrap_or(5);
1423 let initial_entries = env::var("INITIAL_ENTRIES")
1424 .map(|o| o.parse().unwrap())
1425 .unwrap_or(20);
1426
1427 let root_dir = Path::new(path!("/test"));
1428 let fs = FakeFs::new(cx.background_executor.clone()) as Arc<dyn Fs>;
1429 fs.as_fake().insert_tree(root_dir, json!({})).await;
1430 for _ in 0..initial_entries {
1431 randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
1432 }
1433 log::info!("generated initial tree");
1434
1435 let worktree = Worktree::local(
1436 root_dir,
1437 true,
1438 fs.clone(),
1439 Default::default(),
1440 &mut cx.to_async(),
1441 )
1442 .await
1443 .unwrap();
1444
1445 let mut snapshots = vec![worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot())];
1446 let updates = Arc::new(Mutex::new(Vec::new()));
1447 worktree.update(cx, |tree, cx| {
1448 check_worktree_change_events(tree, cx);
1449
1450 tree.as_local_mut().unwrap().observe_updates(0, cx, {
1451 let updates = updates.clone();
1452 move |update| {
1453 updates.lock().push(update);
1454 async { true }
1455 }
1456 });
1457 });
1458
1459 for _ in 0..operations {
1460 worktree
1461 .update(cx, |worktree, cx| {
1462 randomly_mutate_worktree(worktree, &mut rng, cx)
1463 })
1464 .await
1465 .log_err();
1466 worktree.read_with(cx, |tree, _| {
1467 tree.as_local().unwrap().snapshot().check_invariants(true)
1468 });
1469
1470 if rng.random_bool(0.6) {
1471 snapshots.push(worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot()));
1472 }
1473 }
1474
1475 worktree
1476 .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
1477 .await;
1478
1479 cx.executor().run_until_parked();
1480
1481 let final_snapshot = worktree.read_with(cx, |tree, _| {
1482 let tree = tree.as_local().unwrap();
1483 let snapshot = tree.snapshot();
1484 snapshot.check_invariants(true);
1485 snapshot
1486 });
1487
1488 let settings = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().settings());
1489
1490 for (i, snapshot) in snapshots.into_iter().enumerate().rev() {
1491 let mut updated_snapshot = snapshot.clone();
1492 for update in updates.lock().iter() {
1493 if update.scan_id >= updated_snapshot.scan_id() as u64 {
1494 updated_snapshot
1495 .apply_remote_update(update.clone(), &settings.file_scan_inclusions)
1496 .unwrap();
1497 }
1498 }
1499
1500 assert_eq!(
1501 updated_snapshot.entries(true, 0).collect::<Vec<_>>(),
1502 final_snapshot.entries(true, 0).collect::<Vec<_>>(),
1503 "wrong updates after snapshot {i}: {snapshot:#?} {updates:#?}",
1504 );
1505 }
1506}
1507
1508#[gpui::test(iterations = 100)]
1509async fn test_random_worktree_changes(cx: &mut TestAppContext, mut rng: StdRng) {
1510 init_test(cx);
1511 let operations = env::var("OPERATIONS")
1512 .map(|o| o.parse().unwrap())
1513 .unwrap_or(40);
1514 let initial_entries = env::var("INITIAL_ENTRIES")
1515 .map(|o| o.parse().unwrap())
1516 .unwrap_or(20);
1517
1518 let root_dir = Path::new(path!("/test"));
1519 let fs = FakeFs::new(cx.background_executor.clone()) as Arc<dyn Fs>;
1520 fs.as_fake().insert_tree(root_dir, json!({})).await;
1521 for _ in 0..initial_entries {
1522 randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
1523 }
1524 log::info!("generated initial tree");
1525
1526 let worktree = Worktree::local(
1527 root_dir,
1528 true,
1529 fs.clone(),
1530 Default::default(),
1531 &mut cx.to_async(),
1532 )
1533 .await
1534 .unwrap();
1535
1536 let updates = Arc::new(Mutex::new(Vec::new()));
1537 worktree.update(cx, |tree, cx| {
1538 check_worktree_change_events(tree, cx);
1539
1540 tree.as_local_mut().unwrap().observe_updates(0, cx, {
1541 let updates = updates.clone();
1542 move |update| {
1543 updates.lock().push(update);
1544 async { true }
1545 }
1546 });
1547 });
1548
1549 worktree
1550 .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
1551 .await;
1552
1553 fs.as_fake().pause_events();
1554 let mut snapshots = Vec::new();
1555 let mut mutations_len = operations;
1556 while mutations_len > 1 {
1557 if rng.random_bool(0.2) {
1558 worktree
1559 .update(cx, |worktree, cx| {
1560 randomly_mutate_worktree(worktree, &mut rng, cx)
1561 })
1562 .await
1563 .log_err();
1564 } else {
1565 randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
1566 }
1567
1568 let buffered_event_count = fs.as_fake().buffered_event_count();
1569 if buffered_event_count > 0 && rng.random_bool(0.3) {
1570 let len = rng.random_range(0..=buffered_event_count);
1571 log::info!("flushing {} events", len);
1572 fs.as_fake().flush_events(len);
1573 } else {
1574 randomly_mutate_fs(&fs, root_dir, 0.6, &mut rng).await;
1575 mutations_len -= 1;
1576 }
1577
1578 cx.executor().run_until_parked();
1579 if rng.random_bool(0.2) {
1580 log::info!("storing snapshot {}", snapshots.len());
1581 let snapshot = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
1582 snapshots.push(snapshot);
1583 }
1584 }
1585
1586 log::info!("quiescing");
1587 fs.as_fake().flush_events(usize::MAX);
1588 cx.executor().run_until_parked();
1589
1590 let snapshot = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
1591 snapshot.check_invariants(true);
1592 let expanded_paths = snapshot
1593 .expanded_entries()
1594 .map(|e| e.path.clone())
1595 .collect::<Vec<_>>();
1596
1597 {
1598 let new_worktree = Worktree::local(
1599 root_dir,
1600 true,
1601 fs.clone(),
1602 Default::default(),
1603 &mut cx.to_async(),
1604 )
1605 .await
1606 .unwrap();
1607 new_worktree
1608 .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
1609 .await;
1610 new_worktree
1611 .update(cx, |tree, _| {
1612 tree.as_local_mut()
1613 .unwrap()
1614 .refresh_entries_for_paths(expanded_paths)
1615 })
1616 .recv()
1617 .await;
1618 let new_snapshot =
1619 new_worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
1620 assert_eq!(
1621 snapshot.entries_without_ids(true),
1622 new_snapshot.entries_without_ids(true)
1623 );
1624 }
1625
1626 let settings = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().settings());
1627
1628 for (i, mut prev_snapshot) in snapshots.into_iter().enumerate().rev() {
1629 for update in updates.lock().iter() {
1630 if update.scan_id >= prev_snapshot.scan_id() as u64 {
1631 prev_snapshot
1632 .apply_remote_update(update.clone(), &settings.file_scan_inclusions)
1633 .unwrap();
1634 }
1635 }
1636
1637 assert_eq!(
1638 prev_snapshot
1639 .entries(true, 0)
1640 .map(ignore_pending_dir)
1641 .collect::<Vec<_>>(),
1642 snapshot
1643 .entries(true, 0)
1644 .map(ignore_pending_dir)
1645 .collect::<Vec<_>>(),
1646 "wrong updates after snapshot {i}: {updates:#?}",
1647 );
1648 }
1649
1650 fn ignore_pending_dir(entry: &Entry) -> Entry {
1651 let mut entry = entry.clone();
1652 if entry.kind.is_dir() {
1653 entry.kind = EntryKind::Dir
1654 }
1655 entry
1656 }
1657}
1658
1659// The worktree's `UpdatedEntries` event can be used to follow along with
1660// all changes to the worktree's snapshot.
1661fn check_worktree_change_events(tree: &mut Worktree, cx: &mut Context<Worktree>) {
1662 let mut entries = tree.entries(true, 0).cloned().collect::<Vec<_>>();
1663 cx.subscribe(&cx.entity(), move |tree, _, event, _| {
1664 if let Event::UpdatedEntries(changes) = event {
1665 for (path, _, change_type) in changes.iter() {
1666 let entry = tree.entry_for_path(path).cloned();
1667 let ix = match entries.binary_search_by_key(&path, |e| &e.path) {
1668 Ok(ix) | Err(ix) => ix,
1669 };
1670 match change_type {
1671 PathChange::Added => entries.insert(ix, entry.unwrap()),
1672 PathChange::Removed => drop(entries.remove(ix)),
1673 PathChange::Updated => {
1674 let entry = entry.unwrap();
1675 let existing_entry = entries.get_mut(ix).unwrap();
1676 assert_eq!(existing_entry.path, entry.path);
1677 *existing_entry = entry;
1678 }
1679 PathChange::AddedOrUpdated | PathChange::Loaded => {
1680 let entry = entry.unwrap();
1681 if entries.get(ix).map(|e| &e.path) == Some(&entry.path) {
1682 *entries.get_mut(ix).unwrap() = entry;
1683 } else {
1684 entries.insert(ix, entry);
1685 }
1686 }
1687 }
1688 }
1689
1690 let new_entries = tree.entries(true, 0).cloned().collect::<Vec<_>>();
1691 assert_eq!(entries, new_entries, "incorrect changes: {:?}", changes);
1692 }
1693 })
1694 .detach();
1695}
1696
1697fn randomly_mutate_worktree(
1698 worktree: &mut Worktree,
1699 rng: &mut impl Rng,
1700 cx: &mut Context<Worktree>,
1701) -> Task<Result<()>> {
1702 log::info!("mutating worktree");
1703 let worktree = worktree.as_local_mut().unwrap();
1704 let snapshot = worktree.snapshot();
1705 let entry = snapshot.entries(false, 0).choose(rng).unwrap();
1706
1707 match rng.random_range(0_u32..100) {
1708 0..=33 if entry.path.as_ref() != Path::new("") => {
1709 log::info!("deleting entry {:?} ({})", entry.path, entry.id.0);
1710 worktree.delete_entry(entry.id, false, cx).unwrap()
1711 }
1712 ..=66 if entry.path.as_ref() != Path::new("") => {
1713 let other_entry = snapshot.entries(false, 0).choose(rng).unwrap();
1714 let new_parent_path = if other_entry.is_dir() {
1715 other_entry.path.clone()
1716 } else {
1717 other_entry.path.parent().unwrap().into()
1718 };
1719 let mut new_path = new_parent_path.join(random_filename(rng));
1720 if new_path.starts_with(&entry.path) {
1721 new_path = random_filename(rng).into();
1722 }
1723
1724 log::info!(
1725 "renaming entry {:?} ({}) to {:?}",
1726 entry.path,
1727 entry.id.0,
1728 new_path
1729 );
1730 let task = worktree.rename_entry(entry.id, new_path, cx);
1731 cx.background_spawn(async move {
1732 task.await?.into_included().unwrap();
1733 Ok(())
1734 })
1735 }
1736 _ => {
1737 if entry.is_dir() {
1738 let child_path = entry.path.join(random_filename(rng));
1739 let is_dir = rng.random_bool(0.3);
1740 log::info!(
1741 "creating {} at {:?}",
1742 if is_dir { "dir" } else { "file" },
1743 child_path,
1744 );
1745 let task = worktree.create_entry(child_path, is_dir, None, cx);
1746 cx.background_spawn(async move {
1747 task.await?;
1748 Ok(())
1749 })
1750 } else {
1751 log::info!("overwriting file {:?} ({})", entry.path, entry.id.0);
1752 let task =
1753 worktree.write_file(entry.path.clone(), "".into(), Default::default(), cx);
1754 cx.background_spawn(async move {
1755 task.await?;
1756 Ok(())
1757 })
1758 }
1759 }
1760 }
1761}
1762
1763async fn randomly_mutate_fs(
1764 fs: &Arc<dyn Fs>,
1765 root_path: &Path,
1766 insertion_probability: f64,
1767 rng: &mut impl Rng,
1768) {
1769 log::info!("mutating fs");
1770 let mut files = Vec::new();
1771 let mut dirs = Vec::new();
1772 for path in fs.as_fake().paths(false) {
1773 if path.starts_with(root_path) {
1774 if fs.is_file(&path).await {
1775 files.push(path);
1776 } else {
1777 dirs.push(path);
1778 }
1779 }
1780 }
1781
1782 if (files.is_empty() && dirs.len() == 1) || rng.random_bool(insertion_probability) {
1783 let path = dirs.choose(rng).unwrap();
1784 let new_path = path.join(random_filename(rng));
1785
1786 if rng.random() {
1787 log::info!(
1788 "creating dir {:?}",
1789 new_path.strip_prefix(root_path).unwrap()
1790 );
1791 fs.create_dir(&new_path).await.unwrap();
1792 } else {
1793 log::info!(
1794 "creating file {:?}",
1795 new_path.strip_prefix(root_path).unwrap()
1796 );
1797 fs.create_file(&new_path, Default::default()).await.unwrap();
1798 }
1799 } else if rng.random_bool(0.05) {
1800 let ignore_dir_path = dirs.choose(rng).unwrap();
1801 let ignore_path = ignore_dir_path.join(*GITIGNORE);
1802
1803 let subdirs = dirs
1804 .iter()
1805 .filter(|d| d.starts_with(ignore_dir_path))
1806 .cloned()
1807 .collect::<Vec<_>>();
1808 let subfiles = files
1809 .iter()
1810 .filter(|d| d.starts_with(ignore_dir_path))
1811 .cloned()
1812 .collect::<Vec<_>>();
1813 let files_to_ignore = {
1814 let len = rng.random_range(0..=subfiles.len());
1815 subfiles.choose_multiple(rng, len)
1816 };
1817 let dirs_to_ignore = {
1818 let len = rng.random_range(0..subdirs.len());
1819 subdirs.choose_multiple(rng, len)
1820 };
1821
1822 let mut ignore_contents = String::new();
1823 for path_to_ignore in files_to_ignore.chain(dirs_to_ignore) {
1824 writeln!(
1825 ignore_contents,
1826 "{}",
1827 path_to_ignore
1828 .strip_prefix(ignore_dir_path)
1829 .unwrap()
1830 .to_str()
1831 .unwrap()
1832 )
1833 .unwrap();
1834 }
1835 log::info!(
1836 "creating gitignore {:?} with contents:\n{}",
1837 ignore_path.strip_prefix(root_path).unwrap(),
1838 ignore_contents
1839 );
1840 fs.save(
1841 &ignore_path,
1842 &ignore_contents.as_str().into(),
1843 Default::default(),
1844 )
1845 .await
1846 .unwrap();
1847 } else {
1848 let old_path = {
1849 let file_path = files.choose(rng);
1850 let dir_path = dirs[1..].choose(rng);
1851 file_path.into_iter().chain(dir_path).choose(rng).unwrap()
1852 };
1853
1854 let is_rename = rng.random();
1855 if is_rename {
1856 let new_path_parent = dirs
1857 .iter()
1858 .filter(|d| !d.starts_with(old_path))
1859 .choose(rng)
1860 .unwrap();
1861
1862 let overwrite_existing_dir =
1863 !old_path.starts_with(new_path_parent) && rng.random_bool(0.3);
1864 let new_path = if overwrite_existing_dir {
1865 fs.remove_dir(
1866 new_path_parent,
1867 RemoveOptions {
1868 recursive: true,
1869 ignore_if_not_exists: true,
1870 },
1871 )
1872 .await
1873 .unwrap();
1874 new_path_parent.to_path_buf()
1875 } else {
1876 new_path_parent.join(random_filename(rng))
1877 };
1878
1879 log::info!(
1880 "renaming {:?} to {}{:?}",
1881 old_path.strip_prefix(root_path).unwrap(),
1882 if overwrite_existing_dir {
1883 "overwrite "
1884 } else {
1885 ""
1886 },
1887 new_path.strip_prefix(root_path).unwrap()
1888 );
1889 fs.rename(
1890 old_path,
1891 &new_path,
1892 fs::RenameOptions {
1893 overwrite: true,
1894 ignore_if_exists: true,
1895 },
1896 )
1897 .await
1898 .unwrap();
1899 } else if fs.is_file(old_path).await {
1900 log::info!(
1901 "deleting file {:?}",
1902 old_path.strip_prefix(root_path).unwrap()
1903 );
1904 fs.remove_file(old_path, Default::default()).await.unwrap();
1905 } else {
1906 log::info!(
1907 "deleting dir {:?}",
1908 old_path.strip_prefix(root_path).unwrap()
1909 );
1910 fs.remove_dir(
1911 old_path,
1912 RemoveOptions {
1913 recursive: true,
1914 ignore_if_not_exists: true,
1915 },
1916 )
1917 .await
1918 .unwrap();
1919 }
1920 }
1921}
1922
1923fn random_filename(rng: &mut impl Rng) -> String {
1924 (0..6)
1925 .map(|_| rng.sample(rand::distr::Alphanumeric))
1926 .map(char::from)
1927 .collect()
1928}
1929
1930#[gpui::test]
1931async fn test_rename_file_to_new_directory(cx: &mut TestAppContext) {
1932 init_test(cx);
1933 let fs = FakeFs::new(cx.background_executor.clone());
1934 let expected_contents = "content";
1935 fs.as_fake()
1936 .insert_tree(
1937 "/root",
1938 json!({
1939 "test.txt": expected_contents
1940 }),
1941 )
1942 .await;
1943 let worktree = Worktree::local(
1944 Path::new("/root"),
1945 true,
1946 fs.clone(),
1947 Arc::default(),
1948 &mut cx.to_async(),
1949 )
1950 .await
1951 .unwrap();
1952 cx.read(|cx| worktree.read(cx).as_local().unwrap().scan_complete())
1953 .await;
1954
1955 let entry_id = worktree.read_with(cx, |worktree, _| {
1956 worktree.entry_for_path("test.txt").unwrap().id
1957 });
1958 let _result = worktree
1959 .update(cx, |worktree, cx| {
1960 worktree.rename_entry(entry_id, Path::new("dir1/dir2/dir3/test.txt"), cx)
1961 })
1962 .await
1963 .unwrap();
1964 worktree.read_with(cx, |worktree, _| {
1965 assert!(
1966 worktree.entry_for_path("test.txt").is_none(),
1967 "Old file should have been removed"
1968 );
1969 assert!(
1970 worktree.entry_for_path("dir1/dir2/dir3/test.txt").is_some(),
1971 "Whole directory hierarchy and the new file should have been created"
1972 );
1973 });
1974 assert_eq!(
1975 worktree
1976 .update(cx, |worktree, cx| {
1977 worktree.load_file("dir1/dir2/dir3/test.txt".as_ref(), cx)
1978 })
1979 .await
1980 .unwrap()
1981 .text,
1982 expected_contents,
1983 "Moved file's contents should be preserved"
1984 );
1985
1986 let entry_id = worktree.read_with(cx, |worktree, _| {
1987 worktree
1988 .entry_for_path("dir1/dir2/dir3/test.txt")
1989 .unwrap()
1990 .id
1991 });
1992 let _result = worktree
1993 .update(cx, |worktree, cx| {
1994 worktree.rename_entry(entry_id, Path::new("dir1/dir2/test.txt"), cx)
1995 })
1996 .await
1997 .unwrap();
1998 worktree.read_with(cx, |worktree, _| {
1999 assert!(
2000 worktree.entry_for_path("test.txt").is_none(),
2001 "First file should not reappear"
2002 );
2003 assert!(
2004 worktree.entry_for_path("dir1/dir2/dir3/test.txt").is_none(),
2005 "Old file should have been removed"
2006 );
2007 assert!(
2008 worktree.entry_for_path("dir1/dir2/test.txt").is_some(),
2009 "No error should have occurred after moving into existing directory"
2010 );
2011 });
2012 assert_eq!(
2013 worktree
2014 .update(cx, |worktree, cx| {
2015 worktree.load_file("dir1/dir2/test.txt".as_ref(), cx)
2016 })
2017 .await
2018 .unwrap()
2019 .text,
2020 expected_contents,
2021 "Moved file's contents should be preserved"
2022 );
2023}
2024
2025#[gpui::test]
2026async fn test_private_single_file_worktree(cx: &mut TestAppContext) {
2027 init_test(cx);
2028 let fs = FakeFs::new(cx.background_executor.clone());
2029 fs.insert_tree("/", json!({".env": "PRIVATE=secret\n"}))
2030 .await;
2031 let tree = Worktree::local(
2032 Path::new("/.env"),
2033 true,
2034 fs.clone(),
2035 Default::default(),
2036 &mut cx.to_async(),
2037 )
2038 .await
2039 .unwrap();
2040 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2041 .await;
2042 tree.read_with(cx, |tree, _| {
2043 let entry = tree.entry_for_path("").unwrap();
2044 assert!(entry.is_private);
2045 });
2046}
2047
2048#[gpui::test]
2049fn test_unrelativize() {
2050 let work_directory = WorkDirectory::in_project("");
2051 pretty_assertions::assert_eq!(
2052 work_directory.try_unrelativize(&"crates/gpui/gpui.rs".into()),
2053 Some(Path::new("crates/gpui/gpui.rs").into())
2054 );
2055
2056 let work_directory = WorkDirectory::in_project("vendor/some-submodule");
2057 pretty_assertions::assert_eq!(
2058 work_directory.try_unrelativize(&"src/thing.c".into()),
2059 Some(Path::new("vendor/some-submodule/src/thing.c").into())
2060 );
2061
2062 let work_directory = WorkDirectory::AboveProject {
2063 absolute_path: Path::new("/projects/zed").into(),
2064 location_in_repo: Path::new("crates/gpui").into(),
2065 };
2066
2067 pretty_assertions::assert_eq!(
2068 work_directory.try_unrelativize(&"crates/util/util.rs".into()),
2069 None,
2070 );
2071
2072 pretty_assertions::assert_eq!(
2073 work_directory.unrelativize(&"crates/util/util.rs".into()),
2074 Path::new("../util/util.rs").into()
2075 );
2076
2077 pretty_assertions::assert_eq!(work_directory.try_unrelativize(&"README.md".into()), None,);
2078
2079 pretty_assertions::assert_eq!(
2080 work_directory.unrelativize(&"README.md".into()),
2081 Path::new("../../README.md").into()
2082 );
2083}
2084
2085#[gpui::test]
2086async fn test_repository_above_root(executor: BackgroundExecutor, cx: &mut TestAppContext) {
2087 init_test(cx);
2088
2089 let fs = FakeFs::new(executor);
2090 fs.insert_tree(
2091 path!("/root"),
2092 json!({
2093 ".git": {},
2094 "subproject": {
2095 "a.txt": "A"
2096 }
2097 }),
2098 )
2099 .await;
2100 let worktree = Worktree::local(
2101 path!("/root/subproject").as_ref(),
2102 true,
2103 fs.clone(),
2104 Arc::default(),
2105 &mut cx.to_async(),
2106 )
2107 .await
2108 .unwrap();
2109 worktree
2110 .update(cx, |worktree, _| {
2111 worktree.as_local().unwrap().scan_complete()
2112 })
2113 .await;
2114 cx.run_until_parked();
2115 let repos = worktree.update(cx, |worktree, _| {
2116 worktree
2117 .as_local()
2118 .unwrap()
2119 .git_repositories
2120 .values()
2121 .map(|entry| entry.work_directory_abs_path.clone())
2122 .collect::<Vec<_>>()
2123 });
2124 pretty_assertions::assert_eq!(repos, [Path::new(path!("/root")).into()]);
2125
2126 fs.touch_path(path!("/root/subproject")).await;
2127 worktree
2128 .update(cx, |worktree, _| {
2129 worktree.as_local().unwrap().scan_complete()
2130 })
2131 .await;
2132 cx.run_until_parked();
2133
2134 let repos = worktree.update(cx, |worktree, _| {
2135 worktree
2136 .as_local()
2137 .unwrap()
2138 .git_repositories
2139 .values()
2140 .map(|entry| entry.work_directory_abs_path.clone())
2141 .collect::<Vec<_>>()
2142 });
2143 pretty_assertions::assert_eq!(repos, [Path::new(path!("/root")).into()]);
2144}
2145
2146#[gpui::test]
2147async fn test_global_gitignore(executor: BackgroundExecutor, cx: &mut TestAppContext) {
2148 init_test(cx);
2149
2150 let home = paths::home_dir();
2151 let fs = FakeFs::new(executor);
2152 fs.insert_tree(
2153 home,
2154 json!({
2155 ".config": {
2156 "git": {
2157 "ignore": "foo\n/bar\nbaz\n"
2158 }
2159 },
2160 "project": {
2161 ".git": {},
2162 ".gitignore": "!baz",
2163 "foo": "",
2164 "bar": "",
2165 "sub": {
2166 "bar": "",
2167 },
2168 "subrepo": {
2169 ".git": {},
2170 "bar": ""
2171 },
2172 "baz": ""
2173 }
2174 }),
2175 )
2176 .await;
2177 let worktree = Worktree::local(
2178 home.join("project"),
2179 true,
2180 fs.clone(),
2181 Arc::default(),
2182 &mut cx.to_async(),
2183 )
2184 .await
2185 .unwrap();
2186 worktree
2187 .update(cx, |worktree, _| {
2188 worktree.as_local().unwrap().scan_complete()
2189 })
2190 .await;
2191 cx.run_until_parked();
2192
2193 // .gitignore overrides excludesFile, and anchored paths in excludesFile are resolved
2194 // relative to the nearest containing repository
2195 worktree.update(cx, |worktree, _cx| {
2196 check_worktree_entries(
2197 worktree,
2198 &[],
2199 &["foo", "bar", "subrepo/bar"],
2200 &["sub/bar", "baz"],
2201 &[],
2202 );
2203 });
2204
2205 // Ignore statuses are updated when excludesFile changes
2206 fs.write(
2207 &home.join(".config").join("git").join("ignore"),
2208 "/bar\nbaz\n".as_bytes(),
2209 )
2210 .await
2211 .unwrap();
2212 worktree
2213 .update(cx, |worktree, _| {
2214 worktree.as_local().unwrap().scan_complete()
2215 })
2216 .await;
2217 cx.run_until_parked();
2218
2219 worktree.update(cx, |worktree, _cx| {
2220 check_worktree_entries(
2221 worktree,
2222 &[],
2223 &["bar", "subrepo/bar"],
2224 &["foo", "sub/bar", "baz"],
2225 &[],
2226 );
2227 });
2228
2229 // Statuses are updated when .git added/removed
2230 fs.remove_dir(
2231 &home.join("project").join("subrepo").join(".git"),
2232 RemoveOptions {
2233 recursive: true,
2234 ..Default::default()
2235 },
2236 )
2237 .await
2238 .unwrap();
2239 worktree
2240 .update(cx, |worktree, _| {
2241 worktree.as_local().unwrap().scan_complete()
2242 })
2243 .await;
2244 cx.run_until_parked();
2245
2246 worktree.update(cx, |worktree, _cx| {
2247 check_worktree_entries(
2248 worktree,
2249 &[],
2250 &["bar"],
2251 &["foo", "sub/bar", "baz", "subrepo/bar"],
2252 &[],
2253 );
2254 });
2255}
2256
2257#[track_caller]
2258fn check_worktree_entries(
2259 tree: &Worktree,
2260 expected_excluded_paths: &[&str],
2261 expected_ignored_paths: &[&str],
2262 expected_tracked_paths: &[&str],
2263 expected_included_paths: &[&str],
2264) {
2265 for path in expected_excluded_paths {
2266 let entry = tree.entry_for_path(path);
2267 assert!(
2268 entry.is_none(),
2269 "expected path '{path}' to be excluded, but got entry: {entry:?}",
2270 );
2271 }
2272 for path in expected_ignored_paths {
2273 let entry = tree
2274 .entry_for_path(path)
2275 .unwrap_or_else(|| panic!("Missing entry for expected ignored path '{path}'"));
2276 assert!(
2277 entry.is_ignored,
2278 "expected path '{path}' to be ignored, but got entry: {entry:?}",
2279 );
2280 }
2281 for path in expected_tracked_paths {
2282 let entry = tree
2283 .entry_for_path(path)
2284 .unwrap_or_else(|| panic!("Missing entry for expected tracked path '{path}'"));
2285 assert!(
2286 !entry.is_ignored || entry.is_always_included,
2287 "expected path '{path}' to be tracked, but got entry: {entry:?}",
2288 );
2289 }
2290 for path in expected_included_paths {
2291 let entry = tree
2292 .entry_for_path(path)
2293 .unwrap_or_else(|| panic!("Missing entry for expected included path '{path}'"));
2294 assert!(
2295 entry.is_always_included,
2296 "expected path '{path}' to always be included, but got entry: {entry:?}",
2297 );
2298 }
2299}
2300
2301fn init_test(cx: &mut gpui::TestAppContext) {
2302 zlog::init_test();
2303
2304 cx.update(|cx| {
2305 let settings_store = SettingsStore::test(cx);
2306 cx.set_global(settings_store);
2307 WorktreeSettings::register(cx);
2308 });
2309}