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