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