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::{FakeFs, Fs, RealFs, RemoveOptions};
9use git::{repository::GitFileStatus, GITIGNORE};
10use gpui::{BorrowAppContext, ModelContext, Task, TestAppContext};
11use http::FakeHttpClient;
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 util::{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()
581 .unwrap()
582 .load_buffer("one/node_modules/b/b1.js".as_ref(), cx)
583 })
584 .await
585 .unwrap();
586
587 tree.read_with(cx, |tree, cx| {
588 assert_eq!(
589 tree.entries(true)
590 .map(|entry| (entry.path.as_ref(), entry.is_ignored))
591 .collect::<Vec<_>>(),
592 vec![
593 (Path::new(""), false),
594 (Path::new(".gitignore"), false),
595 (Path::new("one"), false),
596 (Path::new("one/node_modules"), true),
597 (Path::new("one/node_modules/a"), true),
598 (Path::new("one/node_modules/b"), true),
599 (Path::new("one/node_modules/b/b1.js"), true),
600 (Path::new("one/node_modules/b/b2.js"), true),
601 (Path::new("one/node_modules/c"), true),
602 (Path::new("two"), false),
603 (Path::new("two/x.js"), false),
604 (Path::new("two/y.js"), false),
605 ]
606 );
607
608 assert_eq!(
609 buffer.read(cx).file().unwrap().path().as_ref(),
610 Path::new("one/node_modules/b/b1.js")
611 );
612
613 // Only the newly-expanded directories are scanned.
614 assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 2);
615 });
616
617 // Open another file in a different subdirectory of the same
618 // gitignored directory.
619 let prev_read_dir_count = fs.read_dir_call_count();
620 let buffer = tree
621 .update(cx, |tree, cx| {
622 tree.as_local_mut()
623 .unwrap()
624 .load_buffer("one/node_modules/a/a2.js".as_ref(), cx)
625 })
626 .await
627 .unwrap();
628
629 tree.read_with(cx, |tree, cx| {
630 assert_eq!(
631 tree.entries(true)
632 .map(|entry| (entry.path.as_ref(), entry.is_ignored))
633 .collect::<Vec<_>>(),
634 vec![
635 (Path::new(""), false),
636 (Path::new(".gitignore"), false),
637 (Path::new("one"), false),
638 (Path::new("one/node_modules"), true),
639 (Path::new("one/node_modules/a"), true),
640 (Path::new("one/node_modules/a/a1.js"), true),
641 (Path::new("one/node_modules/a/a2.js"), true),
642 (Path::new("one/node_modules/b"), true),
643 (Path::new("one/node_modules/b/b1.js"), true),
644 (Path::new("one/node_modules/b/b2.js"), true),
645 (Path::new("one/node_modules/c"), true),
646 (Path::new("two"), false),
647 (Path::new("two/x.js"), false),
648 (Path::new("two/y.js"), false),
649 ]
650 );
651
652 assert_eq!(
653 buffer.read(cx).file().unwrap().path().as_ref(),
654 Path::new("one/node_modules/a/a2.js")
655 );
656
657 // Only the newly-expanded directory is scanned.
658 assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 1);
659 });
660
661 // No work happens when files and directories change within an unloaded directory.
662 let prev_fs_call_count = fs.read_dir_call_count() + fs.metadata_call_count();
663 fs.create_dir("/root/one/node_modules/c/lib".as_ref())
664 .await
665 .unwrap();
666 cx.executor().run_until_parked();
667 assert_eq!(
668 fs.read_dir_call_count() + fs.metadata_call_count() - prev_fs_call_count,
669 0
670 );
671}
672
673#[gpui::test]
674async fn test_dirs_no_longer_ignored(cx: &mut TestAppContext) {
675 init_test(cx);
676 let fs = FakeFs::new(cx.background_executor.clone());
677 fs.insert_tree(
678 "/root",
679 json!({
680 ".gitignore": "node_modules\n",
681 "a": {
682 "a.js": "",
683 },
684 "b": {
685 "b.js": "",
686 },
687 "node_modules": {
688 "c": {
689 "c.js": "",
690 },
691 "d": {
692 "d.js": "",
693 "e": {
694 "e1.js": "",
695 "e2.js": "",
696 },
697 "f": {
698 "f1.js": "",
699 "f2.js": "",
700 }
701 },
702 },
703 }),
704 )
705 .await;
706
707 let tree = Worktree::local(
708 build_client(cx),
709 Path::new("/root"),
710 true,
711 fs.clone(),
712 Default::default(),
713 &mut cx.to_async(),
714 )
715 .await
716 .unwrap();
717
718 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
719 .await;
720
721 // Open a file within the gitignored directory, forcing some of its
722 // subdirectories to be read, but not all.
723 let read_dir_count_1 = fs.read_dir_call_count();
724 tree.read_with(cx, |tree, _| {
725 tree.as_local()
726 .unwrap()
727 .refresh_entries_for_paths(vec![Path::new("node_modules/d/d.js").into()])
728 })
729 .recv()
730 .await;
731
732 // Those subdirectories are now loaded.
733 tree.read_with(cx, |tree, _| {
734 assert_eq!(
735 tree.entries(true)
736 .map(|e| (e.path.as_ref(), e.is_ignored))
737 .collect::<Vec<_>>(),
738 &[
739 (Path::new(""), false),
740 (Path::new(".gitignore"), false),
741 (Path::new("a"), false),
742 (Path::new("a/a.js"), false),
743 (Path::new("b"), false),
744 (Path::new("b/b.js"), false),
745 (Path::new("node_modules"), true),
746 (Path::new("node_modules/c"), true),
747 (Path::new("node_modules/d"), true),
748 (Path::new("node_modules/d/d.js"), true),
749 (Path::new("node_modules/d/e"), true),
750 (Path::new("node_modules/d/f"), true),
751 ]
752 );
753 });
754 let read_dir_count_2 = fs.read_dir_call_count();
755 assert_eq!(read_dir_count_2 - read_dir_count_1, 2);
756
757 // Update the gitignore so that node_modules is no longer ignored,
758 // but a subdirectory is ignored
759 fs.save("/root/.gitignore".as_ref(), &"e".into(), Default::default())
760 .await
761 .unwrap();
762 cx.executor().run_until_parked();
763
764 // All of the directories that are no longer ignored are now loaded.
765 tree.read_with(cx, |tree, _| {
766 assert_eq!(
767 tree.entries(true)
768 .map(|e| (e.path.as_ref(), e.is_ignored))
769 .collect::<Vec<_>>(),
770 &[
771 (Path::new(""), false),
772 (Path::new(".gitignore"), false),
773 (Path::new("a"), false),
774 (Path::new("a/a.js"), false),
775 (Path::new("b"), false),
776 (Path::new("b/b.js"), false),
777 // This directory is no longer ignored
778 (Path::new("node_modules"), false),
779 (Path::new("node_modules/c"), false),
780 (Path::new("node_modules/c/c.js"), false),
781 (Path::new("node_modules/d"), false),
782 (Path::new("node_modules/d/d.js"), false),
783 // This subdirectory is now ignored
784 (Path::new("node_modules/d/e"), true),
785 (Path::new("node_modules/d/f"), false),
786 (Path::new("node_modules/d/f/f1.js"), false),
787 (Path::new("node_modules/d/f/f2.js"), false),
788 ]
789 );
790 });
791
792 // Each of the newly-loaded directories is scanned only once.
793 let read_dir_count_3 = fs.read_dir_call_count();
794 assert_eq!(read_dir_count_3 - read_dir_count_2, 2);
795}
796
797#[gpui::test(iterations = 10)]
798async fn test_rescan_with_gitignore(cx: &mut TestAppContext) {
799 init_test(cx);
800 cx.update(|cx| {
801 cx.update_global::<SettingsStore, _>(|store, cx| {
802 store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
803 project_settings.file_scan_exclusions = Some(Vec::new());
804 });
805 });
806 });
807 let fs = FakeFs::new(cx.background_executor.clone());
808 fs.insert_tree(
809 "/root",
810 json!({
811 ".gitignore": "ancestor-ignored-file1\nancestor-ignored-file2\n",
812 "tree": {
813 ".git": {},
814 ".gitignore": "ignored-dir\n",
815 "tracked-dir": {
816 "tracked-file1": "",
817 "ancestor-ignored-file1": "",
818 },
819 "ignored-dir": {
820 "ignored-file1": ""
821 }
822 }
823 }),
824 )
825 .await;
826
827 let tree = Worktree::local(
828 build_client(cx),
829 "/root/tree".as_ref(),
830 true,
831 fs.clone(),
832 Default::default(),
833 &mut cx.to_async(),
834 )
835 .await
836 .unwrap();
837 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
838 .await;
839
840 tree.read_with(cx, |tree, _| {
841 tree.as_local()
842 .unwrap()
843 .refresh_entries_for_paths(vec![Path::new("ignored-dir").into()])
844 })
845 .recv()
846 .await;
847
848 cx.read(|cx| {
849 let tree = tree.read(cx);
850 assert_entry_git_state(tree, "tracked-dir/tracked-file1", None, false);
851 assert_entry_git_state(tree, "tracked-dir/ancestor-ignored-file1", None, true);
852 assert_entry_git_state(tree, "ignored-dir/ignored-file1", None, true);
853 });
854
855 fs.set_status_for_repo_via_working_copy_change(
856 &Path::new("/root/tree/.git"),
857 &[(Path::new("tracked-dir/tracked-file2"), GitFileStatus::Added)],
858 );
859
860 fs.create_file(
861 "/root/tree/tracked-dir/tracked-file2".as_ref(),
862 Default::default(),
863 )
864 .await
865 .unwrap();
866 fs.create_file(
867 "/root/tree/tracked-dir/ancestor-ignored-file2".as_ref(),
868 Default::default(),
869 )
870 .await
871 .unwrap();
872 fs.create_file(
873 "/root/tree/ignored-dir/ignored-file2".as_ref(),
874 Default::default(),
875 )
876 .await
877 .unwrap();
878
879 cx.executor().run_until_parked();
880 cx.read(|cx| {
881 let tree = tree.read(cx);
882 assert_entry_git_state(
883 tree,
884 "tracked-dir/tracked-file2",
885 Some(GitFileStatus::Added),
886 false,
887 );
888 assert_entry_git_state(tree, "tracked-dir/ancestor-ignored-file2", None, true);
889 assert_entry_git_state(tree, "ignored-dir/ignored-file2", None, true);
890 assert!(tree.entry_for_path(".git").unwrap().is_ignored);
891 });
892}
893
894#[gpui::test]
895async fn test_update_gitignore(cx: &mut TestAppContext) {
896 init_test(cx);
897 let fs = FakeFs::new(cx.background_executor.clone());
898 fs.insert_tree(
899 "/root",
900 json!({
901 ".git": {},
902 ".gitignore": "*.txt\n",
903 "a.xml": "<a></a>",
904 "b.txt": "Some text"
905 }),
906 )
907 .await;
908
909 let tree = Worktree::local(
910 build_client(cx),
911 "/root".as_ref(),
912 true,
913 fs.clone(),
914 Default::default(),
915 &mut cx.to_async(),
916 )
917 .await
918 .unwrap();
919 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
920 .await;
921
922 tree.read_with(cx, |tree, _| {
923 tree.as_local()
924 .unwrap()
925 .refresh_entries_for_paths(vec![Path::new("").into()])
926 })
927 .recv()
928 .await;
929
930 cx.read(|cx| {
931 let tree = tree.read(cx);
932 assert_entry_git_state(tree, "a.xml", None, false);
933 assert_entry_git_state(tree, "b.txt", None, true);
934 });
935
936 fs.atomic_write("/root/.gitignore".into(), "*.xml".into())
937 .await
938 .unwrap();
939
940 fs.set_status_for_repo_via_working_copy_change(
941 &Path::new("/root/.git"),
942 &[(Path::new("b.txt"), GitFileStatus::Added)],
943 );
944
945 cx.executor().run_until_parked();
946 cx.read(|cx| {
947 let tree = tree.read(cx);
948 assert_entry_git_state(tree, "a.xml", None, true);
949 assert_entry_git_state(tree, "b.txt", Some(GitFileStatus::Added), false);
950 });
951}
952
953#[gpui::test]
954async fn test_write_file(cx: &mut TestAppContext) {
955 init_test(cx);
956 cx.executor().allow_parking();
957 let dir = temp_tree(json!({
958 ".git": {},
959 ".gitignore": "ignored-dir\n",
960 "tracked-dir": {},
961 "ignored-dir": {}
962 }));
963
964 let tree = Worktree::local(
965 build_client(cx),
966 dir.path(),
967 true,
968 Arc::new(RealFs::default()),
969 Default::default(),
970 &mut cx.to_async(),
971 )
972 .await
973 .unwrap();
974 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
975 .await;
976 tree.flush_fs_events(cx).await;
977
978 tree.update(cx, |tree, cx| {
979 tree.as_local().unwrap().write_file(
980 Path::new("tracked-dir/file.txt"),
981 "hello".into(),
982 Default::default(),
983 cx,
984 )
985 })
986 .await
987 .unwrap();
988 tree.update(cx, |tree, cx| {
989 tree.as_local().unwrap().write_file(
990 Path::new("ignored-dir/file.txt"),
991 "world".into(),
992 Default::default(),
993 cx,
994 )
995 })
996 .await
997 .unwrap();
998
999 tree.read_with(cx, |tree, _| {
1000 let tracked = tree.entry_for_path("tracked-dir/file.txt").unwrap();
1001 let ignored = tree.entry_for_path("ignored-dir/file.txt").unwrap();
1002 assert!(!tracked.is_ignored);
1003 assert!(ignored.is_ignored);
1004 });
1005}
1006
1007#[gpui::test]
1008async fn test_file_scan_exclusions(cx: &mut TestAppContext) {
1009 init_test(cx);
1010 cx.executor().allow_parking();
1011 let dir = temp_tree(json!({
1012 ".gitignore": "**/target\n/node_modules\n",
1013 "target": {
1014 "index": "blah2"
1015 },
1016 "node_modules": {
1017 ".DS_Store": "",
1018 "prettier": {
1019 "package.json": "{}",
1020 },
1021 },
1022 "src": {
1023 ".DS_Store": "",
1024 "foo": {
1025 "foo.rs": "mod another;\n",
1026 "another.rs": "// another",
1027 },
1028 "bar": {
1029 "bar.rs": "// bar",
1030 },
1031 "lib.rs": "mod foo;\nmod bar;\n",
1032 },
1033 ".DS_Store": "",
1034 }));
1035 cx.update(|cx| {
1036 cx.update_global::<SettingsStore, _>(|store, cx| {
1037 store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
1038 project_settings.file_scan_exclusions =
1039 Some(vec!["**/foo/**".to_string(), "**/.DS_Store".to_string()]);
1040 });
1041 });
1042 });
1043
1044 let tree = Worktree::local(
1045 build_client(cx),
1046 dir.path(),
1047 true,
1048 Arc::new(RealFs::default()),
1049 Default::default(),
1050 &mut cx.to_async(),
1051 )
1052 .await
1053 .unwrap();
1054 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1055 .await;
1056 tree.flush_fs_events(cx).await;
1057 tree.read_with(cx, |tree, _| {
1058 check_worktree_entries(
1059 tree,
1060 &[
1061 "src/foo/foo.rs",
1062 "src/foo/another.rs",
1063 "node_modules/.DS_Store",
1064 "src/.DS_Store",
1065 ".DS_Store",
1066 ],
1067 &["target", "node_modules"],
1068 &["src/lib.rs", "src/bar/bar.rs", ".gitignore"],
1069 )
1070 });
1071
1072 cx.update(|cx| {
1073 cx.update_global::<SettingsStore, _>(|store, cx| {
1074 store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
1075 project_settings.file_scan_exclusions =
1076 Some(vec!["**/node_modules/**".to_string()]);
1077 });
1078 });
1079 });
1080 tree.flush_fs_events(cx).await;
1081 cx.executor().run_until_parked();
1082 tree.read_with(cx, |tree, _| {
1083 check_worktree_entries(
1084 tree,
1085 &[
1086 "node_modules/prettier/package.json",
1087 "node_modules/.DS_Store",
1088 "node_modules",
1089 ],
1090 &["target"],
1091 &[
1092 ".gitignore",
1093 "src/lib.rs",
1094 "src/bar/bar.rs",
1095 "src/foo/foo.rs",
1096 "src/foo/another.rs",
1097 "src/.DS_Store",
1098 ".DS_Store",
1099 ],
1100 )
1101 });
1102}
1103
1104#[gpui::test]
1105async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) {
1106 init_test(cx);
1107 cx.executor().allow_parking();
1108 let dir = temp_tree(json!({
1109 ".git": {
1110 "HEAD": "ref: refs/heads/main\n",
1111 "foo": "bar",
1112 },
1113 ".gitignore": "**/target\n/node_modules\ntest_output\n",
1114 "target": {
1115 "index": "blah2"
1116 },
1117 "node_modules": {
1118 ".DS_Store": "",
1119 "prettier": {
1120 "package.json": "{}",
1121 },
1122 },
1123 "src": {
1124 ".DS_Store": "",
1125 "foo": {
1126 "foo.rs": "mod another;\n",
1127 "another.rs": "// another",
1128 },
1129 "bar": {
1130 "bar.rs": "// bar",
1131 },
1132 "lib.rs": "mod foo;\nmod bar;\n",
1133 },
1134 ".DS_Store": "",
1135 }));
1136 cx.update(|cx| {
1137 cx.update_global::<SettingsStore, _>(|store, cx| {
1138 store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
1139 project_settings.file_scan_exclusions = Some(vec![
1140 "**/.git".to_string(),
1141 "node_modules/".to_string(),
1142 "build_output".to_string(),
1143 ]);
1144 });
1145 });
1146 });
1147
1148 let tree = Worktree::local(
1149 build_client(cx),
1150 dir.path(),
1151 true,
1152 Arc::new(RealFs::default()),
1153 Default::default(),
1154 &mut cx.to_async(),
1155 )
1156 .await
1157 .unwrap();
1158 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1159 .await;
1160 tree.flush_fs_events(cx).await;
1161 tree.read_with(cx, |tree, _| {
1162 check_worktree_entries(
1163 tree,
1164 &[
1165 ".git/HEAD",
1166 ".git/foo",
1167 "node_modules",
1168 "node_modules/.DS_Store",
1169 "node_modules/prettier",
1170 "node_modules/prettier/package.json",
1171 ],
1172 &["target"],
1173 &[
1174 ".DS_Store",
1175 "src/.DS_Store",
1176 "src/lib.rs",
1177 "src/foo/foo.rs",
1178 "src/foo/another.rs",
1179 "src/bar/bar.rs",
1180 ".gitignore",
1181 ],
1182 )
1183 });
1184
1185 let new_excluded_dir = dir.path().join("build_output");
1186 let new_ignored_dir = dir.path().join("test_output");
1187 std::fs::create_dir_all(&new_excluded_dir)
1188 .unwrap_or_else(|e| panic!("Failed to create a {new_excluded_dir:?} directory: {e}"));
1189 std::fs::create_dir_all(&new_ignored_dir)
1190 .unwrap_or_else(|e| panic!("Failed to create a {new_ignored_dir:?} directory: {e}"));
1191 let node_modules_dir = dir.path().join("node_modules");
1192 let dot_git_dir = dir.path().join(".git");
1193 let src_dir = dir.path().join("src");
1194 for existing_dir in [&node_modules_dir, &dot_git_dir, &src_dir] {
1195 assert!(
1196 existing_dir.is_dir(),
1197 "Expect {existing_dir:?} to be present in the FS already"
1198 );
1199 }
1200
1201 for directory_for_new_file in [
1202 new_excluded_dir,
1203 new_ignored_dir,
1204 node_modules_dir,
1205 dot_git_dir,
1206 src_dir,
1207 ] {
1208 std::fs::write(directory_for_new_file.join("new_file"), "new file contents")
1209 .unwrap_or_else(|e| {
1210 panic!("Failed to create in {directory_for_new_file:?} a new file: {e}")
1211 });
1212 }
1213 tree.flush_fs_events(cx).await;
1214
1215 tree.read_with(cx, |tree, _| {
1216 check_worktree_entries(
1217 tree,
1218 &[
1219 ".git/HEAD",
1220 ".git/foo",
1221 ".git/new_file",
1222 "node_modules",
1223 "node_modules/.DS_Store",
1224 "node_modules/prettier",
1225 "node_modules/prettier/package.json",
1226 "node_modules/new_file",
1227 "build_output",
1228 "build_output/new_file",
1229 "test_output/new_file",
1230 ],
1231 &["target", "test_output"],
1232 &[
1233 ".DS_Store",
1234 "src/.DS_Store",
1235 "src/lib.rs",
1236 "src/foo/foo.rs",
1237 "src/foo/another.rs",
1238 "src/bar/bar.rs",
1239 "src/new_file",
1240 ".gitignore",
1241 ],
1242 )
1243 });
1244}
1245
1246#[gpui::test]
1247async fn test_fs_events_in_dot_git_worktree(cx: &mut TestAppContext) {
1248 init_test(cx);
1249 cx.executor().allow_parking();
1250 let dir = temp_tree(json!({
1251 ".git": {
1252 "HEAD": "ref: refs/heads/main\n",
1253 "foo": "foo contents",
1254 },
1255 }));
1256 let dot_git_worktree_dir = dir.path().join(".git");
1257
1258 let tree = Worktree::local(
1259 build_client(cx),
1260 dot_git_worktree_dir.clone(),
1261 true,
1262 Arc::new(RealFs::default()),
1263 Default::default(),
1264 &mut cx.to_async(),
1265 )
1266 .await
1267 .unwrap();
1268 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1269 .await;
1270 tree.flush_fs_events(cx).await;
1271 tree.read_with(cx, |tree, _| {
1272 check_worktree_entries(tree, &[], &["HEAD", "foo"], &[])
1273 });
1274
1275 std::fs::write(dot_git_worktree_dir.join("new_file"), "new file contents")
1276 .unwrap_or_else(|e| panic!("Failed to create in {dot_git_worktree_dir:?} a new file: {e}"));
1277 tree.flush_fs_events(cx).await;
1278 tree.read_with(cx, |tree, _| {
1279 check_worktree_entries(tree, &[], &["HEAD", "foo", "new_file"], &[])
1280 });
1281}
1282
1283#[gpui::test(iterations = 30)]
1284async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
1285 init_test(cx);
1286 let fs = FakeFs::new(cx.background_executor.clone());
1287 fs.insert_tree(
1288 "/root",
1289 json!({
1290 "b": {},
1291 "c": {},
1292 "d": {},
1293 }),
1294 )
1295 .await;
1296
1297 let tree = Worktree::local(
1298 build_client(cx),
1299 "/root".as_ref(),
1300 true,
1301 fs,
1302 Default::default(),
1303 &mut cx.to_async(),
1304 )
1305 .await
1306 .unwrap();
1307
1308 let snapshot1 = tree.update(cx, |tree, cx| {
1309 let tree = tree.as_local_mut().unwrap();
1310 let snapshot = Arc::new(Mutex::new(tree.snapshot()));
1311 let _ = tree.observe_updates(0, cx, {
1312 let snapshot = snapshot.clone();
1313 move |update| {
1314 snapshot.lock().apply_remote_update(update).unwrap();
1315 async { true }
1316 }
1317 });
1318 snapshot
1319 });
1320
1321 let entry = tree
1322 .update(cx, |tree, cx| {
1323 tree.as_local_mut()
1324 .unwrap()
1325 .create_entry("a/e".as_ref(), true, cx)
1326 })
1327 .await
1328 .unwrap()
1329 .unwrap();
1330 assert!(entry.is_dir());
1331
1332 cx.executor().run_until_parked();
1333 tree.read_with(cx, |tree, _| {
1334 assert_eq!(tree.entry_for_path("a/e").unwrap().kind, EntryKind::Dir);
1335 });
1336
1337 let snapshot2 = tree.update(cx, |tree, _| tree.as_local().unwrap().snapshot());
1338 assert_eq!(
1339 snapshot1.lock().entries(true).collect::<Vec<_>>(),
1340 snapshot2.entries(true).collect::<Vec<_>>()
1341 );
1342}
1343
1344#[gpui::test]
1345async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
1346 init_test(cx);
1347 cx.executor().allow_parking();
1348 let client_fake = cx.update(|cx| {
1349 Client::new(
1350 Arc::new(FakeSystemClock::default()),
1351 FakeHttpClient::with_404_response(),
1352 cx,
1353 )
1354 });
1355
1356 let fs_fake = FakeFs::new(cx.background_executor.clone());
1357 fs_fake
1358 .insert_tree(
1359 "/root",
1360 json!({
1361 "a": {},
1362 }),
1363 )
1364 .await;
1365
1366 let tree_fake = Worktree::local(
1367 client_fake,
1368 "/root".as_ref(),
1369 true,
1370 fs_fake,
1371 Default::default(),
1372 &mut cx.to_async(),
1373 )
1374 .await
1375 .unwrap();
1376
1377 let entry = tree_fake
1378 .update(cx, |tree, cx| {
1379 tree.as_local_mut()
1380 .unwrap()
1381 .create_entry("a/b/c/d.txt".as_ref(), false, cx)
1382 })
1383 .await
1384 .unwrap()
1385 .unwrap();
1386 assert!(entry.is_file());
1387
1388 cx.executor().run_until_parked();
1389 tree_fake.read_with(cx, |tree, _| {
1390 assert!(tree.entry_for_path("a/b/c/d.txt").unwrap().is_file());
1391 assert!(tree.entry_for_path("a/b/c/").unwrap().is_dir());
1392 assert!(tree.entry_for_path("a/b/").unwrap().is_dir());
1393 });
1394
1395 let client_real = cx.update(|cx| {
1396 Client::new(
1397 Arc::new(FakeSystemClock::default()),
1398 FakeHttpClient::with_404_response(),
1399 cx,
1400 )
1401 });
1402
1403 let fs_real = Arc::new(RealFs::default());
1404 let temp_root = temp_tree(json!({
1405 "a": {}
1406 }));
1407
1408 let tree_real = Worktree::local(
1409 client_real,
1410 temp_root.path(),
1411 true,
1412 fs_real,
1413 Default::default(),
1414 &mut cx.to_async(),
1415 )
1416 .await
1417 .unwrap();
1418
1419 let entry = tree_real
1420 .update(cx, |tree, cx| {
1421 tree.as_local_mut()
1422 .unwrap()
1423 .create_entry("a/b/c/d.txt".as_ref(), false, cx)
1424 })
1425 .await
1426 .unwrap()
1427 .unwrap();
1428 assert!(entry.is_file());
1429
1430 cx.executor().run_until_parked();
1431 tree_real.read_with(cx, |tree, _| {
1432 assert!(tree.entry_for_path("a/b/c/d.txt").unwrap().is_file());
1433 assert!(tree.entry_for_path("a/b/c/").unwrap().is_dir());
1434 assert!(tree.entry_for_path("a/b/").unwrap().is_dir());
1435 });
1436
1437 // Test smallest change
1438 let entry = tree_real
1439 .update(cx, |tree, cx| {
1440 tree.as_local_mut()
1441 .unwrap()
1442 .create_entry("a/b/c/e.txt".as_ref(), false, cx)
1443 })
1444 .await
1445 .unwrap()
1446 .unwrap();
1447 assert!(entry.is_file());
1448
1449 cx.executor().run_until_parked();
1450 tree_real.read_with(cx, |tree, _| {
1451 assert!(tree.entry_for_path("a/b/c/e.txt").unwrap().is_file());
1452 });
1453
1454 // Test largest change
1455 let entry = tree_real
1456 .update(cx, |tree, cx| {
1457 tree.as_local_mut()
1458 .unwrap()
1459 .create_entry("d/e/f/g.txt".as_ref(), false, cx)
1460 })
1461 .await
1462 .unwrap()
1463 .unwrap();
1464 assert!(entry.is_file());
1465
1466 cx.executor().run_until_parked();
1467 tree_real.read_with(cx, |tree, _| {
1468 assert!(tree.entry_for_path("d/e/f/g.txt").unwrap().is_file());
1469 assert!(tree.entry_for_path("d/e/f").unwrap().is_dir());
1470 assert!(tree.entry_for_path("d/e/").unwrap().is_dir());
1471 assert!(tree.entry_for_path("d/").unwrap().is_dir());
1472 });
1473}
1474
1475#[gpui::test(iterations = 100)]
1476async fn test_random_worktree_operations_during_initial_scan(
1477 cx: &mut TestAppContext,
1478 mut rng: StdRng,
1479) {
1480 init_test(cx);
1481 let operations = env::var("OPERATIONS")
1482 .map(|o| o.parse().unwrap())
1483 .unwrap_or(5);
1484 let initial_entries = env::var("INITIAL_ENTRIES")
1485 .map(|o| o.parse().unwrap())
1486 .unwrap_or(20);
1487
1488 let root_dir = Path::new("/test");
1489 let fs = FakeFs::new(cx.background_executor.clone()) as Arc<dyn Fs>;
1490 fs.as_fake().insert_tree(root_dir, json!({})).await;
1491 for _ in 0..initial_entries {
1492 randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
1493 }
1494 log::info!("generated initial tree");
1495
1496 let worktree = Worktree::local(
1497 build_client(cx),
1498 root_dir,
1499 true,
1500 fs.clone(),
1501 Default::default(),
1502 &mut cx.to_async(),
1503 )
1504 .await
1505 .unwrap();
1506
1507 let mut snapshots = vec![worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot())];
1508 let updates = Arc::new(Mutex::new(Vec::new()));
1509 worktree.update(cx, |tree, cx| {
1510 check_worktree_change_events(tree, cx);
1511
1512 let _ = tree.as_local_mut().unwrap().observe_updates(0, cx, {
1513 let updates = updates.clone();
1514 move |update| {
1515 updates.lock().push(update);
1516 async { true }
1517 }
1518 });
1519 });
1520
1521 for _ in 0..operations {
1522 worktree
1523 .update(cx, |worktree, cx| {
1524 randomly_mutate_worktree(worktree, &mut rng, cx)
1525 })
1526 .await
1527 .log_err();
1528 worktree.read_with(cx, |tree, _| {
1529 tree.as_local().unwrap().snapshot().check_invariants(true)
1530 });
1531
1532 if rng.gen_bool(0.6) {
1533 snapshots.push(worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot()));
1534 }
1535 }
1536
1537 worktree
1538 .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
1539 .await;
1540
1541 cx.executor().run_until_parked();
1542
1543 let final_snapshot = worktree.read_with(cx, |tree, _| {
1544 let tree = tree.as_local().unwrap();
1545 let snapshot = tree.snapshot();
1546 snapshot.check_invariants(true);
1547 snapshot
1548 });
1549
1550 for (i, snapshot) in snapshots.into_iter().enumerate().rev() {
1551 let mut updated_snapshot = snapshot.clone();
1552 for update in updates.lock().iter() {
1553 if update.scan_id >= updated_snapshot.scan_id() as u64 {
1554 updated_snapshot
1555 .apply_remote_update(update.clone())
1556 .unwrap();
1557 }
1558 }
1559
1560 assert_eq!(
1561 updated_snapshot.entries(true).collect::<Vec<_>>(),
1562 final_snapshot.entries(true).collect::<Vec<_>>(),
1563 "wrong updates after snapshot {i}: {snapshot:#?} {updates:#?}",
1564 );
1565 }
1566}
1567
1568#[gpui::test(iterations = 100)]
1569async fn test_random_worktree_changes(cx: &mut TestAppContext, mut rng: StdRng) {
1570 init_test(cx);
1571 let operations = env::var("OPERATIONS")
1572 .map(|o| o.parse().unwrap())
1573 .unwrap_or(40);
1574 let initial_entries = env::var("INITIAL_ENTRIES")
1575 .map(|o| o.parse().unwrap())
1576 .unwrap_or(20);
1577
1578 let root_dir = Path::new("/test");
1579 let fs = FakeFs::new(cx.background_executor.clone()) as Arc<dyn Fs>;
1580 fs.as_fake().insert_tree(root_dir, json!({})).await;
1581 for _ in 0..initial_entries {
1582 randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
1583 }
1584 log::info!("generated initial tree");
1585
1586 let worktree = Worktree::local(
1587 build_client(cx),
1588 root_dir,
1589 true,
1590 fs.clone(),
1591 Default::default(),
1592 &mut cx.to_async(),
1593 )
1594 .await
1595 .unwrap();
1596
1597 let updates = Arc::new(Mutex::new(Vec::new()));
1598 worktree.update(cx, |tree, cx| {
1599 check_worktree_change_events(tree, cx);
1600
1601 let _ = tree.as_local_mut().unwrap().observe_updates(0, cx, {
1602 let updates = updates.clone();
1603 move |update| {
1604 updates.lock().push(update);
1605 async { true }
1606 }
1607 });
1608 });
1609
1610 worktree
1611 .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
1612 .await;
1613
1614 fs.as_fake().pause_events();
1615 let mut snapshots = Vec::new();
1616 let mut mutations_len = operations;
1617 while mutations_len > 1 {
1618 if rng.gen_bool(0.2) {
1619 worktree
1620 .update(cx, |worktree, cx| {
1621 randomly_mutate_worktree(worktree, &mut rng, cx)
1622 })
1623 .await
1624 .log_err();
1625 } else {
1626 randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
1627 }
1628
1629 let buffered_event_count = fs.as_fake().buffered_event_count();
1630 if buffered_event_count > 0 && rng.gen_bool(0.3) {
1631 let len = rng.gen_range(0..=buffered_event_count);
1632 log::info!("flushing {} events", len);
1633 fs.as_fake().flush_events(len);
1634 } else {
1635 randomly_mutate_fs(&fs, root_dir, 0.6, &mut rng).await;
1636 mutations_len -= 1;
1637 }
1638
1639 cx.executor().run_until_parked();
1640 if rng.gen_bool(0.2) {
1641 log::info!("storing snapshot {}", snapshots.len());
1642 let snapshot = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
1643 snapshots.push(snapshot);
1644 }
1645 }
1646
1647 log::info!("quiescing");
1648 fs.as_fake().flush_events(usize::MAX);
1649 cx.executor().run_until_parked();
1650
1651 let snapshot = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
1652 snapshot.check_invariants(true);
1653 let expanded_paths = snapshot
1654 .expanded_entries()
1655 .map(|e| e.path.clone())
1656 .collect::<Vec<_>>();
1657
1658 {
1659 let new_worktree = Worktree::local(
1660 build_client(cx),
1661 root_dir,
1662 true,
1663 fs.clone(),
1664 Default::default(),
1665 &mut cx.to_async(),
1666 )
1667 .await
1668 .unwrap();
1669 new_worktree
1670 .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
1671 .await;
1672 new_worktree
1673 .update(cx, |tree, _| {
1674 tree.as_local_mut()
1675 .unwrap()
1676 .refresh_entries_for_paths(expanded_paths)
1677 })
1678 .recv()
1679 .await;
1680 let new_snapshot =
1681 new_worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
1682 assert_eq!(
1683 snapshot.entries_without_ids(true),
1684 new_snapshot.entries_without_ids(true)
1685 );
1686 }
1687
1688 for (i, mut prev_snapshot) in snapshots.into_iter().enumerate().rev() {
1689 for update in updates.lock().iter() {
1690 if update.scan_id >= prev_snapshot.scan_id() as u64 {
1691 prev_snapshot.apply_remote_update(update.clone()).unwrap();
1692 }
1693 }
1694
1695 assert_eq!(
1696 prev_snapshot
1697 .entries(true)
1698 .map(ignore_pending_dir)
1699 .collect::<Vec<_>>(),
1700 snapshot
1701 .entries(true)
1702 .map(ignore_pending_dir)
1703 .collect::<Vec<_>>(),
1704 "wrong updates after snapshot {i}: {updates:#?}",
1705 );
1706 }
1707
1708 fn ignore_pending_dir(entry: &Entry) -> Entry {
1709 let mut entry = entry.clone();
1710 if entry.kind.is_dir() {
1711 entry.kind = EntryKind::Dir
1712 }
1713 entry
1714 }
1715}
1716
1717// The worktree's `UpdatedEntries` event can be used to follow along with
1718// all changes to the worktree's snapshot.
1719fn check_worktree_change_events(tree: &mut Worktree, cx: &mut ModelContext<Worktree>) {
1720 let mut entries = tree.entries(true).cloned().collect::<Vec<_>>();
1721 cx.subscribe(&cx.handle(), move |tree, _, event, _| {
1722 if let Event::UpdatedEntries(changes) = event {
1723 for (path, _, change_type) in changes.iter() {
1724 let entry = tree.entry_for_path(&path).cloned();
1725 let ix = match entries.binary_search_by_key(&path, |e| &e.path) {
1726 Ok(ix) | Err(ix) => ix,
1727 };
1728 match change_type {
1729 PathChange::Added => entries.insert(ix, entry.unwrap()),
1730 PathChange::Removed => drop(entries.remove(ix)),
1731 PathChange::Updated => {
1732 let entry = entry.unwrap();
1733 let existing_entry = entries.get_mut(ix).unwrap();
1734 assert_eq!(existing_entry.path, entry.path);
1735 *existing_entry = entry;
1736 }
1737 PathChange::AddedOrUpdated | PathChange::Loaded => {
1738 let entry = entry.unwrap();
1739 if entries.get(ix).map(|e| &e.path) == Some(&entry.path) {
1740 *entries.get_mut(ix).unwrap() = entry;
1741 } else {
1742 entries.insert(ix, entry);
1743 }
1744 }
1745 }
1746 }
1747
1748 let new_entries = tree.entries(true).cloned().collect::<Vec<_>>();
1749 assert_eq!(entries, new_entries, "incorrect changes: {:?}", changes);
1750 }
1751 })
1752 .detach();
1753}
1754
1755fn randomly_mutate_worktree(
1756 worktree: &mut Worktree,
1757 rng: &mut impl Rng,
1758 cx: &mut ModelContext<Worktree>,
1759) -> Task<Result<()>> {
1760 log::info!("mutating worktree");
1761 let worktree = worktree.as_local_mut().unwrap();
1762 let snapshot = worktree.snapshot();
1763 let entry = snapshot.entries(false).choose(rng).unwrap();
1764
1765 match rng.gen_range(0_u32..100) {
1766 0..=33 if entry.path.as_ref() != Path::new("") => {
1767 log::info!("deleting entry {:?} ({})", entry.path, entry.id.0);
1768 worktree.delete_entry(entry.id, false, cx).unwrap()
1769 }
1770 ..=66 if entry.path.as_ref() != Path::new("") => {
1771 let other_entry = snapshot.entries(false).choose(rng).unwrap();
1772 let new_parent_path = if other_entry.is_dir() {
1773 other_entry.path.clone()
1774 } else {
1775 other_entry.path.parent().unwrap().into()
1776 };
1777 let mut new_path = new_parent_path.join(random_filename(rng));
1778 if new_path.starts_with(&entry.path) {
1779 new_path = random_filename(rng).into();
1780 }
1781
1782 log::info!(
1783 "renaming entry {:?} ({}) to {:?}",
1784 entry.path,
1785 entry.id.0,
1786 new_path
1787 );
1788 let task = worktree.rename_entry(entry.id, new_path, cx);
1789 cx.background_executor().spawn(async move {
1790 task.await?.unwrap();
1791 Ok(())
1792 })
1793 }
1794 _ => {
1795 if entry.is_dir() {
1796 let child_path = entry.path.join(random_filename(rng));
1797 let is_dir = rng.gen_bool(0.3);
1798 log::info!(
1799 "creating {} at {:?}",
1800 if is_dir { "dir" } else { "file" },
1801 child_path,
1802 );
1803 let task = worktree.create_entry(child_path, is_dir, cx);
1804 cx.background_executor().spawn(async move {
1805 task.await?;
1806 Ok(())
1807 })
1808 } else {
1809 log::info!("overwriting file {:?} ({})", entry.path, entry.id.0);
1810 let task =
1811 worktree.write_file(entry.path.clone(), "".into(), Default::default(), cx);
1812 cx.background_executor().spawn(async move {
1813 task.await?;
1814 Ok(())
1815 })
1816 }
1817 }
1818 }
1819}
1820
1821async fn randomly_mutate_fs(
1822 fs: &Arc<dyn Fs>,
1823 root_path: &Path,
1824 insertion_probability: f64,
1825 rng: &mut impl Rng,
1826) {
1827 log::info!("mutating fs");
1828 let mut files = Vec::new();
1829 let mut dirs = Vec::new();
1830 for path in fs.as_fake().paths(false) {
1831 if path.starts_with(root_path) {
1832 if fs.is_file(&path).await {
1833 files.push(path);
1834 } else {
1835 dirs.push(path);
1836 }
1837 }
1838 }
1839
1840 if (files.is_empty() && dirs.len() == 1) || rng.gen_bool(insertion_probability) {
1841 let path = dirs.choose(rng).unwrap();
1842 let new_path = path.join(random_filename(rng));
1843
1844 if rng.gen() {
1845 log::info!(
1846 "creating dir {:?}",
1847 new_path.strip_prefix(root_path).unwrap()
1848 );
1849 fs.create_dir(&new_path).await.unwrap();
1850 } else {
1851 log::info!(
1852 "creating file {:?}",
1853 new_path.strip_prefix(root_path).unwrap()
1854 );
1855 fs.create_file(&new_path, Default::default()).await.unwrap();
1856 }
1857 } else if rng.gen_bool(0.05) {
1858 let ignore_dir_path = dirs.choose(rng).unwrap();
1859 let ignore_path = ignore_dir_path.join(&*GITIGNORE);
1860
1861 let subdirs = dirs
1862 .iter()
1863 .filter(|d| d.starts_with(&ignore_dir_path))
1864 .cloned()
1865 .collect::<Vec<_>>();
1866 let subfiles = files
1867 .iter()
1868 .filter(|d| d.starts_with(&ignore_dir_path))
1869 .cloned()
1870 .collect::<Vec<_>>();
1871 let files_to_ignore = {
1872 let len = rng.gen_range(0..=subfiles.len());
1873 subfiles.choose_multiple(rng, len)
1874 };
1875 let dirs_to_ignore = {
1876 let len = rng.gen_range(0..subdirs.len());
1877 subdirs.choose_multiple(rng, len)
1878 };
1879
1880 let mut ignore_contents = String::new();
1881 for path_to_ignore in files_to_ignore.chain(dirs_to_ignore) {
1882 writeln!(
1883 ignore_contents,
1884 "{}",
1885 path_to_ignore
1886 .strip_prefix(&ignore_dir_path)
1887 .unwrap()
1888 .to_str()
1889 .unwrap()
1890 )
1891 .unwrap();
1892 }
1893 log::info!(
1894 "creating gitignore {:?} with contents:\n{}",
1895 ignore_path.strip_prefix(&root_path).unwrap(),
1896 ignore_contents
1897 );
1898 fs.save(
1899 &ignore_path,
1900 &ignore_contents.as_str().into(),
1901 Default::default(),
1902 )
1903 .await
1904 .unwrap();
1905 } else {
1906 let old_path = {
1907 let file_path = files.choose(rng);
1908 let dir_path = dirs[1..].choose(rng);
1909 file_path.into_iter().chain(dir_path).choose(rng).unwrap()
1910 };
1911
1912 let is_rename = rng.gen();
1913 if is_rename {
1914 let new_path_parent = dirs
1915 .iter()
1916 .filter(|d| !d.starts_with(old_path))
1917 .choose(rng)
1918 .unwrap();
1919
1920 let overwrite_existing_dir =
1921 !old_path.starts_with(&new_path_parent) && rng.gen_bool(0.3);
1922 let new_path = if overwrite_existing_dir {
1923 fs.remove_dir(
1924 &new_path_parent,
1925 RemoveOptions {
1926 recursive: true,
1927 ignore_if_not_exists: true,
1928 },
1929 )
1930 .await
1931 .unwrap();
1932 new_path_parent.to_path_buf()
1933 } else {
1934 new_path_parent.join(random_filename(rng))
1935 };
1936
1937 log::info!(
1938 "renaming {:?} to {}{:?}",
1939 old_path.strip_prefix(&root_path).unwrap(),
1940 if overwrite_existing_dir {
1941 "overwrite "
1942 } else {
1943 ""
1944 },
1945 new_path.strip_prefix(&root_path).unwrap()
1946 );
1947 fs.rename(
1948 &old_path,
1949 &new_path,
1950 fs::RenameOptions {
1951 overwrite: true,
1952 ignore_if_exists: true,
1953 },
1954 )
1955 .await
1956 .unwrap();
1957 } else if fs.is_file(&old_path).await {
1958 log::info!(
1959 "deleting file {:?}",
1960 old_path.strip_prefix(&root_path).unwrap()
1961 );
1962 fs.remove_file(old_path, Default::default()).await.unwrap();
1963 } else {
1964 log::info!(
1965 "deleting dir {:?}",
1966 old_path.strip_prefix(&root_path).unwrap()
1967 );
1968 fs.remove_dir(
1969 &old_path,
1970 RemoveOptions {
1971 recursive: true,
1972 ignore_if_not_exists: true,
1973 },
1974 )
1975 .await
1976 .unwrap();
1977 }
1978 }
1979}
1980
1981fn random_filename(rng: &mut impl Rng) -> String {
1982 (0..6)
1983 .map(|_| rng.sample(rand::distributions::Alphanumeric))
1984 .map(char::from)
1985 .collect()
1986}
1987
1988#[gpui::test]
1989async fn test_rename_work_directory(cx: &mut TestAppContext) {
1990 init_test(cx);
1991 cx.executor().allow_parking();
1992 let root = temp_tree(json!({
1993 "projects": {
1994 "project1": {
1995 "a": "",
1996 "b": "",
1997 }
1998 },
1999
2000 }));
2001 let root_path = root.path();
2002
2003 let tree = Worktree::local(
2004 build_client(cx),
2005 root_path,
2006 true,
2007 Arc::new(RealFs::default()),
2008 Default::default(),
2009 &mut cx.to_async(),
2010 )
2011 .await
2012 .unwrap();
2013
2014 let repo = git_init(&root_path.join("projects/project1"));
2015 git_add("a", &repo);
2016 git_commit("init", &repo);
2017 std::fs::write(root_path.join("projects/project1/a"), "aa").ok();
2018
2019 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2020 .await;
2021
2022 tree.flush_fs_events(cx).await;
2023
2024 cx.read(|cx| {
2025 let tree = tree.read(cx);
2026 let (work_dir, _) = tree.repositories().next().unwrap();
2027 assert_eq!(work_dir.as_ref(), Path::new("projects/project1"));
2028 assert_eq!(
2029 tree.status_for_file(Path::new("projects/project1/a")),
2030 Some(GitFileStatus::Modified)
2031 );
2032 assert_eq!(
2033 tree.status_for_file(Path::new("projects/project1/b")),
2034 Some(GitFileStatus::Added)
2035 );
2036 });
2037
2038 std::fs::rename(
2039 root_path.join("projects/project1"),
2040 root_path.join("projects/project2"),
2041 )
2042 .ok();
2043 tree.flush_fs_events(cx).await;
2044
2045 cx.read(|cx| {
2046 let tree = tree.read(cx);
2047 let (work_dir, _) = tree.repositories().next().unwrap();
2048 assert_eq!(work_dir.as_ref(), Path::new("projects/project2"));
2049 assert_eq!(
2050 tree.status_for_file(Path::new("projects/project2/a")),
2051 Some(GitFileStatus::Modified)
2052 );
2053 assert_eq!(
2054 tree.status_for_file(Path::new("projects/project2/b")),
2055 Some(GitFileStatus::Added)
2056 );
2057 });
2058}
2059
2060#[gpui::test]
2061async fn test_git_repository_for_path(cx: &mut TestAppContext) {
2062 init_test(cx);
2063 cx.executor().allow_parking();
2064 let root = temp_tree(json!({
2065 "c.txt": "",
2066 "dir1": {
2067 ".git": {},
2068 "deps": {
2069 "dep1": {
2070 ".git": {},
2071 "src": {
2072 "a.txt": ""
2073 }
2074 }
2075 },
2076 "src": {
2077 "b.txt": ""
2078 }
2079 },
2080 }));
2081
2082 let tree = Worktree::local(
2083 build_client(cx),
2084 root.path(),
2085 true,
2086 Arc::new(RealFs::default()),
2087 Default::default(),
2088 &mut cx.to_async(),
2089 )
2090 .await
2091 .unwrap();
2092
2093 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2094 .await;
2095 tree.flush_fs_events(cx).await;
2096
2097 tree.read_with(cx, |tree, _cx| {
2098 let tree = tree.as_local().unwrap();
2099
2100 assert!(tree.repository_for_path("c.txt".as_ref()).is_none());
2101
2102 let entry = tree.repository_for_path("dir1/src/b.txt".as_ref()).unwrap();
2103 assert_eq!(
2104 entry
2105 .work_directory(tree)
2106 .map(|directory| directory.as_ref().to_owned()),
2107 Some(Path::new("dir1").to_owned())
2108 );
2109
2110 let entry = tree
2111 .repository_for_path("dir1/deps/dep1/src/a.txt".as_ref())
2112 .unwrap();
2113 assert_eq!(
2114 entry
2115 .work_directory(tree)
2116 .map(|directory| directory.as_ref().to_owned()),
2117 Some(Path::new("dir1/deps/dep1").to_owned())
2118 );
2119
2120 let entries = tree.files(false, 0);
2121
2122 let paths_with_repos = tree
2123 .entries_with_repositories(entries)
2124 .map(|(entry, repo)| {
2125 (
2126 entry.path.as_ref(),
2127 repo.and_then(|repo| {
2128 repo.work_directory(&tree)
2129 .map(|work_directory| work_directory.0.to_path_buf())
2130 }),
2131 )
2132 })
2133 .collect::<Vec<_>>();
2134
2135 assert_eq!(
2136 paths_with_repos,
2137 &[
2138 (Path::new("c.txt"), None),
2139 (
2140 Path::new("dir1/deps/dep1/src/a.txt"),
2141 Some(Path::new("dir1/deps/dep1").into())
2142 ),
2143 (Path::new("dir1/src/b.txt"), Some(Path::new("dir1").into())),
2144 ]
2145 );
2146 });
2147
2148 let repo_update_events = Arc::new(Mutex::new(vec![]));
2149 tree.update(cx, |_, cx| {
2150 let repo_update_events = repo_update_events.clone();
2151 cx.subscribe(&tree, move |_, _, event, _| {
2152 if let Event::UpdatedGitRepositories(update) = event {
2153 repo_update_events.lock().push(update.clone());
2154 }
2155 })
2156 .detach();
2157 });
2158
2159 std::fs::write(root.path().join("dir1/.git/random_new_file"), "hello").unwrap();
2160 tree.flush_fs_events(cx).await;
2161
2162 assert_eq!(
2163 repo_update_events.lock()[0]
2164 .iter()
2165 .map(|e| e.0.clone())
2166 .collect::<Vec<Arc<Path>>>(),
2167 vec![Path::new("dir1").into()]
2168 );
2169
2170 std::fs::remove_dir_all(root.path().join("dir1/.git")).unwrap();
2171 tree.flush_fs_events(cx).await;
2172
2173 tree.read_with(cx, |tree, _cx| {
2174 let tree = tree.as_local().unwrap();
2175
2176 assert!(tree
2177 .repository_for_path("dir1/src/b.txt".as_ref())
2178 .is_none());
2179 });
2180}
2181
2182#[gpui::test]
2183async fn test_git_status(cx: &mut TestAppContext) {
2184 init_test(cx);
2185 cx.executor().allow_parking();
2186 const IGNORE_RULE: &str = "**/target";
2187
2188 let root = temp_tree(json!({
2189 "project": {
2190 "a.txt": "a",
2191 "b.txt": "bb",
2192 "c": {
2193 "d": {
2194 "e.txt": "eee"
2195 }
2196 },
2197 "f.txt": "ffff",
2198 "target": {
2199 "build_file": "???"
2200 },
2201 ".gitignore": IGNORE_RULE
2202 },
2203
2204 }));
2205
2206 const A_TXT: &str = "a.txt";
2207 const B_TXT: &str = "b.txt";
2208 const E_TXT: &str = "c/d/e.txt";
2209 const F_TXT: &str = "f.txt";
2210 const DOTGITIGNORE: &str = ".gitignore";
2211 const BUILD_FILE: &str = "target/build_file";
2212 let project_path = Path::new("project");
2213
2214 // Set up git repository before creating the worktree.
2215 let work_dir = root.path().join("project");
2216 let mut repo = git_init(work_dir.as_path());
2217 repo.add_ignore_rule(IGNORE_RULE).unwrap();
2218 git_add(A_TXT, &repo);
2219 git_add(E_TXT, &repo);
2220 git_add(DOTGITIGNORE, &repo);
2221 git_commit("Initial commit", &repo);
2222
2223 let tree = Worktree::local(
2224 build_client(cx),
2225 root.path(),
2226 true,
2227 Arc::new(RealFs::default()),
2228 Default::default(),
2229 &mut cx.to_async(),
2230 )
2231 .await
2232 .unwrap();
2233
2234 tree.flush_fs_events(cx).await;
2235 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2236 .await;
2237 cx.executor().run_until_parked();
2238
2239 // Check that the right git state is observed on startup
2240 tree.read_with(cx, |tree, _cx| {
2241 let snapshot = tree.snapshot();
2242 assert_eq!(snapshot.repositories().count(), 1);
2243 let (dir, repo_entry) = snapshot.repositories().next().unwrap();
2244 assert_eq!(dir.as_ref(), Path::new("project"));
2245 assert!(repo_entry.location_in_repo.is_none());
2246
2247 assert_eq!(
2248 snapshot.status_for_file(project_path.join(B_TXT)),
2249 Some(GitFileStatus::Added)
2250 );
2251 assert_eq!(
2252 snapshot.status_for_file(project_path.join(F_TXT)),
2253 Some(GitFileStatus::Added)
2254 );
2255 });
2256
2257 // Modify a file in the working copy.
2258 std::fs::write(work_dir.join(A_TXT), "aa").unwrap();
2259 tree.flush_fs_events(cx).await;
2260 cx.executor().run_until_parked();
2261
2262 // The worktree detects that the file's git status has changed.
2263 tree.read_with(cx, |tree, _cx| {
2264 let snapshot = tree.snapshot();
2265 assert_eq!(
2266 snapshot.status_for_file(project_path.join(A_TXT)),
2267 Some(GitFileStatus::Modified)
2268 );
2269 });
2270
2271 // Create a commit in the git repository.
2272 git_add(A_TXT, &repo);
2273 git_add(B_TXT, &repo);
2274 git_commit("Committing modified and added", &repo);
2275 tree.flush_fs_events(cx).await;
2276 cx.executor().run_until_parked();
2277
2278 // The worktree detects that the files' git status have changed.
2279 tree.read_with(cx, |tree, _cx| {
2280 let snapshot = tree.snapshot();
2281 assert_eq!(
2282 snapshot.status_for_file(project_path.join(F_TXT)),
2283 Some(GitFileStatus::Added)
2284 );
2285 assert_eq!(snapshot.status_for_file(project_path.join(B_TXT)), None);
2286 assert_eq!(snapshot.status_for_file(project_path.join(A_TXT)), None);
2287 });
2288
2289 // Modify files in the working copy and perform git operations on other files.
2290 git_reset(0, &repo);
2291 git_remove_index(Path::new(B_TXT), &repo);
2292 git_stash(&mut repo);
2293 std::fs::write(work_dir.join(E_TXT), "eeee").unwrap();
2294 std::fs::write(work_dir.join(BUILD_FILE), "this should be ignored").unwrap();
2295 tree.flush_fs_events(cx).await;
2296 cx.executor().run_until_parked();
2297
2298 // Check that more complex repo changes are tracked
2299 tree.read_with(cx, |tree, _cx| {
2300 let snapshot = tree.snapshot();
2301
2302 assert_eq!(snapshot.status_for_file(project_path.join(A_TXT)), None);
2303 assert_eq!(
2304 snapshot.status_for_file(project_path.join(B_TXT)),
2305 Some(GitFileStatus::Added)
2306 );
2307 assert_eq!(
2308 snapshot.status_for_file(project_path.join(E_TXT)),
2309 Some(GitFileStatus::Modified)
2310 );
2311 });
2312
2313 std::fs::remove_file(work_dir.join(B_TXT)).unwrap();
2314 std::fs::remove_dir_all(work_dir.join("c")).unwrap();
2315 std::fs::write(
2316 work_dir.join(DOTGITIGNORE),
2317 [IGNORE_RULE, "f.txt"].join("\n"),
2318 )
2319 .unwrap();
2320
2321 git_add(Path::new(DOTGITIGNORE), &repo);
2322 git_commit("Committing modified git ignore", &repo);
2323
2324 tree.flush_fs_events(cx).await;
2325 cx.executor().run_until_parked();
2326
2327 let mut renamed_dir_name = "first_directory/second_directory";
2328 const RENAMED_FILE: &str = "rf.txt";
2329
2330 std::fs::create_dir_all(work_dir.join(renamed_dir_name)).unwrap();
2331 std::fs::write(
2332 work_dir.join(renamed_dir_name).join(RENAMED_FILE),
2333 "new-contents",
2334 )
2335 .unwrap();
2336
2337 tree.flush_fs_events(cx).await;
2338 cx.executor().run_until_parked();
2339
2340 tree.read_with(cx, |tree, _cx| {
2341 let snapshot = tree.snapshot();
2342 assert_eq!(
2343 snapshot.status_for_file(&project_path.join(renamed_dir_name).join(RENAMED_FILE)),
2344 Some(GitFileStatus::Added)
2345 );
2346 });
2347
2348 renamed_dir_name = "new_first_directory/second_directory";
2349
2350 std::fs::rename(
2351 work_dir.join("first_directory"),
2352 work_dir.join("new_first_directory"),
2353 )
2354 .unwrap();
2355
2356 tree.flush_fs_events(cx).await;
2357 cx.executor().run_until_parked();
2358
2359 tree.read_with(cx, |tree, _cx| {
2360 let snapshot = tree.snapshot();
2361
2362 assert_eq!(
2363 snapshot.status_for_file(
2364 project_path
2365 .join(Path::new(renamed_dir_name))
2366 .join(RENAMED_FILE)
2367 ),
2368 Some(GitFileStatus::Added)
2369 );
2370 });
2371}
2372
2373#[gpui::test]
2374async fn test_repository_subfolder_git_status(cx: &mut TestAppContext) {
2375 init_test(cx);
2376 cx.executor().allow_parking();
2377
2378 let root = temp_tree(json!({
2379 "my-repo": {
2380 // .git folder will go here
2381 "a.txt": "a",
2382 "sub-folder-1": {
2383 "sub-folder-2": {
2384 "c.txt": "cc",
2385 "d": {
2386 "e.txt": "eee"
2387 }
2388 },
2389 }
2390 },
2391
2392 }));
2393
2394 const C_TXT: &str = "sub-folder-1/sub-folder-2/c.txt";
2395 const E_TXT: &str = "sub-folder-1/sub-folder-2/d/e.txt";
2396
2397 // Set up git repository before creating the worktree.
2398 let git_repo_work_dir = root.path().join("my-repo");
2399 let repo = git_init(git_repo_work_dir.as_path());
2400 git_add(C_TXT, &repo);
2401 git_commit("Initial commit", &repo);
2402
2403 // Open the worktree in subfolder
2404 let project_root = Path::new("my-repo/sub-folder-1/sub-folder-2");
2405 let tree = Worktree::local(
2406 build_client(cx),
2407 root.path().join(project_root),
2408 true,
2409 Arc::new(RealFs::default()),
2410 Default::default(),
2411 &mut cx.to_async(),
2412 )
2413 .await
2414 .unwrap();
2415
2416 tree.flush_fs_events(cx).await;
2417 tree.flush_fs_events_in_root_git_repository(cx).await;
2418 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2419 .await;
2420 cx.executor().run_until_parked();
2421
2422 // Ensure that the git status is loaded correctly
2423 tree.read_with(cx, |tree, _cx| {
2424 let snapshot = tree.snapshot();
2425 assert_eq!(snapshot.repositories().count(), 1);
2426 let (dir, repo_entry) = snapshot.repositories().next().unwrap();
2427 // Path is blank because the working directory of
2428 // the git repository is located at the root of the project
2429 assert_eq!(dir.as_ref(), Path::new(""));
2430
2431 // This is the missing path between the root of the project (sub-folder-2) and its
2432 // location relative to the root of the repository.
2433 assert_eq!(
2434 repo_entry.location_in_repo,
2435 Some(Arc::from(Path::new("sub-folder-1/sub-folder-2")))
2436 );
2437
2438 assert_eq!(snapshot.status_for_file("c.txt"), None);
2439 assert_eq!(
2440 snapshot.status_for_file("d/e.txt"),
2441 Some(GitFileStatus::Added)
2442 );
2443 });
2444
2445 // Now we simulate FS events, but ONLY in the .git folder that's outside
2446 // of out project root.
2447 // Meaning: we don't produce any FS events for files inside the project.
2448 git_add(E_TXT, &repo);
2449 git_commit("Second commit", &repo);
2450 tree.flush_fs_events_in_root_git_repository(cx).await;
2451 cx.executor().run_until_parked();
2452
2453 tree.read_with(cx, |tree, _cx| {
2454 let snapshot = tree.snapshot();
2455
2456 assert!(snapshot.repositories().next().is_some());
2457
2458 assert_eq!(snapshot.status_for_file("c.txt"), None);
2459 assert_eq!(snapshot.status_for_file("d/e.txt"), None);
2460 });
2461}
2462
2463#[gpui::test]
2464async fn test_propagate_git_statuses(cx: &mut TestAppContext) {
2465 init_test(cx);
2466 let fs = FakeFs::new(cx.background_executor.clone());
2467 fs.insert_tree(
2468 "/root",
2469 json!({
2470 ".git": {},
2471 "a": {
2472 "b": {
2473 "c1.txt": "",
2474 "c2.txt": "",
2475 },
2476 "d": {
2477 "e1.txt": "",
2478 "e2.txt": "",
2479 "e3.txt": "",
2480 }
2481 },
2482 "f": {
2483 "no-status.txt": ""
2484 },
2485 "g": {
2486 "h1.txt": "",
2487 "h2.txt": ""
2488 },
2489
2490 }),
2491 )
2492 .await;
2493
2494 fs.set_status_for_repo_via_git_operation(
2495 &Path::new("/root/.git"),
2496 &[
2497 (Path::new("a/b/c1.txt"), GitFileStatus::Added),
2498 (Path::new("a/d/e2.txt"), GitFileStatus::Modified),
2499 (Path::new("g/h2.txt"), GitFileStatus::Conflict),
2500 ],
2501 );
2502
2503 let tree = Worktree::local(
2504 build_client(cx),
2505 Path::new("/root"),
2506 true,
2507 fs.clone(),
2508 Default::default(),
2509 &mut cx.to_async(),
2510 )
2511 .await
2512 .unwrap();
2513
2514 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2515 .await;
2516
2517 cx.executor().run_until_parked();
2518 let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
2519
2520 check_propagated_statuses(
2521 &snapshot,
2522 &[
2523 (Path::new(""), Some(GitFileStatus::Conflict)),
2524 (Path::new("a"), Some(GitFileStatus::Modified)),
2525 (Path::new("a/b"), Some(GitFileStatus::Added)),
2526 (Path::new("a/b/c1.txt"), Some(GitFileStatus::Added)),
2527 (Path::new("a/b/c2.txt"), None),
2528 (Path::new("a/d"), Some(GitFileStatus::Modified)),
2529 (Path::new("a/d/e2.txt"), Some(GitFileStatus::Modified)),
2530 (Path::new("f"), None),
2531 (Path::new("f/no-status.txt"), None),
2532 (Path::new("g"), Some(GitFileStatus::Conflict)),
2533 (Path::new("g/h2.txt"), Some(GitFileStatus::Conflict)),
2534 ],
2535 );
2536
2537 check_propagated_statuses(
2538 &snapshot,
2539 &[
2540 (Path::new("a/b"), Some(GitFileStatus::Added)),
2541 (Path::new("a/b/c1.txt"), Some(GitFileStatus::Added)),
2542 (Path::new("a/b/c2.txt"), None),
2543 (Path::new("a/d"), Some(GitFileStatus::Modified)),
2544 (Path::new("a/d/e1.txt"), None),
2545 (Path::new("a/d/e2.txt"), Some(GitFileStatus::Modified)),
2546 (Path::new("f"), None),
2547 (Path::new("f/no-status.txt"), None),
2548 (Path::new("g"), Some(GitFileStatus::Conflict)),
2549 ],
2550 );
2551
2552 check_propagated_statuses(
2553 &snapshot,
2554 &[
2555 (Path::new("a/b/c1.txt"), Some(GitFileStatus::Added)),
2556 (Path::new("a/b/c2.txt"), None),
2557 (Path::new("a/d/e1.txt"), None),
2558 (Path::new("a/d/e2.txt"), Some(GitFileStatus::Modified)),
2559 (Path::new("f/no-status.txt"), None),
2560 ],
2561 );
2562
2563 #[track_caller]
2564 fn check_propagated_statuses(
2565 snapshot: &Snapshot,
2566 expected_statuses: &[(&Path, Option<GitFileStatus>)],
2567 ) {
2568 let mut entries = expected_statuses
2569 .iter()
2570 .map(|(path, _)| snapshot.entry_for_path(path).unwrap().clone())
2571 .collect::<Vec<_>>();
2572 snapshot.propagate_git_statuses(&mut entries);
2573 assert_eq!(
2574 entries
2575 .iter()
2576 .map(|e| (e.path.as_ref(), e.git_status))
2577 .collect::<Vec<_>>(),
2578 expected_statuses
2579 );
2580 }
2581}
2582
2583fn build_client(cx: &mut TestAppContext) -> Arc<Client> {
2584 let clock = Arc::new(FakeSystemClock::default());
2585 let http_client = FakeHttpClient::with_404_response();
2586 cx.update(|cx| Client::new(clock, http_client, cx))
2587}
2588
2589#[track_caller]
2590fn git_init(path: &Path) -> git2::Repository {
2591 git2::Repository::init(path).expect("Failed to initialize git repository")
2592}
2593
2594#[track_caller]
2595fn git_add<P: AsRef<Path>>(path: P, repo: &git2::Repository) {
2596 let path = path.as_ref();
2597 let mut index = repo.index().expect("Failed to get index");
2598 index.add_path(path).expect("Failed to add a.txt");
2599 index.write().expect("Failed to write index");
2600}
2601
2602#[track_caller]
2603fn git_remove_index(path: &Path, repo: &git2::Repository) {
2604 let mut index = repo.index().expect("Failed to get index");
2605 index.remove_path(path).expect("Failed to add a.txt");
2606 index.write().expect("Failed to write index");
2607}
2608
2609#[track_caller]
2610fn git_commit(msg: &'static str, repo: &git2::Repository) {
2611 use git2::Signature;
2612
2613 let signature = Signature::now("test", "test@zed.dev").unwrap();
2614 let oid = repo.index().unwrap().write_tree().unwrap();
2615 let tree = repo.find_tree(oid).unwrap();
2616 if let Some(head) = repo.head().ok() {
2617 let parent_obj = head.peel(git2::ObjectType::Commit).unwrap();
2618
2619 let parent_commit = parent_obj.as_commit().unwrap();
2620
2621 repo.commit(
2622 Some("HEAD"),
2623 &signature,
2624 &signature,
2625 msg,
2626 &tree,
2627 &[parent_commit],
2628 )
2629 .expect("Failed to commit with parent");
2630 } else {
2631 repo.commit(Some("HEAD"), &signature, &signature, msg, &tree, &[])
2632 .expect("Failed to commit");
2633 }
2634}
2635
2636#[track_caller]
2637fn git_stash(repo: &mut git2::Repository) {
2638 use git2::Signature;
2639
2640 let signature = Signature::now("test", "test@zed.dev").unwrap();
2641 repo.stash_save(&signature, "N/A", None)
2642 .expect("Failed to stash");
2643}
2644
2645#[track_caller]
2646fn git_reset(offset: usize, repo: &git2::Repository) {
2647 let head = repo.head().expect("Couldn't get repo head");
2648 let object = head.peel(git2::ObjectType::Commit).unwrap();
2649 let commit = object.as_commit().unwrap();
2650 let new_head = commit
2651 .parents()
2652 .inspect(|parnet| {
2653 parnet.message();
2654 })
2655 .skip(offset)
2656 .next()
2657 .expect("Not enough history");
2658 repo.reset(&new_head.as_object(), git2::ResetType::Soft, None)
2659 .expect("Could not reset");
2660}
2661
2662#[allow(dead_code)]
2663#[track_caller]
2664fn git_status(repo: &git2::Repository) -> collections::HashMap<String, git2::Status> {
2665 repo.statuses(None)
2666 .unwrap()
2667 .iter()
2668 .map(|status| (status.path().unwrap().to_string(), status.status()))
2669 .collect()
2670}
2671
2672#[track_caller]
2673fn check_worktree_entries(
2674 tree: &Worktree,
2675 expected_excluded_paths: &[&str],
2676 expected_ignored_paths: &[&str],
2677 expected_tracked_paths: &[&str],
2678) {
2679 for path in expected_excluded_paths {
2680 let entry = tree.entry_for_path(path);
2681 assert!(
2682 entry.is_none(),
2683 "expected path '{path}' to be excluded, but got entry: {entry:?}",
2684 );
2685 }
2686 for path in expected_ignored_paths {
2687 let entry = tree
2688 .entry_for_path(path)
2689 .unwrap_or_else(|| panic!("Missing entry for expected ignored path '{path}'"));
2690 assert!(
2691 entry.is_ignored,
2692 "expected path '{path}' to be ignored, but got entry: {entry:?}",
2693 );
2694 }
2695 for path in expected_tracked_paths {
2696 let entry = tree
2697 .entry_for_path(path)
2698 .unwrap_or_else(|| panic!("Missing entry for expected tracked path '{path}'"));
2699 assert!(
2700 !entry.is_ignored,
2701 "expected path '{path}' to be tracked, but got entry: {entry:?}",
2702 );
2703 }
2704}
2705
2706fn init_test(cx: &mut gpui::TestAppContext) {
2707 cx.update(|cx| {
2708 let settings_store = SettingsStore::test(cx);
2709 cx.set_global(settings_store);
2710 WorktreeSettings::register(cx);
2711 });
2712}
2713
2714fn assert_entry_git_state(
2715 tree: &Worktree,
2716 path: &str,
2717 git_status: Option<GitFileStatus>,
2718 is_ignored: bool,
2719) {
2720 let entry = tree.entry_for_path(path).expect("entry {path} not found");
2721 assert_eq!(entry.git_status, git_status);
2722 assert_eq!(entry.is_ignored, is_ignored);
2723}