1use crate::{
2 worktree_settings::WorktreeSettings, Entry, EntryKind, Event, PathChange, StatusEntry,
3 WorkDirectory, Worktree, WorktreeModelHandle,
4};
5use anyhow::Result;
6use fs::{FakeFs, Fs, RealFs, RemoveOptions};
7use git::{
8 repository::RepoPath,
9 status::{FileStatus, StatusCode, TrackedStatus},
10 GITIGNORE,
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::{path, test::TempTree, ResultExt};
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!(tree
1100 .entry_for_path("node_modules")
1101 .is_some_and(|f| f.is_always_included));
1102 assert!(tree
1103 .entry_for_path("node_modules/prettier/package.json")
1104 .is_some_and(|f| f.is_always_included));
1105 });
1106
1107 cx.update(|cx| {
1108 cx.update_global::<SettingsStore, _>(|store, cx| {
1109 store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
1110 project_settings.file_scan_exclusions = Some(vec![]);
1111 project_settings.file_scan_inclusions = Some(vec![]);
1112 });
1113 });
1114 });
1115 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1116 .await;
1117 tree.flush_fs_events(cx).await;
1118
1119 tree.read_with(cx, |tree, _| {
1120 assert!(tree
1121 .entry_for_path("node_modules")
1122 .is_some_and(|f| !f.is_always_included));
1123 assert!(tree
1124 .entry_for_path("node_modules/prettier/package.json")
1125 .is_some_and(|f| !f.is_always_included));
1126 });
1127}
1128
1129#[gpui::test]
1130async fn test_file_scan_exclusions(cx: &mut TestAppContext) {
1131 init_test(cx);
1132 cx.executor().allow_parking();
1133 let dir = TempTree::new(json!({
1134 ".gitignore": "**/target\n/node_modules\n",
1135 "target": {
1136 "index": "blah2"
1137 },
1138 "node_modules": {
1139 ".DS_Store": "",
1140 "prettier": {
1141 "package.json": "{}",
1142 },
1143 },
1144 "src": {
1145 ".DS_Store": "",
1146 "foo": {
1147 "foo.rs": "mod another;\n",
1148 "another.rs": "// another",
1149 },
1150 "bar": {
1151 "bar.rs": "// bar",
1152 },
1153 "lib.rs": "mod foo;\nmod bar;\n",
1154 },
1155 ".DS_Store": "",
1156 }));
1157 cx.update(|cx| {
1158 cx.update_global::<SettingsStore, _>(|store, cx| {
1159 store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
1160 project_settings.file_scan_exclusions =
1161 Some(vec!["**/foo/**".to_string(), "**/.DS_Store".to_string()]);
1162 });
1163 });
1164 });
1165
1166 let tree = Worktree::local(
1167 dir.path(),
1168 true,
1169 Arc::new(RealFs::new(None, cx.executor())),
1170 Default::default(),
1171 &mut cx.to_async(),
1172 )
1173 .await
1174 .unwrap();
1175 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1176 .await;
1177 tree.flush_fs_events(cx).await;
1178 tree.read_with(cx, |tree, _| {
1179 check_worktree_entries(
1180 tree,
1181 &[
1182 "src/foo/foo.rs",
1183 "src/foo/another.rs",
1184 "node_modules/.DS_Store",
1185 "src/.DS_Store",
1186 ".DS_Store",
1187 ],
1188 &["target", "node_modules"],
1189 &["src/lib.rs", "src/bar/bar.rs", ".gitignore"],
1190 &[],
1191 )
1192 });
1193
1194 cx.update(|cx| {
1195 cx.update_global::<SettingsStore, _>(|store, cx| {
1196 store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
1197 project_settings.file_scan_exclusions =
1198 Some(vec!["**/node_modules/**".to_string()]);
1199 });
1200 });
1201 });
1202 tree.flush_fs_events(cx).await;
1203 cx.executor().run_until_parked();
1204 tree.read_with(cx, |tree, _| {
1205 check_worktree_entries(
1206 tree,
1207 &[
1208 "node_modules/prettier/package.json",
1209 "node_modules/.DS_Store",
1210 "node_modules",
1211 ],
1212 &["target"],
1213 &[
1214 ".gitignore",
1215 "src/lib.rs",
1216 "src/bar/bar.rs",
1217 "src/foo/foo.rs",
1218 "src/foo/another.rs",
1219 "src/.DS_Store",
1220 ".DS_Store",
1221 ],
1222 &[],
1223 )
1224 });
1225}
1226
1227#[gpui::test]
1228async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) {
1229 init_test(cx);
1230 cx.executor().allow_parking();
1231 let dir = TempTree::new(json!({
1232 ".git": {
1233 "HEAD": "ref: refs/heads/main\n",
1234 "foo": "bar",
1235 },
1236 ".gitignore": "**/target\n/node_modules\ntest_output\n",
1237 "target": {
1238 "index": "blah2"
1239 },
1240 "node_modules": {
1241 ".DS_Store": "",
1242 "prettier": {
1243 "package.json": "{}",
1244 },
1245 },
1246 "src": {
1247 ".DS_Store": "",
1248 "foo": {
1249 "foo.rs": "mod another;\n",
1250 "another.rs": "// another",
1251 },
1252 "bar": {
1253 "bar.rs": "// bar",
1254 },
1255 "lib.rs": "mod foo;\nmod bar;\n",
1256 },
1257 ".DS_Store": "",
1258 }));
1259 cx.update(|cx| {
1260 cx.update_global::<SettingsStore, _>(|store, cx| {
1261 store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
1262 project_settings.file_scan_exclusions = Some(vec![
1263 "**/.git".to_string(),
1264 "node_modules/".to_string(),
1265 "build_output".to_string(),
1266 ]);
1267 });
1268 });
1269 });
1270
1271 let tree = Worktree::local(
1272 dir.path(),
1273 true,
1274 Arc::new(RealFs::new(None, cx.executor())),
1275 Default::default(),
1276 &mut cx.to_async(),
1277 )
1278 .await
1279 .unwrap();
1280 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1281 .await;
1282 tree.flush_fs_events(cx).await;
1283 tree.read_with(cx, |tree, _| {
1284 check_worktree_entries(
1285 tree,
1286 &[
1287 ".git/HEAD",
1288 ".git/foo",
1289 "node_modules",
1290 "node_modules/.DS_Store",
1291 "node_modules/prettier",
1292 "node_modules/prettier/package.json",
1293 ],
1294 &["target"],
1295 &[
1296 ".DS_Store",
1297 "src/.DS_Store",
1298 "src/lib.rs",
1299 "src/foo/foo.rs",
1300 "src/foo/another.rs",
1301 "src/bar/bar.rs",
1302 ".gitignore",
1303 ],
1304 &[],
1305 )
1306 });
1307
1308 let new_excluded_dir = dir.path().join("build_output");
1309 let new_ignored_dir = dir.path().join("test_output");
1310 std::fs::create_dir_all(&new_excluded_dir)
1311 .unwrap_or_else(|e| panic!("Failed to create a {new_excluded_dir:?} directory: {e}"));
1312 std::fs::create_dir_all(&new_ignored_dir)
1313 .unwrap_or_else(|e| panic!("Failed to create a {new_ignored_dir:?} directory: {e}"));
1314 let node_modules_dir = dir.path().join("node_modules");
1315 let dot_git_dir = dir.path().join(".git");
1316 let src_dir = dir.path().join("src");
1317 for existing_dir in [&node_modules_dir, &dot_git_dir, &src_dir] {
1318 assert!(
1319 existing_dir.is_dir(),
1320 "Expect {existing_dir:?} to be present in the FS already"
1321 );
1322 }
1323
1324 for directory_for_new_file in [
1325 new_excluded_dir,
1326 new_ignored_dir,
1327 node_modules_dir,
1328 dot_git_dir,
1329 src_dir,
1330 ] {
1331 std::fs::write(directory_for_new_file.join("new_file"), "new file contents")
1332 .unwrap_or_else(|e| {
1333 panic!("Failed to create in {directory_for_new_file:?} a new file: {e}")
1334 });
1335 }
1336 tree.flush_fs_events(cx).await;
1337
1338 tree.read_with(cx, |tree, _| {
1339 check_worktree_entries(
1340 tree,
1341 &[
1342 ".git/HEAD",
1343 ".git/foo",
1344 ".git/new_file",
1345 "node_modules",
1346 "node_modules/.DS_Store",
1347 "node_modules/prettier",
1348 "node_modules/prettier/package.json",
1349 "node_modules/new_file",
1350 "build_output",
1351 "build_output/new_file",
1352 "test_output/new_file",
1353 ],
1354 &["target", "test_output"],
1355 &[
1356 ".DS_Store",
1357 "src/.DS_Store",
1358 "src/lib.rs",
1359 "src/foo/foo.rs",
1360 "src/foo/another.rs",
1361 "src/bar/bar.rs",
1362 "src/new_file",
1363 ".gitignore",
1364 ],
1365 &[],
1366 )
1367 });
1368}
1369
1370#[gpui::test]
1371async fn test_fs_events_in_dot_git_worktree(cx: &mut TestAppContext) {
1372 init_test(cx);
1373 cx.executor().allow_parking();
1374 let dir = TempTree::new(json!({
1375 ".git": {
1376 "HEAD": "ref: refs/heads/main\n",
1377 "foo": "foo contents",
1378 },
1379 }));
1380 let dot_git_worktree_dir = dir.path().join(".git");
1381
1382 let tree = Worktree::local(
1383 dot_git_worktree_dir.clone(),
1384 true,
1385 Arc::new(RealFs::new(None, cx.executor())),
1386 Default::default(),
1387 &mut cx.to_async(),
1388 )
1389 .await
1390 .unwrap();
1391 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1392 .await;
1393 tree.flush_fs_events(cx).await;
1394 tree.read_with(cx, |tree, _| {
1395 check_worktree_entries(tree, &[], &["HEAD", "foo"], &[], &[])
1396 });
1397
1398 std::fs::write(dot_git_worktree_dir.join("new_file"), "new file contents")
1399 .unwrap_or_else(|e| panic!("Failed to create in {dot_git_worktree_dir:?} a new file: {e}"));
1400 tree.flush_fs_events(cx).await;
1401 tree.read_with(cx, |tree, _| {
1402 check_worktree_entries(tree, &[], &["HEAD", "foo", "new_file"], &[], &[])
1403 });
1404}
1405
1406#[gpui::test(iterations = 30)]
1407async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
1408 init_test(cx);
1409 let fs = FakeFs::new(cx.background_executor.clone());
1410 fs.insert_tree(
1411 "/root",
1412 json!({
1413 "b": {},
1414 "c": {},
1415 "d": {},
1416 }),
1417 )
1418 .await;
1419
1420 let tree = Worktree::local(
1421 "/root".as_ref(),
1422 true,
1423 fs,
1424 Default::default(),
1425 &mut cx.to_async(),
1426 )
1427 .await
1428 .unwrap();
1429
1430 let snapshot1 = tree.update(cx, |tree, cx| {
1431 let tree = tree.as_local_mut().unwrap();
1432 let snapshot = Arc::new(Mutex::new(tree.snapshot()));
1433 tree.observe_updates(0, cx, {
1434 let snapshot = snapshot.clone();
1435 let settings = tree.settings().clone();
1436 move |update| {
1437 snapshot
1438 .lock()
1439 .apply_remote_update(update, &settings.file_scan_inclusions)
1440 .unwrap();
1441 async { true }
1442 }
1443 });
1444 snapshot
1445 });
1446
1447 let entry = tree
1448 .update(cx, |tree, cx| {
1449 tree.as_local_mut()
1450 .unwrap()
1451 .create_entry("a/e".as_ref(), true, cx)
1452 })
1453 .await
1454 .unwrap()
1455 .to_included()
1456 .unwrap();
1457 assert!(entry.is_dir());
1458
1459 cx.executor().run_until_parked();
1460 tree.read_with(cx, |tree, _| {
1461 assert_eq!(tree.entry_for_path("a/e").unwrap().kind, EntryKind::Dir);
1462 });
1463
1464 let snapshot2 = tree.update(cx, |tree, _| tree.as_local().unwrap().snapshot());
1465 assert_eq!(
1466 snapshot1.lock().entries(true, 0).collect::<Vec<_>>(),
1467 snapshot2.entries(true, 0).collect::<Vec<_>>()
1468 );
1469}
1470
1471#[gpui::test]
1472async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
1473 init_test(cx);
1474 cx.executor().allow_parking();
1475
1476 let fs_fake = FakeFs::new(cx.background_executor.clone());
1477 fs_fake
1478 .insert_tree(
1479 "/root",
1480 json!({
1481 "a": {},
1482 }),
1483 )
1484 .await;
1485
1486 let tree_fake = Worktree::local(
1487 "/root".as_ref(),
1488 true,
1489 fs_fake,
1490 Default::default(),
1491 &mut cx.to_async(),
1492 )
1493 .await
1494 .unwrap();
1495
1496 let entry = tree_fake
1497 .update(cx, |tree, cx| {
1498 tree.as_local_mut()
1499 .unwrap()
1500 .create_entry("a/b/c/d.txt".as_ref(), false, cx)
1501 })
1502 .await
1503 .unwrap()
1504 .to_included()
1505 .unwrap();
1506 assert!(entry.is_file());
1507
1508 cx.executor().run_until_parked();
1509 tree_fake.read_with(cx, |tree, _| {
1510 assert!(tree.entry_for_path("a/b/c/d.txt").unwrap().is_file());
1511 assert!(tree.entry_for_path("a/b/c/").unwrap().is_dir());
1512 assert!(tree.entry_for_path("a/b/").unwrap().is_dir());
1513 });
1514
1515 let fs_real = Arc::new(RealFs::new(None, cx.executor()));
1516 let temp_root = TempTree::new(json!({
1517 "a": {}
1518 }));
1519
1520 let tree_real = Worktree::local(
1521 temp_root.path(),
1522 true,
1523 fs_real,
1524 Default::default(),
1525 &mut cx.to_async(),
1526 )
1527 .await
1528 .unwrap();
1529
1530 let entry = tree_real
1531 .update(cx, |tree, cx| {
1532 tree.as_local_mut()
1533 .unwrap()
1534 .create_entry("a/b/c/d.txt".as_ref(), false, cx)
1535 })
1536 .await
1537 .unwrap()
1538 .to_included()
1539 .unwrap();
1540 assert!(entry.is_file());
1541
1542 cx.executor().run_until_parked();
1543 tree_real.read_with(cx, |tree, _| {
1544 assert!(tree.entry_for_path("a/b/c/d.txt").unwrap().is_file());
1545 assert!(tree.entry_for_path("a/b/c/").unwrap().is_dir());
1546 assert!(tree.entry_for_path("a/b/").unwrap().is_dir());
1547 });
1548
1549 // Test smallest change
1550 let entry = tree_real
1551 .update(cx, |tree, cx| {
1552 tree.as_local_mut()
1553 .unwrap()
1554 .create_entry("a/b/c/e.txt".as_ref(), false, cx)
1555 })
1556 .await
1557 .unwrap()
1558 .to_included()
1559 .unwrap();
1560 assert!(entry.is_file());
1561
1562 cx.executor().run_until_parked();
1563 tree_real.read_with(cx, |tree, _| {
1564 assert!(tree.entry_for_path("a/b/c/e.txt").unwrap().is_file());
1565 });
1566
1567 // Test largest change
1568 let entry = tree_real
1569 .update(cx, |tree, cx| {
1570 tree.as_local_mut()
1571 .unwrap()
1572 .create_entry("d/e/f/g.txt".as_ref(), false, cx)
1573 })
1574 .await
1575 .unwrap()
1576 .to_included()
1577 .unwrap();
1578 assert!(entry.is_file());
1579
1580 cx.executor().run_until_parked();
1581 tree_real.read_with(cx, |tree, _| {
1582 assert!(tree.entry_for_path("d/e/f/g.txt").unwrap().is_file());
1583 assert!(tree.entry_for_path("d/e/f").unwrap().is_dir());
1584 assert!(tree.entry_for_path("d/e/").unwrap().is_dir());
1585 assert!(tree.entry_for_path("d/").unwrap().is_dir());
1586 });
1587}
1588
1589#[gpui::test(iterations = 100)]
1590async fn test_random_worktree_operations_during_initial_scan(
1591 cx: &mut TestAppContext,
1592 mut rng: StdRng,
1593) {
1594 init_test(cx);
1595 let operations = env::var("OPERATIONS")
1596 .map(|o| o.parse().unwrap())
1597 .unwrap_or(5);
1598 let initial_entries = env::var("INITIAL_ENTRIES")
1599 .map(|o| o.parse().unwrap())
1600 .unwrap_or(20);
1601
1602 let root_dir = Path::new(path!("/test"));
1603 let fs = FakeFs::new(cx.background_executor.clone()) as Arc<dyn Fs>;
1604 fs.as_fake().insert_tree(root_dir, json!({})).await;
1605 for _ in 0..initial_entries {
1606 randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
1607 }
1608 log::info!("generated initial tree");
1609
1610 let worktree = Worktree::local(
1611 root_dir,
1612 true,
1613 fs.clone(),
1614 Default::default(),
1615 &mut cx.to_async(),
1616 )
1617 .await
1618 .unwrap();
1619
1620 let mut snapshots = vec![worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot())];
1621 let updates = Arc::new(Mutex::new(Vec::new()));
1622 worktree.update(cx, |tree, cx| {
1623 check_worktree_change_events(tree, cx);
1624
1625 tree.as_local_mut().unwrap().observe_updates(0, cx, {
1626 let updates = updates.clone();
1627 move |update| {
1628 updates.lock().push(update);
1629 async { true }
1630 }
1631 });
1632 });
1633
1634 for _ in 0..operations {
1635 worktree
1636 .update(cx, |worktree, cx| {
1637 randomly_mutate_worktree(worktree, &mut rng, cx)
1638 })
1639 .await
1640 .log_err();
1641 worktree.read_with(cx, |tree, _| {
1642 tree.as_local().unwrap().snapshot().check_invariants(true)
1643 });
1644
1645 if rng.gen_bool(0.6) {
1646 snapshots.push(worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot()));
1647 }
1648 }
1649
1650 worktree
1651 .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
1652 .await;
1653
1654 cx.executor().run_until_parked();
1655
1656 let final_snapshot = worktree.read_with(cx, |tree, _| {
1657 let tree = tree.as_local().unwrap();
1658 let snapshot = tree.snapshot();
1659 snapshot.check_invariants(true);
1660 snapshot
1661 });
1662
1663 let settings = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().settings());
1664
1665 for (i, snapshot) in snapshots.into_iter().enumerate().rev() {
1666 let mut updated_snapshot = snapshot.clone();
1667 for update in updates.lock().iter() {
1668 if update.scan_id >= updated_snapshot.scan_id() as u64 {
1669 updated_snapshot
1670 .apply_remote_update(update.clone(), &settings.file_scan_inclusions)
1671 .unwrap();
1672 }
1673 }
1674
1675 assert_eq!(
1676 updated_snapshot.entries(true, 0).collect::<Vec<_>>(),
1677 final_snapshot.entries(true, 0).collect::<Vec<_>>(),
1678 "wrong updates after snapshot {i}: {snapshot:#?} {updates:#?}",
1679 );
1680 }
1681}
1682
1683#[gpui::test(iterations = 100)]
1684async fn test_random_worktree_changes(cx: &mut TestAppContext, mut rng: StdRng) {
1685 init_test(cx);
1686 let operations = env::var("OPERATIONS")
1687 .map(|o| o.parse().unwrap())
1688 .unwrap_or(40);
1689 let initial_entries = env::var("INITIAL_ENTRIES")
1690 .map(|o| o.parse().unwrap())
1691 .unwrap_or(20);
1692
1693 let root_dir = Path::new(path!("/test"));
1694 let fs = FakeFs::new(cx.background_executor.clone()) as Arc<dyn Fs>;
1695 fs.as_fake().insert_tree(root_dir, json!({})).await;
1696 for _ in 0..initial_entries {
1697 randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
1698 }
1699 log::info!("generated initial tree");
1700
1701 let worktree = Worktree::local(
1702 root_dir,
1703 true,
1704 fs.clone(),
1705 Default::default(),
1706 &mut cx.to_async(),
1707 )
1708 .await
1709 .unwrap();
1710
1711 let updates = Arc::new(Mutex::new(Vec::new()));
1712 worktree.update(cx, |tree, cx| {
1713 check_worktree_change_events(tree, cx);
1714
1715 tree.as_local_mut().unwrap().observe_updates(0, cx, {
1716 let updates = updates.clone();
1717 move |update| {
1718 updates.lock().push(update);
1719 async { true }
1720 }
1721 });
1722 });
1723
1724 worktree
1725 .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
1726 .await;
1727
1728 fs.as_fake().pause_events();
1729 let mut snapshots = Vec::new();
1730 let mut mutations_len = operations;
1731 while mutations_len > 1 {
1732 if rng.gen_bool(0.2) {
1733 worktree
1734 .update(cx, |worktree, cx| {
1735 randomly_mutate_worktree(worktree, &mut rng, cx)
1736 })
1737 .await
1738 .log_err();
1739 } else {
1740 randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
1741 }
1742
1743 let buffered_event_count = fs.as_fake().buffered_event_count();
1744 if buffered_event_count > 0 && rng.gen_bool(0.3) {
1745 let len = rng.gen_range(0..=buffered_event_count);
1746 log::info!("flushing {} events", len);
1747 fs.as_fake().flush_events(len);
1748 } else {
1749 randomly_mutate_fs(&fs, root_dir, 0.6, &mut rng).await;
1750 mutations_len -= 1;
1751 }
1752
1753 cx.executor().run_until_parked();
1754 if rng.gen_bool(0.2) {
1755 log::info!("storing snapshot {}", snapshots.len());
1756 let snapshot = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
1757 snapshots.push(snapshot);
1758 }
1759 }
1760
1761 log::info!("quiescing");
1762 fs.as_fake().flush_events(usize::MAX);
1763 cx.executor().run_until_parked();
1764
1765 let snapshot = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
1766 snapshot.check_invariants(true);
1767 let expanded_paths = snapshot
1768 .expanded_entries()
1769 .map(|e| e.path.clone())
1770 .collect::<Vec<_>>();
1771
1772 {
1773 let new_worktree = Worktree::local(
1774 root_dir,
1775 true,
1776 fs.clone(),
1777 Default::default(),
1778 &mut cx.to_async(),
1779 )
1780 .await
1781 .unwrap();
1782 new_worktree
1783 .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
1784 .await;
1785 new_worktree
1786 .update(cx, |tree, _| {
1787 tree.as_local_mut()
1788 .unwrap()
1789 .refresh_entries_for_paths(expanded_paths)
1790 })
1791 .recv()
1792 .await;
1793 let new_snapshot =
1794 new_worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
1795 assert_eq!(
1796 snapshot.entries_without_ids(true),
1797 new_snapshot.entries_without_ids(true)
1798 );
1799 }
1800
1801 let settings = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().settings());
1802
1803 for (i, mut prev_snapshot) in snapshots.into_iter().enumerate().rev() {
1804 for update in updates.lock().iter() {
1805 if update.scan_id >= prev_snapshot.scan_id() as u64 {
1806 prev_snapshot
1807 .apply_remote_update(update.clone(), &settings.file_scan_inclusions)
1808 .unwrap();
1809 }
1810 }
1811
1812 assert_eq!(
1813 prev_snapshot
1814 .entries(true, 0)
1815 .map(ignore_pending_dir)
1816 .collect::<Vec<_>>(),
1817 snapshot
1818 .entries(true, 0)
1819 .map(ignore_pending_dir)
1820 .collect::<Vec<_>>(),
1821 "wrong updates after snapshot {i}: {updates:#?}",
1822 );
1823 }
1824
1825 fn ignore_pending_dir(entry: &Entry) -> Entry {
1826 let mut entry = entry.clone();
1827 if entry.kind.is_dir() {
1828 entry.kind = EntryKind::Dir
1829 }
1830 entry
1831 }
1832}
1833
1834// The worktree's `UpdatedEntries` event can be used to follow along with
1835// all changes to the worktree's snapshot.
1836fn check_worktree_change_events(tree: &mut Worktree, cx: &mut Context<Worktree>) {
1837 let mut entries = tree.entries(true, 0).cloned().collect::<Vec<_>>();
1838 cx.subscribe(&cx.entity(), move |tree, _, event, _| {
1839 if let Event::UpdatedEntries(changes) = event {
1840 for (path, _, change_type) in changes.iter() {
1841 let entry = tree.entry_for_path(path).cloned();
1842 let ix = match entries.binary_search_by_key(&path, |e| &e.path) {
1843 Ok(ix) | Err(ix) => ix,
1844 };
1845 match change_type {
1846 PathChange::Added => entries.insert(ix, entry.unwrap()),
1847 PathChange::Removed => drop(entries.remove(ix)),
1848 PathChange::Updated => {
1849 let entry = entry.unwrap();
1850 let existing_entry = entries.get_mut(ix).unwrap();
1851 assert_eq!(existing_entry.path, entry.path);
1852 *existing_entry = entry;
1853 }
1854 PathChange::AddedOrUpdated | PathChange::Loaded => {
1855 let entry = entry.unwrap();
1856 if entries.get(ix).map(|e| &e.path) == Some(&entry.path) {
1857 *entries.get_mut(ix).unwrap() = entry;
1858 } else {
1859 entries.insert(ix, entry);
1860 }
1861 }
1862 }
1863 }
1864
1865 let new_entries = tree.entries(true, 0).cloned().collect::<Vec<_>>();
1866 assert_eq!(entries, new_entries, "incorrect changes: {:?}", changes);
1867 }
1868 })
1869 .detach();
1870}
1871
1872fn randomly_mutate_worktree(
1873 worktree: &mut Worktree,
1874 rng: &mut impl Rng,
1875 cx: &mut Context<Worktree>,
1876) -> Task<Result<()>> {
1877 log::info!("mutating worktree");
1878 let worktree = worktree.as_local_mut().unwrap();
1879 let snapshot = worktree.snapshot();
1880 let entry = snapshot.entries(false, 0).choose(rng).unwrap();
1881
1882 match rng.gen_range(0_u32..100) {
1883 0..=33 if entry.path.as_ref() != Path::new("") => {
1884 log::info!("deleting entry {:?} ({})", entry.path, entry.id.0);
1885 worktree.delete_entry(entry.id, false, cx).unwrap()
1886 }
1887 ..=66 if entry.path.as_ref() != Path::new("") => {
1888 let other_entry = snapshot.entries(false, 0).choose(rng).unwrap();
1889 let new_parent_path = if other_entry.is_dir() {
1890 other_entry.path.clone()
1891 } else {
1892 other_entry.path.parent().unwrap().into()
1893 };
1894 let mut new_path = new_parent_path.join(random_filename(rng));
1895 if new_path.starts_with(&entry.path) {
1896 new_path = random_filename(rng).into();
1897 }
1898
1899 log::info!(
1900 "renaming entry {:?} ({}) to {:?}",
1901 entry.path,
1902 entry.id.0,
1903 new_path
1904 );
1905 let task = worktree.rename_entry(entry.id, new_path, cx);
1906 cx.background_spawn(async move {
1907 task.await?.to_included().unwrap();
1908 Ok(())
1909 })
1910 }
1911 _ => {
1912 if entry.is_dir() {
1913 let child_path = entry.path.join(random_filename(rng));
1914 let is_dir = rng.gen_bool(0.3);
1915 log::info!(
1916 "creating {} at {:?}",
1917 if is_dir { "dir" } else { "file" },
1918 child_path,
1919 );
1920 let task = worktree.create_entry(child_path, is_dir, cx);
1921 cx.background_spawn(async move {
1922 task.await?;
1923 Ok(())
1924 })
1925 } else {
1926 log::info!("overwriting file {:?} ({})", entry.path, entry.id.0);
1927 let task =
1928 worktree.write_file(entry.path.clone(), "".into(), Default::default(), cx);
1929 cx.background_spawn(async move {
1930 task.await?;
1931 Ok(())
1932 })
1933 }
1934 }
1935 }
1936}
1937
1938async fn randomly_mutate_fs(
1939 fs: &Arc<dyn Fs>,
1940 root_path: &Path,
1941 insertion_probability: f64,
1942 rng: &mut impl Rng,
1943) {
1944 log::info!("mutating fs");
1945 let mut files = Vec::new();
1946 let mut dirs = Vec::new();
1947 for path in fs.as_fake().paths(false) {
1948 if path.starts_with(root_path) {
1949 if fs.is_file(&path).await {
1950 files.push(path);
1951 } else {
1952 dirs.push(path);
1953 }
1954 }
1955 }
1956
1957 if (files.is_empty() && dirs.len() == 1) || rng.gen_bool(insertion_probability) {
1958 let path = dirs.choose(rng).unwrap();
1959 let new_path = path.join(random_filename(rng));
1960
1961 if rng.r#gen() {
1962 log::info!(
1963 "creating dir {:?}",
1964 new_path.strip_prefix(root_path).unwrap()
1965 );
1966 fs.create_dir(&new_path).await.unwrap();
1967 } else {
1968 log::info!(
1969 "creating file {:?}",
1970 new_path.strip_prefix(root_path).unwrap()
1971 );
1972 fs.create_file(&new_path, Default::default()).await.unwrap();
1973 }
1974 } else if rng.gen_bool(0.05) {
1975 let ignore_dir_path = dirs.choose(rng).unwrap();
1976 let ignore_path = ignore_dir_path.join(*GITIGNORE);
1977
1978 let subdirs = dirs
1979 .iter()
1980 .filter(|d| d.starts_with(ignore_dir_path))
1981 .cloned()
1982 .collect::<Vec<_>>();
1983 let subfiles = files
1984 .iter()
1985 .filter(|d| d.starts_with(ignore_dir_path))
1986 .cloned()
1987 .collect::<Vec<_>>();
1988 let files_to_ignore = {
1989 let len = rng.gen_range(0..=subfiles.len());
1990 subfiles.choose_multiple(rng, len)
1991 };
1992 let dirs_to_ignore = {
1993 let len = rng.gen_range(0..subdirs.len());
1994 subdirs.choose_multiple(rng, len)
1995 };
1996
1997 let mut ignore_contents = String::new();
1998 for path_to_ignore in files_to_ignore.chain(dirs_to_ignore) {
1999 writeln!(
2000 ignore_contents,
2001 "{}",
2002 path_to_ignore
2003 .strip_prefix(ignore_dir_path)
2004 .unwrap()
2005 .to_str()
2006 .unwrap()
2007 )
2008 .unwrap();
2009 }
2010 log::info!(
2011 "creating gitignore {:?} with contents:\n{}",
2012 ignore_path.strip_prefix(root_path).unwrap(),
2013 ignore_contents
2014 );
2015 fs.save(
2016 &ignore_path,
2017 &ignore_contents.as_str().into(),
2018 Default::default(),
2019 )
2020 .await
2021 .unwrap();
2022 } else {
2023 let old_path = {
2024 let file_path = files.choose(rng);
2025 let dir_path = dirs[1..].choose(rng);
2026 file_path.into_iter().chain(dir_path).choose(rng).unwrap()
2027 };
2028
2029 let is_rename = rng.r#gen();
2030 if is_rename {
2031 let new_path_parent = dirs
2032 .iter()
2033 .filter(|d| !d.starts_with(old_path))
2034 .choose(rng)
2035 .unwrap();
2036
2037 let overwrite_existing_dir =
2038 !old_path.starts_with(new_path_parent) && rng.gen_bool(0.3);
2039 let new_path = if overwrite_existing_dir {
2040 fs.remove_dir(
2041 new_path_parent,
2042 RemoveOptions {
2043 recursive: true,
2044 ignore_if_not_exists: true,
2045 },
2046 )
2047 .await
2048 .unwrap();
2049 new_path_parent.to_path_buf()
2050 } else {
2051 new_path_parent.join(random_filename(rng))
2052 };
2053
2054 log::info!(
2055 "renaming {:?} to {}{:?}",
2056 old_path.strip_prefix(root_path).unwrap(),
2057 if overwrite_existing_dir {
2058 "overwrite "
2059 } else {
2060 ""
2061 },
2062 new_path.strip_prefix(root_path).unwrap()
2063 );
2064 fs.rename(
2065 old_path,
2066 &new_path,
2067 fs::RenameOptions {
2068 overwrite: true,
2069 ignore_if_exists: true,
2070 },
2071 )
2072 .await
2073 .unwrap();
2074 } else if fs.is_file(old_path).await {
2075 log::info!(
2076 "deleting file {:?}",
2077 old_path.strip_prefix(root_path).unwrap()
2078 );
2079 fs.remove_file(old_path, Default::default()).await.unwrap();
2080 } else {
2081 log::info!(
2082 "deleting dir {:?}",
2083 old_path.strip_prefix(root_path).unwrap()
2084 );
2085 fs.remove_dir(
2086 old_path,
2087 RemoveOptions {
2088 recursive: true,
2089 ignore_if_not_exists: true,
2090 },
2091 )
2092 .await
2093 .unwrap();
2094 }
2095 }
2096}
2097
2098fn random_filename(rng: &mut impl Rng) -> String {
2099 (0..6)
2100 .map(|_| rng.sample(rand::distributions::Alphanumeric))
2101 .map(char::from)
2102 .collect()
2103}
2104
2105// NOTE:
2106// This test always fails on Windows, because on Windows, unlike on Unix, you can't rename
2107// a directory which some program has already open.
2108// This is a limitation of the Windows.
2109// See: https://stackoverflow.com/questions/41365318/access-is-denied-when-renaming-folder
2110#[gpui::test]
2111#[cfg_attr(target_os = "windows", ignore)]
2112async fn test_rename_work_directory(cx: &mut TestAppContext) {
2113 init_test(cx);
2114 cx.executor().allow_parking();
2115 let root = TempTree::new(json!({
2116 "projects": {
2117 "project1": {
2118 "a": "",
2119 "b": "",
2120 }
2121 },
2122
2123 }));
2124 let root_path = root.path();
2125
2126 let tree = Worktree::local(
2127 root_path,
2128 true,
2129 Arc::new(RealFs::new(None, cx.executor())),
2130 Default::default(),
2131 &mut cx.to_async(),
2132 )
2133 .await
2134 .unwrap();
2135
2136 let repo = git_init(&root_path.join("projects/project1"));
2137 git_add("a", &repo);
2138 git_commit("init", &repo);
2139 std::fs::write(root_path.join("projects/project1/a"), "aa").unwrap();
2140
2141 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2142 .await;
2143
2144 tree.flush_fs_events(cx).await;
2145
2146 cx.read(|cx| {
2147 let tree = tree.read(cx);
2148 let repo = tree.repositories.iter().next().unwrap();
2149 assert_eq!(
2150 repo.work_directory_abs_path,
2151 root_path.join("projects/project1")
2152 );
2153 assert_eq!(
2154 repo.status_for_path(&"a".into()).map(|entry| entry.status),
2155 Some(StatusCode::Modified.worktree()),
2156 );
2157 assert_eq!(
2158 repo.status_for_path(&"b".into()).map(|entry| entry.status),
2159 Some(FileStatus::Untracked),
2160 );
2161 });
2162
2163 std::fs::rename(
2164 root_path.join("projects/project1"),
2165 root_path.join("projects/project2"),
2166 )
2167 .unwrap();
2168 tree.flush_fs_events(cx).await;
2169
2170 cx.read(|cx| {
2171 let tree = tree.read(cx);
2172 let repo = tree.repositories.iter().next().unwrap();
2173 assert_eq!(
2174 repo.work_directory_abs_path,
2175 root_path.join("projects/project2")
2176 );
2177 assert_eq!(
2178 repo.status_for_path(&"a".into()).unwrap().status,
2179 StatusCode::Modified.worktree(),
2180 );
2181 assert_eq!(
2182 repo.status_for_path(&"b".into()).unwrap().status,
2183 FileStatus::Untracked,
2184 );
2185 });
2186}
2187
2188// NOTE: This test always fails on Windows, because on Windows, unlike on Unix,
2189// you can't rename a directory which some program has already open. This is a
2190// limitation of the Windows. See:
2191// https://stackoverflow.com/questions/41365318/access-is-denied-when-renaming-folder
2192#[gpui::test]
2193#[cfg_attr(target_os = "windows", ignore)]
2194async fn test_file_status(cx: &mut TestAppContext) {
2195 init_test(cx);
2196 cx.executor().allow_parking();
2197 const IGNORE_RULE: &str = "**/target";
2198
2199 let root = TempTree::new(json!({
2200 "project": {
2201 "a.txt": "a",
2202 "b.txt": "bb",
2203 "c": {
2204 "d": {
2205 "e.txt": "eee"
2206 }
2207 },
2208 "f.txt": "ffff",
2209 "target": {
2210 "build_file": "???"
2211 },
2212 ".gitignore": IGNORE_RULE
2213 },
2214
2215 }));
2216
2217 const A_TXT: &str = "a.txt";
2218 const B_TXT: &str = "b.txt";
2219 const E_TXT: &str = "c/d/e.txt";
2220 const F_TXT: &str = "f.txt";
2221 const DOTGITIGNORE: &str = ".gitignore";
2222 const BUILD_FILE: &str = "target/build_file";
2223
2224 // Set up git repository before creating the worktree.
2225 let work_dir = root.path().join("project");
2226 let mut repo = git_init(work_dir.as_path());
2227 repo.add_ignore_rule(IGNORE_RULE).unwrap();
2228 git_add(A_TXT, &repo);
2229 git_add(E_TXT, &repo);
2230 git_add(DOTGITIGNORE, &repo);
2231 git_commit("Initial commit", &repo);
2232
2233 let tree = Worktree::local(
2234 root.path(),
2235 true,
2236 Arc::new(RealFs::new(None, cx.executor())),
2237 Default::default(),
2238 &mut cx.to_async(),
2239 )
2240 .await
2241 .unwrap();
2242 let root_path = root.path();
2243
2244 tree.flush_fs_events(cx).await;
2245 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2246 .await;
2247 cx.executor().run_until_parked();
2248
2249 // Check that the right git state is observed on startup
2250 tree.read_with(cx, |tree, _cx| {
2251 let snapshot = tree.snapshot();
2252 assert_eq!(snapshot.repositories.iter().count(), 1);
2253 let repo_entry = snapshot.repositories.iter().next().unwrap();
2254 assert_eq!(
2255 repo_entry.work_directory_abs_path,
2256 root_path.join("project")
2257 );
2258
2259 assert_eq!(
2260 repo_entry.status_for_path(&B_TXT.into()).unwrap().status,
2261 FileStatus::Untracked,
2262 );
2263 assert_eq!(
2264 repo_entry.status_for_path(&F_TXT.into()).unwrap().status,
2265 FileStatus::Untracked,
2266 );
2267 });
2268
2269 // Modify a file in the working copy.
2270 std::fs::write(work_dir.join(A_TXT), "aa").unwrap();
2271 tree.flush_fs_events(cx).await;
2272 cx.executor().run_until_parked();
2273
2274 // The worktree detects that the file's git status has changed.
2275 tree.read_with(cx, |tree, _cx| {
2276 let snapshot = tree.snapshot();
2277 assert_eq!(snapshot.repositories.iter().count(), 1);
2278 let repo_entry = snapshot.repositories.iter().next().unwrap();
2279 assert_eq!(
2280 repo_entry.status_for_path(&A_TXT.into()).unwrap().status,
2281 StatusCode::Modified.worktree(),
2282 );
2283 });
2284
2285 // Create a commit in the git repository.
2286 git_add(A_TXT, &repo);
2287 git_add(B_TXT, &repo);
2288 git_commit("Committing modified and added", &repo);
2289 tree.flush_fs_events(cx).await;
2290 cx.executor().run_until_parked();
2291
2292 // The worktree detects that the files' git status have changed.
2293 tree.read_with(cx, |tree, _cx| {
2294 let snapshot = tree.snapshot();
2295 assert_eq!(snapshot.repositories.iter().count(), 1);
2296 let repo_entry = snapshot.repositories.iter().next().unwrap();
2297 assert_eq!(
2298 repo_entry.status_for_path(&F_TXT.into()).unwrap().status,
2299 FileStatus::Untracked,
2300 );
2301 assert_eq!(repo_entry.status_for_path(&B_TXT.into()), None);
2302 assert_eq!(repo_entry.status_for_path(&A_TXT.into()), None);
2303 });
2304
2305 // Modify files in the working copy and perform git operations on other files.
2306 git_reset(0, &repo);
2307 git_remove_index(Path::new(B_TXT), &repo);
2308 git_stash(&mut repo);
2309 std::fs::write(work_dir.join(E_TXT), "eeee").unwrap();
2310 std::fs::write(work_dir.join(BUILD_FILE), "this should be ignored").unwrap();
2311 tree.flush_fs_events(cx).await;
2312 cx.executor().run_until_parked();
2313
2314 // Check that more complex repo changes are tracked
2315 tree.read_with(cx, |tree, _cx| {
2316 let snapshot = tree.snapshot();
2317 assert_eq!(snapshot.repositories.iter().count(), 1);
2318 let repo_entry = snapshot.repositories.iter().next().unwrap();
2319
2320 assert_eq!(repo_entry.status_for_path(&A_TXT.into()), None);
2321 assert_eq!(
2322 repo_entry.status_for_path(&B_TXT.into()).unwrap().status,
2323 FileStatus::Untracked,
2324 );
2325 assert_eq!(
2326 repo_entry.status_for_path(&E_TXT.into()).unwrap().status,
2327 StatusCode::Modified.worktree(),
2328 );
2329 });
2330
2331 std::fs::remove_file(work_dir.join(B_TXT)).unwrap();
2332 std::fs::remove_dir_all(work_dir.join("c")).unwrap();
2333 std::fs::write(
2334 work_dir.join(DOTGITIGNORE),
2335 [IGNORE_RULE, "f.txt"].join("\n"),
2336 )
2337 .unwrap();
2338
2339 git_add(Path::new(DOTGITIGNORE), &repo);
2340 git_commit("Committing modified git ignore", &repo);
2341
2342 tree.flush_fs_events(cx).await;
2343 cx.executor().run_until_parked();
2344
2345 let mut renamed_dir_name = "first_directory/second_directory";
2346 const RENAMED_FILE: &str = "rf.txt";
2347
2348 std::fs::create_dir_all(work_dir.join(renamed_dir_name)).unwrap();
2349 std::fs::write(
2350 work_dir.join(renamed_dir_name).join(RENAMED_FILE),
2351 "new-contents",
2352 )
2353 .unwrap();
2354
2355 tree.flush_fs_events(cx).await;
2356 cx.executor().run_until_parked();
2357
2358 tree.read_with(cx, |tree, _cx| {
2359 let snapshot = tree.snapshot();
2360 assert_eq!(snapshot.repositories.iter().count(), 1);
2361 let repo_entry = snapshot.repositories.iter().next().unwrap();
2362 assert_eq!(
2363 repo_entry
2364 .status_for_path(&Path::new(renamed_dir_name).join(RENAMED_FILE).into())
2365 .unwrap()
2366 .status,
2367 FileStatus::Untracked,
2368 );
2369 });
2370
2371 renamed_dir_name = "new_first_directory/second_directory";
2372
2373 std::fs::rename(
2374 work_dir.join("first_directory"),
2375 work_dir.join("new_first_directory"),
2376 )
2377 .unwrap();
2378
2379 tree.flush_fs_events(cx).await;
2380 cx.executor().run_until_parked();
2381
2382 tree.read_with(cx, |tree, _cx| {
2383 let snapshot = tree.snapshot();
2384 assert_eq!(snapshot.repositories.iter().count(), 1);
2385 let repo_entry = snapshot.repositories.iter().next().unwrap();
2386
2387 assert_eq!(
2388 repo_entry
2389 .status_for_path(&Path::new(renamed_dir_name).join(RENAMED_FILE).into())
2390 .unwrap()
2391 .status,
2392 FileStatus::Untracked,
2393 );
2394 });
2395}
2396
2397#[gpui::test]
2398async fn test_git_repository_status(cx: &mut TestAppContext) {
2399 init_test(cx);
2400 cx.executor().allow_parking();
2401
2402 let root = TempTree::new(json!({
2403 "project": {
2404 "a.txt": "a", // Modified
2405 "b.txt": "bb", // Added
2406 "c.txt": "ccc", // Unchanged
2407 "d.txt": "dddd", // Deleted
2408 },
2409
2410 }));
2411
2412 // Set up git repository before creating the worktree.
2413 let work_dir = root.path().join("project");
2414 let repo = git_init(work_dir.as_path());
2415 git_add("a.txt", &repo);
2416 git_add("c.txt", &repo);
2417 git_add("d.txt", &repo);
2418 git_commit("Initial commit", &repo);
2419 std::fs::remove_file(work_dir.join("d.txt")).unwrap();
2420 std::fs::write(work_dir.join("a.txt"), "aa").unwrap();
2421
2422 let tree = Worktree::local(
2423 root.path(),
2424 true,
2425 Arc::new(RealFs::new(None, cx.executor())),
2426 Default::default(),
2427 &mut cx.to_async(),
2428 )
2429 .await
2430 .unwrap();
2431
2432 tree.flush_fs_events(cx).await;
2433 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2434 .await;
2435 cx.executor().run_until_parked();
2436
2437 // Check that the right git state is observed on startup
2438 tree.read_with(cx, |tree, _cx| {
2439 let snapshot = tree.snapshot();
2440 let repo = snapshot.repositories.iter().next().unwrap();
2441 let entries = repo.status().collect::<Vec<_>>();
2442
2443 assert_eq!(
2444 entries,
2445 [
2446 StatusEntry {
2447 repo_path: "a.txt".into(),
2448 status: StatusCode::Modified.worktree(),
2449 },
2450 StatusEntry {
2451 repo_path: "b.txt".into(),
2452 status: FileStatus::Untracked,
2453 },
2454 StatusEntry {
2455 repo_path: "d.txt".into(),
2456 status: StatusCode::Deleted.worktree(),
2457 },
2458 ]
2459 );
2460 });
2461
2462 std::fs::write(work_dir.join("c.txt"), "some changes").unwrap();
2463
2464 tree.flush_fs_events(cx).await;
2465 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2466 .await;
2467 cx.executor().run_until_parked();
2468
2469 tree.read_with(cx, |tree, _cx| {
2470 let snapshot = tree.snapshot();
2471 let repository = snapshot.repositories.iter().next().unwrap();
2472 let entries = repository.status().collect::<Vec<_>>();
2473
2474 assert_eq!(
2475 entries,
2476 [
2477 StatusEntry {
2478 repo_path: "a.txt".into(),
2479 status: StatusCode::Modified.worktree(),
2480 },
2481 StatusEntry {
2482 repo_path: "b.txt".into(),
2483 status: FileStatus::Untracked,
2484 },
2485 StatusEntry {
2486 repo_path: "c.txt".into(),
2487 status: StatusCode::Modified.worktree(),
2488 },
2489 StatusEntry {
2490 repo_path: "d.txt".into(),
2491 status: StatusCode::Deleted.worktree(),
2492 },
2493 ]
2494 );
2495 });
2496
2497 git_add("a.txt", &repo);
2498 git_add("c.txt", &repo);
2499 git_remove_index(Path::new("d.txt"), &repo);
2500 git_commit("Another commit", &repo);
2501 tree.flush_fs_events(cx).await;
2502 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2503 .await;
2504 cx.executor().run_until_parked();
2505
2506 std::fs::remove_file(work_dir.join("a.txt")).unwrap();
2507 std::fs::remove_file(work_dir.join("b.txt")).unwrap();
2508 tree.flush_fs_events(cx).await;
2509 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2510 .await;
2511 cx.executor().run_until_parked();
2512
2513 tree.read_with(cx, |tree, _cx| {
2514 let snapshot = tree.snapshot();
2515 let repo = snapshot.repositories.iter().next().unwrap();
2516 let entries = repo.status().collect::<Vec<_>>();
2517
2518 // Deleting an untracked entry, b.txt, should leave no status
2519 // a.txt was tracked, and so should have a status
2520 assert_eq!(
2521 entries,
2522 [StatusEntry {
2523 repo_path: "a.txt".into(),
2524 status: StatusCode::Deleted.worktree(),
2525 }]
2526 );
2527 });
2528}
2529
2530#[gpui::test]
2531async fn test_git_status_postprocessing(cx: &mut TestAppContext) {
2532 init_test(cx);
2533 cx.executor().allow_parking();
2534
2535 let root = TempTree::new(json!({
2536 "project": {
2537 "sub": {},
2538 "a.txt": "",
2539 },
2540 }));
2541
2542 let work_dir = root.path().join("project");
2543 let repo = git_init(work_dir.as_path());
2544 // a.txt exists in HEAD and the working copy but is deleted in the index.
2545 git_add("a.txt", &repo);
2546 git_commit("Initial commit", &repo);
2547 git_remove_index("a.txt".as_ref(), &repo);
2548 // `sub` is a nested git repository.
2549 let _sub = git_init(&work_dir.join("sub"));
2550
2551 let tree = Worktree::local(
2552 root.path(),
2553 true,
2554 Arc::new(RealFs::new(None, cx.executor())),
2555 Default::default(),
2556 &mut cx.to_async(),
2557 )
2558 .await
2559 .unwrap();
2560
2561 tree.flush_fs_events(cx).await;
2562 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2563 .await;
2564 cx.executor().run_until_parked();
2565
2566 tree.read_with(cx, |tree, _cx| {
2567 let snapshot = tree.snapshot();
2568 let repo = snapshot.repositories.iter().next().unwrap();
2569 let entries = repo.status().collect::<Vec<_>>();
2570
2571 // `sub` doesn't appear in our computed statuses.
2572 // a.txt appears with a combined `DA` status.
2573 assert_eq!(
2574 entries,
2575 [StatusEntry {
2576 repo_path: "a.txt".into(),
2577 status: TrackedStatus {
2578 index_status: StatusCode::Deleted,
2579 worktree_status: StatusCode::Added
2580 }
2581 .into(),
2582 }]
2583 )
2584 });
2585}
2586
2587#[gpui::test]
2588async fn test_repository_subfolder_git_status(cx: &mut TestAppContext) {
2589 init_test(cx);
2590 cx.executor().allow_parking();
2591
2592 let root = TempTree::new(json!({
2593 "my-repo": {
2594 // .git folder will go here
2595 "a.txt": "a",
2596 "sub-folder-1": {
2597 "sub-folder-2": {
2598 "c.txt": "cc",
2599 "d": {
2600 "e.txt": "eee"
2601 }
2602 },
2603 }
2604 },
2605
2606 }));
2607
2608 const C_TXT: &str = "sub-folder-1/sub-folder-2/c.txt";
2609 const E_TXT: &str = "sub-folder-1/sub-folder-2/d/e.txt";
2610
2611 // Set up git repository before creating the worktree.
2612 let git_repo_work_dir = root.path().join("my-repo");
2613 let repo = git_init(git_repo_work_dir.as_path());
2614 git_add(C_TXT, &repo);
2615 git_commit("Initial commit", &repo);
2616
2617 // Open the worktree in subfolder
2618 let project_root = Path::new("my-repo/sub-folder-1/sub-folder-2");
2619 let tree = Worktree::local(
2620 root.path().join(project_root),
2621 true,
2622 Arc::new(RealFs::new(None, cx.executor())),
2623 Default::default(),
2624 &mut cx.to_async(),
2625 )
2626 .await
2627 .unwrap();
2628
2629 tree.flush_fs_events(cx).await;
2630 tree.flush_fs_events_in_root_git_repository(cx).await;
2631 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2632 .await;
2633 cx.executor().run_until_parked();
2634
2635 // Ensure that the git status is loaded correctly
2636 tree.read_with(cx, |tree, _cx| {
2637 let snapshot = tree.snapshot();
2638 assert_eq!(snapshot.repositories.iter().count(), 1);
2639 let repo = snapshot.repositories.iter().next().unwrap();
2640 assert_eq!(
2641 repo.work_directory_abs_path.canonicalize().unwrap(),
2642 root.path().join("my-repo").canonicalize().unwrap()
2643 );
2644
2645 assert_eq!(repo.status_for_path(&C_TXT.into()), None);
2646 assert_eq!(
2647 repo.status_for_path(&E_TXT.into()).unwrap().status,
2648 FileStatus::Untracked
2649 );
2650 });
2651
2652 // Now we simulate FS events, but ONLY in the .git folder that's outside
2653 // of out project root.
2654 // Meaning: we don't produce any FS events for files inside the project.
2655 git_add(E_TXT, &repo);
2656 git_commit("Second commit", &repo);
2657 tree.flush_fs_events_in_root_git_repository(cx).await;
2658 cx.executor().run_until_parked();
2659
2660 tree.read_with(cx, |tree, _cx| {
2661 let snapshot = tree.snapshot();
2662 let repos = snapshot.repositories().iter().cloned().collect::<Vec<_>>();
2663 assert_eq!(repos.len(), 1);
2664 let repo_entry = repos.into_iter().next().unwrap();
2665
2666 assert!(snapshot.repositories.iter().next().is_some());
2667
2668 assert_eq!(repo_entry.status_for_path(&C_TXT.into()), None);
2669 assert_eq!(repo_entry.status_for_path(&E_TXT.into()), None);
2670 });
2671}
2672
2673#[gpui::test]
2674async fn test_conflicted_cherry_pick(cx: &mut TestAppContext) {
2675 init_test(cx);
2676 cx.executor().allow_parking();
2677
2678 let root = TempTree::new(json!({
2679 "project": {
2680 "a.txt": "a",
2681 },
2682 }));
2683 let root_path = root.path();
2684
2685 let tree = Worktree::local(
2686 root_path,
2687 true,
2688 Arc::new(RealFs::new(None, cx.executor())),
2689 Default::default(),
2690 &mut cx.to_async(),
2691 )
2692 .await
2693 .unwrap();
2694
2695 let repo = git_init(&root_path.join("project"));
2696 git_add("a.txt", &repo);
2697 git_commit("init", &repo);
2698
2699 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2700 .await;
2701
2702 tree.flush_fs_events(cx).await;
2703
2704 git_branch("other-branch", &repo);
2705 git_checkout("refs/heads/other-branch", &repo);
2706 std::fs::write(root_path.join("project/a.txt"), "A").unwrap();
2707 git_add("a.txt", &repo);
2708 git_commit("capitalize", &repo);
2709 let commit = repo
2710 .head()
2711 .expect("Failed to get HEAD")
2712 .peel_to_commit()
2713 .expect("HEAD is not a commit");
2714 git_checkout("refs/heads/main", &repo);
2715 std::fs::write(root_path.join("project/a.txt"), "b").unwrap();
2716 git_add("a.txt", &repo);
2717 git_commit("improve letter", &repo);
2718 git_cherry_pick(&commit, &repo);
2719 std::fs::read_to_string(root_path.join("project/.git/CHERRY_PICK_HEAD"))
2720 .expect("No CHERRY_PICK_HEAD");
2721 pretty_assertions::assert_eq!(
2722 git_status(&repo),
2723 collections::HashMap::from_iter([("a.txt".to_owned(), git2::Status::CONFLICTED)])
2724 );
2725 tree.flush_fs_events(cx).await;
2726 let conflicts = tree.update(cx, |tree, _| {
2727 let entry = tree.repositories.first().expect("No git entry").clone();
2728 entry
2729 .current_merge_conflicts
2730 .iter()
2731 .cloned()
2732 .collect::<Vec<_>>()
2733 });
2734 pretty_assertions::assert_eq!(conflicts, [RepoPath::from("a.txt")]);
2735
2736 git_add("a.txt", &repo);
2737 // Attempt to manually simulate what `git cherry-pick --continue` would do.
2738 git_commit("whatevs", &repo);
2739 std::fs::remove_file(root.path().join("project/.git/CHERRY_PICK_HEAD"))
2740 .expect("Failed to remove CHERRY_PICK_HEAD");
2741 pretty_assertions::assert_eq!(git_status(&repo), collections::HashMap::default());
2742 tree.flush_fs_events(cx).await;
2743 let conflicts = tree.update(cx, |tree, _| {
2744 let entry = tree.repositories.first().expect("No git entry").clone();
2745 entry
2746 .current_merge_conflicts
2747 .iter()
2748 .cloned()
2749 .collect::<Vec<_>>()
2750 });
2751 pretty_assertions::assert_eq!(conflicts, []);
2752}
2753
2754#[gpui::test]
2755async fn test_private_single_file_worktree(cx: &mut TestAppContext) {
2756 init_test(cx);
2757 let fs = FakeFs::new(cx.background_executor.clone());
2758 fs.insert_tree("/", json!({".env": "PRIVATE=secret\n"}))
2759 .await;
2760 let tree = Worktree::local(
2761 Path::new("/.env"),
2762 true,
2763 fs.clone(),
2764 Default::default(),
2765 &mut cx.to_async(),
2766 )
2767 .await
2768 .unwrap();
2769 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2770 .await;
2771 tree.read_with(cx, |tree, _| {
2772 let entry = tree.entry_for_path("").unwrap();
2773 assert!(entry.is_private);
2774 });
2775}
2776
2777#[gpui::test]
2778fn test_unrelativize() {
2779 let work_directory = WorkDirectory::in_project("");
2780 pretty_assertions::assert_eq!(
2781 work_directory.try_unrelativize(&"crates/gpui/gpui.rs".into()),
2782 Some(Path::new("crates/gpui/gpui.rs").into())
2783 );
2784
2785 let work_directory = WorkDirectory::in_project("vendor/some-submodule");
2786 pretty_assertions::assert_eq!(
2787 work_directory.try_unrelativize(&"src/thing.c".into()),
2788 Some(Path::new("vendor/some-submodule/src/thing.c").into())
2789 );
2790
2791 let work_directory = WorkDirectory::AboveProject {
2792 absolute_path: Path::new("/projects/zed").into(),
2793 location_in_repo: Path::new("crates/gpui").into(),
2794 };
2795
2796 pretty_assertions::assert_eq!(
2797 work_directory.try_unrelativize(&"crates/util/util.rs".into()),
2798 None,
2799 );
2800
2801 pretty_assertions::assert_eq!(
2802 work_directory.unrelativize(&"crates/util/util.rs".into()),
2803 Path::new("../util/util.rs").into()
2804 );
2805
2806 pretty_assertions::assert_eq!(work_directory.try_unrelativize(&"README.md".into()), None,);
2807
2808 pretty_assertions::assert_eq!(
2809 work_directory.unrelativize(&"README.md".into()),
2810 Path::new("../../README.md").into()
2811 );
2812}
2813
2814#[track_caller]
2815fn git_init(path: &Path) -> git2::Repository {
2816 let mut init_opts = RepositoryInitOptions::new();
2817 init_opts.initial_head("main");
2818 git2::Repository::init_opts(path, &init_opts).expect("Failed to initialize git repository")
2819}
2820
2821#[track_caller]
2822fn git_add<P: AsRef<Path>>(path: P, repo: &git2::Repository) {
2823 let path = path.as_ref();
2824 let mut index = repo.index().expect("Failed to get index");
2825 index.add_path(path).expect("Failed to add file");
2826 index.write().expect("Failed to write index");
2827}
2828
2829#[track_caller]
2830fn git_remove_index(path: &Path, repo: &git2::Repository) {
2831 let mut index = repo.index().expect("Failed to get index");
2832 index.remove_path(path).expect("Failed to add file");
2833 index.write().expect("Failed to write index");
2834}
2835
2836#[track_caller]
2837fn git_commit(msg: &'static str, repo: &git2::Repository) {
2838 use git2::Signature;
2839
2840 let signature = Signature::now("test", "test@zed.dev").unwrap();
2841 let oid = repo.index().unwrap().write_tree().unwrap();
2842 let tree = repo.find_tree(oid).unwrap();
2843 if let Ok(head) = repo.head() {
2844 let parent_obj = head.peel(git2::ObjectType::Commit).unwrap();
2845
2846 let parent_commit = parent_obj.as_commit().unwrap();
2847
2848 repo.commit(
2849 Some("HEAD"),
2850 &signature,
2851 &signature,
2852 msg,
2853 &tree,
2854 &[parent_commit],
2855 )
2856 .expect("Failed to commit with parent");
2857 } else {
2858 repo.commit(Some("HEAD"), &signature, &signature, msg, &tree, &[])
2859 .expect("Failed to commit");
2860 }
2861}
2862
2863#[track_caller]
2864fn git_cherry_pick(commit: &git2::Commit<'_>, repo: &git2::Repository) {
2865 repo.cherrypick(commit, None).expect("Failed to cherrypick");
2866}
2867
2868#[track_caller]
2869fn git_stash(repo: &mut git2::Repository) {
2870 use git2::Signature;
2871
2872 let signature = Signature::now("test", "test@zed.dev").unwrap();
2873 repo.stash_save(&signature, "N/A", None)
2874 .expect("Failed to stash");
2875}
2876
2877#[track_caller]
2878fn git_reset(offset: usize, repo: &git2::Repository) {
2879 let head = repo.head().expect("Couldn't get repo head");
2880 let object = head.peel(git2::ObjectType::Commit).unwrap();
2881 let commit = object.as_commit().unwrap();
2882 let new_head = commit
2883 .parents()
2884 .inspect(|parnet| {
2885 parnet.message();
2886 })
2887 .nth(offset)
2888 .expect("Not enough history");
2889 repo.reset(new_head.as_object(), git2::ResetType::Soft, None)
2890 .expect("Could not reset");
2891}
2892
2893#[track_caller]
2894fn git_branch(name: &str, repo: &git2::Repository) {
2895 let head = repo
2896 .head()
2897 .expect("Couldn't get repo head")
2898 .peel_to_commit()
2899 .expect("HEAD is not a commit");
2900 repo.branch(name, &head, false).expect("Failed to commit");
2901}
2902
2903#[track_caller]
2904fn git_checkout(name: &str, repo: &git2::Repository) {
2905 repo.set_head(name).expect("Failed to set head");
2906 repo.checkout_head(None).expect("Failed to check out head");
2907}
2908
2909#[track_caller]
2910fn git_status(repo: &git2::Repository) -> collections::HashMap<String, git2::Status> {
2911 repo.statuses(None)
2912 .unwrap()
2913 .iter()
2914 .map(|status| (status.path().unwrap().to_string(), status.status()))
2915 .collect()
2916}
2917
2918#[track_caller]
2919fn check_worktree_entries(
2920 tree: &Worktree,
2921 expected_excluded_paths: &[&str],
2922 expected_ignored_paths: &[&str],
2923 expected_tracked_paths: &[&str],
2924 expected_included_paths: &[&str],
2925) {
2926 for path in expected_excluded_paths {
2927 let entry = tree.entry_for_path(path);
2928 assert!(
2929 entry.is_none(),
2930 "expected path '{path}' to be excluded, but got entry: {entry:?}",
2931 );
2932 }
2933 for path in expected_ignored_paths {
2934 let entry = tree
2935 .entry_for_path(path)
2936 .unwrap_or_else(|| panic!("Missing entry for expected ignored path '{path}'"));
2937 assert!(
2938 entry.is_ignored,
2939 "expected path '{path}' to be ignored, but got entry: {entry:?}",
2940 );
2941 }
2942 for path in expected_tracked_paths {
2943 let entry = tree
2944 .entry_for_path(path)
2945 .unwrap_or_else(|| panic!("Missing entry for expected tracked path '{path}'"));
2946 assert!(
2947 !entry.is_ignored || entry.is_always_included,
2948 "expected path '{path}' to be tracked, but got entry: {entry:?}",
2949 );
2950 }
2951 for path in expected_included_paths {
2952 let entry = tree
2953 .entry_for_path(path)
2954 .unwrap_or_else(|| panic!("Missing entry for expected included path '{path}'"));
2955 assert!(
2956 entry.is_always_included,
2957 "expected path '{path}' to always be included, but got entry: {entry:?}",
2958 );
2959 }
2960}
2961
2962fn init_test(cx: &mut gpui::TestAppContext) {
2963 if std::env::var("RUST_LOG").is_ok() {
2964 env_logger::try_init().ok();
2965 }
2966
2967 cx.update(|cx| {
2968 let settings_store = SettingsStore::test(cx);
2969 cx.set_global(settings_store);
2970 WorktreeSettings::register(cx);
2971 });
2972}
2973
2974#[track_caller]
2975fn assert_entry_git_state(
2976 tree: &Worktree,
2977 path: &str,
2978 index_status: Option<StatusCode>,
2979 is_ignored: bool,
2980) {
2981 let entry = tree.entry_for_path(path).expect("entry {path} not found");
2982 let repos = tree.repositories().iter().cloned().collect::<Vec<_>>();
2983 assert_eq!(repos.len(), 1);
2984 let repo_entry = repos.into_iter().next().unwrap();
2985 let status = repo_entry
2986 .status_for_path(&path.into())
2987 .map(|entry| entry.status);
2988 let expected = index_status.map(|index_status| {
2989 TrackedStatus {
2990 index_status,
2991 worktree_status: StatusCode::Unmodified,
2992 }
2993 .into()
2994 });
2995 assert_eq!(
2996 status, expected,
2997 "expected {path} to have git status: {expected:?}"
2998 );
2999 assert_eq!(
3000 entry.is_ignored, is_ignored,
3001 "expected {path} to have is_ignored: {is_ignored}"
3002 );
3003}