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