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