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