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]
1208async fn test_fs_events_in_dot_git_worktree(cx: &mut TestAppContext) {
1209 init_test(cx);
1210 cx.executor().allow_parking();
1211 let dir = temp_tree(json!({
1212 ".git": {
1213 "HEAD": "ref: refs/heads/main\n",
1214 "foo": "foo contents",
1215 },
1216 }));
1217 let dot_git_worktree_dir = dir.path().join(".git");
1218
1219 let tree = Worktree::local(
1220 build_client(cx),
1221 dot_git_worktree_dir.clone(),
1222 true,
1223 Arc::new(RealFs),
1224 Default::default(),
1225 &mut cx.to_async(),
1226 )
1227 .await
1228 .unwrap();
1229 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1230 .await;
1231 tree.flush_fs_events(cx).await;
1232 tree.read_with(cx, |tree, _| {
1233 check_worktree_entries(tree, &[], &["HEAD", "foo"], &[])
1234 });
1235
1236 std::fs::write(dot_git_worktree_dir.join("new_file"), "new file contents")
1237 .unwrap_or_else(|e| panic!("Failed to create in {dot_git_worktree_dir:?} a new file: {e}"));
1238 tree.flush_fs_events(cx).await;
1239 tree.read_with(cx, |tree, _| {
1240 check_worktree_entries(tree, &[], &["HEAD", "foo", "new_file"], &[])
1241 });
1242}
1243
1244#[gpui::test(iterations = 30)]
1245async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
1246 init_test(cx);
1247 let fs = FakeFs::new(cx.background_executor.clone());
1248 fs.insert_tree(
1249 "/root",
1250 json!({
1251 "b": {},
1252 "c": {},
1253 "d": {},
1254 }),
1255 )
1256 .await;
1257
1258 let tree = Worktree::local(
1259 build_client(cx),
1260 "/root".as_ref(),
1261 true,
1262 fs,
1263 Default::default(),
1264 &mut cx.to_async(),
1265 )
1266 .await
1267 .unwrap();
1268
1269 let snapshot1 = tree.update(cx, |tree, cx| {
1270 let tree = tree.as_local_mut().unwrap();
1271 let snapshot = Arc::new(Mutex::new(tree.snapshot()));
1272 let _ = tree.observe_updates(0, cx, {
1273 let snapshot = snapshot.clone();
1274 move |update| {
1275 snapshot.lock().apply_remote_update(update).unwrap();
1276 async { true }
1277 }
1278 });
1279 snapshot
1280 });
1281
1282 let entry = tree
1283 .update(cx, |tree, cx| {
1284 tree.as_local_mut()
1285 .unwrap()
1286 .create_entry("a/e".as_ref(), true, cx)
1287 })
1288 .await
1289 .unwrap()
1290 .unwrap();
1291 assert!(entry.is_dir());
1292
1293 cx.executor().run_until_parked();
1294 tree.read_with(cx, |tree, _| {
1295 assert_eq!(tree.entry_for_path("a/e").unwrap().kind, EntryKind::Dir);
1296 });
1297
1298 let snapshot2 = tree.update(cx, |tree, _| tree.as_local().unwrap().snapshot());
1299 assert_eq!(
1300 snapshot1.lock().entries(true).collect::<Vec<_>>(),
1301 snapshot2.entries(true).collect::<Vec<_>>()
1302 );
1303}
1304
1305#[gpui::test]
1306async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
1307 init_test(cx);
1308 cx.executor().allow_parking();
1309 let client_fake = cx.update(|cx| {
1310 Client::new(
1311 Arc::new(FakeSystemClock::default()),
1312 FakeHttpClient::with_404_response(),
1313 cx,
1314 )
1315 });
1316
1317 let fs_fake = FakeFs::new(cx.background_executor.clone());
1318 fs_fake
1319 .insert_tree(
1320 "/root",
1321 json!({
1322 "a": {},
1323 }),
1324 )
1325 .await;
1326
1327 let tree_fake = Worktree::local(
1328 client_fake,
1329 "/root".as_ref(),
1330 true,
1331 fs_fake,
1332 Default::default(),
1333 &mut cx.to_async(),
1334 )
1335 .await
1336 .unwrap();
1337
1338 let entry = tree_fake
1339 .update(cx, |tree, cx| {
1340 tree.as_local_mut()
1341 .unwrap()
1342 .create_entry("a/b/c/d.txt".as_ref(), false, cx)
1343 })
1344 .await
1345 .unwrap()
1346 .unwrap();
1347 assert!(entry.is_file());
1348
1349 cx.executor().run_until_parked();
1350 tree_fake.read_with(cx, |tree, _| {
1351 assert!(tree.entry_for_path("a/b/c/d.txt").unwrap().is_file());
1352 assert!(tree.entry_for_path("a/b/c/").unwrap().is_dir());
1353 assert!(tree.entry_for_path("a/b/").unwrap().is_dir());
1354 });
1355
1356 let client_real = cx.update(|cx| {
1357 Client::new(
1358 Arc::new(FakeSystemClock::default()),
1359 FakeHttpClient::with_404_response(),
1360 cx,
1361 )
1362 });
1363
1364 let fs_real = Arc::new(RealFs);
1365 let temp_root = temp_tree(json!({
1366 "a": {}
1367 }));
1368
1369 let tree_real = Worktree::local(
1370 client_real,
1371 temp_root.path(),
1372 true,
1373 fs_real,
1374 Default::default(),
1375 &mut cx.to_async(),
1376 )
1377 .await
1378 .unwrap();
1379
1380 let entry = tree_real
1381 .update(cx, |tree, cx| {
1382 tree.as_local_mut()
1383 .unwrap()
1384 .create_entry("a/b/c/d.txt".as_ref(), false, cx)
1385 })
1386 .await
1387 .unwrap()
1388 .unwrap();
1389 assert!(entry.is_file());
1390
1391 cx.executor().run_until_parked();
1392 tree_real.read_with(cx, |tree, _| {
1393 assert!(tree.entry_for_path("a/b/c/d.txt").unwrap().is_file());
1394 assert!(tree.entry_for_path("a/b/c/").unwrap().is_dir());
1395 assert!(tree.entry_for_path("a/b/").unwrap().is_dir());
1396 });
1397
1398 // Test smallest change
1399 let entry = tree_real
1400 .update(cx, |tree, cx| {
1401 tree.as_local_mut()
1402 .unwrap()
1403 .create_entry("a/b/c/e.txt".as_ref(), false, cx)
1404 })
1405 .await
1406 .unwrap()
1407 .unwrap();
1408 assert!(entry.is_file());
1409
1410 cx.executor().run_until_parked();
1411 tree_real.read_with(cx, |tree, _| {
1412 assert!(tree.entry_for_path("a/b/c/e.txt").unwrap().is_file());
1413 });
1414
1415 // Test largest change
1416 let entry = tree_real
1417 .update(cx, |tree, cx| {
1418 tree.as_local_mut()
1419 .unwrap()
1420 .create_entry("d/e/f/g.txt".as_ref(), false, cx)
1421 })
1422 .await
1423 .unwrap()
1424 .unwrap();
1425 assert!(entry.is_file());
1426
1427 cx.executor().run_until_parked();
1428 tree_real.read_with(cx, |tree, _| {
1429 assert!(tree.entry_for_path("d/e/f/g.txt").unwrap().is_file());
1430 assert!(tree.entry_for_path("d/e/f").unwrap().is_dir());
1431 assert!(tree.entry_for_path("d/e/").unwrap().is_dir());
1432 assert!(tree.entry_for_path("d/").unwrap().is_dir());
1433 });
1434}
1435
1436#[gpui::test(iterations = 100)]
1437async fn test_random_worktree_operations_during_initial_scan(
1438 cx: &mut TestAppContext,
1439 mut rng: StdRng,
1440) {
1441 init_test(cx);
1442 let operations = env::var("OPERATIONS")
1443 .map(|o| o.parse().unwrap())
1444 .unwrap_or(5);
1445 let initial_entries = env::var("INITIAL_ENTRIES")
1446 .map(|o| o.parse().unwrap())
1447 .unwrap_or(20);
1448
1449 let root_dir = Path::new("/test");
1450 let fs = FakeFs::new(cx.background_executor.clone()) as Arc<dyn Fs>;
1451 fs.as_fake().insert_tree(root_dir, json!({})).await;
1452 for _ in 0..initial_entries {
1453 randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
1454 }
1455 log::info!("generated initial tree");
1456
1457 let worktree = Worktree::local(
1458 build_client(cx),
1459 root_dir,
1460 true,
1461 fs.clone(),
1462 Default::default(),
1463 &mut cx.to_async(),
1464 )
1465 .await
1466 .unwrap();
1467
1468 let mut snapshots = vec![worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot())];
1469 let updates = Arc::new(Mutex::new(Vec::new()));
1470 worktree.update(cx, |tree, cx| {
1471 check_worktree_change_events(tree, cx);
1472
1473 let _ = tree.as_local_mut().unwrap().observe_updates(0, cx, {
1474 let updates = updates.clone();
1475 move |update| {
1476 updates.lock().push(update);
1477 async { true }
1478 }
1479 });
1480 });
1481
1482 for _ in 0..operations {
1483 worktree
1484 .update(cx, |worktree, cx| {
1485 randomly_mutate_worktree(worktree, &mut rng, cx)
1486 })
1487 .await
1488 .log_err();
1489 worktree.read_with(cx, |tree, _| {
1490 tree.as_local().unwrap().snapshot().check_invariants(true)
1491 });
1492
1493 if rng.gen_bool(0.6) {
1494 snapshots.push(worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot()));
1495 }
1496 }
1497
1498 worktree
1499 .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
1500 .await;
1501
1502 cx.executor().run_until_parked();
1503
1504 let final_snapshot = worktree.read_with(cx, |tree, _| {
1505 let tree = tree.as_local().unwrap();
1506 let snapshot = tree.snapshot();
1507 snapshot.check_invariants(true);
1508 snapshot
1509 });
1510
1511 for (i, snapshot) in snapshots.into_iter().enumerate().rev() {
1512 let mut updated_snapshot = snapshot.clone();
1513 for update in updates.lock().iter() {
1514 if update.scan_id >= updated_snapshot.scan_id() as u64 {
1515 updated_snapshot
1516 .apply_remote_update(update.clone())
1517 .unwrap();
1518 }
1519 }
1520
1521 assert_eq!(
1522 updated_snapshot.entries(true).collect::<Vec<_>>(),
1523 final_snapshot.entries(true).collect::<Vec<_>>(),
1524 "wrong updates after snapshot {i}: {snapshot:#?} {updates:#?}",
1525 );
1526 }
1527}
1528
1529#[gpui::test(iterations = 100)]
1530async fn test_random_worktree_changes(cx: &mut TestAppContext, mut rng: StdRng) {
1531 init_test(cx);
1532 let operations = env::var("OPERATIONS")
1533 .map(|o| o.parse().unwrap())
1534 .unwrap_or(40);
1535 let initial_entries = env::var("INITIAL_ENTRIES")
1536 .map(|o| o.parse().unwrap())
1537 .unwrap_or(20);
1538
1539 let root_dir = Path::new("/test");
1540 let fs = FakeFs::new(cx.background_executor.clone()) as Arc<dyn Fs>;
1541 fs.as_fake().insert_tree(root_dir, json!({})).await;
1542 for _ in 0..initial_entries {
1543 randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
1544 }
1545 log::info!("generated initial tree");
1546
1547 let worktree = Worktree::local(
1548 build_client(cx),
1549 root_dir,
1550 true,
1551 fs.clone(),
1552 Default::default(),
1553 &mut cx.to_async(),
1554 )
1555 .await
1556 .unwrap();
1557
1558 let updates = Arc::new(Mutex::new(Vec::new()));
1559 worktree.update(cx, |tree, cx| {
1560 check_worktree_change_events(tree, cx);
1561
1562 let _ = tree.as_local_mut().unwrap().observe_updates(0, cx, {
1563 let updates = updates.clone();
1564 move |update| {
1565 updates.lock().push(update);
1566 async { true }
1567 }
1568 });
1569 });
1570
1571 worktree
1572 .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
1573 .await;
1574
1575 fs.as_fake().pause_events();
1576 let mut snapshots = Vec::new();
1577 let mut mutations_len = operations;
1578 while mutations_len > 1 {
1579 if rng.gen_bool(0.2) {
1580 worktree
1581 .update(cx, |worktree, cx| {
1582 randomly_mutate_worktree(worktree, &mut rng, cx)
1583 })
1584 .await
1585 .log_err();
1586 } else {
1587 randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
1588 }
1589
1590 let buffered_event_count = fs.as_fake().buffered_event_count();
1591 if buffered_event_count > 0 && rng.gen_bool(0.3) {
1592 let len = rng.gen_range(0..=buffered_event_count);
1593 log::info!("flushing {} events", len);
1594 fs.as_fake().flush_events(len);
1595 } else {
1596 randomly_mutate_fs(&fs, root_dir, 0.6, &mut rng).await;
1597 mutations_len -= 1;
1598 }
1599
1600 cx.executor().run_until_parked();
1601 if rng.gen_bool(0.2) {
1602 log::info!("storing snapshot {}", snapshots.len());
1603 let snapshot = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
1604 snapshots.push(snapshot);
1605 }
1606 }
1607
1608 log::info!("quiescing");
1609 fs.as_fake().flush_events(usize::MAX);
1610 cx.executor().run_until_parked();
1611
1612 let snapshot = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
1613 snapshot.check_invariants(true);
1614 let expanded_paths = snapshot
1615 .expanded_entries()
1616 .map(|e| e.path.clone())
1617 .collect::<Vec<_>>();
1618
1619 {
1620 let new_worktree = Worktree::local(
1621 build_client(cx),
1622 root_dir,
1623 true,
1624 fs.clone(),
1625 Default::default(),
1626 &mut cx.to_async(),
1627 )
1628 .await
1629 .unwrap();
1630 new_worktree
1631 .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
1632 .await;
1633 new_worktree
1634 .update(cx, |tree, _| {
1635 tree.as_local_mut()
1636 .unwrap()
1637 .refresh_entries_for_paths(expanded_paths)
1638 })
1639 .recv()
1640 .await;
1641 let new_snapshot =
1642 new_worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
1643 assert_eq!(
1644 snapshot.entries_without_ids(true),
1645 new_snapshot.entries_without_ids(true)
1646 );
1647 }
1648
1649 for (i, mut prev_snapshot) in snapshots.into_iter().enumerate().rev() {
1650 for update in updates.lock().iter() {
1651 if update.scan_id >= prev_snapshot.scan_id() as u64 {
1652 prev_snapshot.apply_remote_update(update.clone()).unwrap();
1653 }
1654 }
1655
1656 assert_eq!(
1657 prev_snapshot
1658 .entries(true)
1659 .map(ignore_pending_dir)
1660 .collect::<Vec<_>>(),
1661 snapshot
1662 .entries(true)
1663 .map(ignore_pending_dir)
1664 .collect::<Vec<_>>(),
1665 "wrong updates after snapshot {i}: {updates:#?}",
1666 );
1667 }
1668
1669 fn ignore_pending_dir(entry: &Entry) -> Entry {
1670 let mut entry = entry.clone();
1671 if entry.kind.is_dir() {
1672 entry.kind = EntryKind::Dir
1673 }
1674 entry
1675 }
1676}
1677
1678// The worktree's `UpdatedEntries` event can be used to follow along with
1679// all changes to the worktree's snapshot.
1680fn check_worktree_change_events(tree: &mut Worktree, cx: &mut ModelContext<Worktree>) {
1681 let mut entries = tree.entries(true).cloned().collect::<Vec<_>>();
1682 cx.subscribe(&cx.handle(), move |tree, _, event, _| {
1683 if let Event::UpdatedEntries(changes) = event {
1684 for (path, _, change_type) in changes.iter() {
1685 let entry = tree.entry_for_path(&path).cloned();
1686 let ix = match entries.binary_search_by_key(&path, |e| &e.path) {
1687 Ok(ix) | Err(ix) => ix,
1688 };
1689 match change_type {
1690 PathChange::Added => entries.insert(ix, entry.unwrap()),
1691 PathChange::Removed => drop(entries.remove(ix)),
1692 PathChange::Updated => {
1693 let entry = entry.unwrap();
1694 let existing_entry = entries.get_mut(ix).unwrap();
1695 assert_eq!(existing_entry.path, entry.path);
1696 *existing_entry = entry;
1697 }
1698 PathChange::AddedOrUpdated | PathChange::Loaded => {
1699 let entry = entry.unwrap();
1700 if entries.get(ix).map(|e| &e.path) == Some(&entry.path) {
1701 *entries.get_mut(ix).unwrap() = entry;
1702 } else {
1703 entries.insert(ix, entry);
1704 }
1705 }
1706 }
1707 }
1708
1709 let new_entries = tree.entries(true).cloned().collect::<Vec<_>>();
1710 assert_eq!(entries, new_entries, "incorrect changes: {:?}", changes);
1711 }
1712 })
1713 .detach();
1714}
1715
1716fn randomly_mutate_worktree(
1717 worktree: &mut Worktree,
1718 rng: &mut impl Rng,
1719 cx: &mut ModelContext<Worktree>,
1720) -> Task<Result<()>> {
1721 log::info!("mutating worktree");
1722 let worktree = worktree.as_local_mut().unwrap();
1723 let snapshot = worktree.snapshot();
1724 let entry = snapshot.entries(false).choose(rng).unwrap();
1725
1726 match rng.gen_range(0_u32..100) {
1727 0..=33 if entry.path.as_ref() != Path::new("") => {
1728 log::info!("deleting entry {:?} ({})", entry.path, entry.id.0);
1729 worktree.delete_entry(entry.id, cx).unwrap()
1730 }
1731 ..=66 if entry.path.as_ref() != Path::new("") => {
1732 let other_entry = snapshot.entries(false).choose(rng).unwrap();
1733 let new_parent_path = if other_entry.is_dir() {
1734 other_entry.path.clone()
1735 } else {
1736 other_entry.path.parent().unwrap().into()
1737 };
1738 let mut new_path = new_parent_path.join(random_filename(rng));
1739 if new_path.starts_with(&entry.path) {
1740 new_path = random_filename(rng).into();
1741 }
1742
1743 log::info!(
1744 "renaming entry {:?} ({}) to {:?}",
1745 entry.path,
1746 entry.id.0,
1747 new_path
1748 );
1749 let task = worktree.rename_entry(entry.id, new_path, cx);
1750 cx.background_executor().spawn(async move {
1751 task.await?.unwrap();
1752 Ok(())
1753 })
1754 }
1755 _ => {
1756 if entry.is_dir() {
1757 let child_path = entry.path.join(random_filename(rng));
1758 let is_dir = rng.gen_bool(0.3);
1759 log::info!(
1760 "creating {} at {:?}",
1761 if is_dir { "dir" } else { "file" },
1762 child_path,
1763 );
1764 let task = worktree.create_entry(child_path, is_dir, cx);
1765 cx.background_executor().spawn(async move {
1766 task.await?;
1767 Ok(())
1768 })
1769 } else {
1770 log::info!("overwriting file {:?} ({})", entry.path, entry.id.0);
1771 let task =
1772 worktree.write_file(entry.path.clone(), "".into(), Default::default(), cx);
1773 cx.background_executor().spawn(async move {
1774 task.await?;
1775 Ok(())
1776 })
1777 }
1778 }
1779 }
1780}
1781
1782async fn randomly_mutate_fs(
1783 fs: &Arc<dyn Fs>,
1784 root_path: &Path,
1785 insertion_probability: f64,
1786 rng: &mut impl Rng,
1787) {
1788 log::info!("mutating fs");
1789 let mut files = Vec::new();
1790 let mut dirs = Vec::new();
1791 for path in fs.as_fake().paths(false) {
1792 if path.starts_with(root_path) {
1793 if fs.is_file(&path).await {
1794 files.push(path);
1795 } else {
1796 dirs.push(path);
1797 }
1798 }
1799 }
1800
1801 if (files.is_empty() && dirs.len() == 1) || rng.gen_bool(insertion_probability) {
1802 let path = dirs.choose(rng).unwrap();
1803 let new_path = path.join(random_filename(rng));
1804
1805 if rng.gen() {
1806 log::info!(
1807 "creating dir {:?}",
1808 new_path.strip_prefix(root_path).unwrap()
1809 );
1810 fs.create_dir(&new_path).await.unwrap();
1811 } else {
1812 log::info!(
1813 "creating file {:?}",
1814 new_path.strip_prefix(root_path).unwrap()
1815 );
1816 fs.create_file(&new_path, Default::default()).await.unwrap();
1817 }
1818 } else if rng.gen_bool(0.05) {
1819 let ignore_dir_path = dirs.choose(rng).unwrap();
1820 let ignore_path = ignore_dir_path.join(&*GITIGNORE);
1821
1822 let subdirs = dirs
1823 .iter()
1824 .filter(|d| d.starts_with(&ignore_dir_path))
1825 .cloned()
1826 .collect::<Vec<_>>();
1827 let subfiles = files
1828 .iter()
1829 .filter(|d| d.starts_with(&ignore_dir_path))
1830 .cloned()
1831 .collect::<Vec<_>>();
1832 let files_to_ignore = {
1833 let len = rng.gen_range(0..=subfiles.len());
1834 subfiles.choose_multiple(rng, len)
1835 };
1836 let dirs_to_ignore = {
1837 let len = rng.gen_range(0..subdirs.len());
1838 subdirs.choose_multiple(rng, len)
1839 };
1840
1841 let mut ignore_contents = String::new();
1842 for path_to_ignore in files_to_ignore.chain(dirs_to_ignore) {
1843 writeln!(
1844 ignore_contents,
1845 "{}",
1846 path_to_ignore
1847 .strip_prefix(&ignore_dir_path)
1848 .unwrap()
1849 .to_str()
1850 .unwrap()
1851 )
1852 .unwrap();
1853 }
1854 log::info!(
1855 "creating gitignore {:?} with contents:\n{}",
1856 ignore_path.strip_prefix(&root_path).unwrap(),
1857 ignore_contents
1858 );
1859 fs.save(
1860 &ignore_path,
1861 &ignore_contents.as_str().into(),
1862 Default::default(),
1863 )
1864 .await
1865 .unwrap();
1866 } else {
1867 let old_path = {
1868 let file_path = files.choose(rng);
1869 let dir_path = dirs[1..].choose(rng);
1870 file_path.into_iter().chain(dir_path).choose(rng).unwrap()
1871 };
1872
1873 let is_rename = rng.gen();
1874 if is_rename {
1875 let new_path_parent = dirs
1876 .iter()
1877 .filter(|d| !d.starts_with(old_path))
1878 .choose(rng)
1879 .unwrap();
1880
1881 let overwrite_existing_dir =
1882 !old_path.starts_with(&new_path_parent) && rng.gen_bool(0.3);
1883 let new_path = if overwrite_existing_dir {
1884 fs.remove_dir(
1885 &new_path_parent,
1886 RemoveOptions {
1887 recursive: true,
1888 ignore_if_not_exists: true,
1889 },
1890 )
1891 .await
1892 .unwrap();
1893 new_path_parent.to_path_buf()
1894 } else {
1895 new_path_parent.join(random_filename(rng))
1896 };
1897
1898 log::info!(
1899 "renaming {:?} to {}{:?}",
1900 old_path.strip_prefix(&root_path).unwrap(),
1901 if overwrite_existing_dir {
1902 "overwrite "
1903 } else {
1904 ""
1905 },
1906 new_path.strip_prefix(&root_path).unwrap()
1907 );
1908 fs.rename(
1909 &old_path,
1910 &new_path,
1911 fs::RenameOptions {
1912 overwrite: true,
1913 ignore_if_exists: true,
1914 },
1915 )
1916 .await
1917 .unwrap();
1918 } else if fs.is_file(&old_path).await {
1919 log::info!(
1920 "deleting file {:?}",
1921 old_path.strip_prefix(&root_path).unwrap()
1922 );
1923 fs.remove_file(old_path, Default::default()).await.unwrap();
1924 } else {
1925 log::info!(
1926 "deleting dir {:?}",
1927 old_path.strip_prefix(&root_path).unwrap()
1928 );
1929 fs.remove_dir(
1930 &old_path,
1931 RemoveOptions {
1932 recursive: true,
1933 ignore_if_not_exists: true,
1934 },
1935 )
1936 .await
1937 .unwrap();
1938 }
1939 }
1940}
1941
1942fn random_filename(rng: &mut impl Rng) -> String {
1943 (0..6)
1944 .map(|_| rng.sample(rand::distributions::Alphanumeric))
1945 .map(char::from)
1946 .collect()
1947}
1948
1949#[gpui::test]
1950async fn test_rename_work_directory(cx: &mut TestAppContext) {
1951 init_test(cx);
1952 cx.executor().allow_parking();
1953 let root = temp_tree(json!({
1954 "projects": {
1955 "project1": {
1956 "a": "",
1957 "b": "",
1958 }
1959 },
1960
1961 }));
1962 let root_path = root.path();
1963
1964 let tree = Worktree::local(
1965 build_client(cx),
1966 root_path,
1967 true,
1968 Arc::new(RealFs),
1969 Default::default(),
1970 &mut cx.to_async(),
1971 )
1972 .await
1973 .unwrap();
1974
1975 let repo = git_init(&root_path.join("projects/project1"));
1976 git_add("a", &repo);
1977 git_commit("init", &repo);
1978 std::fs::write(root_path.join("projects/project1/a"), "aa").ok();
1979
1980 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1981 .await;
1982
1983 tree.flush_fs_events(cx).await;
1984
1985 cx.read(|cx| {
1986 let tree = tree.read(cx);
1987 let (work_dir, _) = tree.repositories().next().unwrap();
1988 assert_eq!(work_dir.as_ref(), Path::new("projects/project1"));
1989 assert_eq!(
1990 tree.status_for_file(Path::new("projects/project1/a")),
1991 Some(GitFileStatus::Modified)
1992 );
1993 assert_eq!(
1994 tree.status_for_file(Path::new("projects/project1/b")),
1995 Some(GitFileStatus::Added)
1996 );
1997 });
1998
1999 std::fs::rename(
2000 root_path.join("projects/project1"),
2001 root_path.join("projects/project2"),
2002 )
2003 .ok();
2004 tree.flush_fs_events(cx).await;
2005
2006 cx.read(|cx| {
2007 let tree = tree.read(cx);
2008 let (work_dir, _) = tree.repositories().next().unwrap();
2009 assert_eq!(work_dir.as_ref(), Path::new("projects/project2"));
2010 assert_eq!(
2011 tree.status_for_file(Path::new("projects/project2/a")),
2012 Some(GitFileStatus::Modified)
2013 );
2014 assert_eq!(
2015 tree.status_for_file(Path::new("projects/project2/b")),
2016 Some(GitFileStatus::Added)
2017 );
2018 });
2019}
2020
2021#[gpui::test]
2022async fn test_git_repository_for_path(cx: &mut TestAppContext) {
2023 init_test(cx);
2024 cx.executor().allow_parking();
2025 let root = temp_tree(json!({
2026 "c.txt": "",
2027 "dir1": {
2028 ".git": {},
2029 "deps": {
2030 "dep1": {
2031 ".git": {},
2032 "src": {
2033 "a.txt": ""
2034 }
2035 }
2036 },
2037 "src": {
2038 "b.txt": ""
2039 }
2040 },
2041 }));
2042
2043 let tree = Worktree::local(
2044 build_client(cx),
2045 root.path(),
2046 true,
2047 Arc::new(RealFs),
2048 Default::default(),
2049 &mut cx.to_async(),
2050 )
2051 .await
2052 .unwrap();
2053
2054 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2055 .await;
2056 tree.flush_fs_events(cx).await;
2057
2058 tree.read_with(cx, |tree, _cx| {
2059 let tree = tree.as_local().unwrap();
2060
2061 assert!(tree.repository_for_path("c.txt".as_ref()).is_none());
2062
2063 let entry = tree.repository_for_path("dir1/src/b.txt".as_ref()).unwrap();
2064 assert_eq!(
2065 entry
2066 .work_directory(tree)
2067 .map(|directory| directory.as_ref().to_owned()),
2068 Some(Path::new("dir1").to_owned())
2069 );
2070
2071 let entry = tree
2072 .repository_for_path("dir1/deps/dep1/src/a.txt".as_ref())
2073 .unwrap();
2074 assert_eq!(
2075 entry
2076 .work_directory(tree)
2077 .map(|directory| directory.as_ref().to_owned()),
2078 Some(Path::new("dir1/deps/dep1").to_owned())
2079 );
2080
2081 let entries = tree.files(false, 0);
2082
2083 let paths_with_repos = tree
2084 .entries_with_repositories(entries)
2085 .map(|(entry, repo)| {
2086 (
2087 entry.path.as_ref(),
2088 repo.and_then(|repo| {
2089 repo.work_directory(&tree)
2090 .map(|work_directory| work_directory.0.to_path_buf())
2091 }),
2092 )
2093 })
2094 .collect::<Vec<_>>();
2095
2096 assert_eq!(
2097 paths_with_repos,
2098 &[
2099 (Path::new("c.txt"), None),
2100 (
2101 Path::new("dir1/deps/dep1/src/a.txt"),
2102 Some(Path::new("dir1/deps/dep1").into())
2103 ),
2104 (Path::new("dir1/src/b.txt"), Some(Path::new("dir1").into())),
2105 ]
2106 );
2107 });
2108
2109 let repo_update_events = Arc::new(Mutex::new(vec![]));
2110 tree.update(cx, |_, cx| {
2111 let repo_update_events = repo_update_events.clone();
2112 cx.subscribe(&tree, move |_, _, event, _| {
2113 if let Event::UpdatedGitRepositories(update) = event {
2114 repo_update_events.lock().push(update.clone());
2115 }
2116 })
2117 .detach();
2118 });
2119
2120 std::fs::write(root.path().join("dir1/.git/random_new_file"), "hello").unwrap();
2121 tree.flush_fs_events(cx).await;
2122
2123 assert_eq!(
2124 repo_update_events.lock()[0]
2125 .iter()
2126 .map(|e| e.0.clone())
2127 .collect::<Vec<Arc<Path>>>(),
2128 vec![Path::new("dir1").into()]
2129 );
2130
2131 std::fs::remove_dir_all(root.path().join("dir1/.git")).unwrap();
2132 tree.flush_fs_events(cx).await;
2133
2134 tree.read_with(cx, |tree, _cx| {
2135 let tree = tree.as_local().unwrap();
2136
2137 assert!(tree
2138 .repository_for_path("dir1/src/b.txt".as_ref())
2139 .is_none());
2140 });
2141}
2142
2143#[gpui::test]
2144async fn test_git_status(cx: &mut TestAppContext) {
2145 init_test(cx);
2146 cx.executor().allow_parking();
2147 const IGNORE_RULE: &str = "**/target";
2148
2149 let root = temp_tree(json!({
2150 "project": {
2151 "a.txt": "a",
2152 "b.txt": "bb",
2153 "c": {
2154 "d": {
2155 "e.txt": "eee"
2156 }
2157 },
2158 "f.txt": "ffff",
2159 "target": {
2160 "build_file": "???"
2161 },
2162 ".gitignore": IGNORE_RULE
2163 },
2164
2165 }));
2166
2167 const A_TXT: &str = "a.txt";
2168 const B_TXT: &str = "b.txt";
2169 const E_TXT: &str = "c/d/e.txt";
2170 const F_TXT: &str = "f.txt";
2171 const DOTGITIGNORE: &str = ".gitignore";
2172 const BUILD_FILE: &str = "target/build_file";
2173 let project_path = Path::new("project");
2174
2175 // Set up git repository before creating the worktree.
2176 let work_dir = root.path().join("project");
2177 let mut repo = git_init(work_dir.as_path());
2178 repo.add_ignore_rule(IGNORE_RULE).unwrap();
2179 git_add(A_TXT, &repo);
2180 git_add(E_TXT, &repo);
2181 git_add(DOTGITIGNORE, &repo);
2182 git_commit("Initial commit", &repo);
2183
2184 let tree = Worktree::local(
2185 build_client(cx),
2186 root.path(),
2187 true,
2188 Arc::new(RealFs),
2189 Default::default(),
2190 &mut cx.to_async(),
2191 )
2192 .await
2193 .unwrap();
2194
2195 tree.flush_fs_events(cx).await;
2196 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2197 .await;
2198 cx.executor().run_until_parked();
2199
2200 // Check that the right git state is observed on startup
2201 tree.read_with(cx, |tree, _cx| {
2202 let snapshot = tree.snapshot();
2203 assert_eq!(snapshot.repositories().count(), 1);
2204 let (dir, _) = snapshot.repositories().next().unwrap();
2205 assert_eq!(dir.as_ref(), Path::new("project"));
2206
2207 assert_eq!(
2208 snapshot.status_for_file(project_path.join(B_TXT)),
2209 Some(GitFileStatus::Added)
2210 );
2211 assert_eq!(
2212 snapshot.status_for_file(project_path.join(F_TXT)),
2213 Some(GitFileStatus::Added)
2214 );
2215 });
2216
2217 // Modify a file in the working copy.
2218 std::fs::write(work_dir.join(A_TXT), "aa").unwrap();
2219 tree.flush_fs_events(cx).await;
2220 cx.executor().run_until_parked();
2221
2222 // The worktree detects that the file's git status has changed.
2223 tree.read_with(cx, |tree, _cx| {
2224 let snapshot = tree.snapshot();
2225 assert_eq!(
2226 snapshot.status_for_file(project_path.join(A_TXT)),
2227 Some(GitFileStatus::Modified)
2228 );
2229 });
2230
2231 // Create a commit in the git repository.
2232 git_add(A_TXT, &repo);
2233 git_add(B_TXT, &repo);
2234 git_commit("Committing modified and added", &repo);
2235 tree.flush_fs_events(cx).await;
2236 cx.executor().run_until_parked();
2237
2238 // The worktree detects that the files' git status have changed.
2239 tree.read_with(cx, |tree, _cx| {
2240 let snapshot = tree.snapshot();
2241 assert_eq!(
2242 snapshot.status_for_file(project_path.join(F_TXT)),
2243 Some(GitFileStatus::Added)
2244 );
2245 assert_eq!(snapshot.status_for_file(project_path.join(B_TXT)), None);
2246 assert_eq!(snapshot.status_for_file(project_path.join(A_TXT)), None);
2247 });
2248
2249 // Modify files in the working copy and perform git operations on other files.
2250 git_reset(0, &repo);
2251 git_remove_index(Path::new(B_TXT), &repo);
2252 git_stash(&mut repo);
2253 std::fs::write(work_dir.join(E_TXT), "eeee").unwrap();
2254 std::fs::write(work_dir.join(BUILD_FILE), "this should be ignored").unwrap();
2255 tree.flush_fs_events(cx).await;
2256 cx.executor().run_until_parked();
2257
2258 // Check that more complex repo changes are tracked
2259 tree.read_with(cx, |tree, _cx| {
2260 let snapshot = tree.snapshot();
2261
2262 assert_eq!(snapshot.status_for_file(project_path.join(A_TXT)), None);
2263 assert_eq!(
2264 snapshot.status_for_file(project_path.join(B_TXT)),
2265 Some(GitFileStatus::Added)
2266 );
2267 assert_eq!(
2268 snapshot.status_for_file(project_path.join(E_TXT)),
2269 Some(GitFileStatus::Modified)
2270 );
2271 });
2272
2273 std::fs::remove_file(work_dir.join(B_TXT)).unwrap();
2274 std::fs::remove_dir_all(work_dir.join("c")).unwrap();
2275 std::fs::write(
2276 work_dir.join(DOTGITIGNORE),
2277 [IGNORE_RULE, "f.txt"].join("\n"),
2278 )
2279 .unwrap();
2280
2281 git_add(Path::new(DOTGITIGNORE), &repo);
2282 git_commit("Committing modified git ignore", &repo);
2283
2284 tree.flush_fs_events(cx).await;
2285 cx.executor().run_until_parked();
2286
2287 let mut renamed_dir_name = "first_directory/second_directory";
2288 const RENAMED_FILE: &str = "rf.txt";
2289
2290 std::fs::create_dir_all(work_dir.join(renamed_dir_name)).unwrap();
2291 std::fs::write(
2292 work_dir.join(renamed_dir_name).join(RENAMED_FILE),
2293 "new-contents",
2294 )
2295 .unwrap();
2296
2297 tree.flush_fs_events(cx).await;
2298 cx.executor().run_until_parked();
2299
2300 tree.read_with(cx, |tree, _cx| {
2301 let snapshot = tree.snapshot();
2302 assert_eq!(
2303 snapshot.status_for_file(&project_path.join(renamed_dir_name).join(RENAMED_FILE)),
2304 Some(GitFileStatus::Added)
2305 );
2306 });
2307
2308 renamed_dir_name = "new_first_directory/second_directory";
2309
2310 std::fs::rename(
2311 work_dir.join("first_directory"),
2312 work_dir.join("new_first_directory"),
2313 )
2314 .unwrap();
2315
2316 tree.flush_fs_events(cx).await;
2317 cx.executor().run_until_parked();
2318
2319 tree.read_with(cx, |tree, _cx| {
2320 let snapshot = tree.snapshot();
2321
2322 assert_eq!(
2323 snapshot.status_for_file(
2324 project_path
2325 .join(Path::new(renamed_dir_name))
2326 .join(RENAMED_FILE)
2327 ),
2328 Some(GitFileStatus::Added)
2329 );
2330 });
2331}
2332
2333#[gpui::test]
2334async fn test_propagate_git_statuses(cx: &mut TestAppContext) {
2335 init_test(cx);
2336 let fs = FakeFs::new(cx.background_executor.clone());
2337 fs.insert_tree(
2338 "/root",
2339 json!({
2340 ".git": {},
2341 "a": {
2342 "b": {
2343 "c1.txt": "",
2344 "c2.txt": "",
2345 },
2346 "d": {
2347 "e1.txt": "",
2348 "e2.txt": "",
2349 "e3.txt": "",
2350 }
2351 },
2352 "f": {
2353 "no-status.txt": ""
2354 },
2355 "g": {
2356 "h1.txt": "",
2357 "h2.txt": ""
2358 },
2359
2360 }),
2361 )
2362 .await;
2363
2364 fs.set_status_for_repo_via_git_operation(
2365 &Path::new("/root/.git"),
2366 &[
2367 (Path::new("a/b/c1.txt"), GitFileStatus::Added),
2368 (Path::new("a/d/e2.txt"), GitFileStatus::Modified),
2369 (Path::new("g/h2.txt"), GitFileStatus::Conflict),
2370 ],
2371 );
2372
2373 let tree = Worktree::local(
2374 build_client(cx),
2375 Path::new("/root"),
2376 true,
2377 fs.clone(),
2378 Default::default(),
2379 &mut cx.to_async(),
2380 )
2381 .await
2382 .unwrap();
2383
2384 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2385 .await;
2386
2387 cx.executor().run_until_parked();
2388 let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
2389
2390 check_propagated_statuses(
2391 &snapshot,
2392 &[
2393 (Path::new(""), Some(GitFileStatus::Conflict)),
2394 (Path::new("a"), Some(GitFileStatus::Modified)),
2395 (Path::new("a/b"), Some(GitFileStatus::Added)),
2396 (Path::new("a/b/c1.txt"), Some(GitFileStatus::Added)),
2397 (Path::new("a/b/c2.txt"), None),
2398 (Path::new("a/d"), Some(GitFileStatus::Modified)),
2399 (Path::new("a/d/e2.txt"), Some(GitFileStatus::Modified)),
2400 (Path::new("f"), None),
2401 (Path::new("f/no-status.txt"), None),
2402 (Path::new("g"), Some(GitFileStatus::Conflict)),
2403 (Path::new("g/h2.txt"), Some(GitFileStatus::Conflict)),
2404 ],
2405 );
2406
2407 check_propagated_statuses(
2408 &snapshot,
2409 &[
2410 (Path::new("a/b"), Some(GitFileStatus::Added)),
2411 (Path::new("a/b/c1.txt"), Some(GitFileStatus::Added)),
2412 (Path::new("a/b/c2.txt"), None),
2413 (Path::new("a/d"), Some(GitFileStatus::Modified)),
2414 (Path::new("a/d/e1.txt"), None),
2415 (Path::new("a/d/e2.txt"), Some(GitFileStatus::Modified)),
2416 (Path::new("f"), None),
2417 (Path::new("f/no-status.txt"), None),
2418 (Path::new("g"), Some(GitFileStatus::Conflict)),
2419 ],
2420 );
2421
2422 check_propagated_statuses(
2423 &snapshot,
2424 &[
2425 (Path::new("a/b/c1.txt"), Some(GitFileStatus::Added)),
2426 (Path::new("a/b/c2.txt"), None),
2427 (Path::new("a/d/e1.txt"), None),
2428 (Path::new("a/d/e2.txt"), Some(GitFileStatus::Modified)),
2429 (Path::new("f/no-status.txt"), None),
2430 ],
2431 );
2432
2433 #[track_caller]
2434 fn check_propagated_statuses(
2435 snapshot: &Snapshot,
2436 expected_statuses: &[(&Path, Option<GitFileStatus>)],
2437 ) {
2438 let mut entries = expected_statuses
2439 .iter()
2440 .map(|(path, _)| snapshot.entry_for_path(path).unwrap().clone())
2441 .collect::<Vec<_>>();
2442 snapshot.propagate_git_statuses(&mut entries);
2443 assert_eq!(
2444 entries
2445 .iter()
2446 .map(|e| (e.path.as_ref(), e.git_status))
2447 .collect::<Vec<_>>(),
2448 expected_statuses
2449 );
2450 }
2451}
2452
2453fn build_client(cx: &mut TestAppContext) -> Arc<Client> {
2454 let clock = Arc::new(FakeSystemClock::default());
2455 let http_client = FakeHttpClient::with_404_response();
2456 cx.update(|cx| Client::new(clock, http_client, cx))
2457}
2458
2459#[track_caller]
2460fn git_init(path: &Path) -> git2::Repository {
2461 git2::Repository::init(path).expect("Failed to initialize git repository")
2462}
2463
2464#[track_caller]
2465fn git_add<P: AsRef<Path>>(path: P, repo: &git2::Repository) {
2466 let path = path.as_ref();
2467 let mut index = repo.index().expect("Failed to get index");
2468 index.add_path(path).expect("Failed to add a.txt");
2469 index.write().expect("Failed to write index");
2470}
2471
2472#[track_caller]
2473fn git_remove_index(path: &Path, repo: &git2::Repository) {
2474 let mut index = repo.index().expect("Failed to get index");
2475 index.remove_path(path).expect("Failed to add a.txt");
2476 index.write().expect("Failed to write index");
2477}
2478
2479#[track_caller]
2480fn git_commit(msg: &'static str, repo: &git2::Repository) {
2481 use git2::Signature;
2482
2483 let signature = Signature::now("test", "test@zed.dev").unwrap();
2484 let oid = repo.index().unwrap().write_tree().unwrap();
2485 let tree = repo.find_tree(oid).unwrap();
2486 if let Some(head) = repo.head().ok() {
2487 let parent_obj = head.peel(git2::ObjectType::Commit).unwrap();
2488
2489 let parent_commit = parent_obj.as_commit().unwrap();
2490
2491 repo.commit(
2492 Some("HEAD"),
2493 &signature,
2494 &signature,
2495 msg,
2496 &tree,
2497 &[parent_commit],
2498 )
2499 .expect("Failed to commit with parent");
2500 } else {
2501 repo.commit(Some("HEAD"), &signature, &signature, msg, &tree, &[])
2502 .expect("Failed to commit");
2503 }
2504}
2505
2506#[track_caller]
2507fn git_stash(repo: &mut git2::Repository) {
2508 use git2::Signature;
2509
2510 let signature = Signature::now("test", "test@zed.dev").unwrap();
2511 repo.stash_save(&signature, "N/A", None)
2512 .expect("Failed to stash");
2513}
2514
2515#[track_caller]
2516fn git_reset(offset: usize, repo: &git2::Repository) {
2517 let head = repo.head().expect("Couldn't get repo head");
2518 let object = head.peel(git2::ObjectType::Commit).unwrap();
2519 let commit = object.as_commit().unwrap();
2520 let new_head = commit
2521 .parents()
2522 .inspect(|parnet| {
2523 parnet.message();
2524 })
2525 .skip(offset)
2526 .next()
2527 .expect("Not enough history");
2528 repo.reset(&new_head.as_object(), git2::ResetType::Soft, None)
2529 .expect("Could not reset");
2530}
2531
2532#[allow(dead_code)]
2533#[track_caller]
2534fn git_status(repo: &git2::Repository) -> collections::HashMap<String, git2::Status> {
2535 repo.statuses(None)
2536 .unwrap()
2537 .iter()
2538 .map(|status| (status.path().unwrap().to_string(), status.status()))
2539 .collect()
2540}
2541
2542#[track_caller]
2543fn check_worktree_entries(
2544 tree: &Worktree,
2545 expected_excluded_paths: &[&str],
2546 expected_ignored_paths: &[&str],
2547 expected_tracked_paths: &[&str],
2548) {
2549 for path in expected_excluded_paths {
2550 let entry = tree.entry_for_path(path);
2551 assert!(
2552 entry.is_none(),
2553 "expected path '{path}' to be excluded, but got entry: {entry:?}",
2554 );
2555 }
2556 for path in expected_ignored_paths {
2557 let entry = tree
2558 .entry_for_path(path)
2559 .unwrap_or_else(|| panic!("Missing entry for expected ignored path '{path}'"));
2560 assert!(
2561 entry.is_ignored,
2562 "expected path '{path}' to be ignored, but got entry: {entry:?}",
2563 );
2564 }
2565 for path in expected_tracked_paths {
2566 let entry = tree
2567 .entry_for_path(path)
2568 .unwrap_or_else(|| panic!("Missing entry for expected tracked path '{path}'"));
2569 assert!(
2570 !entry.is_ignored,
2571 "expected path '{path}' to be tracked, but got entry: {entry:?}",
2572 );
2573 }
2574}
2575
2576fn init_test(cx: &mut gpui::TestAppContext) {
2577 cx.update(|cx| {
2578 let settings_store = SettingsStore::test(cx);
2579 cx.set_global(settings_store);
2580 WorktreeSettings::register(cx);
2581 });
2582}