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 .unwrap();
1179 assert!(entry.is_dir());
1180
1181 cx.foreground().run_until_parked();
1182 tree.read_with(cx, |tree, _| {
1183 assert_eq!(tree.entry_for_path("a/e").unwrap().kind, EntryKind::Dir);
1184 });
1185
1186 let snapshot2 = tree.update(cx, |tree, _| tree.as_local().unwrap().snapshot());
1187 assert_eq!(
1188 snapshot1.lock().entries(true).collect::<Vec<_>>(),
1189 snapshot2.entries(true).collect::<Vec<_>>()
1190 );
1191}
1192
1193#[gpui::test]
1194async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
1195 init_test(cx);
1196 let client_fake = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
1197
1198 let fs_fake = FakeFs::new(cx.background());
1199 fs_fake
1200 .insert_tree(
1201 "/root",
1202 json!({
1203 "a": {},
1204 }),
1205 )
1206 .await;
1207
1208 let tree_fake = Worktree::local(
1209 client_fake,
1210 "/root".as_ref(),
1211 true,
1212 fs_fake,
1213 Default::default(),
1214 &mut cx.to_async(),
1215 )
1216 .await
1217 .unwrap();
1218
1219 let entry = tree_fake
1220 .update(cx, |tree, cx| {
1221 tree.as_local_mut()
1222 .unwrap()
1223 .create_entry("a/b/c/d.txt".as_ref(), false, cx)
1224 })
1225 .await
1226 .unwrap()
1227 .unwrap();
1228 assert!(entry.is_file());
1229
1230 cx.foreground().run_until_parked();
1231 tree_fake.read_with(cx, |tree, _| {
1232 assert!(tree.entry_for_path("a/b/c/d.txt").unwrap().is_file());
1233 assert!(tree.entry_for_path("a/b/c/").unwrap().is_dir());
1234 assert!(tree.entry_for_path("a/b/").unwrap().is_dir());
1235 });
1236
1237 let client_real = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
1238
1239 let fs_real = Arc::new(RealFs);
1240 let temp_root = temp_tree(json!({
1241 "a": {}
1242 }));
1243
1244 let tree_real = Worktree::local(
1245 client_real,
1246 temp_root.path(),
1247 true,
1248 fs_real,
1249 Default::default(),
1250 &mut cx.to_async(),
1251 )
1252 .await
1253 .unwrap();
1254
1255 let entry = tree_real
1256 .update(cx, |tree, cx| {
1257 tree.as_local_mut()
1258 .unwrap()
1259 .create_entry("a/b/c/d.txt".as_ref(), false, cx)
1260 })
1261 .await
1262 .unwrap()
1263 .unwrap();
1264 assert!(entry.is_file());
1265
1266 cx.foreground().run_until_parked();
1267 tree_real.read_with(cx, |tree, _| {
1268 assert!(tree.entry_for_path("a/b/c/d.txt").unwrap().is_file());
1269 assert!(tree.entry_for_path("a/b/c/").unwrap().is_dir());
1270 assert!(tree.entry_for_path("a/b/").unwrap().is_dir());
1271 });
1272
1273 // Test smallest change
1274 let entry = tree_real
1275 .update(cx, |tree, cx| {
1276 tree.as_local_mut()
1277 .unwrap()
1278 .create_entry("a/b/c/e.txt".as_ref(), false, cx)
1279 })
1280 .await
1281 .unwrap()
1282 .unwrap();
1283 assert!(entry.is_file());
1284
1285 cx.foreground().run_until_parked();
1286 tree_real.read_with(cx, |tree, _| {
1287 assert!(tree.entry_for_path("a/b/c/e.txt").unwrap().is_file());
1288 });
1289
1290 // Test largest change
1291 let entry = tree_real
1292 .update(cx, |tree, cx| {
1293 tree.as_local_mut()
1294 .unwrap()
1295 .create_entry("d/e/f/g.txt".as_ref(), false, cx)
1296 })
1297 .await
1298 .unwrap()
1299 .unwrap();
1300 assert!(entry.is_file());
1301
1302 cx.foreground().run_until_parked();
1303 tree_real.read_with(cx, |tree, _| {
1304 assert!(tree.entry_for_path("d/e/f/g.txt").unwrap().is_file());
1305 assert!(tree.entry_for_path("d/e/f").unwrap().is_dir());
1306 assert!(tree.entry_for_path("d/e/").unwrap().is_dir());
1307 assert!(tree.entry_for_path("d/").unwrap().is_dir());
1308 });
1309}
1310
1311#[gpui::test(iterations = 100)]
1312async fn test_random_worktree_operations_during_initial_scan(
1313 cx: &mut TestAppContext,
1314 mut rng: StdRng,
1315) {
1316 init_test(cx);
1317 let operations = env::var("OPERATIONS")
1318 .map(|o| o.parse().unwrap())
1319 .unwrap_or(5);
1320 let initial_entries = env::var("INITIAL_ENTRIES")
1321 .map(|o| o.parse().unwrap())
1322 .unwrap_or(20);
1323
1324 let root_dir = Path::new("/test");
1325 let fs = FakeFs::new(cx.background()) as Arc<dyn Fs>;
1326 fs.as_fake().insert_tree(root_dir, json!({})).await;
1327 for _ in 0..initial_entries {
1328 randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
1329 }
1330 log::info!("generated initial tree");
1331
1332 let worktree = Worktree::local(
1333 build_client(cx),
1334 root_dir,
1335 true,
1336 fs.clone(),
1337 Default::default(),
1338 &mut cx.to_async(),
1339 )
1340 .await
1341 .unwrap();
1342
1343 let mut snapshots = vec![worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot())];
1344 let updates = Arc::new(Mutex::new(Vec::new()));
1345 worktree.update(cx, |tree, cx| {
1346 check_worktree_change_events(tree, cx);
1347
1348 let _ = tree.as_local_mut().unwrap().observe_updates(0, cx, {
1349 let updates = updates.clone();
1350 move |update| {
1351 updates.lock().push(update);
1352 async { true }
1353 }
1354 });
1355 });
1356
1357 for _ in 0..operations {
1358 worktree
1359 .update(cx, |worktree, cx| {
1360 randomly_mutate_worktree(worktree, &mut rng, cx)
1361 })
1362 .await
1363 .log_err();
1364 worktree.read_with(cx, |tree, _| {
1365 tree.as_local().unwrap().snapshot().check_invariants(true)
1366 });
1367
1368 if rng.gen_bool(0.6) {
1369 snapshots.push(worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot()));
1370 }
1371 }
1372
1373 worktree
1374 .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
1375 .await;
1376
1377 cx.foreground().run_until_parked();
1378
1379 let final_snapshot = worktree.read_with(cx, |tree, _| {
1380 let tree = tree.as_local().unwrap();
1381 let snapshot = tree.snapshot();
1382 snapshot.check_invariants(true);
1383 snapshot
1384 });
1385
1386 for (i, snapshot) in snapshots.into_iter().enumerate().rev() {
1387 let mut updated_snapshot = snapshot.clone();
1388 for update in updates.lock().iter() {
1389 if update.scan_id >= updated_snapshot.scan_id() as u64 {
1390 updated_snapshot
1391 .apply_remote_update(update.clone())
1392 .unwrap();
1393 }
1394 }
1395
1396 assert_eq!(
1397 updated_snapshot.entries(true).collect::<Vec<_>>(),
1398 final_snapshot.entries(true).collect::<Vec<_>>(),
1399 "wrong updates after snapshot {i}: {snapshot:#?} {updates:#?}",
1400 );
1401 }
1402}
1403
1404#[gpui::test(iterations = 100)]
1405async fn test_random_worktree_changes(cx: &mut TestAppContext, mut rng: StdRng) {
1406 init_test(cx);
1407 let operations = env::var("OPERATIONS")
1408 .map(|o| o.parse().unwrap())
1409 .unwrap_or(40);
1410 let initial_entries = env::var("INITIAL_ENTRIES")
1411 .map(|o| o.parse().unwrap())
1412 .unwrap_or(20);
1413
1414 let root_dir = Path::new("/test");
1415 let fs = FakeFs::new(cx.background()) as Arc<dyn Fs>;
1416 fs.as_fake().insert_tree(root_dir, json!({})).await;
1417 for _ in 0..initial_entries {
1418 randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
1419 }
1420 log::info!("generated initial tree");
1421
1422 let worktree = Worktree::local(
1423 build_client(cx),
1424 root_dir,
1425 true,
1426 fs.clone(),
1427 Default::default(),
1428 &mut cx.to_async(),
1429 )
1430 .await
1431 .unwrap();
1432
1433 let updates = Arc::new(Mutex::new(Vec::new()));
1434 worktree.update(cx, |tree, cx| {
1435 check_worktree_change_events(tree, cx);
1436
1437 let _ = tree.as_local_mut().unwrap().observe_updates(0, cx, {
1438 let updates = updates.clone();
1439 move |update| {
1440 updates.lock().push(update);
1441 async { true }
1442 }
1443 });
1444 });
1445
1446 worktree
1447 .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
1448 .await;
1449
1450 fs.as_fake().pause_events();
1451 let mut snapshots = Vec::new();
1452 let mut mutations_len = operations;
1453 while mutations_len > 1 {
1454 if rng.gen_bool(0.2) {
1455 worktree
1456 .update(cx, |worktree, cx| {
1457 randomly_mutate_worktree(worktree, &mut rng, cx)
1458 })
1459 .await
1460 .log_err();
1461 } else {
1462 randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
1463 }
1464
1465 let buffered_event_count = fs.as_fake().buffered_event_count();
1466 if buffered_event_count > 0 && rng.gen_bool(0.3) {
1467 let len = rng.gen_range(0..=buffered_event_count);
1468 log::info!("flushing {} events", len);
1469 fs.as_fake().flush_events(len);
1470 } else {
1471 randomly_mutate_fs(&fs, root_dir, 0.6, &mut rng).await;
1472 mutations_len -= 1;
1473 }
1474
1475 cx.foreground().run_until_parked();
1476 if rng.gen_bool(0.2) {
1477 log::info!("storing snapshot {}", snapshots.len());
1478 let snapshot = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
1479 snapshots.push(snapshot);
1480 }
1481 }
1482
1483 log::info!("quiescing");
1484 fs.as_fake().flush_events(usize::MAX);
1485 cx.foreground().run_until_parked();
1486
1487 let snapshot = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
1488 snapshot.check_invariants(true);
1489 let expanded_paths = snapshot
1490 .expanded_entries()
1491 .map(|e| e.path.clone())
1492 .collect::<Vec<_>>();
1493
1494 {
1495 let new_worktree = Worktree::local(
1496 build_client(cx),
1497 root_dir,
1498 true,
1499 fs.clone(),
1500 Default::default(),
1501 &mut cx.to_async(),
1502 )
1503 .await
1504 .unwrap();
1505 new_worktree
1506 .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
1507 .await;
1508 new_worktree
1509 .update(cx, |tree, _| {
1510 tree.as_local_mut()
1511 .unwrap()
1512 .refresh_entries_for_paths(expanded_paths)
1513 })
1514 .recv()
1515 .await;
1516 let new_snapshot =
1517 new_worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
1518 assert_eq!(
1519 snapshot.entries_without_ids(true),
1520 new_snapshot.entries_without_ids(true)
1521 );
1522 }
1523
1524 for (i, mut prev_snapshot) in snapshots.into_iter().enumerate().rev() {
1525 for update in updates.lock().iter() {
1526 if update.scan_id >= prev_snapshot.scan_id() as u64 {
1527 prev_snapshot.apply_remote_update(update.clone()).unwrap();
1528 }
1529 }
1530
1531 assert_eq!(
1532 prev_snapshot
1533 .entries(true)
1534 .map(ignore_pending_dir)
1535 .collect::<Vec<_>>(),
1536 snapshot
1537 .entries(true)
1538 .map(ignore_pending_dir)
1539 .collect::<Vec<_>>(),
1540 "wrong updates after snapshot {i}: {updates:#?}",
1541 );
1542 }
1543
1544 fn ignore_pending_dir(entry: &Entry) -> Entry {
1545 let mut entry = entry.clone();
1546 if entry.kind.is_dir() {
1547 entry.kind = EntryKind::Dir
1548 }
1549 entry
1550 }
1551}
1552
1553// The worktree's `UpdatedEntries` event can be used to follow along with
1554// all changes to the worktree's snapshot.
1555fn check_worktree_change_events(tree: &mut Worktree, cx: &mut ModelContext<Worktree>) {
1556 let mut entries = tree.entries(true).cloned().collect::<Vec<_>>();
1557 cx.subscribe(&cx.handle(), move |tree, _, event, _| {
1558 if let Event::UpdatedEntries(changes) = event {
1559 for (path, _, change_type) in changes.iter() {
1560 let entry = tree.entry_for_path(&path).cloned();
1561 let ix = match entries.binary_search_by_key(&path, |e| &e.path) {
1562 Ok(ix) | Err(ix) => ix,
1563 };
1564 match change_type {
1565 PathChange::Added => entries.insert(ix, entry.unwrap()),
1566 PathChange::Removed => drop(entries.remove(ix)),
1567 PathChange::Updated => {
1568 let entry = entry.unwrap();
1569 let existing_entry = entries.get_mut(ix).unwrap();
1570 assert_eq!(existing_entry.path, entry.path);
1571 *existing_entry = entry;
1572 }
1573 PathChange::AddedOrUpdated | PathChange::Loaded => {
1574 let entry = entry.unwrap();
1575 if entries.get(ix).map(|e| &e.path) == Some(&entry.path) {
1576 *entries.get_mut(ix).unwrap() = entry;
1577 } else {
1578 entries.insert(ix, entry);
1579 }
1580 }
1581 }
1582 }
1583
1584 let new_entries = tree.entries(true).cloned().collect::<Vec<_>>();
1585 assert_eq!(entries, new_entries, "incorrect changes: {:?}", changes);
1586 }
1587 })
1588 .detach();
1589}
1590
1591fn randomly_mutate_worktree(
1592 worktree: &mut Worktree,
1593 rng: &mut impl Rng,
1594 cx: &mut ModelContext<Worktree>,
1595) -> Task<Result<()>> {
1596 log::info!("mutating worktree");
1597 let worktree = worktree.as_local_mut().unwrap();
1598 let snapshot = worktree.snapshot();
1599 let entry = snapshot.entries(false).choose(rng).unwrap();
1600
1601 match rng.gen_range(0_u32..100) {
1602 0..=33 if entry.path.as_ref() != Path::new("") => {
1603 log::info!("deleting entry {:?} ({})", entry.path, entry.id.0);
1604 worktree.delete_entry(entry.id, cx).unwrap()
1605 }
1606 ..=66 if entry.path.as_ref() != Path::new("") => {
1607 let other_entry = snapshot.entries(false).choose(rng).unwrap();
1608 let new_parent_path = if other_entry.is_dir() {
1609 other_entry.path.clone()
1610 } else {
1611 other_entry.path.parent().unwrap().into()
1612 };
1613 let mut new_path = new_parent_path.join(random_filename(rng));
1614 if new_path.starts_with(&entry.path) {
1615 new_path = random_filename(rng).into();
1616 }
1617
1618 log::info!(
1619 "renaming entry {:?} ({}) to {:?}",
1620 entry.path,
1621 entry.id.0,
1622 new_path
1623 );
1624 let task = worktree.rename_entry(entry.id, new_path, cx);
1625 cx.foreground().spawn(async move {
1626 task.await?.unwrap();
1627 Ok(())
1628 })
1629 }
1630 _ => {
1631 if entry.is_dir() {
1632 let child_path = entry.path.join(random_filename(rng));
1633 let is_dir = rng.gen_bool(0.3);
1634 log::info!(
1635 "creating {} at {:?}",
1636 if is_dir { "dir" } else { "file" },
1637 child_path,
1638 );
1639 let task = worktree.create_entry(child_path, is_dir, cx);
1640 cx.foreground().spawn(async move {
1641 task.await?;
1642 Ok(())
1643 })
1644 } else {
1645 log::info!("overwriting file {:?} ({})", entry.path, entry.id.0);
1646 let task =
1647 worktree.write_file(entry.path.clone(), "".into(), Default::default(), cx);
1648 cx.foreground().spawn(async move {
1649 task.await?;
1650 Ok(())
1651 })
1652 }
1653 }
1654 }
1655}
1656
1657async fn randomly_mutate_fs(
1658 fs: &Arc<dyn Fs>,
1659 root_path: &Path,
1660 insertion_probability: f64,
1661 rng: &mut impl Rng,
1662) {
1663 log::info!("mutating fs");
1664 let mut files = Vec::new();
1665 let mut dirs = Vec::new();
1666 for path in fs.as_fake().paths(false) {
1667 if path.starts_with(root_path) {
1668 if fs.is_file(&path).await {
1669 files.push(path);
1670 } else {
1671 dirs.push(path);
1672 }
1673 }
1674 }
1675
1676 if (files.is_empty() && dirs.len() == 1) || rng.gen_bool(insertion_probability) {
1677 let path = dirs.choose(rng).unwrap();
1678 let new_path = path.join(random_filename(rng));
1679
1680 if rng.gen() {
1681 log::info!(
1682 "creating dir {:?}",
1683 new_path.strip_prefix(root_path).unwrap()
1684 );
1685 fs.create_dir(&new_path).await.unwrap();
1686 } else {
1687 log::info!(
1688 "creating file {:?}",
1689 new_path.strip_prefix(root_path).unwrap()
1690 );
1691 fs.create_file(&new_path, Default::default()).await.unwrap();
1692 }
1693 } else if rng.gen_bool(0.05) {
1694 let ignore_dir_path = dirs.choose(rng).unwrap();
1695 let ignore_path = ignore_dir_path.join(&*GITIGNORE);
1696
1697 let subdirs = dirs
1698 .iter()
1699 .filter(|d| d.starts_with(&ignore_dir_path))
1700 .cloned()
1701 .collect::<Vec<_>>();
1702 let subfiles = files
1703 .iter()
1704 .filter(|d| d.starts_with(&ignore_dir_path))
1705 .cloned()
1706 .collect::<Vec<_>>();
1707 let files_to_ignore = {
1708 let len = rng.gen_range(0..=subfiles.len());
1709 subfiles.choose_multiple(rng, len)
1710 };
1711 let dirs_to_ignore = {
1712 let len = rng.gen_range(0..subdirs.len());
1713 subdirs.choose_multiple(rng, len)
1714 };
1715
1716 let mut ignore_contents = String::new();
1717 for path_to_ignore in files_to_ignore.chain(dirs_to_ignore) {
1718 writeln!(
1719 ignore_contents,
1720 "{}",
1721 path_to_ignore
1722 .strip_prefix(&ignore_dir_path)
1723 .unwrap()
1724 .to_str()
1725 .unwrap()
1726 )
1727 .unwrap();
1728 }
1729 log::info!(
1730 "creating gitignore {:?} with contents:\n{}",
1731 ignore_path.strip_prefix(&root_path).unwrap(),
1732 ignore_contents
1733 );
1734 fs.save(
1735 &ignore_path,
1736 &ignore_contents.as_str().into(),
1737 Default::default(),
1738 )
1739 .await
1740 .unwrap();
1741 } else {
1742 let old_path = {
1743 let file_path = files.choose(rng);
1744 let dir_path = dirs[1..].choose(rng);
1745 file_path.into_iter().chain(dir_path).choose(rng).unwrap()
1746 };
1747
1748 let is_rename = rng.gen();
1749 if is_rename {
1750 let new_path_parent = dirs
1751 .iter()
1752 .filter(|d| !d.starts_with(old_path))
1753 .choose(rng)
1754 .unwrap();
1755
1756 let overwrite_existing_dir =
1757 !old_path.starts_with(&new_path_parent) && rng.gen_bool(0.3);
1758 let new_path = if overwrite_existing_dir {
1759 fs.remove_dir(
1760 &new_path_parent,
1761 RemoveOptions {
1762 recursive: true,
1763 ignore_if_not_exists: true,
1764 },
1765 )
1766 .await
1767 .unwrap();
1768 new_path_parent.to_path_buf()
1769 } else {
1770 new_path_parent.join(random_filename(rng))
1771 };
1772
1773 log::info!(
1774 "renaming {:?} to {}{:?}",
1775 old_path.strip_prefix(&root_path).unwrap(),
1776 if overwrite_existing_dir {
1777 "overwrite "
1778 } else {
1779 ""
1780 },
1781 new_path.strip_prefix(&root_path).unwrap()
1782 );
1783 fs.rename(
1784 &old_path,
1785 &new_path,
1786 fs::RenameOptions {
1787 overwrite: true,
1788 ignore_if_exists: true,
1789 },
1790 )
1791 .await
1792 .unwrap();
1793 } else if fs.is_file(&old_path).await {
1794 log::info!(
1795 "deleting file {:?}",
1796 old_path.strip_prefix(&root_path).unwrap()
1797 );
1798 fs.remove_file(old_path, Default::default()).await.unwrap();
1799 } else {
1800 log::info!(
1801 "deleting dir {:?}",
1802 old_path.strip_prefix(&root_path).unwrap()
1803 );
1804 fs.remove_dir(
1805 &old_path,
1806 RemoveOptions {
1807 recursive: true,
1808 ignore_if_not_exists: true,
1809 },
1810 )
1811 .await
1812 .unwrap();
1813 }
1814 }
1815}
1816
1817fn random_filename(rng: &mut impl Rng) -> String {
1818 (0..6)
1819 .map(|_| rng.sample(rand::distributions::Alphanumeric))
1820 .map(char::from)
1821 .collect()
1822}
1823
1824#[gpui::test]
1825async fn test_rename_work_directory(cx: &mut TestAppContext) {
1826 init_test(cx);
1827 let root = temp_tree(json!({
1828 "projects": {
1829 "project1": {
1830 "a": "",
1831 "b": "",
1832 }
1833 },
1834
1835 }));
1836 let root_path = root.path();
1837
1838 let tree = Worktree::local(
1839 build_client(cx),
1840 root_path,
1841 true,
1842 Arc::new(RealFs),
1843 Default::default(),
1844 &mut cx.to_async(),
1845 )
1846 .await
1847 .unwrap();
1848
1849 let repo = git_init(&root_path.join("projects/project1"));
1850 git_add("a", &repo);
1851 git_commit("init", &repo);
1852 std::fs::write(root_path.join("projects/project1/a"), "aa").ok();
1853
1854 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1855 .await;
1856
1857 tree.flush_fs_events(cx).await;
1858
1859 cx.read(|cx| {
1860 let tree = tree.read(cx);
1861 let (work_dir, _) = tree.repositories().next().unwrap();
1862 assert_eq!(work_dir.as_ref(), Path::new("projects/project1"));
1863 assert_eq!(
1864 tree.status_for_file(Path::new("projects/project1/a")),
1865 Some(GitFileStatus::Modified)
1866 );
1867 assert_eq!(
1868 tree.status_for_file(Path::new("projects/project1/b")),
1869 Some(GitFileStatus::Added)
1870 );
1871 });
1872
1873 std::fs::rename(
1874 root_path.join("projects/project1"),
1875 root_path.join("projects/project2"),
1876 )
1877 .ok();
1878 tree.flush_fs_events(cx).await;
1879
1880 cx.read(|cx| {
1881 let tree = tree.read(cx);
1882 let (work_dir, _) = tree.repositories().next().unwrap();
1883 assert_eq!(work_dir.as_ref(), Path::new("projects/project2"));
1884 assert_eq!(
1885 tree.status_for_file(Path::new("projects/project2/a")),
1886 Some(GitFileStatus::Modified)
1887 );
1888 assert_eq!(
1889 tree.status_for_file(Path::new("projects/project2/b")),
1890 Some(GitFileStatus::Added)
1891 );
1892 });
1893}
1894
1895#[gpui::test]
1896async fn test_git_repository_for_path(cx: &mut TestAppContext) {
1897 init_test(cx);
1898 let root = temp_tree(json!({
1899 "c.txt": "",
1900 "dir1": {
1901 ".git": {},
1902 "deps": {
1903 "dep1": {
1904 ".git": {},
1905 "src": {
1906 "a.txt": ""
1907 }
1908 }
1909 },
1910 "src": {
1911 "b.txt": ""
1912 }
1913 },
1914 }));
1915
1916 let tree = Worktree::local(
1917 build_client(cx),
1918 root.path(),
1919 true,
1920 Arc::new(RealFs),
1921 Default::default(),
1922 &mut cx.to_async(),
1923 )
1924 .await
1925 .unwrap();
1926
1927 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1928 .await;
1929 tree.flush_fs_events(cx).await;
1930
1931 tree.read_with(cx, |tree, _cx| {
1932 let tree = tree.as_local().unwrap();
1933
1934 assert!(tree.repository_for_path("c.txt".as_ref()).is_none());
1935
1936 let entry = tree.repository_for_path("dir1/src/b.txt".as_ref()).unwrap();
1937 assert_eq!(
1938 entry
1939 .work_directory(tree)
1940 .map(|directory| directory.as_ref().to_owned()),
1941 Some(Path::new("dir1").to_owned())
1942 );
1943
1944 let entry = tree
1945 .repository_for_path("dir1/deps/dep1/src/a.txt".as_ref())
1946 .unwrap();
1947 assert_eq!(
1948 entry
1949 .work_directory(tree)
1950 .map(|directory| directory.as_ref().to_owned()),
1951 Some(Path::new("dir1/deps/dep1").to_owned())
1952 );
1953
1954 let entries = tree.files(false, 0);
1955
1956 let paths_with_repos = tree
1957 .entries_with_repositories(entries)
1958 .map(|(entry, repo)| {
1959 (
1960 entry.path.as_ref(),
1961 repo.and_then(|repo| {
1962 repo.work_directory(&tree)
1963 .map(|work_directory| work_directory.0.to_path_buf())
1964 }),
1965 )
1966 })
1967 .collect::<Vec<_>>();
1968
1969 assert_eq!(
1970 paths_with_repos,
1971 &[
1972 (Path::new("c.txt"), None),
1973 (
1974 Path::new("dir1/deps/dep1/src/a.txt"),
1975 Some(Path::new("dir1/deps/dep1").into())
1976 ),
1977 (Path::new("dir1/src/b.txt"), Some(Path::new("dir1").into())),
1978 ]
1979 );
1980 });
1981
1982 let repo_update_events = Arc::new(Mutex::new(vec![]));
1983 tree.update(cx, |_, cx| {
1984 let repo_update_events = repo_update_events.clone();
1985 cx.subscribe(&tree, move |_, _, event, _| {
1986 if let Event::UpdatedGitRepositories(update) = event {
1987 repo_update_events.lock().push(update.clone());
1988 }
1989 })
1990 .detach();
1991 });
1992
1993 std::fs::write(root.path().join("dir1/.git/random_new_file"), "hello").unwrap();
1994 tree.flush_fs_events(cx).await;
1995
1996 assert_eq!(
1997 repo_update_events.lock()[0]
1998 .iter()
1999 .map(|e| e.0.clone())
2000 .collect::<Vec<Arc<Path>>>(),
2001 vec![Path::new("dir1").into()]
2002 );
2003
2004 std::fs::remove_dir_all(root.path().join("dir1/.git")).unwrap();
2005 tree.flush_fs_events(cx).await;
2006
2007 tree.read_with(cx, |tree, _cx| {
2008 let tree = tree.as_local().unwrap();
2009
2010 assert!(tree
2011 .repository_for_path("dir1/src/b.txt".as_ref())
2012 .is_none());
2013 });
2014}
2015
2016#[gpui::test]
2017async fn test_git_status(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
2018 init_test(cx);
2019 cx.update(|cx| {
2020 cx.update_global::<SettingsStore, _, _>(|store, cx| {
2021 store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
2022 project_settings.file_scan_exclusions =
2023 Some(vec!["**/.git".to_string(), "**/.gitignore".to_string()]);
2024 });
2025 });
2026 });
2027 const IGNORE_RULE: &'static str = "**/target";
2028
2029 let root = temp_tree(json!({
2030 "project": {
2031 "a.txt": "a",
2032 "b.txt": "bb",
2033 "c": {
2034 "d": {
2035 "e.txt": "eee"
2036 }
2037 },
2038 "f.txt": "ffff",
2039 "target": {
2040 "build_file": "???"
2041 },
2042 ".gitignore": IGNORE_RULE
2043 },
2044
2045 }));
2046
2047 const A_TXT: &'static str = "a.txt";
2048 const B_TXT: &'static str = "b.txt";
2049 const E_TXT: &'static str = "c/d/e.txt";
2050 const F_TXT: &'static str = "f.txt";
2051 const DOTGITIGNORE: &'static str = ".gitignore";
2052 const BUILD_FILE: &'static str = "target/build_file";
2053 let project_path = Path::new("project");
2054
2055 // Set up git repository before creating the worktree.
2056 let work_dir = root.path().join("project");
2057 let mut repo = git_init(work_dir.as_path());
2058 repo.add_ignore_rule(IGNORE_RULE).unwrap();
2059 git_add(A_TXT, &repo);
2060 git_add(E_TXT, &repo);
2061 git_add(DOTGITIGNORE, &repo);
2062 git_commit("Initial commit", &repo);
2063
2064 let tree = Worktree::local(
2065 build_client(cx),
2066 root.path(),
2067 true,
2068 Arc::new(RealFs),
2069 Default::default(),
2070 &mut cx.to_async(),
2071 )
2072 .await
2073 .unwrap();
2074
2075 tree.flush_fs_events(cx).await;
2076 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2077 .await;
2078 deterministic.run_until_parked();
2079
2080 // Check that the right git state is observed on startup
2081 tree.read_with(cx, |tree, _cx| {
2082 let snapshot = tree.snapshot();
2083 assert_eq!(snapshot.repositories().count(), 1);
2084 let (dir, _) = snapshot.repositories().next().unwrap();
2085 assert_eq!(dir.as_ref(), Path::new("project"));
2086
2087 assert_eq!(
2088 snapshot.status_for_file(project_path.join(B_TXT)),
2089 Some(GitFileStatus::Added)
2090 );
2091 assert_eq!(
2092 snapshot.status_for_file(project_path.join(F_TXT)),
2093 Some(GitFileStatus::Added)
2094 );
2095 });
2096
2097 // Modify a file in the working copy.
2098 std::fs::write(work_dir.join(A_TXT), "aa").unwrap();
2099 tree.flush_fs_events(cx).await;
2100 deterministic.run_until_parked();
2101
2102 // The worktree detects that the file's git status has changed.
2103 tree.read_with(cx, |tree, _cx| {
2104 let snapshot = tree.snapshot();
2105 assert_eq!(
2106 snapshot.status_for_file(project_path.join(A_TXT)),
2107 Some(GitFileStatus::Modified)
2108 );
2109 });
2110
2111 // Create a commit in the git repository.
2112 git_add(A_TXT, &repo);
2113 git_add(B_TXT, &repo);
2114 git_commit("Committing modified and added", &repo);
2115 tree.flush_fs_events(cx).await;
2116 deterministic.run_until_parked();
2117
2118 // The worktree detects that the files' git status have changed.
2119 tree.read_with(cx, |tree, _cx| {
2120 let snapshot = tree.snapshot();
2121 assert_eq!(
2122 snapshot.status_for_file(project_path.join(F_TXT)),
2123 Some(GitFileStatus::Added)
2124 );
2125 assert_eq!(snapshot.status_for_file(project_path.join(B_TXT)), None);
2126 assert_eq!(snapshot.status_for_file(project_path.join(A_TXT)), None);
2127 });
2128
2129 // Modify files in the working copy and perform git operations on other files.
2130 git_reset(0, &repo);
2131 git_remove_index(Path::new(B_TXT), &repo);
2132 git_stash(&mut repo);
2133 std::fs::write(work_dir.join(E_TXT), "eeee").unwrap();
2134 std::fs::write(work_dir.join(BUILD_FILE), "this should be ignored").unwrap();
2135 tree.flush_fs_events(cx).await;
2136 deterministic.run_until_parked();
2137
2138 // Check that more complex repo changes are tracked
2139 tree.read_with(cx, |tree, _cx| {
2140 let snapshot = tree.snapshot();
2141
2142 assert_eq!(snapshot.status_for_file(project_path.join(A_TXT)), None);
2143 assert_eq!(
2144 snapshot.status_for_file(project_path.join(B_TXT)),
2145 Some(GitFileStatus::Added)
2146 );
2147 assert_eq!(
2148 snapshot.status_for_file(project_path.join(E_TXT)),
2149 Some(GitFileStatus::Modified)
2150 );
2151 });
2152
2153 std::fs::remove_file(work_dir.join(B_TXT)).unwrap();
2154 std::fs::remove_dir_all(work_dir.join("c")).unwrap();
2155 std::fs::write(
2156 work_dir.join(DOTGITIGNORE),
2157 [IGNORE_RULE, "f.txt"].join("\n"),
2158 )
2159 .unwrap();
2160
2161 git_add(Path::new(DOTGITIGNORE), &repo);
2162 git_commit("Committing modified git ignore", &repo);
2163
2164 tree.flush_fs_events(cx).await;
2165 deterministic.run_until_parked();
2166
2167 let mut renamed_dir_name = "first_directory/second_directory";
2168 const RENAMED_FILE: &'static str = "rf.txt";
2169
2170 std::fs::create_dir_all(work_dir.join(renamed_dir_name)).unwrap();
2171 std::fs::write(
2172 work_dir.join(renamed_dir_name).join(RENAMED_FILE),
2173 "new-contents",
2174 )
2175 .unwrap();
2176
2177 tree.flush_fs_events(cx).await;
2178 deterministic.run_until_parked();
2179
2180 tree.read_with(cx, |tree, _cx| {
2181 let snapshot = tree.snapshot();
2182 assert_eq!(
2183 snapshot.status_for_file(&project_path.join(renamed_dir_name).join(RENAMED_FILE)),
2184 Some(GitFileStatus::Added)
2185 );
2186 });
2187
2188 renamed_dir_name = "new_first_directory/second_directory";
2189
2190 std::fs::rename(
2191 work_dir.join("first_directory"),
2192 work_dir.join("new_first_directory"),
2193 )
2194 .unwrap();
2195
2196 tree.flush_fs_events(cx).await;
2197 deterministic.run_until_parked();
2198
2199 tree.read_with(cx, |tree, _cx| {
2200 let snapshot = tree.snapshot();
2201
2202 assert_eq!(
2203 snapshot.status_for_file(
2204 project_path
2205 .join(Path::new(renamed_dir_name))
2206 .join(RENAMED_FILE)
2207 ),
2208 Some(GitFileStatus::Added)
2209 );
2210 });
2211}
2212
2213#[gpui::test]
2214async fn test_propagate_git_statuses(cx: &mut TestAppContext) {
2215 init_test(cx);
2216 let fs = FakeFs::new(cx.background());
2217 fs.insert_tree(
2218 "/root",
2219 json!({
2220 ".git": {},
2221 "a": {
2222 "b": {
2223 "c1.txt": "",
2224 "c2.txt": "",
2225 },
2226 "d": {
2227 "e1.txt": "",
2228 "e2.txt": "",
2229 "e3.txt": "",
2230 }
2231 },
2232 "f": {
2233 "no-status.txt": ""
2234 },
2235 "g": {
2236 "h1.txt": "",
2237 "h2.txt": ""
2238 },
2239
2240 }),
2241 )
2242 .await;
2243
2244 fs.set_status_for_repo_via_git_operation(
2245 &Path::new("/root/.git"),
2246 &[
2247 (Path::new("a/b/c1.txt"), GitFileStatus::Added),
2248 (Path::new("a/d/e2.txt"), GitFileStatus::Modified),
2249 (Path::new("g/h2.txt"), GitFileStatus::Conflict),
2250 ],
2251 );
2252
2253 let tree = Worktree::local(
2254 build_client(cx),
2255 Path::new("/root"),
2256 true,
2257 fs.clone(),
2258 Default::default(),
2259 &mut cx.to_async(),
2260 )
2261 .await
2262 .unwrap();
2263
2264 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2265 .await;
2266
2267 cx.foreground().run_until_parked();
2268 let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
2269
2270 check_propagated_statuses(
2271 &snapshot,
2272 &[
2273 (Path::new(""), Some(GitFileStatus::Conflict)),
2274 (Path::new("a"), Some(GitFileStatus::Modified)),
2275 (Path::new("a/b"), Some(GitFileStatus::Added)),
2276 (Path::new("a/b/c1.txt"), Some(GitFileStatus::Added)),
2277 (Path::new("a/b/c2.txt"), None),
2278 (Path::new("a/d"), Some(GitFileStatus::Modified)),
2279 (Path::new("a/d/e2.txt"), Some(GitFileStatus::Modified)),
2280 (Path::new("f"), None),
2281 (Path::new("f/no-status.txt"), None),
2282 (Path::new("g"), Some(GitFileStatus::Conflict)),
2283 (Path::new("g/h2.txt"), Some(GitFileStatus::Conflict)),
2284 ],
2285 );
2286
2287 check_propagated_statuses(
2288 &snapshot,
2289 &[
2290 (Path::new("a/b"), Some(GitFileStatus::Added)),
2291 (Path::new("a/b/c1.txt"), Some(GitFileStatus::Added)),
2292 (Path::new("a/b/c2.txt"), None),
2293 (Path::new("a/d"), Some(GitFileStatus::Modified)),
2294 (Path::new("a/d/e1.txt"), None),
2295 (Path::new("a/d/e2.txt"), Some(GitFileStatus::Modified)),
2296 (Path::new("f"), None),
2297 (Path::new("f/no-status.txt"), None),
2298 (Path::new("g"), Some(GitFileStatus::Conflict)),
2299 ],
2300 );
2301
2302 check_propagated_statuses(
2303 &snapshot,
2304 &[
2305 (Path::new("a/b/c1.txt"), Some(GitFileStatus::Added)),
2306 (Path::new("a/b/c2.txt"), None),
2307 (Path::new("a/d/e1.txt"), None),
2308 (Path::new("a/d/e2.txt"), Some(GitFileStatus::Modified)),
2309 (Path::new("f/no-status.txt"), None),
2310 ],
2311 );
2312
2313 #[track_caller]
2314 fn check_propagated_statuses(
2315 snapshot: &Snapshot,
2316 expected_statuses: &[(&Path, Option<GitFileStatus>)],
2317 ) {
2318 let mut entries = expected_statuses
2319 .iter()
2320 .map(|(path, _)| snapshot.entry_for_path(path).unwrap().clone())
2321 .collect::<Vec<_>>();
2322 snapshot.propagate_git_statuses(&mut entries);
2323 assert_eq!(
2324 entries
2325 .iter()
2326 .map(|e| (e.path.as_ref(), e.git_status))
2327 .collect::<Vec<_>>(),
2328 expected_statuses
2329 );
2330 }
2331}
2332
2333fn build_client(cx: &mut TestAppContext) -> Arc<Client> {
2334 let http_client = FakeHttpClient::with_404_response();
2335 cx.read(|cx| Client::new(http_client, cx))
2336}
2337
2338#[track_caller]
2339fn git_init(path: &Path) -> git2::Repository {
2340 git2::Repository::init(path).expect("Failed to initialize git repository")
2341}
2342
2343#[track_caller]
2344fn git_add<P: AsRef<Path>>(path: P, repo: &git2::Repository) {
2345 let path = path.as_ref();
2346 let mut index = repo.index().expect("Failed to get index");
2347 index.add_path(path).expect("Failed to add a.txt");
2348 index.write().expect("Failed to write index");
2349}
2350
2351#[track_caller]
2352fn git_remove_index(path: &Path, repo: &git2::Repository) {
2353 let mut index = repo.index().expect("Failed to get index");
2354 index.remove_path(path).expect("Failed to add a.txt");
2355 index.write().expect("Failed to write index");
2356}
2357
2358#[track_caller]
2359fn git_commit(msg: &'static str, repo: &git2::Repository) {
2360 use git2::Signature;
2361
2362 let signature = Signature::now("test", "test@zed.dev").unwrap();
2363 let oid = repo.index().unwrap().write_tree().unwrap();
2364 let tree = repo.find_tree(oid).unwrap();
2365 if let Some(head) = repo.head().ok() {
2366 let parent_obj = head.peel(git2::ObjectType::Commit).unwrap();
2367
2368 let parent_commit = parent_obj.as_commit().unwrap();
2369
2370 repo.commit(
2371 Some("HEAD"),
2372 &signature,
2373 &signature,
2374 msg,
2375 &tree,
2376 &[parent_commit],
2377 )
2378 .expect("Failed to commit with parent");
2379 } else {
2380 repo.commit(Some("HEAD"), &signature, &signature, msg, &tree, &[])
2381 .expect("Failed to commit");
2382 }
2383}
2384
2385#[track_caller]
2386fn git_stash(repo: &mut git2::Repository) {
2387 use git2::Signature;
2388
2389 let signature = Signature::now("test", "test@zed.dev").unwrap();
2390 repo.stash_save(&signature, "N/A", None)
2391 .expect("Failed to stash");
2392}
2393
2394#[track_caller]
2395fn git_reset(offset: usize, repo: &git2::Repository) {
2396 let head = repo.head().expect("Couldn't get repo head");
2397 let object = head.peel(git2::ObjectType::Commit).unwrap();
2398 let commit = object.as_commit().unwrap();
2399 let new_head = commit
2400 .parents()
2401 .inspect(|parnet| {
2402 parnet.message();
2403 })
2404 .skip(offset)
2405 .next()
2406 .expect("Not enough history");
2407 repo.reset(&new_head.as_object(), git2::ResetType::Soft, None)
2408 .expect("Could not reset");
2409}
2410
2411#[allow(dead_code)]
2412#[track_caller]
2413fn git_status(repo: &git2::Repository) -> collections::HashMap<String, git2::Status> {
2414 repo.statuses(None)
2415 .unwrap()
2416 .iter()
2417 .map(|status| (status.path().unwrap().to_string(), status.status()))
2418 .collect()
2419}
2420
2421#[track_caller]
2422fn check_worktree_entries(
2423 tree: &Worktree,
2424 expected_excluded_paths: &[&str],
2425 expected_ignored_paths: &[&str],
2426 expected_tracked_paths: &[&str],
2427) {
2428 for path in expected_excluded_paths {
2429 let entry = tree.entry_for_path(path);
2430 assert!(
2431 entry.is_none(),
2432 "expected path '{path}' to be excluded, but got entry: {entry:?}",
2433 );
2434 }
2435 for path in expected_ignored_paths {
2436 let entry = tree
2437 .entry_for_path(path)
2438 .unwrap_or_else(|| panic!("Missing entry for expected ignored path '{path}'"));
2439 assert!(
2440 entry.is_ignored,
2441 "expected path '{path}' to be ignored, but got entry: {entry:?}",
2442 );
2443 }
2444 for path in expected_tracked_paths {
2445 let entry = tree
2446 .entry_for_path(path)
2447 .unwrap_or_else(|| panic!("Missing entry for expected tracked path '{path}'"));
2448 assert!(
2449 !entry.is_ignored,
2450 "expected path '{path}' to be tracked, but got entry: {entry:?}",
2451 );
2452 }
2453}
2454
2455fn init_test(cx: &mut gpui::TestAppContext) {
2456 cx.update(|cx| {
2457 cx.set_global(SettingsStore::test(cx));
2458 Project::init_settings(cx);
2459 });
2460}