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