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