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