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