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