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