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