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