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_exclusions(cx: &mut TestAppContext) {
883 init_test(cx);
884 cx.executor().allow_parking();
885 let dir = temp_tree(json!({
886 ".gitignore": "**/target\n/node_modules\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 ".DS_Store": "",
908 }));
909 cx.update(|cx| {
910 cx.update_global::<SettingsStore, _>(|store, cx| {
911 store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
912 project_settings.file_scan_exclusions =
913 Some(vec!["**/foo/**".to_string(), "**/.DS_Store".to_string()]);
914 });
915 });
916 });
917
918 let tree = Worktree::local(
919 dir.path(),
920 true,
921 Arc::new(RealFs::default()),
922 Default::default(),
923 &mut cx.to_async(),
924 )
925 .await
926 .unwrap();
927 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
928 .await;
929 tree.flush_fs_events(cx).await;
930 tree.read_with(cx, |tree, _| {
931 check_worktree_entries(
932 tree,
933 &[
934 "src/foo/foo.rs",
935 "src/foo/another.rs",
936 "node_modules/.DS_Store",
937 "src/.DS_Store",
938 ".DS_Store",
939 ],
940 &["target", "node_modules"],
941 &["src/lib.rs", "src/bar/bar.rs", ".gitignore"],
942 )
943 });
944
945 cx.update(|cx| {
946 cx.update_global::<SettingsStore, _>(|store, cx| {
947 store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
948 project_settings.file_scan_exclusions =
949 Some(vec!["**/node_modules/**".to_string()]);
950 });
951 });
952 });
953 tree.flush_fs_events(cx).await;
954 cx.executor().run_until_parked();
955 tree.read_with(cx, |tree, _| {
956 check_worktree_entries(
957 tree,
958 &[
959 "node_modules/prettier/package.json",
960 "node_modules/.DS_Store",
961 "node_modules",
962 ],
963 &["target"],
964 &[
965 ".gitignore",
966 "src/lib.rs",
967 "src/bar/bar.rs",
968 "src/foo/foo.rs",
969 "src/foo/another.rs",
970 "src/.DS_Store",
971 ".DS_Store",
972 ],
973 )
974 });
975}
976
977#[gpui::test]
978async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) {
979 init_test(cx);
980 cx.executor().allow_parking();
981 let dir = temp_tree(json!({
982 ".git": {
983 "HEAD": "ref: refs/heads/main\n",
984 "foo": "bar",
985 },
986 ".gitignore": "**/target\n/node_modules\ntest_output\n",
987 "target": {
988 "index": "blah2"
989 },
990 "node_modules": {
991 ".DS_Store": "",
992 "prettier": {
993 "package.json": "{}",
994 },
995 },
996 "src": {
997 ".DS_Store": "",
998 "foo": {
999 "foo.rs": "mod another;\n",
1000 "another.rs": "// another",
1001 },
1002 "bar": {
1003 "bar.rs": "// bar",
1004 },
1005 "lib.rs": "mod foo;\nmod bar;\n",
1006 },
1007 ".DS_Store": "",
1008 }));
1009 cx.update(|cx| {
1010 cx.update_global::<SettingsStore, _>(|store, cx| {
1011 store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
1012 project_settings.file_scan_exclusions = Some(vec![
1013 "**/.git".to_string(),
1014 "node_modules/".to_string(),
1015 "build_output".to_string(),
1016 ]);
1017 });
1018 });
1019 });
1020
1021 let tree = Worktree::local(
1022 dir.path(),
1023 true,
1024 Arc::new(RealFs::default()),
1025 Default::default(),
1026 &mut cx.to_async(),
1027 )
1028 .await
1029 .unwrap();
1030 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1031 .await;
1032 tree.flush_fs_events(cx).await;
1033 tree.read_with(cx, |tree, _| {
1034 check_worktree_entries(
1035 tree,
1036 &[
1037 ".git/HEAD",
1038 ".git/foo",
1039 "node_modules",
1040 "node_modules/.DS_Store",
1041 "node_modules/prettier",
1042 "node_modules/prettier/package.json",
1043 ],
1044 &["target"],
1045 &[
1046 ".DS_Store",
1047 "src/.DS_Store",
1048 "src/lib.rs",
1049 "src/foo/foo.rs",
1050 "src/foo/another.rs",
1051 "src/bar/bar.rs",
1052 ".gitignore",
1053 ],
1054 )
1055 });
1056
1057 let new_excluded_dir = dir.path().join("build_output");
1058 let new_ignored_dir = dir.path().join("test_output");
1059 std::fs::create_dir_all(&new_excluded_dir)
1060 .unwrap_or_else(|e| panic!("Failed to create a {new_excluded_dir:?} directory: {e}"));
1061 std::fs::create_dir_all(&new_ignored_dir)
1062 .unwrap_or_else(|e| panic!("Failed to create a {new_ignored_dir:?} directory: {e}"));
1063 let node_modules_dir = dir.path().join("node_modules");
1064 let dot_git_dir = dir.path().join(".git");
1065 let src_dir = dir.path().join("src");
1066 for existing_dir in [&node_modules_dir, &dot_git_dir, &src_dir] {
1067 assert!(
1068 existing_dir.is_dir(),
1069 "Expect {existing_dir:?} to be present in the FS already"
1070 );
1071 }
1072
1073 for directory_for_new_file in [
1074 new_excluded_dir,
1075 new_ignored_dir,
1076 node_modules_dir,
1077 dot_git_dir,
1078 src_dir,
1079 ] {
1080 std::fs::write(directory_for_new_file.join("new_file"), "new file contents")
1081 .unwrap_or_else(|e| {
1082 panic!("Failed to create in {directory_for_new_file:?} a new file: {e}")
1083 });
1084 }
1085 tree.flush_fs_events(cx).await;
1086
1087 tree.read_with(cx, |tree, _| {
1088 check_worktree_entries(
1089 tree,
1090 &[
1091 ".git/HEAD",
1092 ".git/foo",
1093 ".git/new_file",
1094 "node_modules",
1095 "node_modules/.DS_Store",
1096 "node_modules/prettier",
1097 "node_modules/prettier/package.json",
1098 "node_modules/new_file",
1099 "build_output",
1100 "build_output/new_file",
1101 "test_output/new_file",
1102 ],
1103 &["target", "test_output"],
1104 &[
1105 ".DS_Store",
1106 "src/.DS_Store",
1107 "src/lib.rs",
1108 "src/foo/foo.rs",
1109 "src/foo/another.rs",
1110 "src/bar/bar.rs",
1111 "src/new_file",
1112 ".gitignore",
1113 ],
1114 )
1115 });
1116}
1117
1118#[gpui::test]
1119async fn test_fs_events_in_dot_git_worktree(cx: &mut TestAppContext) {
1120 init_test(cx);
1121 cx.executor().allow_parking();
1122 let dir = temp_tree(json!({
1123 ".git": {
1124 "HEAD": "ref: refs/heads/main\n",
1125 "foo": "foo contents",
1126 },
1127 }));
1128 let dot_git_worktree_dir = dir.path().join(".git");
1129
1130 let tree = Worktree::local(
1131 dot_git_worktree_dir.clone(),
1132 true,
1133 Arc::new(RealFs::default()),
1134 Default::default(),
1135 &mut cx.to_async(),
1136 )
1137 .await
1138 .unwrap();
1139 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1140 .await;
1141 tree.flush_fs_events(cx).await;
1142 tree.read_with(cx, |tree, _| {
1143 check_worktree_entries(tree, &[], &["HEAD", "foo"], &[])
1144 });
1145
1146 std::fs::write(dot_git_worktree_dir.join("new_file"), "new file contents")
1147 .unwrap_or_else(|e| panic!("Failed to create in {dot_git_worktree_dir:?} a new file: {e}"));
1148 tree.flush_fs_events(cx).await;
1149 tree.read_with(cx, |tree, _| {
1150 check_worktree_entries(tree, &[], &["HEAD", "foo", "new_file"], &[])
1151 });
1152}
1153
1154#[gpui::test(iterations = 30)]
1155async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
1156 init_test(cx);
1157 let fs = FakeFs::new(cx.background_executor.clone());
1158 fs.insert_tree(
1159 "/root",
1160 json!({
1161 "b": {},
1162 "c": {},
1163 "d": {},
1164 }),
1165 )
1166 .await;
1167
1168 let tree = Worktree::local(
1169 "/root".as_ref(),
1170 true,
1171 fs,
1172 Default::default(),
1173 &mut cx.to_async(),
1174 )
1175 .await
1176 .unwrap();
1177
1178 let snapshot1 = tree.update(cx, |tree, cx| {
1179 let tree = tree.as_local_mut().unwrap();
1180 let snapshot = Arc::new(Mutex::new(tree.snapshot()));
1181 tree.observe_updates(0, cx, {
1182 let snapshot = snapshot.clone();
1183 move |update| {
1184 snapshot.lock().apply_remote_update(update).unwrap();
1185 async { true }
1186 }
1187 });
1188 snapshot
1189 });
1190
1191 let entry = tree
1192 .update(cx, |tree, cx| {
1193 tree.as_local_mut()
1194 .unwrap()
1195 .create_entry("a/e".as_ref(), true, cx)
1196 })
1197 .await
1198 .unwrap()
1199 .to_included()
1200 .unwrap();
1201 assert!(entry.is_dir());
1202
1203 cx.executor().run_until_parked();
1204 tree.read_with(cx, |tree, _| {
1205 assert_eq!(tree.entry_for_path("a/e").unwrap().kind, EntryKind::Dir);
1206 });
1207
1208 let snapshot2 = tree.update(cx, |tree, _| tree.as_local().unwrap().snapshot());
1209 assert_eq!(
1210 snapshot1.lock().entries(true, 0).collect::<Vec<_>>(),
1211 snapshot2.entries(true, 0).collect::<Vec<_>>()
1212 );
1213}
1214
1215#[gpui::test]
1216async fn test_bump_mtime_of_git_repo_workdir(cx: &mut TestAppContext) {
1217 init_test(cx);
1218
1219 // Create a worktree with a git directory.
1220 let fs = FakeFs::new(cx.background_executor.clone());
1221 fs.insert_tree(
1222 "/root",
1223 json!({
1224 ".git": {},
1225 "a.txt": "",
1226 "b": {
1227 "c.txt": "",
1228 },
1229 }),
1230 )
1231 .await;
1232
1233 let tree = Worktree::local(
1234 "/root".as_ref(),
1235 true,
1236 fs.clone(),
1237 Default::default(),
1238 &mut cx.to_async(),
1239 )
1240 .await
1241 .unwrap();
1242 cx.executor().run_until_parked();
1243
1244 let (old_entry_ids, old_mtimes) = tree.read_with(cx, |tree, _| {
1245 (
1246 tree.entries(true, 0).map(|e| e.id).collect::<Vec<_>>(),
1247 tree.entries(true, 0).map(|e| e.mtime).collect::<Vec<_>>(),
1248 )
1249 });
1250
1251 // Regression test: after the directory is scanned, touch the git repo's
1252 // working directory, bumping its mtime. That directory keeps its project
1253 // entry id after the directories are re-scanned.
1254 fs.touch_path("/root").await;
1255 cx.executor().run_until_parked();
1256
1257 let (new_entry_ids, new_mtimes) = tree.read_with(cx, |tree, _| {
1258 (
1259 tree.entries(true, 0).map(|e| e.id).collect::<Vec<_>>(),
1260 tree.entries(true, 0).map(|e| e.mtime).collect::<Vec<_>>(),
1261 )
1262 });
1263 assert_eq!(new_entry_ids, old_entry_ids);
1264 assert_ne!(new_mtimes, old_mtimes);
1265
1266 // Regression test: changes to the git repository should still be
1267 // detected.
1268 fs.set_status_for_repo_via_git_operation(
1269 Path::new("/root/.git"),
1270 &[(Path::new("b/c.txt"), GitFileStatus::Modified)],
1271 );
1272 cx.executor().run_until_parked();
1273
1274 let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
1275 check_propagated_statuses(
1276 &snapshot,
1277 &[
1278 (Path::new(""), Some(GitFileStatus::Modified)),
1279 (Path::new("a.txt"), None),
1280 (Path::new("b/c.txt"), Some(GitFileStatus::Modified)),
1281 ],
1282 );
1283}
1284
1285#[gpui::test]
1286async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
1287 init_test(cx);
1288 cx.executor().allow_parking();
1289
1290 let fs_fake = FakeFs::new(cx.background_executor.clone());
1291 fs_fake
1292 .insert_tree(
1293 "/root",
1294 json!({
1295 "a": {},
1296 }),
1297 )
1298 .await;
1299
1300 let tree_fake = Worktree::local(
1301 "/root".as_ref(),
1302 true,
1303 fs_fake,
1304 Default::default(),
1305 &mut cx.to_async(),
1306 )
1307 .await
1308 .unwrap();
1309
1310 let entry = tree_fake
1311 .update(cx, |tree, cx| {
1312 tree.as_local_mut()
1313 .unwrap()
1314 .create_entry("a/b/c/d.txt".as_ref(), false, cx)
1315 })
1316 .await
1317 .unwrap()
1318 .to_included()
1319 .unwrap();
1320 assert!(entry.is_file());
1321
1322 cx.executor().run_until_parked();
1323 tree_fake.read_with(cx, |tree, _| {
1324 assert!(tree.entry_for_path("a/b/c/d.txt").unwrap().is_file());
1325 assert!(tree.entry_for_path("a/b/c/").unwrap().is_dir());
1326 assert!(tree.entry_for_path("a/b/").unwrap().is_dir());
1327 });
1328
1329 let fs_real = Arc::new(RealFs::default());
1330 let temp_root = temp_tree(json!({
1331 "a": {}
1332 }));
1333
1334 let tree_real = Worktree::local(
1335 temp_root.path(),
1336 true,
1337 fs_real,
1338 Default::default(),
1339 &mut cx.to_async(),
1340 )
1341 .await
1342 .unwrap();
1343
1344 let entry = tree_real
1345 .update(cx, |tree, cx| {
1346 tree.as_local_mut()
1347 .unwrap()
1348 .create_entry("a/b/c/d.txt".as_ref(), false, cx)
1349 })
1350 .await
1351 .unwrap()
1352 .to_included()
1353 .unwrap();
1354 assert!(entry.is_file());
1355
1356 cx.executor().run_until_parked();
1357 tree_real.read_with(cx, |tree, _| {
1358 assert!(tree.entry_for_path("a/b/c/d.txt").unwrap().is_file());
1359 assert!(tree.entry_for_path("a/b/c/").unwrap().is_dir());
1360 assert!(tree.entry_for_path("a/b/").unwrap().is_dir());
1361 });
1362
1363 // Test smallest change
1364 let entry = tree_real
1365 .update(cx, |tree, cx| {
1366 tree.as_local_mut()
1367 .unwrap()
1368 .create_entry("a/b/c/e.txt".as_ref(), false, cx)
1369 })
1370 .await
1371 .unwrap()
1372 .to_included()
1373 .unwrap();
1374 assert!(entry.is_file());
1375
1376 cx.executor().run_until_parked();
1377 tree_real.read_with(cx, |tree, _| {
1378 assert!(tree.entry_for_path("a/b/c/e.txt").unwrap().is_file());
1379 });
1380
1381 // Test largest change
1382 let entry = tree_real
1383 .update(cx, |tree, cx| {
1384 tree.as_local_mut()
1385 .unwrap()
1386 .create_entry("d/e/f/g.txt".as_ref(), false, cx)
1387 })
1388 .await
1389 .unwrap()
1390 .to_included()
1391 .unwrap();
1392 assert!(entry.is_file());
1393
1394 cx.executor().run_until_parked();
1395 tree_real.read_with(cx, |tree, _| {
1396 assert!(tree.entry_for_path("d/e/f/g.txt").unwrap().is_file());
1397 assert!(tree.entry_for_path("d/e/f").unwrap().is_dir());
1398 assert!(tree.entry_for_path("d/e/").unwrap().is_dir());
1399 assert!(tree.entry_for_path("d/").unwrap().is_dir());
1400 });
1401}
1402
1403#[gpui::test(iterations = 100)]
1404async fn test_random_worktree_operations_during_initial_scan(
1405 cx: &mut TestAppContext,
1406 mut rng: StdRng,
1407) {
1408 init_test(cx);
1409 let operations = env::var("OPERATIONS")
1410 .map(|o| o.parse().unwrap())
1411 .unwrap_or(5);
1412 let initial_entries = env::var("INITIAL_ENTRIES")
1413 .map(|o| o.parse().unwrap())
1414 .unwrap_or(20);
1415
1416 let root_dir = Path::new("/test");
1417 let fs = FakeFs::new(cx.background_executor.clone()) as Arc<dyn Fs>;
1418 fs.as_fake().insert_tree(root_dir, json!({})).await;
1419 for _ in 0..initial_entries {
1420 randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
1421 }
1422 log::info!("generated initial tree");
1423
1424 let worktree = Worktree::local(
1425 root_dir,
1426 true,
1427 fs.clone(),
1428 Default::default(),
1429 &mut cx.to_async(),
1430 )
1431 .await
1432 .unwrap();
1433
1434 let mut snapshots = vec![worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot())];
1435 let updates = Arc::new(Mutex::new(Vec::new()));
1436 worktree.update(cx, |tree, cx| {
1437 check_worktree_change_events(tree, cx);
1438
1439 tree.as_local_mut().unwrap().observe_updates(0, cx, {
1440 let updates = updates.clone();
1441 move |update| {
1442 updates.lock().push(update);
1443 async { true }
1444 }
1445 });
1446 });
1447
1448 for _ in 0..operations {
1449 worktree
1450 .update(cx, |worktree, cx| {
1451 randomly_mutate_worktree(worktree, &mut rng, cx)
1452 })
1453 .await
1454 .log_err();
1455 worktree.read_with(cx, |tree, _| {
1456 tree.as_local().unwrap().snapshot().check_invariants(true)
1457 });
1458
1459 if rng.gen_bool(0.6) {
1460 snapshots.push(worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot()));
1461 }
1462 }
1463
1464 worktree
1465 .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
1466 .await;
1467
1468 cx.executor().run_until_parked();
1469
1470 let final_snapshot = worktree.read_with(cx, |tree, _| {
1471 let tree = tree.as_local().unwrap();
1472 let snapshot = tree.snapshot();
1473 snapshot.check_invariants(true);
1474 snapshot
1475 });
1476
1477 for (i, snapshot) in snapshots.into_iter().enumerate().rev() {
1478 let mut updated_snapshot = snapshot.clone();
1479 for update in updates.lock().iter() {
1480 if update.scan_id >= updated_snapshot.scan_id() as u64 {
1481 updated_snapshot
1482 .apply_remote_update(update.clone())
1483 .unwrap();
1484 }
1485 }
1486
1487 assert_eq!(
1488 updated_snapshot.entries(true, 0).collect::<Vec<_>>(),
1489 final_snapshot.entries(true, 0).collect::<Vec<_>>(),
1490 "wrong updates after snapshot {i}: {snapshot:#?} {updates:#?}",
1491 );
1492 }
1493}
1494
1495#[gpui::test(iterations = 100)]
1496async fn test_random_worktree_changes(cx: &mut TestAppContext, mut rng: StdRng) {
1497 init_test(cx);
1498 let operations = env::var("OPERATIONS")
1499 .map(|o| o.parse().unwrap())
1500 .unwrap_or(40);
1501 let initial_entries = env::var("INITIAL_ENTRIES")
1502 .map(|o| o.parse().unwrap())
1503 .unwrap_or(20);
1504
1505 let root_dir = Path::new("/test");
1506 let fs = FakeFs::new(cx.background_executor.clone()) as Arc<dyn Fs>;
1507 fs.as_fake().insert_tree(root_dir, json!({})).await;
1508 for _ in 0..initial_entries {
1509 randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
1510 }
1511 log::info!("generated initial tree");
1512
1513 let worktree = Worktree::local(
1514 root_dir,
1515 true,
1516 fs.clone(),
1517 Default::default(),
1518 &mut cx.to_async(),
1519 )
1520 .await
1521 .unwrap();
1522
1523 let updates = Arc::new(Mutex::new(Vec::new()));
1524 worktree.update(cx, |tree, cx| {
1525 check_worktree_change_events(tree, cx);
1526
1527 tree.as_local_mut().unwrap().observe_updates(0, cx, {
1528 let updates = updates.clone();
1529 move |update| {
1530 updates.lock().push(update);
1531 async { true }
1532 }
1533 });
1534 });
1535
1536 worktree
1537 .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
1538 .await;
1539
1540 fs.as_fake().pause_events();
1541 let mut snapshots = Vec::new();
1542 let mut mutations_len = operations;
1543 while mutations_len > 1 {
1544 if rng.gen_bool(0.2) {
1545 worktree
1546 .update(cx, |worktree, cx| {
1547 randomly_mutate_worktree(worktree, &mut rng, cx)
1548 })
1549 .await
1550 .log_err();
1551 } else {
1552 randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
1553 }
1554
1555 let buffered_event_count = fs.as_fake().buffered_event_count();
1556 if buffered_event_count > 0 && rng.gen_bool(0.3) {
1557 let len = rng.gen_range(0..=buffered_event_count);
1558 log::info!("flushing {} events", len);
1559 fs.as_fake().flush_events(len);
1560 } else {
1561 randomly_mutate_fs(&fs, root_dir, 0.6, &mut rng).await;
1562 mutations_len -= 1;
1563 }
1564
1565 cx.executor().run_until_parked();
1566 if rng.gen_bool(0.2) {
1567 log::info!("storing snapshot {}", snapshots.len());
1568 let snapshot = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
1569 snapshots.push(snapshot);
1570 }
1571 }
1572
1573 log::info!("quiescing");
1574 fs.as_fake().flush_events(usize::MAX);
1575 cx.executor().run_until_parked();
1576
1577 let snapshot = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
1578 snapshot.check_invariants(true);
1579 let expanded_paths = snapshot
1580 .expanded_entries()
1581 .map(|e| e.path.clone())
1582 .collect::<Vec<_>>();
1583
1584 {
1585 let new_worktree = Worktree::local(
1586 root_dir,
1587 true,
1588 fs.clone(),
1589 Default::default(),
1590 &mut cx.to_async(),
1591 )
1592 .await
1593 .unwrap();
1594 new_worktree
1595 .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
1596 .await;
1597 new_worktree
1598 .update(cx, |tree, _| {
1599 tree.as_local_mut()
1600 .unwrap()
1601 .refresh_entries_for_paths(expanded_paths)
1602 })
1603 .recv()
1604 .await;
1605 let new_snapshot =
1606 new_worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
1607 assert_eq!(
1608 snapshot.entries_without_ids(true),
1609 new_snapshot.entries_without_ids(true)
1610 );
1611 }
1612
1613 for (i, mut prev_snapshot) in snapshots.into_iter().enumerate().rev() {
1614 for update in updates.lock().iter() {
1615 if update.scan_id >= prev_snapshot.scan_id() as u64 {
1616 prev_snapshot.apply_remote_update(update.clone()).unwrap();
1617 }
1618 }
1619
1620 assert_eq!(
1621 prev_snapshot
1622 .entries(true, 0)
1623 .map(ignore_pending_dir)
1624 .collect::<Vec<_>>(),
1625 snapshot
1626 .entries(true, 0)
1627 .map(ignore_pending_dir)
1628 .collect::<Vec<_>>(),
1629 "wrong updates after snapshot {i}: {updates:#?}",
1630 );
1631 }
1632
1633 fn ignore_pending_dir(entry: &Entry) -> Entry {
1634 let mut entry = entry.clone();
1635 if entry.kind.is_dir() {
1636 entry.kind = EntryKind::Dir
1637 }
1638 entry
1639 }
1640}
1641
1642// The worktree's `UpdatedEntries` event can be used to follow along with
1643// all changes to the worktree's snapshot.
1644fn check_worktree_change_events(tree: &mut Worktree, cx: &mut ModelContext<Worktree>) {
1645 let mut entries = tree.entries(true, 0).cloned().collect::<Vec<_>>();
1646 cx.subscribe(&cx.handle(), move |tree, _, event, _| {
1647 if let Event::UpdatedEntries(changes) = event {
1648 for (path, _, change_type) in changes.iter() {
1649 let entry = tree.entry_for_path(path).cloned();
1650 let ix = match entries.binary_search_by_key(&path, |e| &e.path) {
1651 Ok(ix) | Err(ix) => ix,
1652 };
1653 match change_type {
1654 PathChange::Added => entries.insert(ix, entry.unwrap()),
1655 PathChange::Removed => drop(entries.remove(ix)),
1656 PathChange::Updated => {
1657 let entry = entry.unwrap();
1658 let existing_entry = entries.get_mut(ix).unwrap();
1659 assert_eq!(existing_entry.path, entry.path);
1660 *existing_entry = entry;
1661 }
1662 PathChange::AddedOrUpdated | PathChange::Loaded => {
1663 let entry = entry.unwrap();
1664 if entries.get(ix).map(|e| &e.path) == Some(&entry.path) {
1665 *entries.get_mut(ix).unwrap() = entry;
1666 } else {
1667 entries.insert(ix, entry);
1668 }
1669 }
1670 }
1671 }
1672
1673 let new_entries = tree.entries(true, 0).cloned().collect::<Vec<_>>();
1674 assert_eq!(entries, new_entries, "incorrect changes: {:?}", changes);
1675 }
1676 })
1677 .detach();
1678}
1679
1680fn randomly_mutate_worktree(
1681 worktree: &mut Worktree,
1682 rng: &mut impl Rng,
1683 cx: &mut ModelContext<Worktree>,
1684) -> Task<Result<()>> {
1685 log::info!("mutating worktree");
1686 let worktree = worktree.as_local_mut().unwrap();
1687 let snapshot = worktree.snapshot();
1688 let entry = snapshot.entries(false, 0).choose(rng).unwrap();
1689
1690 match rng.gen_range(0_u32..100) {
1691 0..=33 if entry.path.as_ref() != Path::new("") => {
1692 log::info!("deleting entry {:?} ({})", entry.path, entry.id.0);
1693 worktree.delete_entry(entry.id, false, cx).unwrap()
1694 }
1695 ..=66 if entry.path.as_ref() != Path::new("") => {
1696 let other_entry = snapshot.entries(false, 0).choose(rng).unwrap();
1697 let new_parent_path = if other_entry.is_dir() {
1698 other_entry.path.clone()
1699 } else {
1700 other_entry.path.parent().unwrap().into()
1701 };
1702 let mut new_path = new_parent_path.join(random_filename(rng));
1703 if new_path.starts_with(&entry.path) {
1704 new_path = random_filename(rng).into();
1705 }
1706
1707 log::info!(
1708 "renaming entry {:?} ({}) to {:?}",
1709 entry.path,
1710 entry.id.0,
1711 new_path
1712 );
1713 let task = worktree.rename_entry(entry.id, new_path, cx);
1714 cx.background_executor().spawn(async move {
1715 task.await?.to_included().unwrap();
1716 Ok(())
1717 })
1718 }
1719 _ => {
1720 if entry.is_dir() {
1721 let child_path = entry.path.join(random_filename(rng));
1722 let is_dir = rng.gen_bool(0.3);
1723 log::info!(
1724 "creating {} at {:?}",
1725 if is_dir { "dir" } else { "file" },
1726 child_path,
1727 );
1728 let task = worktree.create_entry(child_path, is_dir, cx);
1729 cx.background_executor().spawn(async move {
1730 task.await?;
1731 Ok(())
1732 })
1733 } else {
1734 log::info!("overwriting file {:?} ({})", entry.path, entry.id.0);
1735 let task =
1736 worktree.write_file(entry.path.clone(), "".into(), Default::default(), cx);
1737 cx.background_executor().spawn(async move {
1738 task.await?;
1739 Ok(())
1740 })
1741 }
1742 }
1743 }
1744}
1745
1746async fn randomly_mutate_fs(
1747 fs: &Arc<dyn Fs>,
1748 root_path: &Path,
1749 insertion_probability: f64,
1750 rng: &mut impl Rng,
1751) {
1752 log::info!("mutating fs");
1753 let mut files = Vec::new();
1754 let mut dirs = Vec::new();
1755 for path in fs.as_fake().paths(false) {
1756 if path.starts_with(root_path) {
1757 if fs.is_file(&path).await {
1758 files.push(path);
1759 } else {
1760 dirs.push(path);
1761 }
1762 }
1763 }
1764
1765 if (files.is_empty() && dirs.len() == 1) || rng.gen_bool(insertion_probability) {
1766 let path = dirs.choose(rng).unwrap();
1767 let new_path = path.join(random_filename(rng));
1768
1769 if rng.gen() {
1770 log::info!(
1771 "creating dir {:?}",
1772 new_path.strip_prefix(root_path).unwrap()
1773 );
1774 fs.create_dir(&new_path).await.unwrap();
1775 } else {
1776 log::info!(
1777 "creating file {:?}",
1778 new_path.strip_prefix(root_path).unwrap()
1779 );
1780 fs.create_file(&new_path, Default::default()).await.unwrap();
1781 }
1782 } else if rng.gen_bool(0.05) {
1783 let ignore_dir_path = dirs.choose(rng).unwrap();
1784 let ignore_path = ignore_dir_path.join(*GITIGNORE);
1785
1786 let subdirs = dirs
1787 .iter()
1788 .filter(|d| d.starts_with(ignore_dir_path))
1789 .cloned()
1790 .collect::<Vec<_>>();
1791 let subfiles = files
1792 .iter()
1793 .filter(|d| d.starts_with(ignore_dir_path))
1794 .cloned()
1795 .collect::<Vec<_>>();
1796 let files_to_ignore = {
1797 let len = rng.gen_range(0..=subfiles.len());
1798 subfiles.choose_multiple(rng, len)
1799 };
1800 let dirs_to_ignore = {
1801 let len = rng.gen_range(0..subdirs.len());
1802 subdirs.choose_multiple(rng, len)
1803 };
1804
1805 let mut ignore_contents = String::new();
1806 for path_to_ignore in files_to_ignore.chain(dirs_to_ignore) {
1807 writeln!(
1808 ignore_contents,
1809 "{}",
1810 path_to_ignore
1811 .strip_prefix(ignore_dir_path)
1812 .unwrap()
1813 .to_str()
1814 .unwrap()
1815 )
1816 .unwrap();
1817 }
1818 log::info!(
1819 "creating gitignore {:?} with contents:\n{}",
1820 ignore_path.strip_prefix(root_path).unwrap(),
1821 ignore_contents
1822 );
1823 fs.save(
1824 &ignore_path,
1825 &ignore_contents.as_str().into(),
1826 Default::default(),
1827 )
1828 .await
1829 .unwrap();
1830 } else {
1831 let old_path = {
1832 let file_path = files.choose(rng);
1833 let dir_path = dirs[1..].choose(rng);
1834 file_path.into_iter().chain(dir_path).choose(rng).unwrap()
1835 };
1836
1837 let is_rename = rng.gen();
1838 if is_rename {
1839 let new_path_parent = dirs
1840 .iter()
1841 .filter(|d| !d.starts_with(old_path))
1842 .choose(rng)
1843 .unwrap();
1844
1845 let overwrite_existing_dir =
1846 !old_path.starts_with(new_path_parent) && rng.gen_bool(0.3);
1847 let new_path = if overwrite_existing_dir {
1848 fs.remove_dir(
1849 new_path_parent,
1850 RemoveOptions {
1851 recursive: true,
1852 ignore_if_not_exists: true,
1853 },
1854 )
1855 .await
1856 .unwrap();
1857 new_path_parent.to_path_buf()
1858 } else {
1859 new_path_parent.join(random_filename(rng))
1860 };
1861
1862 log::info!(
1863 "renaming {:?} to {}{:?}",
1864 old_path.strip_prefix(root_path).unwrap(),
1865 if overwrite_existing_dir {
1866 "overwrite "
1867 } else {
1868 ""
1869 },
1870 new_path.strip_prefix(root_path).unwrap()
1871 );
1872 fs.rename(
1873 old_path,
1874 &new_path,
1875 fs::RenameOptions {
1876 overwrite: true,
1877 ignore_if_exists: true,
1878 },
1879 )
1880 .await
1881 .unwrap();
1882 } else if fs.is_file(old_path).await {
1883 log::info!(
1884 "deleting file {:?}",
1885 old_path.strip_prefix(root_path).unwrap()
1886 );
1887 fs.remove_file(old_path, Default::default()).await.unwrap();
1888 } else {
1889 log::info!(
1890 "deleting dir {:?}",
1891 old_path.strip_prefix(root_path).unwrap()
1892 );
1893 fs.remove_dir(
1894 old_path,
1895 RemoveOptions {
1896 recursive: true,
1897 ignore_if_not_exists: true,
1898 },
1899 )
1900 .await
1901 .unwrap();
1902 }
1903 }
1904}
1905
1906fn random_filename(rng: &mut impl Rng) -> String {
1907 (0..6)
1908 .map(|_| rng.sample(rand::distributions::Alphanumeric))
1909 .map(char::from)
1910 .collect()
1911}
1912
1913#[gpui::test]
1914async fn test_rename_work_directory(cx: &mut TestAppContext) {
1915 init_test(cx);
1916 cx.executor().allow_parking();
1917 let root = temp_tree(json!({
1918 "projects": {
1919 "project1": {
1920 "a": "",
1921 "b": "",
1922 }
1923 },
1924
1925 }));
1926 let root_path = root.path();
1927
1928 let tree = Worktree::local(
1929 root_path,
1930 true,
1931 Arc::new(RealFs::default()),
1932 Default::default(),
1933 &mut cx.to_async(),
1934 )
1935 .await
1936 .unwrap();
1937
1938 let repo = git_init(&root_path.join("projects/project1"));
1939 git_add("a", &repo);
1940 git_commit("init", &repo);
1941 std::fs::write(root_path.join("projects/project1/a"), "aa").ok();
1942
1943 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1944 .await;
1945
1946 tree.flush_fs_events(cx).await;
1947
1948 cx.read(|cx| {
1949 let tree = tree.read(cx);
1950 let (work_dir, _) = tree.repositories().next().unwrap();
1951 assert_eq!(work_dir.as_ref(), Path::new("projects/project1"));
1952 assert_eq!(
1953 tree.status_for_file(Path::new("projects/project1/a")),
1954 Some(GitFileStatus::Modified)
1955 );
1956 assert_eq!(
1957 tree.status_for_file(Path::new("projects/project1/b")),
1958 Some(GitFileStatus::Added)
1959 );
1960 });
1961
1962 std::fs::rename(
1963 root_path.join("projects/project1"),
1964 root_path.join("projects/project2"),
1965 )
1966 .ok();
1967 tree.flush_fs_events(cx).await;
1968
1969 cx.read(|cx| {
1970 let tree = tree.read(cx);
1971 let (work_dir, _) = tree.repositories().next().unwrap();
1972 assert_eq!(work_dir.as_ref(), Path::new("projects/project2"));
1973 assert_eq!(
1974 tree.status_for_file(Path::new("projects/project2/a")),
1975 Some(GitFileStatus::Modified)
1976 );
1977 assert_eq!(
1978 tree.status_for_file(Path::new("projects/project2/b")),
1979 Some(GitFileStatus::Added)
1980 );
1981 });
1982}
1983
1984#[gpui::test]
1985async fn test_git_repository_for_path(cx: &mut TestAppContext) {
1986 init_test(cx);
1987 cx.executor().allow_parking();
1988 let root = temp_tree(json!({
1989 "c.txt": "",
1990 "dir1": {
1991 ".git": {},
1992 "deps": {
1993 "dep1": {
1994 ".git": {},
1995 "src": {
1996 "a.txt": ""
1997 }
1998 }
1999 },
2000 "src": {
2001 "b.txt": ""
2002 }
2003 },
2004 }));
2005
2006 let tree = Worktree::local(
2007 root.path(),
2008 true,
2009 Arc::new(RealFs::default()),
2010 Default::default(),
2011 &mut cx.to_async(),
2012 )
2013 .await
2014 .unwrap();
2015
2016 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2017 .await;
2018 tree.flush_fs_events(cx).await;
2019
2020 tree.read_with(cx, |tree, _cx| {
2021 let tree = tree.as_local().unwrap();
2022
2023 assert!(tree.repository_for_path("c.txt".as_ref()).is_none());
2024
2025 let entry = tree.repository_for_path("dir1/src/b.txt".as_ref()).unwrap();
2026 assert_eq!(
2027 entry
2028 .work_directory(tree)
2029 .map(|directory| directory.as_ref().to_owned()),
2030 Some(Path::new("dir1").to_owned())
2031 );
2032
2033 let entry = tree
2034 .repository_for_path("dir1/deps/dep1/src/a.txt".as_ref())
2035 .unwrap();
2036 assert_eq!(
2037 entry
2038 .work_directory(tree)
2039 .map(|directory| directory.as_ref().to_owned()),
2040 Some(Path::new("dir1/deps/dep1").to_owned())
2041 );
2042
2043 let entries = tree.files(false, 0);
2044
2045 let paths_with_repos = tree
2046 .entries_with_repositories(entries)
2047 .map(|(entry, repo)| {
2048 (
2049 entry.path.as_ref(),
2050 repo.and_then(|repo| {
2051 repo.work_directory(tree)
2052 .map(|work_directory| work_directory.0.to_path_buf())
2053 }),
2054 )
2055 })
2056 .collect::<Vec<_>>();
2057
2058 assert_eq!(
2059 paths_with_repos,
2060 &[
2061 (Path::new("c.txt"), None),
2062 (
2063 Path::new("dir1/deps/dep1/src/a.txt"),
2064 Some(Path::new("dir1/deps/dep1").into())
2065 ),
2066 (Path::new("dir1/src/b.txt"), Some(Path::new("dir1").into())),
2067 ]
2068 );
2069 });
2070
2071 let repo_update_events = Arc::new(Mutex::new(vec![]));
2072 tree.update(cx, |_, cx| {
2073 let repo_update_events = repo_update_events.clone();
2074 cx.subscribe(&tree, move |_, _, event, _| {
2075 if let Event::UpdatedGitRepositories(update) = event {
2076 repo_update_events.lock().push(update.clone());
2077 }
2078 })
2079 .detach();
2080 });
2081
2082 std::fs::write(root.path().join("dir1/.git/random_new_file"), "hello").unwrap();
2083 tree.flush_fs_events(cx).await;
2084
2085 assert_eq!(
2086 repo_update_events.lock()[0]
2087 .iter()
2088 .map(|e| e.0.clone())
2089 .collect::<Vec<Arc<Path>>>(),
2090 vec![Path::new("dir1").into()]
2091 );
2092
2093 std::fs::remove_dir_all(root.path().join("dir1/.git")).unwrap();
2094 tree.flush_fs_events(cx).await;
2095
2096 tree.read_with(cx, |tree, _cx| {
2097 let tree = tree.as_local().unwrap();
2098
2099 assert!(tree
2100 .repository_for_path("dir1/src/b.txt".as_ref())
2101 .is_none());
2102 });
2103}
2104
2105#[gpui::test]
2106async fn test_git_status(cx: &mut TestAppContext) {
2107 init_test(cx);
2108 cx.executor().allow_parking();
2109 const IGNORE_RULE: &str = "**/target";
2110
2111 let root = temp_tree(json!({
2112 "project": {
2113 "a.txt": "a",
2114 "b.txt": "bb",
2115 "c": {
2116 "d": {
2117 "e.txt": "eee"
2118 }
2119 },
2120 "f.txt": "ffff",
2121 "target": {
2122 "build_file": "???"
2123 },
2124 ".gitignore": IGNORE_RULE
2125 },
2126
2127 }));
2128
2129 const A_TXT: &str = "a.txt";
2130 const B_TXT: &str = "b.txt";
2131 const E_TXT: &str = "c/d/e.txt";
2132 const F_TXT: &str = "f.txt";
2133 const DOTGITIGNORE: &str = ".gitignore";
2134 const BUILD_FILE: &str = "target/build_file";
2135 let project_path = Path::new("project");
2136
2137 // Set up git repository before creating the worktree.
2138 let work_dir = root.path().join("project");
2139 let mut repo = git_init(work_dir.as_path());
2140 repo.add_ignore_rule(IGNORE_RULE).unwrap();
2141 git_add(A_TXT, &repo);
2142 git_add(E_TXT, &repo);
2143 git_add(DOTGITIGNORE, &repo);
2144 git_commit("Initial commit", &repo);
2145
2146 let tree = Worktree::local(
2147 root.path(),
2148 true,
2149 Arc::new(RealFs::default()),
2150 Default::default(),
2151 &mut cx.to_async(),
2152 )
2153 .await
2154 .unwrap();
2155
2156 tree.flush_fs_events(cx).await;
2157 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2158 .await;
2159 cx.executor().run_until_parked();
2160
2161 // Check that the right git state is observed on startup
2162 tree.read_with(cx, |tree, _cx| {
2163 let snapshot = tree.snapshot();
2164 assert_eq!(snapshot.repositories().count(), 1);
2165 let (dir, repo_entry) = snapshot.repositories().next().unwrap();
2166 assert_eq!(dir.as_ref(), Path::new("project"));
2167 assert!(repo_entry.location_in_repo.is_none());
2168
2169 assert_eq!(
2170 snapshot.status_for_file(project_path.join(B_TXT)),
2171 Some(GitFileStatus::Added)
2172 );
2173 assert_eq!(
2174 snapshot.status_for_file(project_path.join(F_TXT)),
2175 Some(GitFileStatus::Added)
2176 );
2177 });
2178
2179 // Modify a file in the working copy.
2180 std::fs::write(work_dir.join(A_TXT), "aa").unwrap();
2181 tree.flush_fs_events(cx).await;
2182 cx.executor().run_until_parked();
2183
2184 // The worktree detects that the file's git status has changed.
2185 tree.read_with(cx, |tree, _cx| {
2186 let snapshot = tree.snapshot();
2187 assert_eq!(
2188 snapshot.status_for_file(project_path.join(A_TXT)),
2189 Some(GitFileStatus::Modified)
2190 );
2191 });
2192
2193 // Create a commit in the git repository.
2194 git_add(A_TXT, &repo);
2195 git_add(B_TXT, &repo);
2196 git_commit("Committing modified and added", &repo);
2197 tree.flush_fs_events(cx).await;
2198 cx.executor().run_until_parked();
2199
2200 // The worktree detects that the files' git status have changed.
2201 tree.read_with(cx, |tree, _cx| {
2202 let snapshot = tree.snapshot();
2203 assert_eq!(
2204 snapshot.status_for_file(project_path.join(F_TXT)),
2205 Some(GitFileStatus::Added)
2206 );
2207 assert_eq!(snapshot.status_for_file(project_path.join(B_TXT)), None);
2208 assert_eq!(snapshot.status_for_file(project_path.join(A_TXT)), None);
2209 });
2210
2211 // Modify files in the working copy and perform git operations on other files.
2212 git_reset(0, &repo);
2213 git_remove_index(Path::new(B_TXT), &repo);
2214 git_stash(&mut repo);
2215 std::fs::write(work_dir.join(E_TXT), "eeee").unwrap();
2216 std::fs::write(work_dir.join(BUILD_FILE), "this should be ignored").unwrap();
2217 tree.flush_fs_events(cx).await;
2218 cx.executor().run_until_parked();
2219
2220 // Check that more complex repo changes are tracked
2221 tree.read_with(cx, |tree, _cx| {
2222 let snapshot = tree.snapshot();
2223
2224 assert_eq!(snapshot.status_for_file(project_path.join(A_TXT)), None);
2225 assert_eq!(
2226 snapshot.status_for_file(project_path.join(B_TXT)),
2227 Some(GitFileStatus::Added)
2228 );
2229 assert_eq!(
2230 snapshot.status_for_file(project_path.join(E_TXT)),
2231 Some(GitFileStatus::Modified)
2232 );
2233 });
2234
2235 std::fs::remove_file(work_dir.join(B_TXT)).unwrap();
2236 std::fs::remove_dir_all(work_dir.join("c")).unwrap();
2237 std::fs::write(
2238 work_dir.join(DOTGITIGNORE),
2239 [IGNORE_RULE, "f.txt"].join("\n"),
2240 )
2241 .unwrap();
2242
2243 git_add(Path::new(DOTGITIGNORE), &repo);
2244 git_commit("Committing modified git ignore", &repo);
2245
2246 tree.flush_fs_events(cx).await;
2247 cx.executor().run_until_parked();
2248
2249 let mut renamed_dir_name = "first_directory/second_directory";
2250 const RENAMED_FILE: &str = "rf.txt";
2251
2252 std::fs::create_dir_all(work_dir.join(renamed_dir_name)).unwrap();
2253 std::fs::write(
2254 work_dir.join(renamed_dir_name).join(RENAMED_FILE),
2255 "new-contents",
2256 )
2257 .unwrap();
2258
2259 tree.flush_fs_events(cx).await;
2260 cx.executor().run_until_parked();
2261
2262 tree.read_with(cx, |tree, _cx| {
2263 let snapshot = tree.snapshot();
2264 assert_eq!(
2265 snapshot.status_for_file(project_path.join(renamed_dir_name).join(RENAMED_FILE)),
2266 Some(GitFileStatus::Added)
2267 );
2268 });
2269
2270 renamed_dir_name = "new_first_directory/second_directory";
2271
2272 std::fs::rename(
2273 work_dir.join("first_directory"),
2274 work_dir.join("new_first_directory"),
2275 )
2276 .unwrap();
2277
2278 tree.flush_fs_events(cx).await;
2279 cx.executor().run_until_parked();
2280
2281 tree.read_with(cx, |tree, _cx| {
2282 let snapshot = tree.snapshot();
2283
2284 assert_eq!(
2285 snapshot.status_for_file(
2286 project_path
2287 .join(Path::new(renamed_dir_name))
2288 .join(RENAMED_FILE)
2289 ),
2290 Some(GitFileStatus::Added)
2291 );
2292 });
2293}
2294
2295#[gpui::test]
2296async fn test_repository_subfolder_git_status(cx: &mut TestAppContext) {
2297 init_test(cx);
2298 cx.executor().allow_parking();
2299
2300 let root = temp_tree(json!({
2301 "my-repo": {
2302 // .git folder will go here
2303 "a.txt": "a",
2304 "sub-folder-1": {
2305 "sub-folder-2": {
2306 "c.txt": "cc",
2307 "d": {
2308 "e.txt": "eee"
2309 }
2310 },
2311 }
2312 },
2313
2314 }));
2315
2316 const C_TXT: &str = "sub-folder-1/sub-folder-2/c.txt";
2317 const E_TXT: &str = "sub-folder-1/sub-folder-2/d/e.txt";
2318
2319 // Set up git repository before creating the worktree.
2320 let git_repo_work_dir = root.path().join("my-repo");
2321 let repo = git_init(git_repo_work_dir.as_path());
2322 git_add(C_TXT, &repo);
2323 git_commit("Initial commit", &repo);
2324
2325 // Open the worktree in subfolder
2326 let project_root = Path::new("my-repo/sub-folder-1/sub-folder-2");
2327 let tree = Worktree::local(
2328 root.path().join(project_root),
2329 true,
2330 Arc::new(RealFs::default()),
2331 Default::default(),
2332 &mut cx.to_async(),
2333 )
2334 .await
2335 .unwrap();
2336
2337 tree.flush_fs_events(cx).await;
2338 tree.flush_fs_events_in_root_git_repository(cx).await;
2339 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2340 .await;
2341 cx.executor().run_until_parked();
2342
2343 // Ensure that the git status is loaded correctly
2344 tree.read_with(cx, |tree, _cx| {
2345 let snapshot = tree.snapshot();
2346 assert_eq!(snapshot.repositories().count(), 1);
2347 let (dir, repo_entry) = snapshot.repositories().next().unwrap();
2348 // Path is blank because the working directory of
2349 // the git repository is located at the root of the project
2350 assert_eq!(dir.as_ref(), Path::new(""));
2351
2352 // This is the missing path between the root of the project (sub-folder-2) and its
2353 // location relative to the root of the repository.
2354 assert_eq!(
2355 repo_entry.location_in_repo,
2356 Some(Arc::from(Path::new("sub-folder-1/sub-folder-2")))
2357 );
2358
2359 assert_eq!(snapshot.status_for_file("c.txt"), None);
2360 assert_eq!(
2361 snapshot.status_for_file("d/e.txt"),
2362 Some(GitFileStatus::Added)
2363 );
2364 });
2365
2366 // Now we simulate FS events, but ONLY in the .git folder that's outside
2367 // of out project root.
2368 // Meaning: we don't produce any FS events for files inside the project.
2369 git_add(E_TXT, &repo);
2370 git_commit("Second commit", &repo);
2371 tree.flush_fs_events_in_root_git_repository(cx).await;
2372 cx.executor().run_until_parked();
2373
2374 tree.read_with(cx, |tree, _cx| {
2375 let snapshot = tree.snapshot();
2376
2377 assert!(snapshot.repositories().next().is_some());
2378
2379 assert_eq!(snapshot.status_for_file("c.txt"), None);
2380 assert_eq!(snapshot.status_for_file("d/e.txt"), None);
2381 });
2382}
2383
2384#[gpui::test]
2385async fn test_propagate_git_statuses(cx: &mut TestAppContext) {
2386 init_test(cx);
2387 let fs = FakeFs::new(cx.background_executor.clone());
2388 fs.insert_tree(
2389 "/root",
2390 json!({
2391 ".git": {},
2392 "a": {
2393 "b": {
2394 "c1.txt": "",
2395 "c2.txt": "",
2396 },
2397 "d": {
2398 "e1.txt": "",
2399 "e2.txt": "",
2400 "e3.txt": "",
2401 }
2402 },
2403 "f": {
2404 "no-status.txt": ""
2405 },
2406 "g": {
2407 "h1.txt": "",
2408 "h2.txt": ""
2409 },
2410
2411 }),
2412 )
2413 .await;
2414
2415 fs.set_status_for_repo_via_git_operation(
2416 Path::new("/root/.git"),
2417 &[
2418 (Path::new("a/b/c1.txt"), GitFileStatus::Added),
2419 (Path::new("a/d/e2.txt"), GitFileStatus::Modified),
2420 (Path::new("g/h2.txt"), GitFileStatus::Conflict),
2421 ],
2422 );
2423
2424 let tree = Worktree::local(
2425 Path::new("/root"),
2426 true,
2427 fs.clone(),
2428 Default::default(),
2429 &mut cx.to_async(),
2430 )
2431 .await
2432 .unwrap();
2433
2434 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
2435 .await;
2436
2437 cx.executor().run_until_parked();
2438 let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
2439
2440 check_propagated_statuses(
2441 &snapshot,
2442 &[
2443 (Path::new(""), Some(GitFileStatus::Conflict)),
2444 (Path::new("a"), Some(GitFileStatus::Modified)),
2445 (Path::new("a/b"), Some(GitFileStatus::Added)),
2446 (Path::new("a/b/c1.txt"), Some(GitFileStatus::Added)),
2447 (Path::new("a/b/c2.txt"), None),
2448 (Path::new("a/d"), Some(GitFileStatus::Modified)),
2449 (Path::new("a/d/e2.txt"), Some(GitFileStatus::Modified)),
2450 (Path::new("f"), None),
2451 (Path::new("f/no-status.txt"), None),
2452 (Path::new("g"), Some(GitFileStatus::Conflict)),
2453 (Path::new("g/h2.txt"), Some(GitFileStatus::Conflict)),
2454 ],
2455 );
2456
2457 check_propagated_statuses(
2458 &snapshot,
2459 &[
2460 (Path::new("a/b"), Some(GitFileStatus::Added)),
2461 (Path::new("a/b/c1.txt"), Some(GitFileStatus::Added)),
2462 (Path::new("a/b/c2.txt"), None),
2463 (Path::new("a/d"), Some(GitFileStatus::Modified)),
2464 (Path::new("a/d/e1.txt"), None),
2465 (Path::new("a/d/e2.txt"), Some(GitFileStatus::Modified)),
2466 (Path::new("f"), None),
2467 (Path::new("f/no-status.txt"), None),
2468 (Path::new("g"), Some(GitFileStatus::Conflict)),
2469 ],
2470 );
2471
2472 check_propagated_statuses(
2473 &snapshot,
2474 &[
2475 (Path::new("a/b/c1.txt"), Some(GitFileStatus::Added)),
2476 (Path::new("a/b/c2.txt"), None),
2477 (Path::new("a/d/e1.txt"), None),
2478 (Path::new("a/d/e2.txt"), Some(GitFileStatus::Modified)),
2479 (Path::new("f/no-status.txt"), None),
2480 ],
2481 );
2482}
2483
2484#[track_caller]
2485fn check_propagated_statuses(
2486 snapshot: &Snapshot,
2487 expected_statuses: &[(&Path, Option<GitFileStatus>)],
2488) {
2489 let mut entries = expected_statuses
2490 .iter()
2491 .map(|(path, _)| snapshot.entry_for_path(path).unwrap().clone())
2492 .collect::<Vec<_>>();
2493 snapshot.propagate_git_statuses(&mut entries);
2494 assert_eq!(
2495 entries
2496 .iter()
2497 .map(|e| (e.path.as_ref(), e.git_status))
2498 .collect::<Vec<_>>(),
2499 expected_statuses
2500 );
2501}
2502
2503#[track_caller]
2504fn git_init(path: &Path) -> git2::Repository {
2505 git2::Repository::init(path).expect("Failed to initialize git repository")
2506}
2507
2508#[track_caller]
2509fn git_add<P: AsRef<Path>>(path: P, repo: &git2::Repository) {
2510 let path = path.as_ref();
2511 let mut index = repo.index().expect("Failed to get index");
2512 index.add_path(path).expect("Failed to add a.txt");
2513 index.write().expect("Failed to write index");
2514}
2515
2516#[track_caller]
2517fn git_remove_index(path: &Path, repo: &git2::Repository) {
2518 let mut index = repo.index().expect("Failed to get index");
2519 index.remove_path(path).expect("Failed to add a.txt");
2520 index.write().expect("Failed to write index");
2521}
2522
2523#[track_caller]
2524fn git_commit(msg: &'static str, repo: &git2::Repository) {
2525 use git2::Signature;
2526
2527 let signature = Signature::now("test", "test@zed.dev").unwrap();
2528 let oid = repo.index().unwrap().write_tree().unwrap();
2529 let tree = repo.find_tree(oid).unwrap();
2530 if let Ok(head) = repo.head() {
2531 let parent_obj = head.peel(git2::ObjectType::Commit).unwrap();
2532
2533 let parent_commit = parent_obj.as_commit().unwrap();
2534
2535 repo.commit(
2536 Some("HEAD"),
2537 &signature,
2538 &signature,
2539 msg,
2540 &tree,
2541 &[parent_commit],
2542 )
2543 .expect("Failed to commit with parent");
2544 } else {
2545 repo.commit(Some("HEAD"), &signature, &signature, msg, &tree, &[])
2546 .expect("Failed to commit");
2547 }
2548}
2549
2550#[track_caller]
2551fn git_stash(repo: &mut git2::Repository) {
2552 use git2::Signature;
2553
2554 let signature = Signature::now("test", "test@zed.dev").unwrap();
2555 repo.stash_save(&signature, "N/A", None)
2556 .expect("Failed to stash");
2557}
2558
2559#[track_caller]
2560fn git_reset(offset: usize, repo: &git2::Repository) {
2561 let head = repo.head().expect("Couldn't get repo head");
2562 let object = head.peel(git2::ObjectType::Commit).unwrap();
2563 let commit = object.as_commit().unwrap();
2564 let new_head = commit
2565 .parents()
2566 .inspect(|parnet| {
2567 parnet.message();
2568 })
2569 .nth(offset)
2570 .expect("Not enough history");
2571 repo.reset(new_head.as_object(), git2::ResetType::Soft, None)
2572 .expect("Could not reset");
2573}
2574
2575#[allow(dead_code)]
2576#[track_caller]
2577fn git_status(repo: &git2::Repository) -> collections::HashMap<String, git2::Status> {
2578 repo.statuses(None)
2579 .unwrap()
2580 .iter()
2581 .map(|status| (status.path().unwrap().to_string(), status.status()))
2582 .collect()
2583}
2584
2585#[track_caller]
2586fn check_worktree_entries(
2587 tree: &Worktree,
2588 expected_excluded_paths: &[&str],
2589 expected_ignored_paths: &[&str],
2590 expected_tracked_paths: &[&str],
2591) {
2592 for path in expected_excluded_paths {
2593 let entry = tree.entry_for_path(path);
2594 assert!(
2595 entry.is_none(),
2596 "expected path '{path}' to be excluded, but got entry: {entry:?}",
2597 );
2598 }
2599 for path in expected_ignored_paths {
2600 let entry = tree
2601 .entry_for_path(path)
2602 .unwrap_or_else(|| panic!("Missing entry for expected ignored path '{path}'"));
2603 assert!(
2604 entry.is_ignored,
2605 "expected path '{path}' to be ignored, but got entry: {entry:?}",
2606 );
2607 }
2608 for path in expected_tracked_paths {
2609 let entry = tree
2610 .entry_for_path(path)
2611 .unwrap_or_else(|| panic!("Missing entry for expected tracked path '{path}'"));
2612 assert!(
2613 !entry.is_ignored,
2614 "expected path '{path}' to be tracked, but got entry: {entry:?}",
2615 );
2616 }
2617}
2618
2619fn init_test(cx: &mut gpui::TestAppContext) {
2620 if std::env::var("RUST_LOG").is_ok() {
2621 env_logger::try_init().ok();
2622 }
2623
2624 cx.update(|cx| {
2625 let settings_store = SettingsStore::test(cx);
2626 cx.set_global(settings_store);
2627 WorktreeSettings::register(cx);
2628 });
2629}
2630
2631fn assert_entry_git_state(
2632 tree: &Worktree,
2633 path: &str,
2634 git_status: Option<GitFileStatus>,
2635 is_ignored: bool,
2636) {
2637 let entry = tree.entry_for_path(path).expect("entry {path} not found");
2638 assert_eq!(
2639 entry.git_status, git_status,
2640 "expected {path} to have git status: {git_status:?}"
2641 );
2642 assert_eq!(
2643 entry.is_ignored, is_ignored,
2644 "expected {path} to have is_ignored: {is_ignored}"
2645 );
2646}