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