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