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