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