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