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