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