1use crate::{
2 worktree::{Event, Snapshot, WorktreeHandle},
3 Entry, EntryKind, PathChange, Worktree,
4};
5use anyhow::Result;
6use client::Client;
7use fs::{repository::GitFileStatus, FakeFs, Fs, RealFs, RemoveOptions};
8use git::GITIGNORE;
9use gpui::{executor::Deterministic, ModelContext, Task, TestAppContext};
10use parking_lot::Mutex;
11use postage::stream::Stream;
12use pretty_assertions::assert_eq;
13use rand::prelude::*;
14use serde_json::json;
15use std::{
16 env,
17 fmt::Write,
18 path::{Path, PathBuf},
19 sync::Arc,
20};
21use util::{http::FakeHttpClient, test::temp_tree, ResultExt};
22
23#[gpui::test]
24async fn test_traversal(cx: &mut TestAppContext) {
25 let fs = FakeFs::new(cx.background());
26 fs.insert_tree(
27 "/root",
28 json!({
29 ".gitignore": "a/b\n",
30 "a": {
31 "b": "",
32 "c": "",
33 }
34 }),
35 )
36 .await;
37
38 let http_client = FakeHttpClient::with_404_response();
39 let client = cx.read(|cx| Client::new(http_client, cx));
40
41 let tree = Worktree::local(
42 client,
43 Path::new("/root"),
44 true,
45 fs,
46 Default::default(),
47 &mut cx.to_async(),
48 )
49 .await
50 .unwrap();
51 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
52 .await;
53
54 tree.read_with(cx, |tree, _| {
55 assert_eq!(
56 tree.entries(false)
57 .map(|entry| entry.path.as_ref())
58 .collect::<Vec<_>>(),
59 vec![
60 Path::new(""),
61 Path::new(".gitignore"),
62 Path::new("a"),
63 Path::new("a/c"),
64 ]
65 );
66 assert_eq!(
67 tree.entries(true)
68 .map(|entry| entry.path.as_ref())
69 .collect::<Vec<_>>(),
70 vec![
71 Path::new(""),
72 Path::new(".gitignore"),
73 Path::new("a"),
74 Path::new("a/b"),
75 Path::new("a/c"),
76 ]
77 );
78 })
79}
80
81#[gpui::test]
82async fn test_descendent_entries(cx: &mut TestAppContext) {
83 let fs = FakeFs::new(cx.background());
84 fs.insert_tree(
85 "/root",
86 json!({
87 "a": "",
88 "b": {
89 "c": {
90 "d": ""
91 },
92 "e": {}
93 },
94 "f": "",
95 "g": {
96 "h": {}
97 },
98 "i": {
99 "j": {
100 "k": ""
101 },
102 "l": {
103
104 }
105 },
106 ".gitignore": "i/j\n",
107 }),
108 )
109 .await;
110
111 let http_client = FakeHttpClient::with_404_response();
112 let client = cx.read(|cx| Client::new(http_client, cx));
113
114 let tree = Worktree::local(
115 client,
116 Path::new("/root"),
117 true,
118 fs,
119 Default::default(),
120 &mut cx.to_async(),
121 )
122 .await
123 .unwrap();
124 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
125 .await;
126
127 tree.read_with(cx, |tree, _| {
128 assert_eq!(
129 tree.descendent_entries(false, false, Path::new("b"))
130 .map(|entry| entry.path.as_ref())
131 .collect::<Vec<_>>(),
132 vec![Path::new("b/c/d"),]
133 );
134 assert_eq!(
135 tree.descendent_entries(true, false, Path::new("b"))
136 .map(|entry| entry.path.as_ref())
137 .collect::<Vec<_>>(),
138 vec![
139 Path::new("b"),
140 Path::new("b/c"),
141 Path::new("b/c/d"),
142 Path::new("b/e"),
143 ]
144 );
145
146 assert_eq!(
147 tree.descendent_entries(false, false, Path::new("g"))
148 .map(|entry| entry.path.as_ref())
149 .collect::<Vec<_>>(),
150 Vec::<PathBuf>::new()
151 );
152 assert_eq!(
153 tree.descendent_entries(true, false, Path::new("g"))
154 .map(|entry| entry.path.as_ref())
155 .collect::<Vec<_>>(),
156 vec![Path::new("g"), Path::new("g/h"),]
157 );
158 });
159
160 // Expand gitignored directory.
161 tree.read_with(cx, |tree, _| {
162 tree.as_local()
163 .unwrap()
164 .refresh_entries_for_paths(vec![Path::new("i/j").into()])
165 })
166 .recv()
167 .await;
168
169 tree.read_with(cx, |tree, _| {
170 assert_eq!(
171 tree.descendent_entries(false, false, Path::new("i"))
172 .map(|entry| entry.path.as_ref())
173 .collect::<Vec<_>>(),
174 Vec::<PathBuf>::new()
175 );
176 assert_eq!(
177 tree.descendent_entries(false, true, Path::new("i"))
178 .map(|entry| entry.path.as_ref())
179 .collect::<Vec<_>>(),
180 vec![Path::new("i/j/k")]
181 );
182 assert_eq!(
183 tree.descendent_entries(true, false, Path::new("i"))
184 .map(|entry| entry.path.as_ref())
185 .collect::<Vec<_>>(),
186 vec![Path::new("i"), Path::new("i/l"),]
187 );
188 })
189}
190
191#[gpui::test(iterations = 10)]
192async fn test_circular_symlinks(executor: Arc<Deterministic>, cx: &mut TestAppContext) {
193 let fs = FakeFs::new(cx.background());
194 fs.insert_tree(
195 "/root",
196 json!({
197 "lib": {
198 "a": {
199 "a.txt": ""
200 },
201 "b": {
202 "b.txt": ""
203 }
204 }
205 }),
206 )
207 .await;
208 fs.insert_symlink("/root/lib/a/lib", "..".into()).await;
209 fs.insert_symlink("/root/lib/b/lib", "..".into()).await;
210
211 let client = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
212 let tree = Worktree::local(
213 client,
214 Path::new("/root"),
215 true,
216 fs.clone(),
217 Default::default(),
218 &mut cx.to_async(),
219 )
220 .await
221 .unwrap();
222
223 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
224 .await;
225
226 tree.read_with(cx, |tree, _| {
227 assert_eq!(
228 tree.entries(false)
229 .map(|entry| entry.path.as_ref())
230 .collect::<Vec<_>>(),
231 vec![
232 Path::new(""),
233 Path::new("lib"),
234 Path::new("lib/a"),
235 Path::new("lib/a/a.txt"),
236 Path::new("lib/a/lib"),
237 Path::new("lib/b"),
238 Path::new("lib/b/b.txt"),
239 Path::new("lib/b/lib"),
240 ]
241 );
242 });
243
244 fs.rename(
245 Path::new("/root/lib/a/lib"),
246 Path::new("/root/lib/a/lib-2"),
247 Default::default(),
248 )
249 .await
250 .unwrap();
251 executor.run_until_parked();
252 tree.read_with(cx, |tree, _| {
253 assert_eq!(
254 tree.entries(false)
255 .map(|entry| entry.path.as_ref())
256 .collect::<Vec<_>>(),
257 vec![
258 Path::new(""),
259 Path::new("lib"),
260 Path::new("lib/a"),
261 Path::new("lib/a/a.txt"),
262 Path::new("lib/a/lib-2"),
263 Path::new("lib/b"),
264 Path::new("lib/b/b.txt"),
265 Path::new("lib/b/lib"),
266 ]
267 );
268 });
269}
270
271#[gpui::test]
272async fn test_symlinks_pointing_outside(cx: &mut TestAppContext) {
273 let fs = FakeFs::new(cx.background());
274 fs.insert_tree(
275 "/root",
276 json!({
277 "dir1": {
278 "deps": {
279 // symlinks here
280 },
281 "src": {
282 "a.rs": "",
283 "b.rs": "",
284 },
285 },
286 "dir2": {
287 "src": {
288 "c.rs": "",
289 "d.rs": "",
290 }
291 },
292 "dir3": {
293 "deps": {},
294 "src": {
295 "e.rs": "",
296 "f.rs": "",
297 },
298 }
299 }),
300 )
301 .await;
302
303 // These symlinks point to directories outside of the worktree's root, dir1.
304 fs.insert_symlink("/root/dir1/deps/dep-dir2", "../../dir2".into())
305 .await;
306 fs.insert_symlink("/root/dir1/deps/dep-dir3", "../../dir3".into())
307 .await;
308
309 let client = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
310 let tree = Worktree::local(
311 client,
312 Path::new("/root/dir1"),
313 true,
314 fs.clone(),
315 Default::default(),
316 &mut cx.to_async(),
317 )
318 .await
319 .unwrap();
320
321 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
322 .await;
323
324 // The symlinked directories are not scanned by default.
325 tree.read_with(cx, |tree, _| {
326 assert_eq!(
327 tree.entries(false)
328 .map(|entry| (entry.path.as_ref(), entry.is_external))
329 .collect::<Vec<_>>(),
330 vec![
331 (Path::new(""), false),
332 (Path::new("deps"), false),
333 (Path::new("deps/dep-dir2"), true),
334 (Path::new("deps/dep-dir3"), true),
335 (Path::new("src"), false),
336 (Path::new("src/a.rs"), false),
337 (Path::new("src/b.rs"), false),
338 ]
339 );
340
341 assert_eq!(
342 tree.entry_for_path("deps/dep-dir2").unwrap().kind,
343 EntryKind::PendingDir
344 );
345 });
346
347 // Expand one of the symlinked directories.
348 tree.read_with(cx, |tree, _| {
349 tree.as_local()
350 .unwrap()
351 .refresh_entries_for_paths(vec![Path::new("deps/dep-dir3").into()])
352 })
353 .recv()
354 .await;
355
356 // The expanded directory's contents are loaded. Subdirectories are
357 // not scanned yet.
358 tree.read_with(cx, |tree, _| {
359 assert_eq!(
360 tree.entries(false)
361 .map(|entry| (entry.path.as_ref(), entry.is_external))
362 .collect::<Vec<_>>(),
363 vec![
364 (Path::new(""), false),
365 (Path::new("deps"), false),
366 (Path::new("deps/dep-dir2"), true),
367 (Path::new("deps/dep-dir3"), true),
368 (Path::new("deps/dep-dir3/deps"), true),
369 (Path::new("deps/dep-dir3/src"), true),
370 (Path::new("src"), false),
371 (Path::new("src/a.rs"), false),
372 (Path::new("src/b.rs"), false),
373 ]
374 );
375 });
376
377 // Expand a subdirectory of one of the symlinked directories.
378 tree.read_with(cx, |tree, _| {
379 tree.as_local()
380 .unwrap()
381 .refresh_entries_for_paths(vec![Path::new("deps/dep-dir3/src").into()])
382 })
383 .recv()
384 .await;
385
386 // The expanded subdirectory's contents are loaded.
387 tree.read_with(cx, |tree, _| {
388 assert_eq!(
389 tree.entries(false)
390 .map(|entry| (entry.path.as_ref(), entry.is_external))
391 .collect::<Vec<_>>(),
392 vec![
393 (Path::new(""), false),
394 (Path::new("deps"), false),
395 (Path::new("deps/dep-dir2"), true),
396 (Path::new("deps/dep-dir3"), true),
397 (Path::new("deps/dep-dir3/deps"), true),
398 (Path::new("deps/dep-dir3/src"), true),
399 (Path::new("deps/dep-dir3/src/e.rs"), true),
400 (Path::new("deps/dep-dir3/src/f.rs"), true),
401 (Path::new("src"), false),
402 (Path::new("src/a.rs"), false),
403 (Path::new("src/b.rs"), false),
404 ]
405 );
406 });
407}
408
409#[gpui::test]
410async fn test_open_gitignored_files(cx: &mut TestAppContext) {
411 let fs = FakeFs::new(cx.background());
412 fs.insert_tree(
413 "/root",
414 json!({
415 ".gitignore": "node_modules\n",
416 "one": {
417 "node_modules": {
418 "a": {
419 "a1.js": "a1",
420 "a2.js": "a2",
421 },
422 "b": {
423 "b1.js": "b1",
424 "b2.js": "b2",
425 },
426 },
427 },
428 "two": {
429 "x.js": "",
430 "y.js": "",
431 },
432 }),
433 )
434 .await;
435
436 let client = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
437 let tree = Worktree::local(
438 client,
439 Path::new("/root"),
440 true,
441 fs.clone(),
442 Default::default(),
443 &mut cx.to_async(),
444 )
445 .await
446 .unwrap();
447
448 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
449 .await;
450
451 tree.read_with(cx, |tree, _| {
452 assert_eq!(
453 tree.entries(true)
454 .map(|entry| (entry.path.as_ref(), entry.is_ignored))
455 .collect::<Vec<_>>(),
456 vec![
457 (Path::new(""), false),
458 (Path::new(".gitignore"), false),
459 (Path::new("one"), false),
460 (Path::new("one/node_modules"), true),
461 (Path::new("two"), false),
462 (Path::new("two/x.js"), false),
463 (Path::new("two/y.js"), false),
464 ]
465 );
466 });
467
468 // Open a file that is nested inside of a gitignored directory that
469 // has not yet been expanded.
470 let prev_read_dir_count = fs.read_dir_call_count();
471 let buffer = tree
472 .update(cx, |tree, cx| {
473 tree.as_local_mut()
474 .unwrap()
475 .load_buffer(0, "one/node_modules/b/b1.js".as_ref(), cx)
476 })
477 .await
478 .unwrap();
479
480 tree.read_with(cx, |tree, cx| {
481 assert_eq!(
482 tree.entries(true)
483 .map(|entry| (entry.path.as_ref(), entry.is_ignored))
484 .collect::<Vec<_>>(),
485 vec![
486 (Path::new(""), false),
487 (Path::new(".gitignore"), false),
488 (Path::new("one"), false),
489 (Path::new("one/node_modules"), true),
490 (Path::new("one/node_modules/a"), true),
491 (Path::new("one/node_modules/b"), true),
492 (Path::new("one/node_modules/b/b1.js"), true),
493 (Path::new("one/node_modules/b/b2.js"), true),
494 (Path::new("two"), false),
495 (Path::new("two/x.js"), false),
496 (Path::new("two/y.js"), false),
497 ]
498 );
499
500 assert_eq!(
501 buffer.read(cx).file().unwrap().path().as_ref(),
502 Path::new("one/node_modules/b/b1.js")
503 );
504
505 // Only the newly-expanded directories are scanned.
506 assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 2);
507 });
508
509 // Open another file in a different subdirectory of the same
510 // gitignored directory.
511 let prev_read_dir_count = fs.read_dir_call_count();
512 let buffer = tree
513 .update(cx, |tree, cx| {
514 tree.as_local_mut()
515 .unwrap()
516 .load_buffer(0, "one/node_modules/a/a2.js".as_ref(), cx)
517 })
518 .await
519 .unwrap();
520
521 tree.read_with(cx, |tree, cx| {
522 assert_eq!(
523 tree.entries(true)
524 .map(|entry| (entry.path.as_ref(), entry.is_ignored))
525 .collect::<Vec<_>>(),
526 vec![
527 (Path::new(""), false),
528 (Path::new(".gitignore"), false),
529 (Path::new("one"), false),
530 (Path::new("one/node_modules"), true),
531 (Path::new("one/node_modules/a"), true),
532 (Path::new("one/node_modules/a/a1.js"), true),
533 (Path::new("one/node_modules/a/a2.js"), true),
534 (Path::new("one/node_modules/b"), true),
535 (Path::new("one/node_modules/b/b1.js"), true),
536 (Path::new("one/node_modules/b/b2.js"), true),
537 (Path::new("two"), false),
538 (Path::new("two/x.js"), false),
539 (Path::new("two/y.js"), false),
540 ]
541 );
542
543 assert_eq!(
544 buffer.read(cx).file().unwrap().path().as_ref(),
545 Path::new("one/node_modules/a/a2.js")
546 );
547
548 // Only the newly-expanded directory is scanned.
549 assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 1);
550 });
551}
552
553#[gpui::test]
554async fn test_rescan_with_gitignore(cx: &mut TestAppContext) {
555 // .gitignores are handled explicitly by Zed and do not use the git
556 // machinery that the git_tests module checks
557 let parent_dir = temp_tree(json!({
558 ".gitignore": "ancestor-ignored-file1\nancestor-ignored-file2\n",
559 "tree": {
560 ".git": {},
561 ".gitignore": "ignored-dir\n",
562 "tracked-dir": {
563 "tracked-file1": "",
564 "ancestor-ignored-file1": "",
565 },
566 "ignored-dir": {
567 "ignored-file1": ""
568 }
569 }
570 }));
571 let dir = parent_dir.path().join("tree");
572
573 let client = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
574
575 let tree = Worktree::local(
576 client,
577 dir.as_path(),
578 true,
579 Arc::new(RealFs),
580 Default::default(),
581 &mut cx.to_async(),
582 )
583 .await
584 .unwrap();
585 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
586 .await;
587
588 tree.read_with(cx, |tree, _| {
589 tree.as_local()
590 .unwrap()
591 .refresh_entries_for_paths(vec![Path::new("ignored-dir").into()])
592 })
593 .recv()
594 .await;
595
596 cx.read(|cx| {
597 let tree = tree.read(cx);
598 assert!(
599 !tree
600 .entry_for_path("tracked-dir/tracked-file1")
601 .unwrap()
602 .is_ignored
603 );
604 assert!(
605 tree.entry_for_path("tracked-dir/ancestor-ignored-file1")
606 .unwrap()
607 .is_ignored
608 );
609 assert!(
610 tree.entry_for_path("ignored-dir/ignored-file1")
611 .unwrap()
612 .is_ignored
613 );
614 });
615
616 std::fs::write(dir.join("tracked-dir/tracked-file2"), "").unwrap();
617 std::fs::write(dir.join("tracked-dir/ancestor-ignored-file2"), "").unwrap();
618 std::fs::write(dir.join("ignored-dir/ignored-file2"), "").unwrap();
619 tree.flush_fs_events(cx).await;
620 cx.read(|cx| {
621 let tree = tree.read(cx);
622 assert!(
623 !tree
624 .entry_for_path("tracked-dir/tracked-file2")
625 .unwrap()
626 .is_ignored
627 );
628 assert!(
629 tree.entry_for_path("tracked-dir/ancestor-ignored-file2")
630 .unwrap()
631 .is_ignored
632 );
633 assert!(
634 tree.entry_for_path("ignored-dir/ignored-file2")
635 .unwrap()
636 .is_ignored
637 );
638 assert!(tree.entry_for_path(".git").unwrap().is_ignored);
639 });
640}
641
642#[gpui::test]
643async fn test_write_file(cx: &mut TestAppContext) {
644 let dir = temp_tree(json!({
645 ".git": {},
646 ".gitignore": "ignored-dir\n",
647 "tracked-dir": {},
648 "ignored-dir": {}
649 }));
650
651 let client = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
652
653 let tree = Worktree::local(
654 client,
655 dir.path(),
656 true,
657 Arc::new(RealFs),
658 Default::default(),
659 &mut cx.to_async(),
660 )
661 .await
662 .unwrap();
663 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
664 .await;
665 tree.flush_fs_events(cx).await;
666
667 tree.update(cx, |tree, cx| {
668 tree.as_local().unwrap().write_file(
669 Path::new("tracked-dir/file.txt"),
670 "hello".into(),
671 Default::default(),
672 cx,
673 )
674 })
675 .await
676 .unwrap();
677 tree.update(cx, |tree, cx| {
678 tree.as_local().unwrap().write_file(
679 Path::new("ignored-dir/file.txt"),
680 "world".into(),
681 Default::default(),
682 cx,
683 )
684 })
685 .await
686 .unwrap();
687
688 tree.read_with(cx, |tree, _| {
689 let tracked = tree.entry_for_path("tracked-dir/file.txt").unwrap();
690 let ignored = tree.entry_for_path("ignored-dir/file.txt").unwrap();
691 assert!(!tracked.is_ignored);
692 assert!(ignored.is_ignored);
693 });
694}
695
696#[gpui::test(iterations = 30)]
697async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
698 let client = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
699
700 let fs = FakeFs::new(cx.background());
701 fs.insert_tree(
702 "/root",
703 json!({
704 "b": {},
705 "c": {},
706 "d": {},
707 }),
708 )
709 .await;
710
711 let tree = Worktree::local(
712 client,
713 "/root".as_ref(),
714 true,
715 fs,
716 Default::default(),
717 &mut cx.to_async(),
718 )
719 .await
720 .unwrap();
721
722 let snapshot1 = tree.update(cx, |tree, cx| {
723 let tree = tree.as_local_mut().unwrap();
724 let snapshot = Arc::new(Mutex::new(tree.snapshot()));
725 let _ = tree.observe_updates(0, cx, {
726 let snapshot = snapshot.clone();
727 move |update| {
728 snapshot.lock().apply_remote_update(update).unwrap();
729 async { true }
730 }
731 });
732 snapshot
733 });
734
735 let entry = tree
736 .update(cx, |tree, cx| {
737 tree.as_local_mut()
738 .unwrap()
739 .create_entry("a/e".as_ref(), true, cx)
740 })
741 .await
742 .unwrap();
743 assert!(entry.is_dir());
744
745 cx.foreground().run_until_parked();
746 tree.read_with(cx, |tree, _| {
747 assert_eq!(tree.entry_for_path("a/e").unwrap().kind, EntryKind::Dir);
748 });
749
750 let snapshot2 = tree.update(cx, |tree, _| tree.as_local().unwrap().snapshot());
751 assert_eq!(
752 snapshot1.lock().entries(true).collect::<Vec<_>>(),
753 snapshot2.entries(true).collect::<Vec<_>>()
754 );
755}
756
757#[gpui::test(iterations = 100)]
758async fn test_random_worktree_operations_during_initial_scan(
759 cx: &mut TestAppContext,
760 mut rng: StdRng,
761) {
762 let operations = env::var("OPERATIONS")
763 .map(|o| o.parse().unwrap())
764 .unwrap_or(5);
765 let initial_entries = env::var("INITIAL_ENTRIES")
766 .map(|o| o.parse().unwrap())
767 .unwrap_or(20);
768
769 let root_dir = Path::new("/test");
770 let fs = FakeFs::new(cx.background()) as Arc<dyn Fs>;
771 fs.as_fake().insert_tree(root_dir, json!({})).await;
772 for _ in 0..initial_entries {
773 randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
774 }
775 log::info!("generated initial tree");
776
777 let client = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
778 let worktree = Worktree::local(
779 client.clone(),
780 root_dir,
781 true,
782 fs.clone(),
783 Default::default(),
784 &mut cx.to_async(),
785 )
786 .await
787 .unwrap();
788
789 let mut snapshots = vec![worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot())];
790 let updates = Arc::new(Mutex::new(Vec::new()));
791 worktree.update(cx, |tree, cx| {
792 check_worktree_change_events(tree, cx);
793
794 let _ = tree.as_local_mut().unwrap().observe_updates(0, cx, {
795 let updates = updates.clone();
796 move |update| {
797 updates.lock().push(update);
798 async { true }
799 }
800 });
801 });
802
803 for _ in 0..operations {
804 worktree
805 .update(cx, |worktree, cx| {
806 randomly_mutate_worktree(worktree, &mut rng, cx)
807 })
808 .await
809 .log_err();
810 worktree.read_with(cx, |tree, _| {
811 tree.as_local().unwrap().snapshot().check_invariants()
812 });
813
814 if rng.gen_bool(0.6) {
815 snapshots.push(worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot()));
816 }
817 }
818
819 worktree
820 .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
821 .await;
822
823 cx.foreground().run_until_parked();
824
825 let final_snapshot = worktree.read_with(cx, |tree, _| {
826 let tree = tree.as_local().unwrap();
827 let snapshot = tree.snapshot();
828 snapshot.check_invariants();
829 snapshot
830 });
831
832 for (i, snapshot) in snapshots.into_iter().enumerate().rev() {
833 let mut updated_snapshot = snapshot.clone();
834 for update in updates.lock().iter() {
835 if update.scan_id >= updated_snapshot.scan_id() as u64 {
836 updated_snapshot
837 .apply_remote_update(update.clone())
838 .unwrap();
839 }
840 }
841
842 assert_eq!(
843 updated_snapshot.entries(true).collect::<Vec<_>>(),
844 final_snapshot.entries(true).collect::<Vec<_>>(),
845 "wrong updates after snapshot {i}: {snapshot:#?} {updates:#?}",
846 );
847 }
848}
849
850#[gpui::test(iterations = 100)]
851async fn test_random_worktree_changes(cx: &mut TestAppContext, mut rng: StdRng) {
852 let operations = env::var("OPERATIONS")
853 .map(|o| o.parse().unwrap())
854 .unwrap_or(40);
855 let initial_entries = env::var("INITIAL_ENTRIES")
856 .map(|o| o.parse().unwrap())
857 .unwrap_or(20);
858
859 let root_dir = Path::new("/test");
860 let fs = FakeFs::new(cx.background()) as Arc<dyn Fs>;
861 fs.as_fake().insert_tree(root_dir, json!({})).await;
862 for _ in 0..initial_entries {
863 randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
864 }
865 log::info!("generated initial tree");
866
867 let client = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
868 let worktree = Worktree::local(
869 client.clone(),
870 root_dir,
871 true,
872 fs.clone(),
873 Default::default(),
874 &mut cx.to_async(),
875 )
876 .await
877 .unwrap();
878
879 let updates = Arc::new(Mutex::new(Vec::new()));
880 worktree.update(cx, |tree, cx| {
881 check_worktree_change_events(tree, cx);
882
883 let _ = tree.as_local_mut().unwrap().observe_updates(0, cx, {
884 let updates = updates.clone();
885 move |update| {
886 updates.lock().push(update);
887 async { true }
888 }
889 });
890 });
891
892 worktree
893 .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
894 .await;
895
896 fs.as_fake().pause_events();
897 let mut snapshots = Vec::new();
898 let mut mutations_len = operations;
899 while mutations_len > 1 {
900 if rng.gen_bool(0.2) {
901 worktree
902 .update(cx, |worktree, cx| {
903 randomly_mutate_worktree(worktree, &mut rng, cx)
904 })
905 .await
906 .log_err();
907 } else {
908 randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await;
909 }
910
911 let buffered_event_count = fs.as_fake().buffered_event_count();
912 if buffered_event_count > 0 && rng.gen_bool(0.3) {
913 let len = rng.gen_range(0..=buffered_event_count);
914 log::info!("flushing {} events", len);
915 fs.as_fake().flush_events(len);
916 } else {
917 randomly_mutate_fs(&fs, root_dir, 0.6, &mut rng).await;
918 mutations_len -= 1;
919 }
920
921 cx.foreground().run_until_parked();
922 if rng.gen_bool(0.2) {
923 log::info!("storing snapshot {}", snapshots.len());
924 let snapshot = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
925 snapshots.push(snapshot);
926 }
927 }
928
929 log::info!("quiescing");
930 fs.as_fake().flush_events(usize::MAX);
931 cx.foreground().run_until_parked();
932
933 let snapshot = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
934 snapshot.check_invariants();
935 let expanded_paths = snapshot
936 .expanded_entries()
937 .map(|e| e.path.clone())
938 .collect::<Vec<_>>();
939
940 {
941 let new_worktree = Worktree::local(
942 client.clone(),
943 root_dir,
944 true,
945 fs.clone(),
946 Default::default(),
947 &mut cx.to_async(),
948 )
949 .await
950 .unwrap();
951 new_worktree
952 .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
953 .await;
954 new_worktree
955 .update(cx, |tree, _| {
956 tree.as_local_mut()
957 .unwrap()
958 .refresh_entries_for_paths(expanded_paths)
959 })
960 .recv()
961 .await;
962 let new_snapshot =
963 new_worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
964 assert_eq!(
965 snapshot.entries_without_ids(true),
966 new_snapshot.entries_without_ids(true)
967 );
968 }
969
970 for (i, mut prev_snapshot) in snapshots.into_iter().enumerate().rev() {
971 for update in updates.lock().iter() {
972 if update.scan_id >= prev_snapshot.scan_id() as u64 {
973 prev_snapshot.apply_remote_update(update.clone()).unwrap();
974 }
975 }
976
977 assert_eq!(
978 prev_snapshot
979 .entries(true)
980 .map(ignore_pending_dir)
981 .collect::<Vec<_>>(),
982 snapshot
983 .entries(true)
984 .map(ignore_pending_dir)
985 .collect::<Vec<_>>(),
986 "wrong updates after snapshot {i}: {updates:#?}",
987 );
988 }
989
990 fn ignore_pending_dir(entry: &Entry) -> Entry {
991 let mut entry = entry.clone();
992 if entry.kind == EntryKind::PendingDir {
993 entry.kind = EntryKind::Dir
994 }
995 entry
996 }
997}
998
999// The worktree's `UpdatedEntries` event can be used to follow along with
1000// all changes to the worktree's snapshot.
1001fn check_worktree_change_events(tree: &mut Worktree, cx: &mut ModelContext<Worktree>) {
1002 let mut entries = tree.entries(true).cloned().collect::<Vec<_>>();
1003 cx.subscribe(&cx.handle(), move |tree, _, event, _| {
1004 if let Event::UpdatedEntries(changes) = event {
1005 for (path, _, change_type) in changes.iter() {
1006 let entry = tree.entry_for_path(&path).cloned();
1007 let ix = match entries.binary_search_by_key(&path, |e| &e.path) {
1008 Ok(ix) | Err(ix) => ix,
1009 };
1010 match change_type {
1011 PathChange::Loaded => entries.insert(ix, entry.unwrap()),
1012 PathChange::Added => entries.insert(ix, entry.unwrap()),
1013 PathChange::Removed => drop(entries.remove(ix)),
1014 PathChange::Updated => {
1015 let entry = entry.unwrap();
1016 let existing_entry = entries.get_mut(ix).unwrap();
1017 assert_eq!(existing_entry.path, entry.path);
1018 *existing_entry = entry;
1019 }
1020 PathChange::AddedOrUpdated => {
1021 let entry = entry.unwrap();
1022 if entries.get(ix).map(|e| &e.path) == Some(&entry.path) {
1023 *entries.get_mut(ix).unwrap() = entry;
1024 } else {
1025 entries.insert(ix, entry);
1026 }
1027 }
1028 }
1029 }
1030
1031 let new_entries = tree.entries(true).cloned().collect::<Vec<_>>();
1032 assert_eq!(entries, new_entries, "incorrect changes: {:?}", changes);
1033 }
1034 })
1035 .detach();
1036}
1037
1038fn randomly_mutate_worktree(
1039 worktree: &mut Worktree,
1040 rng: &mut impl Rng,
1041 cx: &mut ModelContext<Worktree>,
1042) -> Task<Result<()>> {
1043 log::info!("mutating worktree");
1044 let worktree = worktree.as_local_mut().unwrap();
1045 let snapshot = worktree.snapshot();
1046 let entry = snapshot.entries(false).choose(rng).unwrap();
1047
1048 match rng.gen_range(0_u32..100) {
1049 0..=33 if entry.path.as_ref() != Path::new("") => {
1050 log::info!("deleting entry {:?} ({})", entry.path, entry.id.0);
1051 worktree.delete_entry(entry.id, cx).unwrap()
1052 }
1053 ..=66 if entry.path.as_ref() != Path::new("") => {
1054 let other_entry = snapshot.entries(false).choose(rng).unwrap();
1055 let new_parent_path = if other_entry.is_dir() {
1056 other_entry.path.clone()
1057 } else {
1058 other_entry.path.parent().unwrap().into()
1059 };
1060 let mut new_path = new_parent_path.join(random_filename(rng));
1061 if new_path.starts_with(&entry.path) {
1062 new_path = random_filename(rng).into();
1063 }
1064
1065 log::info!(
1066 "renaming entry {:?} ({}) to {:?}",
1067 entry.path,
1068 entry.id.0,
1069 new_path
1070 );
1071 let task = worktree.rename_entry(entry.id, new_path, cx).unwrap();
1072 cx.foreground().spawn(async move {
1073 task.await?;
1074 Ok(())
1075 })
1076 }
1077 _ => {
1078 let task = if entry.is_dir() {
1079 let child_path = entry.path.join(random_filename(rng));
1080 let is_dir = rng.gen_bool(0.3);
1081 log::info!(
1082 "creating {} at {:?}",
1083 if is_dir { "dir" } else { "file" },
1084 child_path,
1085 );
1086 worktree.create_entry(child_path, is_dir, cx)
1087 } else {
1088 log::info!("overwriting file {:?} ({})", entry.path, entry.id.0);
1089 worktree.write_file(entry.path.clone(), "".into(), Default::default(), cx)
1090 };
1091 cx.foreground().spawn(async move {
1092 task.await?;
1093 Ok(())
1094 })
1095 }
1096 }
1097}
1098
1099async fn randomly_mutate_fs(
1100 fs: &Arc<dyn Fs>,
1101 root_path: &Path,
1102 insertion_probability: f64,
1103 rng: &mut impl Rng,
1104) {
1105 log::info!("mutating fs");
1106 let mut files = Vec::new();
1107 let mut dirs = Vec::new();
1108 for path in fs.as_fake().paths(false) {
1109 if path.starts_with(root_path) {
1110 if fs.is_file(&path).await {
1111 files.push(path);
1112 } else {
1113 dirs.push(path);
1114 }
1115 }
1116 }
1117
1118 if (files.is_empty() && dirs.len() == 1) || rng.gen_bool(insertion_probability) {
1119 let path = dirs.choose(rng).unwrap();
1120 let new_path = path.join(random_filename(rng));
1121
1122 if rng.gen() {
1123 log::info!(
1124 "creating dir {:?}",
1125 new_path.strip_prefix(root_path).unwrap()
1126 );
1127 fs.create_dir(&new_path).await.unwrap();
1128 } else {
1129 log::info!(
1130 "creating file {:?}",
1131 new_path.strip_prefix(root_path).unwrap()
1132 );
1133 fs.create_file(&new_path, Default::default()).await.unwrap();
1134 }
1135 } else if rng.gen_bool(0.05) {
1136 let ignore_dir_path = dirs.choose(rng).unwrap();
1137 let ignore_path = ignore_dir_path.join(&*GITIGNORE);
1138
1139 let subdirs = dirs
1140 .iter()
1141 .filter(|d| d.starts_with(&ignore_dir_path))
1142 .cloned()
1143 .collect::<Vec<_>>();
1144 let subfiles = files
1145 .iter()
1146 .filter(|d| d.starts_with(&ignore_dir_path))
1147 .cloned()
1148 .collect::<Vec<_>>();
1149 let files_to_ignore = {
1150 let len = rng.gen_range(0..=subfiles.len());
1151 subfiles.choose_multiple(rng, len)
1152 };
1153 let dirs_to_ignore = {
1154 let len = rng.gen_range(0..subdirs.len());
1155 subdirs.choose_multiple(rng, len)
1156 };
1157
1158 let mut ignore_contents = String::new();
1159 for path_to_ignore in files_to_ignore.chain(dirs_to_ignore) {
1160 writeln!(
1161 ignore_contents,
1162 "{}",
1163 path_to_ignore
1164 .strip_prefix(&ignore_dir_path)
1165 .unwrap()
1166 .to_str()
1167 .unwrap()
1168 )
1169 .unwrap();
1170 }
1171 log::info!(
1172 "creating gitignore {:?} with contents:\n{}",
1173 ignore_path.strip_prefix(&root_path).unwrap(),
1174 ignore_contents
1175 );
1176 fs.save(
1177 &ignore_path,
1178 &ignore_contents.as_str().into(),
1179 Default::default(),
1180 )
1181 .await
1182 .unwrap();
1183 } else {
1184 let old_path = {
1185 let file_path = files.choose(rng);
1186 let dir_path = dirs[1..].choose(rng);
1187 file_path.into_iter().chain(dir_path).choose(rng).unwrap()
1188 };
1189
1190 let is_rename = rng.gen();
1191 if is_rename {
1192 let new_path_parent = dirs
1193 .iter()
1194 .filter(|d| !d.starts_with(old_path))
1195 .choose(rng)
1196 .unwrap();
1197
1198 let overwrite_existing_dir =
1199 !old_path.starts_with(&new_path_parent) && rng.gen_bool(0.3);
1200 let new_path = if overwrite_existing_dir {
1201 fs.remove_dir(
1202 &new_path_parent,
1203 RemoveOptions {
1204 recursive: true,
1205 ignore_if_not_exists: true,
1206 },
1207 )
1208 .await
1209 .unwrap();
1210 new_path_parent.to_path_buf()
1211 } else {
1212 new_path_parent.join(random_filename(rng))
1213 };
1214
1215 log::info!(
1216 "renaming {:?} to {}{:?}",
1217 old_path.strip_prefix(&root_path).unwrap(),
1218 if overwrite_existing_dir {
1219 "overwrite "
1220 } else {
1221 ""
1222 },
1223 new_path.strip_prefix(&root_path).unwrap()
1224 );
1225 fs.rename(
1226 &old_path,
1227 &new_path,
1228 fs::RenameOptions {
1229 overwrite: true,
1230 ignore_if_exists: true,
1231 },
1232 )
1233 .await
1234 .unwrap();
1235 } else if fs.is_file(&old_path).await {
1236 log::info!(
1237 "deleting file {:?}",
1238 old_path.strip_prefix(&root_path).unwrap()
1239 );
1240 fs.remove_file(old_path, Default::default()).await.unwrap();
1241 } else {
1242 log::info!(
1243 "deleting dir {:?}",
1244 old_path.strip_prefix(&root_path).unwrap()
1245 );
1246 fs.remove_dir(
1247 &old_path,
1248 RemoveOptions {
1249 recursive: true,
1250 ignore_if_not_exists: true,
1251 },
1252 )
1253 .await
1254 .unwrap();
1255 }
1256 }
1257}
1258
1259fn random_filename(rng: &mut impl Rng) -> String {
1260 (0..6)
1261 .map(|_| rng.sample(rand::distributions::Alphanumeric))
1262 .map(char::from)
1263 .collect()
1264}
1265
1266#[gpui::test]
1267async fn test_rename_work_directory(cx: &mut TestAppContext) {
1268 let root = temp_tree(json!({
1269 "projects": {
1270 "project1": {
1271 "a": "",
1272 "b": "",
1273 }
1274 },
1275
1276 }));
1277 let root_path = root.path();
1278
1279 let http_client = FakeHttpClient::with_404_response();
1280 let client = cx.read(|cx| Client::new(http_client, cx));
1281 let tree = Worktree::local(
1282 client,
1283 root_path,
1284 true,
1285 Arc::new(RealFs),
1286 Default::default(),
1287 &mut cx.to_async(),
1288 )
1289 .await
1290 .unwrap();
1291
1292 let repo = git_init(&root_path.join("projects/project1"));
1293 git_add("a", &repo);
1294 git_commit("init", &repo);
1295 std::fs::write(root_path.join("projects/project1/a"), "aa").ok();
1296
1297 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1298 .await;
1299
1300 tree.flush_fs_events(cx).await;
1301
1302 cx.read(|cx| {
1303 let tree = tree.read(cx);
1304 let (work_dir, _) = tree.repositories().next().unwrap();
1305 assert_eq!(work_dir.as_ref(), Path::new("projects/project1"));
1306 assert_eq!(
1307 tree.status_for_file(Path::new("projects/project1/a")),
1308 Some(GitFileStatus::Modified)
1309 );
1310 assert_eq!(
1311 tree.status_for_file(Path::new("projects/project1/b")),
1312 Some(GitFileStatus::Added)
1313 );
1314 });
1315
1316 std::fs::rename(
1317 root_path.join("projects/project1"),
1318 root_path.join("projects/project2"),
1319 )
1320 .ok();
1321 tree.flush_fs_events(cx).await;
1322
1323 cx.read(|cx| {
1324 let tree = tree.read(cx);
1325 let (work_dir, _) = tree.repositories().next().unwrap();
1326 assert_eq!(work_dir.as_ref(), Path::new("projects/project2"));
1327 assert_eq!(
1328 tree.status_for_file(Path::new("projects/project2/a")),
1329 Some(GitFileStatus::Modified)
1330 );
1331 assert_eq!(
1332 tree.status_for_file(Path::new("projects/project2/b")),
1333 Some(GitFileStatus::Added)
1334 );
1335 });
1336}
1337
1338#[gpui::test]
1339async fn test_git_repository_for_path(cx: &mut TestAppContext) {
1340 let root = temp_tree(json!({
1341 "c.txt": "",
1342 "dir1": {
1343 ".git": {},
1344 "deps": {
1345 "dep1": {
1346 ".git": {},
1347 "src": {
1348 "a.txt": ""
1349 }
1350 }
1351 },
1352 "src": {
1353 "b.txt": ""
1354 }
1355 },
1356 }));
1357
1358 let http_client = FakeHttpClient::with_404_response();
1359 let client = cx.read(|cx| Client::new(http_client, cx));
1360 let tree = Worktree::local(
1361 client,
1362 root.path(),
1363 true,
1364 Arc::new(RealFs),
1365 Default::default(),
1366 &mut cx.to_async(),
1367 )
1368 .await
1369 .unwrap();
1370
1371 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1372 .await;
1373 tree.flush_fs_events(cx).await;
1374
1375 tree.read_with(cx, |tree, _cx| {
1376 let tree = tree.as_local().unwrap();
1377
1378 assert!(tree.repository_for_path("c.txt".as_ref()).is_none());
1379
1380 let entry = tree.repository_for_path("dir1/src/b.txt".as_ref()).unwrap();
1381 assert_eq!(
1382 entry
1383 .work_directory(tree)
1384 .map(|directory| directory.as_ref().to_owned()),
1385 Some(Path::new("dir1").to_owned())
1386 );
1387
1388 let entry = tree
1389 .repository_for_path("dir1/deps/dep1/src/a.txt".as_ref())
1390 .unwrap();
1391 assert_eq!(
1392 entry
1393 .work_directory(tree)
1394 .map(|directory| directory.as_ref().to_owned()),
1395 Some(Path::new("dir1/deps/dep1").to_owned())
1396 );
1397
1398 let entries = tree.files(false, 0);
1399
1400 let paths_with_repos = tree
1401 .entries_with_repositories(entries)
1402 .map(|(entry, repo)| {
1403 (
1404 entry.path.as_ref(),
1405 repo.and_then(|repo| {
1406 repo.work_directory(&tree)
1407 .map(|work_directory| work_directory.0.to_path_buf())
1408 }),
1409 )
1410 })
1411 .collect::<Vec<_>>();
1412
1413 assert_eq!(
1414 paths_with_repos,
1415 &[
1416 (Path::new("c.txt"), None),
1417 (
1418 Path::new("dir1/deps/dep1/src/a.txt"),
1419 Some(Path::new("dir1/deps/dep1").into())
1420 ),
1421 (Path::new("dir1/src/b.txt"), Some(Path::new("dir1").into())),
1422 ]
1423 );
1424 });
1425
1426 let repo_update_events = Arc::new(Mutex::new(vec![]));
1427 tree.update(cx, |_, cx| {
1428 let repo_update_events = repo_update_events.clone();
1429 cx.subscribe(&tree, move |_, _, event, _| {
1430 if let Event::UpdatedGitRepositories(update) = event {
1431 repo_update_events.lock().push(update.clone());
1432 }
1433 })
1434 .detach();
1435 });
1436
1437 std::fs::write(root.path().join("dir1/.git/random_new_file"), "hello").unwrap();
1438 tree.flush_fs_events(cx).await;
1439
1440 assert_eq!(
1441 repo_update_events.lock()[0]
1442 .iter()
1443 .map(|e| e.0.clone())
1444 .collect::<Vec<Arc<Path>>>(),
1445 vec![Path::new("dir1").into()]
1446 );
1447
1448 std::fs::remove_dir_all(root.path().join("dir1/.git")).unwrap();
1449 tree.flush_fs_events(cx).await;
1450
1451 tree.read_with(cx, |tree, _cx| {
1452 let tree = tree.as_local().unwrap();
1453
1454 assert!(tree
1455 .repository_for_path("dir1/src/b.txt".as_ref())
1456 .is_none());
1457 });
1458}
1459
1460#[gpui::test]
1461async fn test_git_status(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
1462 const IGNORE_RULE: &'static str = "**/target";
1463
1464 let root = temp_tree(json!({
1465 "project": {
1466 "a.txt": "a",
1467 "b.txt": "bb",
1468 "c": {
1469 "d": {
1470 "e.txt": "eee"
1471 }
1472 },
1473 "f.txt": "ffff",
1474 "target": {
1475 "build_file": "???"
1476 },
1477 ".gitignore": IGNORE_RULE
1478 },
1479
1480 }));
1481
1482 let http_client = FakeHttpClient::with_404_response();
1483 let client = cx.read(|cx| Client::new(http_client, cx));
1484 let tree = Worktree::local(
1485 client,
1486 root.path(),
1487 true,
1488 Arc::new(RealFs),
1489 Default::default(),
1490 &mut cx.to_async(),
1491 )
1492 .await
1493 .unwrap();
1494
1495 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1496 .await;
1497
1498 const A_TXT: &'static str = "a.txt";
1499 const B_TXT: &'static str = "b.txt";
1500 const E_TXT: &'static str = "c/d/e.txt";
1501 const F_TXT: &'static str = "f.txt";
1502 const DOTGITIGNORE: &'static str = ".gitignore";
1503 const BUILD_FILE: &'static str = "target/build_file";
1504 let project_path: &Path = &Path::new("project");
1505
1506 let work_dir = root.path().join("project");
1507 let mut repo = git_init(work_dir.as_path());
1508 repo.add_ignore_rule(IGNORE_RULE).unwrap();
1509 git_add(Path::new(A_TXT), &repo);
1510 git_add(Path::new(E_TXT), &repo);
1511 git_add(Path::new(DOTGITIGNORE), &repo);
1512 git_commit("Initial commit", &repo);
1513
1514 tree.flush_fs_events(cx).await;
1515 deterministic.run_until_parked();
1516
1517 // Check that the right git state is observed on startup
1518 tree.read_with(cx, |tree, _cx| {
1519 let snapshot = tree.snapshot();
1520 assert_eq!(snapshot.repositories().count(), 1);
1521 let (dir, _) = snapshot.repositories().next().unwrap();
1522 assert_eq!(dir.as_ref(), Path::new("project"));
1523
1524 assert_eq!(
1525 snapshot.status_for_file(project_path.join(B_TXT)),
1526 Some(GitFileStatus::Added)
1527 );
1528 assert_eq!(
1529 snapshot.status_for_file(project_path.join(F_TXT)),
1530 Some(GitFileStatus::Added)
1531 );
1532 });
1533
1534 std::fs::write(work_dir.join(A_TXT), "aa").unwrap();
1535
1536 tree.flush_fs_events(cx).await;
1537 deterministic.run_until_parked();
1538
1539 tree.read_with(cx, |tree, _cx| {
1540 let snapshot = tree.snapshot();
1541
1542 assert_eq!(
1543 snapshot.status_for_file(project_path.join(A_TXT)),
1544 Some(GitFileStatus::Modified)
1545 );
1546 });
1547
1548 git_add(Path::new(A_TXT), &repo);
1549 git_add(Path::new(B_TXT), &repo);
1550 git_commit("Committing modified and added", &repo);
1551 tree.flush_fs_events(cx).await;
1552 deterministic.run_until_parked();
1553
1554 // Check that repo only changes are tracked
1555 tree.read_with(cx, |tree, _cx| {
1556 let snapshot = tree.snapshot();
1557
1558 assert_eq!(
1559 snapshot.status_for_file(project_path.join(F_TXT)),
1560 Some(GitFileStatus::Added)
1561 );
1562
1563 assert_eq!(snapshot.status_for_file(project_path.join(B_TXT)), None);
1564 assert_eq!(snapshot.status_for_file(project_path.join(A_TXT)), None);
1565 });
1566
1567 git_reset(0, &repo);
1568 git_remove_index(Path::new(B_TXT), &repo);
1569 git_stash(&mut repo);
1570 std::fs::write(work_dir.join(E_TXT), "eeee").unwrap();
1571 std::fs::write(work_dir.join(BUILD_FILE), "this should be ignored").unwrap();
1572 tree.flush_fs_events(cx).await;
1573 deterministic.run_until_parked();
1574
1575 // Check that more complex repo changes are tracked
1576 tree.read_with(cx, |tree, _cx| {
1577 let snapshot = tree.snapshot();
1578
1579 assert_eq!(snapshot.status_for_file(project_path.join(A_TXT)), None);
1580 assert_eq!(
1581 snapshot.status_for_file(project_path.join(B_TXT)),
1582 Some(GitFileStatus::Added)
1583 );
1584 assert_eq!(
1585 snapshot.status_for_file(project_path.join(E_TXT)),
1586 Some(GitFileStatus::Modified)
1587 );
1588 });
1589
1590 std::fs::remove_file(work_dir.join(B_TXT)).unwrap();
1591 std::fs::remove_dir_all(work_dir.join("c")).unwrap();
1592 std::fs::write(
1593 work_dir.join(DOTGITIGNORE),
1594 [IGNORE_RULE, "f.txt"].join("\n"),
1595 )
1596 .unwrap();
1597
1598 git_add(Path::new(DOTGITIGNORE), &repo);
1599 git_commit("Committing modified git ignore", &repo);
1600
1601 tree.flush_fs_events(cx).await;
1602 deterministic.run_until_parked();
1603
1604 let mut renamed_dir_name = "first_directory/second_directory";
1605 const RENAMED_FILE: &'static str = "rf.txt";
1606
1607 std::fs::create_dir_all(work_dir.join(renamed_dir_name)).unwrap();
1608 std::fs::write(
1609 work_dir.join(renamed_dir_name).join(RENAMED_FILE),
1610 "new-contents",
1611 )
1612 .unwrap();
1613
1614 tree.flush_fs_events(cx).await;
1615 deterministic.run_until_parked();
1616
1617 tree.read_with(cx, |tree, _cx| {
1618 let snapshot = tree.snapshot();
1619 assert_eq!(
1620 snapshot.status_for_file(&project_path.join(renamed_dir_name).join(RENAMED_FILE)),
1621 Some(GitFileStatus::Added)
1622 );
1623 });
1624
1625 renamed_dir_name = "new_first_directory/second_directory";
1626
1627 std::fs::rename(
1628 work_dir.join("first_directory"),
1629 work_dir.join("new_first_directory"),
1630 )
1631 .unwrap();
1632
1633 tree.flush_fs_events(cx).await;
1634 deterministic.run_until_parked();
1635
1636 tree.read_with(cx, |tree, _cx| {
1637 let snapshot = tree.snapshot();
1638
1639 assert_eq!(
1640 snapshot.status_for_file(
1641 project_path
1642 .join(Path::new(renamed_dir_name))
1643 .join(RENAMED_FILE)
1644 ),
1645 Some(GitFileStatus::Added)
1646 );
1647 });
1648}
1649
1650#[gpui::test]
1651async fn test_propagate_git_statuses(cx: &mut TestAppContext) {
1652 let fs = FakeFs::new(cx.background());
1653 fs.insert_tree(
1654 "/root",
1655 json!({
1656 ".git": {},
1657 "a": {
1658 "b": {
1659 "c1.txt": "",
1660 "c2.txt": "",
1661 },
1662 "d": {
1663 "e1.txt": "",
1664 "e2.txt": "",
1665 "e3.txt": "",
1666 }
1667 },
1668 "f": {
1669 "no-status.txt": ""
1670 },
1671 "g": {
1672 "h1.txt": "",
1673 "h2.txt": ""
1674 },
1675
1676 }),
1677 )
1678 .await;
1679
1680 fs.set_status_for_repo_via_git_operation(
1681 &Path::new("/root/.git"),
1682 &[
1683 (Path::new("a/b/c1.txt"), GitFileStatus::Added),
1684 (Path::new("a/d/e2.txt"), GitFileStatus::Modified),
1685 (Path::new("g/h2.txt"), GitFileStatus::Conflict),
1686 ],
1687 );
1688
1689 let http_client = FakeHttpClient::with_404_response();
1690 let client = cx.read(|cx| Client::new(http_client, cx));
1691 let tree = Worktree::local(
1692 client,
1693 Path::new("/root"),
1694 true,
1695 fs.clone(),
1696 Default::default(),
1697 &mut cx.to_async(),
1698 )
1699 .await
1700 .unwrap();
1701
1702 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1703 .await;
1704
1705 cx.foreground().run_until_parked();
1706 let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
1707
1708 check_propagated_statuses(
1709 &snapshot,
1710 &[
1711 (Path::new(""), Some(GitFileStatus::Conflict)),
1712 (Path::new("a"), Some(GitFileStatus::Modified)),
1713 (Path::new("a/b"), Some(GitFileStatus::Added)),
1714 (Path::new("a/b/c1.txt"), Some(GitFileStatus::Added)),
1715 (Path::new("a/b/c2.txt"), None),
1716 (Path::new("a/d"), Some(GitFileStatus::Modified)),
1717 (Path::new("a/d/e2.txt"), Some(GitFileStatus::Modified)),
1718 (Path::new("f"), None),
1719 (Path::new("f/no-status.txt"), None),
1720 (Path::new("g"), Some(GitFileStatus::Conflict)),
1721 (Path::new("g/h2.txt"), Some(GitFileStatus::Conflict)),
1722 ],
1723 );
1724
1725 check_propagated_statuses(
1726 &snapshot,
1727 &[
1728 (Path::new("a/b"), Some(GitFileStatus::Added)),
1729 (Path::new("a/b/c1.txt"), Some(GitFileStatus::Added)),
1730 (Path::new("a/b/c2.txt"), None),
1731 (Path::new("a/d"), Some(GitFileStatus::Modified)),
1732 (Path::new("a/d/e1.txt"), None),
1733 (Path::new("a/d/e2.txt"), Some(GitFileStatus::Modified)),
1734 (Path::new("f"), None),
1735 (Path::new("f/no-status.txt"), None),
1736 (Path::new("g"), Some(GitFileStatus::Conflict)),
1737 ],
1738 );
1739
1740 check_propagated_statuses(
1741 &snapshot,
1742 &[
1743 (Path::new("a/b/c1.txt"), Some(GitFileStatus::Added)),
1744 (Path::new("a/b/c2.txt"), None),
1745 (Path::new("a/d/e1.txt"), None),
1746 (Path::new("a/d/e2.txt"), Some(GitFileStatus::Modified)),
1747 (Path::new("f/no-status.txt"), None),
1748 ],
1749 );
1750
1751 #[track_caller]
1752 fn check_propagated_statuses(
1753 snapshot: &Snapshot,
1754 expected_statuses: &[(&Path, Option<GitFileStatus>)],
1755 ) {
1756 let mut entries = expected_statuses
1757 .iter()
1758 .map(|(path, _)| snapshot.entry_for_path(path).unwrap().clone())
1759 .collect::<Vec<_>>();
1760 snapshot.propagate_git_statuses(&mut entries);
1761 assert_eq!(
1762 entries
1763 .iter()
1764 .map(|e| (e.path.as_ref(), e.git_status))
1765 .collect::<Vec<_>>(),
1766 expected_statuses
1767 );
1768 }
1769}
1770
1771#[track_caller]
1772fn git_init(path: &Path) -> git2::Repository {
1773 git2::Repository::init(path).expect("Failed to initialize git repository")
1774}
1775
1776#[track_caller]
1777fn git_add<P: AsRef<Path>>(path: P, repo: &git2::Repository) {
1778 let path = path.as_ref();
1779 let mut index = repo.index().expect("Failed to get index");
1780 index.add_path(path).expect("Failed to add a.txt");
1781 index.write().expect("Failed to write index");
1782}
1783
1784#[track_caller]
1785fn git_remove_index(path: &Path, repo: &git2::Repository) {
1786 let mut index = repo.index().expect("Failed to get index");
1787 index.remove_path(path).expect("Failed to add a.txt");
1788 index.write().expect("Failed to write index");
1789}
1790
1791#[track_caller]
1792fn git_commit(msg: &'static str, repo: &git2::Repository) {
1793 use git2::Signature;
1794
1795 let signature = Signature::now("test", "test@zed.dev").unwrap();
1796 let oid = repo.index().unwrap().write_tree().unwrap();
1797 let tree = repo.find_tree(oid).unwrap();
1798 if let Some(head) = repo.head().ok() {
1799 let parent_obj = head.peel(git2::ObjectType::Commit).unwrap();
1800
1801 let parent_commit = parent_obj.as_commit().unwrap();
1802
1803 repo.commit(
1804 Some("HEAD"),
1805 &signature,
1806 &signature,
1807 msg,
1808 &tree,
1809 &[parent_commit],
1810 )
1811 .expect("Failed to commit with parent");
1812 } else {
1813 repo.commit(Some("HEAD"), &signature, &signature, msg, &tree, &[])
1814 .expect("Failed to commit");
1815 }
1816}
1817
1818#[track_caller]
1819fn git_stash(repo: &mut git2::Repository) {
1820 use git2::Signature;
1821
1822 let signature = Signature::now("test", "test@zed.dev").unwrap();
1823 repo.stash_save(&signature, "N/A", None)
1824 .expect("Failed to stash");
1825}
1826
1827#[track_caller]
1828fn git_reset(offset: usize, repo: &git2::Repository) {
1829 let head = repo.head().expect("Couldn't get repo head");
1830 let object = head.peel(git2::ObjectType::Commit).unwrap();
1831 let commit = object.as_commit().unwrap();
1832 let new_head = commit
1833 .parents()
1834 .inspect(|parnet| {
1835 parnet.message();
1836 })
1837 .skip(offset)
1838 .next()
1839 .expect("Not enough history");
1840 repo.reset(&new_head.as_object(), git2::ResetType::Soft, None)
1841 .expect("Could not reset");
1842}
1843
1844#[allow(dead_code)]
1845#[track_caller]
1846fn git_status(repo: &git2::Repository) -> collections::HashMap<String, git2::Status> {
1847 repo.statuses(None)
1848 .unwrap()
1849 .iter()
1850 .map(|status| (status.path().unwrap().to_string(), status.status()))
1851 .collect()
1852}