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