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::{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_executor.clone());
30 fs.insert_tree(
31 "/root",
32 json!({
33 ".gitignore": "a/b\n",
34 "a": {
35 "b": "",
36 "c": "",
37 }
38 }),
39 )
40 .await;
41
42 let tree = Worktree::local(
43 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_executor.clone());
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(cx: &mut TestAppContext) {
192 init_test(cx);
193 let fs = FakeFs::new(cx.background_executor.clone());
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 cx.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_executor.clone());
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_executor.clone());
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.executor().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_executor.clone());
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.executor().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 cx.update(|cx| {
735 cx.update_global::<SettingsStore, _>(|store, cx| {
736 store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
737 project_settings.file_scan_exclusions = Some(Vec::new());
738 });
739 });
740 });
741 let fs = FakeFs::new(cx.background_executor.clone());
742 fs.insert_tree(
743 "/root",
744 json!({
745 ".gitignore": "ancestor-ignored-file1\nancestor-ignored-file2\n",
746 "tree": {
747 ".git": {},
748 ".gitignore": "ignored-dir\n",
749 "tracked-dir": {
750 "tracked-file1": "",
751 "ancestor-ignored-file1": "",
752 },
753 "ignored-dir": {
754 "ignored-file1": ""
755 }
756 }
757 }),
758 )
759 .await;
760
761 let tree = Worktree::local(
762 build_client(cx),
763 "/root/tree".as_ref(),
764 true,
765 fs.clone(),
766 Default::default(),
767 &mut cx.to_async(),
768 )
769 .await
770 .unwrap();
771 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
772 .await;
773
774 tree.read_with(cx, |tree, _| {
775 tree.as_local()
776 .unwrap()
777 .refresh_entries_for_paths(vec![Path::new("ignored-dir").into()])
778 })
779 .recv()
780 .await;
781
782 cx.read(|cx| {
783 let tree = tree.read(cx);
784 assert!(
785 !tree
786 .entry_for_path("tracked-dir/tracked-file1")
787 .unwrap()
788 .is_ignored
789 );
790 assert!(
791 tree.entry_for_path("tracked-dir/ancestor-ignored-file1")
792 .unwrap()
793 .is_ignored
794 );
795 assert!(
796 tree.entry_for_path("ignored-dir/ignored-file1")
797 .unwrap()
798 .is_ignored
799 );
800 });
801
802 fs.create_file(
803 "/root/tree/tracked-dir/tracked-file2".as_ref(),
804 Default::default(),
805 )
806 .await
807 .unwrap();
808 fs.create_file(
809 "/root/tree/tracked-dir/ancestor-ignored-file2".as_ref(),
810 Default::default(),
811 )
812 .await
813 .unwrap();
814 fs.create_file(
815 "/root/tree/ignored-dir/ignored-file2".as_ref(),
816 Default::default(),
817 )
818 .await
819 .unwrap();
820
821 cx.executor().run_until_parked();
822 cx.read(|cx| {
823 let tree = tree.read(cx);
824 assert!(
825 !tree
826 .entry_for_path("tracked-dir/tracked-file2")
827 .unwrap()
828 .is_ignored
829 );
830 assert!(
831 tree.entry_for_path("tracked-dir/ancestor-ignored-file2")
832 .unwrap()
833 .is_ignored
834 );
835 assert!(
836 tree.entry_for_path("ignored-dir/ignored-file2")
837 .unwrap()
838 .is_ignored
839 );
840 assert!(tree.entry_for_path(".git").unwrap().is_ignored);
841 });
842}
843
844#[gpui::test]
845async fn test_write_file(cx: &mut TestAppContext) {
846 init_test(cx);
847 cx.executor().allow_parking();
848 let dir = temp_tree(json!({
849 ".git": {},
850 ".gitignore": "ignored-dir\n",
851 "tracked-dir": {},
852 "ignored-dir": {}
853 }));
854
855 let tree = Worktree::local(
856 build_client(cx),
857 dir.path(),
858 true,
859 Arc::new(RealFs),
860 Default::default(),
861 &mut cx.to_async(),
862 )
863 .await
864 .unwrap();
865 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
866 .await;
867 tree.flush_fs_events(cx).await;
868
869 tree.update(cx, |tree, cx| {
870 tree.as_local().unwrap().write_file(
871 Path::new("tracked-dir/file.txt"),
872 "hello".into(),
873 Default::default(),
874 cx,
875 )
876 })
877 .await
878 .unwrap();
879 tree.update(cx, |tree, cx| {
880 tree.as_local().unwrap().write_file(
881 Path::new("ignored-dir/file.txt"),
882 "world".into(),
883 Default::default(),
884 cx,
885 )
886 })
887 .await
888 .unwrap();
889
890 tree.read_with(cx, |tree, _| {
891 let tracked = tree.entry_for_path("tracked-dir/file.txt").unwrap();
892 let ignored = tree.entry_for_path("ignored-dir/file.txt").unwrap();
893 assert!(!tracked.is_ignored);
894 assert!(ignored.is_ignored);
895 });
896}
897
898#[gpui::test]
899async fn test_file_scan_exclusions(cx: &mut TestAppContext) {
900 init_test(cx);
901 cx.executor().allow_parking();
902 let dir = temp_tree(json!({
903 ".gitignore": "**/target\n/node_modules\n",
904 "target": {
905 "index": "blah2"
906 },
907 "node_modules": {
908 ".DS_Store": "",
909 "prettier": {
910 "package.json": "{}",
911 },
912 },
913 "src": {
914 ".DS_Store": "",
915 "foo": {
916 "foo.rs": "mod another;\n",
917 "another.rs": "// another",
918 },
919 "bar": {
920 "bar.rs": "// bar",
921 },
922 "lib.rs": "mod foo;\nmod bar;\n",
923 },
924 ".DS_Store": "",
925 }));
926 cx.update(|cx| {
927 cx.update_global::<SettingsStore, _>(|store, cx| {
928 store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
929 project_settings.file_scan_exclusions =
930 Some(vec!["**/foo/**".to_string(), "**/.DS_Store".to_string()]);
931 });
932 });
933 });
934
935 let tree = Worktree::local(
936 build_client(cx),
937 dir.path(),
938 true,
939 Arc::new(RealFs),
940 Default::default(),
941 &mut cx.to_async(),
942 )
943 .await
944 .unwrap();
945 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
946 .await;
947 tree.flush_fs_events(cx).await;
948 tree.read_with(cx, |tree, _| {
949 check_worktree_entries(
950 tree,
951 &[
952 "src/foo/foo.rs",
953 "src/foo/another.rs",
954 "node_modules/.DS_Store",
955 "src/.DS_Store",
956 ".DS_Store",
957 ],
958 &["target", "node_modules"],
959 &["src/lib.rs", "src/bar/bar.rs", ".gitignore"],
960 )
961 });
962
963 cx.update(|cx| {
964 cx.update_global::<SettingsStore, _>(|store, cx| {
965 store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
966 project_settings.file_scan_exclusions =
967 Some(vec!["**/node_modules/**".to_string()]);
968 });
969 });
970 });
971 tree.flush_fs_events(cx).await;
972 cx.executor().run_until_parked();
973 tree.read_with(cx, |tree, _| {
974 check_worktree_entries(
975 tree,
976 &[
977 "node_modules/prettier/package.json",
978 "node_modules/.DS_Store",
979 "node_modules",
980 ],
981 &["target"],
982 &[
983 ".gitignore",
984 "src/lib.rs",
985 "src/bar/bar.rs",
986 "src/foo/foo.rs",
987 "src/foo/another.rs",
988 "src/.DS_Store",
989 ".DS_Store",
990 ],
991 )
992 });
993}
994
995#[gpui::test(iterations = 30)]
996async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
997 init_test(cx);
998 let fs = FakeFs::new(cx.background_executor.clone());
999 fs.insert_tree(
1000 "/root",
1001 json!({
1002 "b": {},
1003 "c": {},
1004 "d": {},
1005 }),
1006 )
1007 .await;
1008
1009 let tree = Worktree::local(
1010 build_client(cx),
1011 "/root".as_ref(),
1012 true,
1013 fs,
1014 Default::default(),
1015 &mut cx.to_async(),
1016 )
1017 .await
1018 .unwrap();
1019
1020 let snapshot1 = tree.update(cx, |tree, cx| {
1021 let tree = tree.as_local_mut().unwrap();
1022 let snapshot = Arc::new(Mutex::new(tree.snapshot()));
1023 let _ = tree.observe_updates(0, cx, {
1024 let snapshot = snapshot.clone();
1025 move |update| {
1026 snapshot.lock().apply_remote_update(update).unwrap();
1027 async { true }
1028 }
1029 });
1030 snapshot
1031 });
1032
1033 let entry = tree
1034 .update(cx, |tree, cx| {
1035 tree.as_local_mut()
1036 .unwrap()
1037 .create_entry("a/e".as_ref(), true, cx)
1038 })
1039 .await
1040 .unwrap();
1041 assert!(entry.is_dir());
1042
1043 cx.executor().run_until_parked();
1044 tree.read_with(cx, |tree, _| {
1045 assert_eq!(tree.entry_for_path("a/e").unwrap().kind, EntryKind::Dir);
1046 });
1047
1048 let snapshot2 = tree.update(cx, |tree, _| tree.as_local().unwrap().snapshot());
1049 assert_eq!(
1050 snapshot1.lock().entries(true).collect::<Vec<_>>(),
1051 snapshot2.entries(true).collect::<Vec<_>>()
1052 );
1053}
1054
1055#[gpui::test]
1056async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
1057 init_test(cx);
1058 cx.executor().allow_parking();
1059 let client_fake = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
1060
1061 let fs_fake = FakeFs::new(cx.background_executor.clone());
1062 fs_fake
1063 .insert_tree(
1064 "/root",
1065 json!({
1066 "a": {},
1067 }),
1068 )
1069 .await;
1070
1071 let tree_fake = Worktree::local(
1072 client_fake,
1073 "/root".as_ref(),
1074 true,
1075 fs_fake,
1076 Default::default(),
1077 &mut cx.to_async(),
1078 )
1079 .await
1080 .unwrap();
1081
1082 let entry = tree_fake
1083 .update(cx, |tree, cx| {
1084 tree.as_local_mut()
1085 .unwrap()
1086 .create_entry("a/b/c/d.txt".as_ref(), false, cx)
1087 })
1088 .await
1089 .unwrap();
1090 assert!(entry.is_file());
1091
1092 cx.executor().run_until_parked();
1093 tree_fake.read_with(cx, |tree, _| {
1094 assert!(tree.entry_for_path("a/b/c/d.txt").unwrap().is_file());
1095 assert!(tree.entry_for_path("a/b/c/").unwrap().is_dir());
1096 assert!(tree.entry_for_path("a/b/").unwrap().is_dir());
1097 });
1098
1099 let client_real = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
1100
1101 let fs_real = Arc::new(RealFs);
1102 let temp_root = temp_tree(json!({
1103 "a": {}
1104 }));
1105
1106 let tree_real = Worktree::local(
1107 client_real,
1108 temp_root.path(),
1109 true,
1110 fs_real,
1111 Default::default(),
1112 &mut cx.to_async(),
1113 )
1114 .await
1115 .unwrap();
1116
1117 let entry = tree_real
1118 .update(cx, |tree, cx| {
1119 tree.as_local_mut()
1120 .unwrap()
1121 .create_entry("a/b/c/d.txt".as_ref(), false, cx)
1122 })
1123 .await
1124 .unwrap();
1125 assert!(entry.is_file());
1126
1127 cx.executor().run_until_parked();
1128 tree_real.read_with(cx, |tree, _| {
1129 assert!(tree.entry_for_path("a/b/c/d.txt").unwrap().is_file());
1130 assert!(tree.entry_for_path("a/b/c/").unwrap().is_dir());
1131 assert!(tree.entry_for_path("a/b/").unwrap().is_dir());
1132 });
1133
1134 // Test smallest change
1135 let entry = tree_real
1136 .update(cx, |tree, cx| {
1137 tree.as_local_mut()
1138 .unwrap()
1139 .create_entry("a/b/c/e.txt".as_ref(), false, cx)
1140 })
1141 .await
1142 .unwrap();
1143 assert!(entry.is_file());
1144
1145 cx.executor().run_until_parked();
1146 tree_real.read_with(cx, |tree, _| {
1147 assert!(tree.entry_for_path("a/b/c/e.txt").unwrap().is_file());
1148 });
1149
1150 // Test largest change
1151 let entry = tree_real
1152 .update(cx, |tree, cx| {
1153 tree.as_local_mut()
1154 .unwrap()
1155 .create_entry("d/e/f/g.txt".as_ref(), false, cx)
1156 })
1157 .await
1158 .unwrap();
1159 assert!(entry.is_file());
1160
1161 cx.executor().run_until_parked();
1162 tree_real.read_with(cx, |tree, _| {
1163 assert!(tree.entry_for_path("d/e/f/g.txt").unwrap().is_file());
1164 assert!(tree.entry_for_path("d/e/f").unwrap().is_dir());
1165 assert!(tree.entry_for_path("d/e/").unwrap().is_dir());
1166 assert!(tree.entry_for_path("d/").unwrap().is_dir());
1167 });
1168}
1169
1170#[gpui::test(iterations = 100)]
1171async fn test_random_worktree_operations_during_initial_scan(
1172 cx: &mut TestAppContext,
1173 mut rng: StdRng,
1174) {
1175 init_test(cx);
1176 let operations = env::var("OPERATIONS")
1177 .map(|o| o.parse().unwrap())
1178 .unwrap_or(5);
1179 let initial_entries = env::var("INITIAL_ENTRIES")
1180 .map(|o| o.parse().unwrap())
1181 .unwrap_or(20);
1182
1183 let root_dir = Path::new("/test");
1184 let fs = FakeFs::new(cx.background_executor.clone()) as Arc<dyn Fs>;
1185 fs.as_fake().insert_tree(root_dir, json!({})).await;
1186 for _ in 0..initial_entries {
1187 randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
1188 }
1189 log::info!("generated initial tree");
1190
1191 let worktree = Worktree::local(
1192 build_client(cx),
1193 root_dir,
1194 true,
1195 fs.clone(),
1196 Default::default(),
1197 &mut cx.to_async(),
1198 )
1199 .await
1200 .unwrap();
1201
1202 let mut snapshots = vec![worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot())];
1203 let updates = Arc::new(Mutex::new(Vec::new()));
1204 worktree.update(cx, |tree, cx| {
1205 check_worktree_change_events(tree, cx);
1206
1207 let _ = tree.as_local_mut().unwrap().observe_updates(0, cx, {
1208 let updates = updates.clone();
1209 move |update| {
1210 updates.lock().push(update);
1211 async { true }
1212 }
1213 });
1214 });
1215
1216 for _ in 0..operations {
1217 worktree
1218 .update(cx, |worktree, cx| {
1219 randomly_mutate_worktree(worktree, &mut rng, cx)
1220 })
1221 .await
1222 .log_err();
1223 worktree.read_with(cx, |tree, _| {
1224 tree.as_local().unwrap().snapshot().check_invariants(true)
1225 });
1226
1227 if rng.gen_bool(0.6) {
1228 snapshots.push(worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot()));
1229 }
1230 }
1231
1232 worktree
1233 .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
1234 .await;
1235
1236 cx.executor().run_until_parked();
1237
1238 let final_snapshot = worktree.read_with(cx, |tree, _| {
1239 let tree = tree.as_local().unwrap();
1240 let snapshot = tree.snapshot();
1241 snapshot.check_invariants(true);
1242 snapshot
1243 });
1244
1245 for (i, snapshot) in snapshots.into_iter().enumerate().rev() {
1246 let mut updated_snapshot = snapshot.clone();
1247 for update in updates.lock().iter() {
1248 if update.scan_id >= updated_snapshot.scan_id() as u64 {
1249 updated_snapshot
1250 .apply_remote_update(update.clone())
1251 .unwrap();
1252 }
1253 }
1254
1255 assert_eq!(
1256 updated_snapshot.entries(true).collect::<Vec<_>>(),
1257 final_snapshot.entries(true).collect::<Vec<_>>(),
1258 "wrong updates after snapshot {i}: {snapshot:#?} {updates:#?}",
1259 );
1260 }
1261}
1262
1263#[gpui::test(iterations = 100)]
1264async fn test_random_worktree_changes(cx: &mut TestAppContext, mut rng: StdRng) {
1265 init_test(cx);
1266 let operations = env::var("OPERATIONS")
1267 .map(|o| o.parse().unwrap())
1268 .unwrap_or(40);
1269 let initial_entries = env::var("INITIAL_ENTRIES")
1270 .map(|o| o.parse().unwrap())
1271 .unwrap_or(20);
1272
1273 let root_dir = Path::new("/test");
1274 let fs = FakeFs::new(cx.background_executor.clone()) as Arc<dyn Fs>;
1275 fs.as_fake().insert_tree(root_dir, json!({})).await;
1276 for _ in 0..initial_entries {
1277 randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
1278 }
1279 log::info!("generated initial tree");
1280
1281 let worktree = Worktree::local(
1282 build_client(cx),
1283 root_dir,
1284 true,
1285 fs.clone(),
1286 Default::default(),
1287 &mut cx.to_async(),
1288 )
1289 .await
1290 .unwrap();
1291
1292 let updates = Arc::new(Mutex::new(Vec::new()));
1293 worktree.update(cx, |tree, cx| {
1294 check_worktree_change_events(tree, cx);
1295
1296 let _ = tree.as_local_mut().unwrap().observe_updates(0, cx, {
1297 let updates = updates.clone();
1298 move |update| {
1299 updates.lock().push(update);
1300 async { true }
1301 }
1302 });
1303 });
1304
1305 worktree
1306 .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
1307 .await;
1308
1309 fs.as_fake().pause_events();
1310 let mut snapshots = Vec::new();
1311 let mut mutations_len = operations;
1312 while mutations_len > 1 {
1313 if rng.gen_bool(0.2) {
1314 worktree
1315 .update(cx, |worktree, cx| {
1316 randomly_mutate_worktree(worktree, &mut rng, cx)
1317 })
1318 .await
1319 .log_err();
1320 } else {
1321 randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
1322 }
1323
1324 let buffered_event_count = fs.as_fake().buffered_event_count();
1325 if buffered_event_count > 0 && rng.gen_bool(0.3) {
1326 let len = rng.gen_range(0..=buffered_event_count);
1327 log::info!("flushing {} events", len);
1328 fs.as_fake().flush_events(len);
1329 } else {
1330 randomly_mutate_fs(&fs, root_dir, 0.6, &mut rng).await;
1331 mutations_len -= 1;
1332 }
1333
1334 cx.executor().run_until_parked();
1335 if rng.gen_bool(0.2) {
1336 log::info!("storing snapshot {}", snapshots.len());
1337 let snapshot = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
1338 snapshots.push(snapshot);
1339 }
1340 }
1341
1342 log::info!("quiescing");
1343 fs.as_fake().flush_events(usize::MAX);
1344 cx.executor().run_until_parked();
1345
1346 let snapshot = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
1347 snapshot.check_invariants(true);
1348 let expanded_paths = snapshot
1349 .expanded_entries()
1350 .map(|e| e.path.clone())
1351 .collect::<Vec<_>>();
1352
1353 {
1354 let new_worktree = Worktree::local(
1355 build_client(cx),
1356 root_dir,
1357 true,
1358 fs.clone(),
1359 Default::default(),
1360 &mut cx.to_async(),
1361 )
1362 .await
1363 .unwrap();
1364 new_worktree
1365 .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
1366 .await;
1367 new_worktree
1368 .update(cx, |tree, _| {
1369 tree.as_local_mut()
1370 .unwrap()
1371 .refresh_entries_for_paths(expanded_paths)
1372 })
1373 .recv()
1374 .await;
1375 let new_snapshot =
1376 new_worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
1377 assert_eq!(
1378 snapshot.entries_without_ids(true),
1379 new_snapshot.entries_without_ids(true)
1380 );
1381 }
1382
1383 for (i, mut prev_snapshot) in snapshots.into_iter().enumerate().rev() {
1384 for update in updates.lock().iter() {
1385 if update.scan_id >= prev_snapshot.scan_id() as u64 {
1386 prev_snapshot.apply_remote_update(update.clone()).unwrap();
1387 }
1388 }
1389
1390 assert_eq!(
1391 prev_snapshot
1392 .entries(true)
1393 .map(ignore_pending_dir)
1394 .collect::<Vec<_>>(),
1395 snapshot
1396 .entries(true)
1397 .map(ignore_pending_dir)
1398 .collect::<Vec<_>>(),
1399 "wrong updates after snapshot {i}: {updates:#?}",
1400 );
1401 }
1402
1403 fn ignore_pending_dir(entry: &Entry) -> Entry {
1404 let mut entry = entry.clone();
1405 if entry.kind.is_dir() {
1406 entry.kind = EntryKind::Dir
1407 }
1408 entry
1409 }
1410}
1411
1412// The worktree's `UpdatedEntries` event can be used to follow along with
1413// all changes to the worktree's snapshot.
1414fn check_worktree_change_events(tree: &mut Worktree, cx: &mut ModelContext<Worktree>) {
1415 let mut entries = tree.entries(true).cloned().collect::<Vec<_>>();
1416 cx.subscribe(&cx.handle(), move |tree, _, event, _| {
1417 if let Event::UpdatedEntries(changes) = event {
1418 for (path, _, change_type) in changes.iter() {
1419 let entry = tree.entry_for_path(&path).cloned();
1420 let ix = match entries.binary_search_by_key(&path, |e| &e.path) {
1421 Ok(ix) | Err(ix) => ix,
1422 };
1423 match change_type {
1424 PathChange::Added => entries.insert(ix, entry.unwrap()),
1425 PathChange::Removed => drop(entries.remove(ix)),
1426 PathChange::Updated => {
1427 let entry = entry.unwrap();
1428 let existing_entry = entries.get_mut(ix).unwrap();
1429 assert_eq!(existing_entry.path, entry.path);
1430 *existing_entry = entry;
1431 }
1432 PathChange::AddedOrUpdated | PathChange::Loaded => {
1433 let entry = entry.unwrap();
1434 if entries.get(ix).map(|e| &e.path) == Some(&entry.path) {
1435 *entries.get_mut(ix).unwrap() = entry;
1436 } else {
1437 entries.insert(ix, entry);
1438 }
1439 }
1440 }
1441 }
1442
1443 let new_entries = tree.entries(true).cloned().collect::<Vec<_>>();
1444 assert_eq!(entries, new_entries, "incorrect changes: {:?}", changes);
1445 }
1446 })
1447 .detach();
1448}
1449
1450fn randomly_mutate_worktree(
1451 worktree: &mut Worktree,
1452 rng: &mut impl Rng,
1453 cx: &mut ModelContext<Worktree>,
1454) -> Task<Result<()>> {
1455 log::info!("mutating worktree");
1456 let worktree = worktree.as_local_mut().unwrap();
1457 let snapshot = worktree.snapshot();
1458 let entry = snapshot.entries(false).choose(rng).unwrap();
1459
1460 match rng.gen_range(0_u32..100) {
1461 0..=33 if entry.path.as_ref() != Path::new("") => {
1462 log::info!("deleting entry {:?} ({})", entry.path, entry.id.0);
1463 worktree.delete_entry(entry.id, cx).unwrap()
1464 }
1465 ..=66 if entry.path.as_ref() != Path::new("") => {
1466 let other_entry = snapshot.entries(false).choose(rng).unwrap();
1467 let new_parent_path = if other_entry.is_dir() {
1468 other_entry.path.clone()
1469 } else {
1470 other_entry.path.parent().unwrap().into()
1471 };
1472 let mut new_path = new_parent_path.join(random_filename(rng));
1473 if new_path.starts_with(&entry.path) {
1474 new_path = random_filename(rng).into();
1475 }
1476
1477 log::info!(
1478 "renaming entry {:?} ({}) to {:?}",
1479 entry.path,
1480 entry.id.0,
1481 new_path
1482 );
1483 let task = worktree.rename_entry(entry.id, new_path, cx).unwrap();
1484 cx.background_executor().spawn(async move {
1485 task.await?;
1486 Ok(())
1487 })
1488 }
1489 _ => {
1490 let task = if entry.is_dir() {
1491 let child_path = entry.path.join(random_filename(rng));
1492 let is_dir = rng.gen_bool(0.3);
1493 log::info!(
1494 "creating {} at {:?}",
1495 if is_dir { "dir" } else { "file" },
1496 child_path,
1497 );
1498 worktree.create_entry(child_path, is_dir, cx)
1499 } else {
1500 log::info!("overwriting file {:?} ({})", entry.path, entry.id.0);
1501 worktree.write_file(entry.path.clone(), "".into(), Default::default(), cx)
1502 };
1503 cx.background_executor().spawn(async move {
1504 task.await?;
1505 Ok(())
1506 })
1507 }
1508 }
1509}
1510
1511async fn randomly_mutate_fs(
1512 fs: &Arc<dyn Fs>,
1513 root_path: &Path,
1514 insertion_probability: f64,
1515 rng: &mut impl Rng,
1516) {
1517 log::info!("mutating fs");
1518 let mut files = Vec::new();
1519 let mut dirs = Vec::new();
1520 for path in fs.as_fake().paths(false) {
1521 if path.starts_with(root_path) {
1522 if fs.is_file(&path).await {
1523 files.push(path);
1524 } else {
1525 dirs.push(path);
1526 }
1527 }
1528 }
1529
1530 if (files.is_empty() && dirs.len() == 1) || rng.gen_bool(insertion_probability) {
1531 let path = dirs.choose(rng).unwrap();
1532 let new_path = path.join(random_filename(rng));
1533
1534 if rng.gen() {
1535 log::info!(
1536 "creating dir {:?}",
1537 new_path.strip_prefix(root_path).unwrap()
1538 );
1539 fs.create_dir(&new_path).await.unwrap();
1540 } else {
1541 log::info!(
1542 "creating file {:?}",
1543 new_path.strip_prefix(root_path).unwrap()
1544 );
1545 fs.create_file(&new_path, Default::default()).await.unwrap();
1546 }
1547 } else if rng.gen_bool(0.05) {
1548 let ignore_dir_path = dirs.choose(rng).unwrap();
1549 let ignore_path = ignore_dir_path.join(&*GITIGNORE);
1550
1551 let subdirs = dirs
1552 .iter()
1553 .filter(|d| d.starts_with(&ignore_dir_path))
1554 .cloned()
1555 .collect::<Vec<_>>();
1556 let subfiles = files
1557 .iter()
1558 .filter(|d| d.starts_with(&ignore_dir_path))
1559 .cloned()
1560 .collect::<Vec<_>>();
1561 let files_to_ignore = {
1562 let len = rng.gen_range(0..=subfiles.len());
1563 subfiles.choose_multiple(rng, len)
1564 };
1565 let dirs_to_ignore = {
1566 let len = rng.gen_range(0..subdirs.len());
1567 subdirs.choose_multiple(rng, len)
1568 };
1569
1570 let mut ignore_contents = String::new();
1571 for path_to_ignore in files_to_ignore.chain(dirs_to_ignore) {
1572 writeln!(
1573 ignore_contents,
1574 "{}",
1575 path_to_ignore
1576 .strip_prefix(&ignore_dir_path)
1577 .unwrap()
1578 .to_str()
1579 .unwrap()
1580 )
1581 .unwrap();
1582 }
1583 log::info!(
1584 "creating gitignore {:?} with contents:\n{}",
1585 ignore_path.strip_prefix(&root_path).unwrap(),
1586 ignore_contents
1587 );
1588 fs.save(
1589 &ignore_path,
1590 &ignore_contents.as_str().into(),
1591 Default::default(),
1592 )
1593 .await
1594 .unwrap();
1595 } else {
1596 let old_path = {
1597 let file_path = files.choose(rng);
1598 let dir_path = dirs[1..].choose(rng);
1599 file_path.into_iter().chain(dir_path).choose(rng).unwrap()
1600 };
1601
1602 let is_rename = rng.gen();
1603 if is_rename {
1604 let new_path_parent = dirs
1605 .iter()
1606 .filter(|d| !d.starts_with(old_path))
1607 .choose(rng)
1608 .unwrap();
1609
1610 let overwrite_existing_dir =
1611 !old_path.starts_with(&new_path_parent) && rng.gen_bool(0.3);
1612 let new_path = if overwrite_existing_dir {
1613 fs.remove_dir(
1614 &new_path_parent,
1615 RemoveOptions {
1616 recursive: true,
1617 ignore_if_not_exists: true,
1618 },
1619 )
1620 .await
1621 .unwrap();
1622 new_path_parent.to_path_buf()
1623 } else {
1624 new_path_parent.join(random_filename(rng))
1625 };
1626
1627 log::info!(
1628 "renaming {:?} to {}{:?}",
1629 old_path.strip_prefix(&root_path).unwrap(),
1630 if overwrite_existing_dir {
1631 "overwrite "
1632 } else {
1633 ""
1634 },
1635 new_path.strip_prefix(&root_path).unwrap()
1636 );
1637 fs.rename(
1638 &old_path,
1639 &new_path,
1640 fs::RenameOptions {
1641 overwrite: true,
1642 ignore_if_exists: true,
1643 },
1644 )
1645 .await
1646 .unwrap();
1647 } else if fs.is_file(&old_path).await {
1648 log::info!(
1649 "deleting file {:?}",
1650 old_path.strip_prefix(&root_path).unwrap()
1651 );
1652 fs.remove_file(old_path, Default::default()).await.unwrap();
1653 } else {
1654 log::info!(
1655 "deleting dir {:?}",
1656 old_path.strip_prefix(&root_path).unwrap()
1657 );
1658 fs.remove_dir(
1659 &old_path,
1660 RemoveOptions {
1661 recursive: true,
1662 ignore_if_not_exists: true,
1663 },
1664 )
1665 .await
1666 .unwrap();
1667 }
1668 }
1669}
1670
1671fn random_filename(rng: &mut impl Rng) -> String {
1672 (0..6)
1673 .map(|_| rng.sample(rand::distributions::Alphanumeric))
1674 .map(char::from)
1675 .collect()
1676}
1677
1678#[gpui::test]
1679async fn test_rename_work_directory(cx: &mut TestAppContext) {
1680 init_test(cx);
1681 cx.executor().allow_parking();
1682 let root = temp_tree(json!({
1683 "projects": {
1684 "project1": {
1685 "a": "",
1686 "b": "",
1687 }
1688 },
1689
1690 }));
1691 let root_path = root.path();
1692
1693 let tree = Worktree::local(
1694 build_client(cx),
1695 root_path,
1696 true,
1697 Arc::new(RealFs),
1698 Default::default(),
1699 &mut cx.to_async(),
1700 )
1701 .await
1702 .unwrap();
1703
1704 let repo = git_init(&root_path.join("projects/project1"));
1705 git_add("a", &repo);
1706 git_commit("init", &repo);
1707 std::fs::write(root_path.join("projects/project1/a"), "aa").ok();
1708
1709 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1710 .await;
1711
1712 tree.flush_fs_events(cx).await;
1713
1714 cx.read(|cx| {
1715 let tree = tree.read(cx);
1716 let (work_dir, _) = tree.repositories().next().unwrap();
1717 assert_eq!(work_dir.as_ref(), Path::new("projects/project1"));
1718 assert_eq!(
1719 tree.status_for_file(Path::new("projects/project1/a")),
1720 Some(GitFileStatus::Modified)
1721 );
1722 assert_eq!(
1723 tree.status_for_file(Path::new("projects/project1/b")),
1724 Some(GitFileStatus::Added)
1725 );
1726 });
1727
1728 std::fs::rename(
1729 root_path.join("projects/project1"),
1730 root_path.join("projects/project2"),
1731 )
1732 .ok();
1733 tree.flush_fs_events(cx).await;
1734
1735 cx.read(|cx| {
1736 let tree = tree.read(cx);
1737 let (work_dir, _) = tree.repositories().next().unwrap();
1738 assert_eq!(work_dir.as_ref(), Path::new("projects/project2"));
1739 assert_eq!(
1740 tree.status_for_file(Path::new("projects/project2/a")),
1741 Some(GitFileStatus::Modified)
1742 );
1743 assert_eq!(
1744 tree.status_for_file(Path::new("projects/project2/b")),
1745 Some(GitFileStatus::Added)
1746 );
1747 });
1748}
1749
1750#[gpui::test]
1751async fn test_git_repository_for_path(cx: &mut TestAppContext) {
1752 init_test(cx);
1753 cx.executor().allow_parking();
1754 let root = temp_tree(json!({
1755 "c.txt": "",
1756 "dir1": {
1757 ".git": {},
1758 "deps": {
1759 "dep1": {
1760 ".git": {},
1761 "src": {
1762 "a.txt": ""
1763 }
1764 }
1765 },
1766 "src": {
1767 "b.txt": ""
1768 }
1769 },
1770 }));
1771
1772 let tree = Worktree::local(
1773 build_client(cx),
1774 root.path(),
1775 true,
1776 Arc::new(RealFs),
1777 Default::default(),
1778 &mut cx.to_async(),
1779 )
1780 .await
1781 .unwrap();
1782
1783 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1784 .await;
1785 tree.flush_fs_events(cx).await;
1786
1787 tree.read_with(cx, |tree, _cx| {
1788 let tree = tree.as_local().unwrap();
1789
1790 assert!(tree.repository_for_path("c.txt".as_ref()).is_none());
1791
1792 let entry = tree.repository_for_path("dir1/src/b.txt".as_ref()).unwrap();
1793 assert_eq!(
1794 entry
1795 .work_directory(tree)
1796 .map(|directory| directory.as_ref().to_owned()),
1797 Some(Path::new("dir1").to_owned())
1798 );
1799
1800 let entry = tree
1801 .repository_for_path("dir1/deps/dep1/src/a.txt".as_ref())
1802 .unwrap();
1803 assert_eq!(
1804 entry
1805 .work_directory(tree)
1806 .map(|directory| directory.as_ref().to_owned()),
1807 Some(Path::new("dir1/deps/dep1").to_owned())
1808 );
1809
1810 let entries = tree.files(false, 0);
1811
1812 let paths_with_repos = tree
1813 .entries_with_repositories(entries)
1814 .map(|(entry, repo)| {
1815 (
1816 entry.path.as_ref(),
1817 repo.and_then(|repo| {
1818 repo.work_directory(&tree)
1819 .map(|work_directory| work_directory.0.to_path_buf())
1820 }),
1821 )
1822 })
1823 .collect::<Vec<_>>();
1824
1825 assert_eq!(
1826 paths_with_repos,
1827 &[
1828 (Path::new("c.txt"), None),
1829 (
1830 Path::new("dir1/deps/dep1/src/a.txt"),
1831 Some(Path::new("dir1/deps/dep1").into())
1832 ),
1833 (Path::new("dir1/src/b.txt"), Some(Path::new("dir1").into())),
1834 ]
1835 );
1836 });
1837
1838 let repo_update_events = Arc::new(Mutex::new(vec![]));
1839 tree.update(cx, |_, cx| {
1840 let repo_update_events = repo_update_events.clone();
1841 cx.subscribe(&tree, move |_, _, event, _| {
1842 if let Event::UpdatedGitRepositories(update) = event {
1843 repo_update_events.lock().push(update.clone());
1844 }
1845 })
1846 .detach();
1847 });
1848
1849 std::fs::write(root.path().join("dir1/.git/random_new_file"), "hello").unwrap();
1850 tree.flush_fs_events(cx).await;
1851
1852 assert_eq!(
1853 repo_update_events.lock()[0]
1854 .iter()
1855 .map(|e| e.0.clone())
1856 .collect::<Vec<Arc<Path>>>(),
1857 vec![Path::new("dir1").into()]
1858 );
1859
1860 std::fs::remove_dir_all(root.path().join("dir1/.git")).unwrap();
1861 tree.flush_fs_events(cx).await;
1862
1863 tree.read_with(cx, |tree, _cx| {
1864 let tree = tree.as_local().unwrap();
1865
1866 assert!(tree
1867 .repository_for_path("dir1/src/b.txt".as_ref())
1868 .is_none());
1869 });
1870}
1871
1872#[gpui::test]
1873async fn test_git_status(cx: &mut TestAppContext) {
1874 init_test(cx);
1875 cx.executor().allow_parking();
1876 const IGNORE_RULE: &'static str = "**/target";
1877
1878 let root = temp_tree(json!({
1879 "project": {
1880 "a.txt": "a",
1881 "b.txt": "bb",
1882 "c": {
1883 "d": {
1884 "e.txt": "eee"
1885 }
1886 },
1887 "f.txt": "ffff",
1888 "target": {
1889 "build_file": "???"
1890 },
1891 ".gitignore": IGNORE_RULE
1892 },
1893
1894 }));
1895
1896 const A_TXT: &'static str = "a.txt";
1897 const B_TXT: &'static str = "b.txt";
1898 const E_TXT: &'static str = "c/d/e.txt";
1899 const F_TXT: &'static str = "f.txt";
1900 const DOTGITIGNORE: &'static str = ".gitignore";
1901 const BUILD_FILE: &'static str = "target/build_file";
1902 let project_path = Path::new("project");
1903
1904 // Set up git repository before creating the worktree.
1905 let work_dir = root.path().join("project");
1906 let mut repo = git_init(work_dir.as_path());
1907 repo.add_ignore_rule(IGNORE_RULE).unwrap();
1908 git_add(A_TXT, &repo);
1909 git_add(E_TXT, &repo);
1910 git_add(DOTGITIGNORE, &repo);
1911 git_commit("Initial commit", &repo);
1912
1913 let tree = Worktree::local(
1914 build_client(cx),
1915 root.path(),
1916 true,
1917 Arc::new(RealFs),
1918 Default::default(),
1919 &mut cx.to_async(),
1920 )
1921 .await
1922 .unwrap();
1923
1924 tree.flush_fs_events(cx).await;
1925 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1926 .await;
1927 cx.executor().run_until_parked();
1928
1929 // Check that the right git state is observed on startup
1930 tree.read_with(cx, |tree, _cx| {
1931 let snapshot = tree.snapshot();
1932 assert_eq!(snapshot.repositories().count(), 1);
1933 let (dir, _) = snapshot.repositories().next().unwrap();
1934 assert_eq!(dir.as_ref(), Path::new("project"));
1935
1936 assert_eq!(
1937 snapshot.status_for_file(project_path.join(B_TXT)),
1938 Some(GitFileStatus::Added)
1939 );
1940 assert_eq!(
1941 snapshot.status_for_file(project_path.join(F_TXT)),
1942 Some(GitFileStatus::Added)
1943 );
1944 });
1945
1946 // Modify a file in the working copy.
1947 std::fs::write(work_dir.join(A_TXT), "aa").unwrap();
1948 tree.flush_fs_events(cx).await;
1949 cx.executor().run_until_parked();
1950
1951 // The worktree detects that the file's git status has changed.
1952 tree.read_with(cx, |tree, _cx| {
1953 let snapshot = tree.snapshot();
1954 assert_eq!(
1955 snapshot.status_for_file(project_path.join(A_TXT)),
1956 Some(GitFileStatus::Modified)
1957 );
1958 });
1959
1960 // Create a commit in the git repository.
1961 git_add(A_TXT, &repo);
1962 git_add(B_TXT, &repo);
1963 git_commit("Committing modified and added", &repo);
1964 tree.flush_fs_events(cx).await;
1965 cx.executor().run_until_parked();
1966
1967 // The worktree detects that the files' git status have changed.
1968 tree.read_with(cx, |tree, _cx| {
1969 let snapshot = tree.snapshot();
1970 assert_eq!(
1971 snapshot.status_for_file(project_path.join(F_TXT)),
1972 Some(GitFileStatus::Added)
1973 );
1974 assert_eq!(snapshot.status_for_file(project_path.join(B_TXT)), None);
1975 assert_eq!(snapshot.status_for_file(project_path.join(A_TXT)), None);
1976 });
1977
1978 // Modify files in the working copy and perform git operations on other files.
1979 git_reset(0, &repo);
1980 git_remove_index(Path::new(B_TXT), &repo);
1981 git_stash(&mut repo);
1982 std::fs::write(work_dir.join(E_TXT), "eeee").unwrap();
1983 std::fs::write(work_dir.join(BUILD_FILE), "this should be ignored").unwrap();
1984 tree.flush_fs_events(cx).await;
1985 cx.executor().run_until_parked();
1986
1987 // Check that more complex repo changes are tracked
1988 tree.read_with(cx, |tree, _cx| {
1989 let snapshot = tree.snapshot();
1990
1991 assert_eq!(snapshot.status_for_file(project_path.join(A_TXT)), None);
1992 assert_eq!(
1993 snapshot.status_for_file(project_path.join(B_TXT)),
1994 Some(GitFileStatus::Added)
1995 );
1996 assert_eq!(
1997 snapshot.status_for_file(project_path.join(E_TXT)),
1998 Some(GitFileStatus::Modified)
1999 );
2000 });
2001
2002 std::fs::remove_file(work_dir.join(B_TXT)).unwrap();
2003 std::fs::remove_dir_all(work_dir.join("c")).unwrap();
2004 std::fs::write(
2005 work_dir.join(DOTGITIGNORE),
2006 [IGNORE_RULE, "f.txt"].join("\n"),
2007 )
2008 .unwrap();
2009
2010 git_add(Path::new(DOTGITIGNORE), &repo);
2011 git_commit("Committing modified git ignore", &repo);
2012
2013 tree.flush_fs_events(cx).await;
2014 cx.executor().run_until_parked();
2015
2016 let mut renamed_dir_name = "first_directory/second_directory";
2017 const RENAMED_FILE: &'static str = "rf.txt";
2018
2019 std::fs::create_dir_all(work_dir.join(renamed_dir_name)).unwrap();
2020 std::fs::write(
2021 work_dir.join(renamed_dir_name).join(RENAMED_FILE),
2022 "new-contents",
2023 )
2024 .unwrap();
2025
2026 tree.flush_fs_events(cx).await;
2027 cx.executor().run_until_parked();
2028
2029 tree.read_with(cx, |tree, _cx| {
2030 let snapshot = tree.snapshot();
2031 assert_eq!(
2032 snapshot.status_for_file(&project_path.join(renamed_dir_name).join(RENAMED_FILE)),
2033 Some(GitFileStatus::Added)
2034 );
2035 });
2036
2037 renamed_dir_name = "new_first_directory/second_directory";
2038
2039 std::fs::rename(
2040 work_dir.join("first_directory"),
2041 work_dir.join("new_first_directory"),
2042 )
2043 .unwrap();
2044
2045 tree.flush_fs_events(cx).await;
2046 cx.executor().run_until_parked();
2047
2048 tree.read_with(cx, |tree, _cx| {
2049 let snapshot = tree.snapshot();
2050
2051 assert_eq!(
2052 snapshot.status_for_file(
2053 project_path
2054 .join(Path::new(renamed_dir_name))
2055 .join(RENAMED_FILE)
2056 ),
2057 Some(GitFileStatus::Added)
2058 );
2059 });
2060}
2061
2062#[gpui::test]
2063async fn test_propagate_git_statuses(cx: &mut TestAppContext) {
2064 init_test(cx);
2065 let fs = FakeFs::new(cx.background_executor.clone());
2066 fs.insert_tree(
2067 "/root",
2068 json!({
2069 ".git": {},
2070 "a": {
2071 "b": {
2072 "c1.txt": "",
2073 "c2.txt": "",
2074 },
2075 "d": {
2076 "e1.txt": "",
2077 "e2.txt": "",
2078 "e3.txt": "",
2079 }
2080 },
2081 "f": {
2082 "no-status.txt": ""
2083 },
2084 "g": {
2085 "h1.txt": "",
2086 "h2.txt": ""
2087 },
2088
2089 }),
2090 )
2091 .await;
2092
2093 fs.set_status_for_repo_via_git_operation(
2094 &Path::new("/root/.git"),
2095 &[
2096 (Path::new("a/b/c1.txt"), GitFileStatus::Added),
2097 (Path::new("a/d/e2.txt"), GitFileStatus::Modified),
2098 (Path::new("g/h2.txt"), GitFileStatus::Conflict),
2099 ],
2100 );
2101
2102 let tree = Worktree::local(
2103 build_client(cx),
2104 Path::new("/root"),
2105 true,
2106 fs.clone(),
2107 Default::default(),
2108 &mut cx.to_async(),
2109 )
2110 .await
2111 .unwrap();
2112
2113 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2114 .await;
2115
2116 cx.executor().run_until_parked();
2117 let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
2118
2119 check_propagated_statuses(
2120 &snapshot,
2121 &[
2122 (Path::new(""), Some(GitFileStatus::Conflict)),
2123 (Path::new("a"), Some(GitFileStatus::Modified)),
2124 (Path::new("a/b"), Some(GitFileStatus::Added)),
2125 (Path::new("a/b/c1.txt"), Some(GitFileStatus::Added)),
2126 (Path::new("a/b/c2.txt"), None),
2127 (Path::new("a/d"), Some(GitFileStatus::Modified)),
2128 (Path::new("a/d/e2.txt"), Some(GitFileStatus::Modified)),
2129 (Path::new("f"), None),
2130 (Path::new("f/no-status.txt"), None),
2131 (Path::new("g"), Some(GitFileStatus::Conflict)),
2132 (Path::new("g/h2.txt"), Some(GitFileStatus::Conflict)),
2133 ],
2134 );
2135
2136 check_propagated_statuses(
2137 &snapshot,
2138 &[
2139 (Path::new("a/b"), Some(GitFileStatus::Added)),
2140 (Path::new("a/b/c1.txt"), Some(GitFileStatus::Added)),
2141 (Path::new("a/b/c2.txt"), None),
2142 (Path::new("a/d"), Some(GitFileStatus::Modified)),
2143 (Path::new("a/d/e1.txt"), None),
2144 (Path::new("a/d/e2.txt"), Some(GitFileStatus::Modified)),
2145 (Path::new("f"), None),
2146 (Path::new("f/no-status.txt"), None),
2147 (Path::new("g"), Some(GitFileStatus::Conflict)),
2148 ],
2149 );
2150
2151 check_propagated_statuses(
2152 &snapshot,
2153 &[
2154 (Path::new("a/b/c1.txt"), Some(GitFileStatus::Added)),
2155 (Path::new("a/b/c2.txt"), None),
2156 (Path::new("a/d/e1.txt"), None),
2157 (Path::new("a/d/e2.txt"), Some(GitFileStatus::Modified)),
2158 (Path::new("f/no-status.txt"), None),
2159 ],
2160 );
2161
2162 #[track_caller]
2163 fn check_propagated_statuses(
2164 snapshot: &Snapshot,
2165 expected_statuses: &[(&Path, Option<GitFileStatus>)],
2166 ) {
2167 let mut entries = expected_statuses
2168 .iter()
2169 .map(|(path, _)| snapshot.entry_for_path(path).unwrap().clone())
2170 .collect::<Vec<_>>();
2171 snapshot.propagate_git_statuses(&mut entries);
2172 assert_eq!(
2173 entries
2174 .iter()
2175 .map(|e| (e.path.as_ref(), e.git_status))
2176 .collect::<Vec<_>>(),
2177 expected_statuses
2178 );
2179 }
2180}
2181
2182fn build_client(cx: &mut TestAppContext) -> Arc<Client> {
2183 let http_client = FakeHttpClient::with_404_response();
2184 cx.read(|cx| Client::new(http_client, cx))
2185}
2186
2187#[track_caller]
2188fn git_init(path: &Path) -> git2::Repository {
2189 git2::Repository::init(path).expect("Failed to initialize git repository")
2190}
2191
2192#[track_caller]
2193fn git_add<P: AsRef<Path>>(path: P, repo: &git2::Repository) {
2194 let path = path.as_ref();
2195 let mut index = repo.index().expect("Failed to get index");
2196 index.add_path(path).expect("Failed to add a.txt");
2197 index.write().expect("Failed to write index");
2198}
2199
2200#[track_caller]
2201fn git_remove_index(path: &Path, repo: &git2::Repository) {
2202 let mut index = repo.index().expect("Failed to get index");
2203 index.remove_path(path).expect("Failed to add a.txt");
2204 index.write().expect("Failed to write index");
2205}
2206
2207#[track_caller]
2208fn git_commit(msg: &'static str, repo: &git2::Repository) {
2209 use git2::Signature;
2210
2211 let signature = Signature::now("test", "test@zed.dev").unwrap();
2212 let oid = repo.index().unwrap().write_tree().unwrap();
2213 let tree = repo.find_tree(oid).unwrap();
2214 if let Some(head) = repo.head().ok() {
2215 let parent_obj = head.peel(git2::ObjectType::Commit).unwrap();
2216
2217 let parent_commit = parent_obj.as_commit().unwrap();
2218
2219 repo.commit(
2220 Some("HEAD"),
2221 &signature,
2222 &signature,
2223 msg,
2224 &tree,
2225 &[parent_commit],
2226 )
2227 .expect("Failed to commit with parent");
2228 } else {
2229 repo.commit(Some("HEAD"), &signature, &signature, msg, &tree, &[])
2230 .expect("Failed to commit");
2231 }
2232}
2233
2234#[track_caller]
2235fn git_stash(repo: &mut git2::Repository) {
2236 use git2::Signature;
2237
2238 let signature = Signature::now("test", "test@zed.dev").unwrap();
2239 repo.stash_save(&signature, "N/A", None)
2240 .expect("Failed to stash");
2241}
2242
2243#[track_caller]
2244fn git_reset(offset: usize, repo: &git2::Repository) {
2245 let head = repo.head().expect("Couldn't get repo head");
2246 let object = head.peel(git2::ObjectType::Commit).unwrap();
2247 let commit = object.as_commit().unwrap();
2248 let new_head = commit
2249 .parents()
2250 .inspect(|parnet| {
2251 parnet.message();
2252 })
2253 .skip(offset)
2254 .next()
2255 .expect("Not enough history");
2256 repo.reset(&new_head.as_object(), git2::ResetType::Soft, None)
2257 .expect("Could not reset");
2258}
2259
2260#[allow(dead_code)]
2261#[track_caller]
2262fn git_status(repo: &git2::Repository) -> collections::HashMap<String, git2::Status> {
2263 repo.statuses(None)
2264 .unwrap()
2265 .iter()
2266 .map(|status| (status.path().unwrap().to_string(), status.status()))
2267 .collect()
2268}
2269
2270#[track_caller]
2271fn check_worktree_entries(
2272 tree: &Worktree,
2273 expected_excluded_paths: &[&str],
2274 expected_ignored_paths: &[&str],
2275 expected_tracked_paths: &[&str],
2276) {
2277 for path in expected_excluded_paths {
2278 let entry = tree.entry_for_path(path);
2279 assert!(
2280 entry.is_none(),
2281 "expected path '{path}' to be excluded, but got entry: {entry:?}",
2282 );
2283 }
2284 for path in expected_ignored_paths {
2285 let entry = tree
2286 .entry_for_path(path)
2287 .unwrap_or_else(|| panic!("Missing entry for expected ignored path '{path}'"));
2288 assert!(
2289 entry.is_ignored,
2290 "expected path '{path}' to be ignored, but got entry: {entry:?}",
2291 );
2292 }
2293 for path in expected_tracked_paths {
2294 let entry = tree
2295 .entry_for_path(path)
2296 .unwrap_or_else(|| panic!("Missing entry for expected tracked path '{path}'"));
2297 assert!(
2298 !entry.is_ignored,
2299 "expected path '{path}' to be tracked, but got entry: {entry:?}",
2300 );
2301 }
2302}
2303
2304fn init_test(cx: &mut gpui::TestAppContext) {
2305 cx.update(|cx| {
2306 let settings_store = SettingsStore::test(cx);
2307 cx.set_global(settings_store);
2308 Project::init_settings(cx);
2309 });
2310}