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