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