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 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(random_filename(rng));
732 if new_path.starts_with(&entry.path) {
733 new_path = random_filename(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(random_filename(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(random_filename(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(random_filename(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 random_filename(rng: &mut impl Rng) -> String {
931 (0..6)
932 .map(|_| rng.sample(rand::distributions::Alphanumeric))
933 .map(char::from)
934 .collect()
935}
936
937#[gpui::test]
938async fn test_rename_work_directory(cx: &mut TestAppContext) {
939 let root = temp_tree(json!({
940 "projects": {
941 "project1": {
942 "a": "",
943 "b": "",
944 }
945 },
946
947 }));
948 let root_path = root.path();
949
950 let http_client = FakeHttpClient::with_404_response();
951 let client = cx.read(|cx| Client::new(http_client, cx));
952 let tree = Worktree::local(
953 client,
954 root_path,
955 true,
956 Arc::new(RealFs),
957 Default::default(),
958 &mut cx.to_async(),
959 )
960 .await
961 .unwrap();
962
963 let repo = git_init(&root_path.join("projects/project1"));
964 git_add("a", &repo);
965 git_commit("init", &repo);
966 std::fs::write(root_path.join("projects/project1/a"), "aa").ok();
967
968 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
969 .await;
970
971 tree.flush_fs_events(cx).await;
972
973 cx.read(|cx| {
974 let tree = tree.read(cx);
975 let (work_dir, _) = tree.repositories().next().unwrap();
976 assert_eq!(work_dir.as_ref(), Path::new("projects/project1"));
977 assert_eq!(
978 tree.status_for_file(Path::new("projects/project1/a")),
979 Some(GitFileStatus::Modified)
980 );
981 assert_eq!(
982 tree.status_for_file(Path::new("projects/project1/b")),
983 Some(GitFileStatus::Added)
984 );
985 });
986
987 std::fs::rename(
988 root_path.join("projects/project1"),
989 root_path.join("projects/project2"),
990 )
991 .ok();
992 tree.flush_fs_events(cx).await;
993
994 cx.read(|cx| {
995 let tree = tree.read(cx);
996 let (work_dir, _) = tree.repositories().next().unwrap();
997 assert_eq!(work_dir.as_ref(), Path::new("projects/project2"));
998 assert_eq!(
999 tree.status_for_file(Path::new("projects/project2/a")),
1000 Some(GitFileStatus::Modified)
1001 );
1002 assert_eq!(
1003 tree.status_for_file(Path::new("projects/project2/b")),
1004 Some(GitFileStatus::Added)
1005 );
1006 });
1007}
1008
1009#[gpui::test]
1010async fn test_git_repository_for_path(cx: &mut TestAppContext) {
1011 let root = temp_tree(json!({
1012 "c.txt": "",
1013 "dir1": {
1014 ".git": {},
1015 "deps": {
1016 "dep1": {
1017 ".git": {},
1018 "src": {
1019 "a.txt": ""
1020 }
1021 }
1022 },
1023 "src": {
1024 "b.txt": ""
1025 }
1026 },
1027 }));
1028
1029 let http_client = FakeHttpClient::with_404_response();
1030 let client = cx.read(|cx| Client::new(http_client, cx));
1031 let tree = Worktree::local(
1032 client,
1033 root.path(),
1034 true,
1035 Arc::new(RealFs),
1036 Default::default(),
1037 &mut cx.to_async(),
1038 )
1039 .await
1040 .unwrap();
1041
1042 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1043 .await;
1044 tree.flush_fs_events(cx).await;
1045
1046 tree.read_with(cx, |tree, _cx| {
1047 let tree = tree.as_local().unwrap();
1048
1049 assert!(tree.repository_for_path("c.txt".as_ref()).is_none());
1050
1051 let entry = tree.repository_for_path("dir1/src/b.txt".as_ref()).unwrap();
1052 assert_eq!(
1053 entry
1054 .work_directory(tree)
1055 .map(|directory| directory.as_ref().to_owned()),
1056 Some(Path::new("dir1").to_owned())
1057 );
1058
1059 let entry = tree
1060 .repository_for_path("dir1/deps/dep1/src/a.txt".as_ref())
1061 .unwrap();
1062 assert_eq!(
1063 entry
1064 .work_directory(tree)
1065 .map(|directory| directory.as_ref().to_owned()),
1066 Some(Path::new("dir1/deps/dep1").to_owned())
1067 );
1068
1069 let entries = tree.files(false, 0);
1070
1071 let paths_with_repos = tree
1072 .entries_with_repositories(entries)
1073 .map(|(entry, repo)| {
1074 (
1075 entry.path.as_ref(),
1076 repo.and_then(|repo| {
1077 repo.work_directory(&tree)
1078 .map(|work_directory| work_directory.0.to_path_buf())
1079 }),
1080 )
1081 })
1082 .collect::<Vec<_>>();
1083
1084 assert_eq!(
1085 paths_with_repos,
1086 &[
1087 (Path::new("c.txt"), None),
1088 (
1089 Path::new("dir1/deps/dep1/src/a.txt"),
1090 Some(Path::new("dir1/deps/dep1").into())
1091 ),
1092 (Path::new("dir1/src/b.txt"), Some(Path::new("dir1").into())),
1093 ]
1094 );
1095 });
1096
1097 let repo_update_events = Arc::new(Mutex::new(vec![]));
1098 tree.update(cx, |_, cx| {
1099 let repo_update_events = repo_update_events.clone();
1100 cx.subscribe(&tree, move |_, _, event, _| {
1101 if let Event::UpdatedGitRepositories(update) = event {
1102 repo_update_events.lock().push(update.clone());
1103 }
1104 })
1105 .detach();
1106 });
1107
1108 std::fs::write(root.path().join("dir1/.git/random_new_file"), "hello").unwrap();
1109 tree.flush_fs_events(cx).await;
1110
1111 assert_eq!(
1112 repo_update_events.lock()[0]
1113 .iter()
1114 .map(|e| e.0.clone())
1115 .collect::<Vec<Arc<Path>>>(),
1116 vec![Path::new("dir1").into()]
1117 );
1118
1119 std::fs::remove_dir_all(root.path().join("dir1/.git")).unwrap();
1120 tree.flush_fs_events(cx).await;
1121
1122 tree.read_with(cx, |tree, _cx| {
1123 let tree = tree.as_local().unwrap();
1124
1125 assert!(tree
1126 .repository_for_path("dir1/src/b.txt".as_ref())
1127 .is_none());
1128 });
1129}
1130
1131#[gpui::test]
1132async fn test_git_status(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
1133 const IGNORE_RULE: &'static str = "**/target";
1134
1135 let root = temp_tree(json!({
1136 "project": {
1137 "a.txt": "a",
1138 "b.txt": "bb",
1139 "c": {
1140 "d": {
1141 "e.txt": "eee"
1142 }
1143 },
1144 "f.txt": "ffff",
1145 "target": {
1146 "build_file": "???"
1147 },
1148 ".gitignore": IGNORE_RULE
1149 },
1150
1151 }));
1152
1153 let http_client = FakeHttpClient::with_404_response();
1154 let client = cx.read(|cx| Client::new(http_client, cx));
1155 let tree = Worktree::local(
1156 client,
1157 root.path(),
1158 true,
1159 Arc::new(RealFs),
1160 Default::default(),
1161 &mut cx.to_async(),
1162 )
1163 .await
1164 .unwrap();
1165
1166 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1167 .await;
1168
1169 const A_TXT: &'static str = "a.txt";
1170 const B_TXT: &'static str = "b.txt";
1171 const E_TXT: &'static str = "c/d/e.txt";
1172 const F_TXT: &'static str = "f.txt";
1173 const DOTGITIGNORE: &'static str = ".gitignore";
1174 const BUILD_FILE: &'static str = "target/build_file";
1175 let project_path: &Path = &Path::new("project");
1176
1177 let work_dir = root.path().join("project");
1178 let mut repo = git_init(work_dir.as_path());
1179 repo.add_ignore_rule(IGNORE_RULE).unwrap();
1180 git_add(Path::new(A_TXT), &repo);
1181 git_add(Path::new(E_TXT), &repo);
1182 git_add(Path::new(DOTGITIGNORE), &repo);
1183 git_commit("Initial commit", &repo);
1184
1185 tree.flush_fs_events(cx).await;
1186 deterministic.run_until_parked();
1187
1188 // Check that the right git state is observed on startup
1189 tree.read_with(cx, |tree, _cx| {
1190 let snapshot = tree.snapshot();
1191 assert_eq!(snapshot.repositories().count(), 1);
1192 let (dir, _) = snapshot.repositories().next().unwrap();
1193 assert_eq!(dir.as_ref(), Path::new("project"));
1194
1195 assert_eq!(
1196 snapshot.status_for_file(project_path.join(B_TXT)),
1197 Some(GitFileStatus::Added)
1198 );
1199 assert_eq!(
1200 snapshot.status_for_file(project_path.join(F_TXT)),
1201 Some(GitFileStatus::Added)
1202 );
1203 });
1204
1205 std::fs::write(work_dir.join(A_TXT), "aa").unwrap();
1206
1207 tree.flush_fs_events(cx).await;
1208 deterministic.run_until_parked();
1209
1210 tree.read_with(cx, |tree, _cx| {
1211 let snapshot = tree.snapshot();
1212
1213 assert_eq!(
1214 snapshot.status_for_file(project_path.join(A_TXT)),
1215 Some(GitFileStatus::Modified)
1216 );
1217 });
1218
1219 git_add(Path::new(A_TXT), &repo);
1220 git_add(Path::new(B_TXT), &repo);
1221 git_commit("Committing modified and added", &repo);
1222 tree.flush_fs_events(cx).await;
1223 deterministic.run_until_parked();
1224
1225 // Check that repo only changes are tracked
1226 tree.read_with(cx, |tree, _cx| {
1227 let snapshot = tree.snapshot();
1228
1229 assert_eq!(
1230 snapshot.status_for_file(project_path.join(F_TXT)),
1231 Some(GitFileStatus::Added)
1232 );
1233
1234 assert_eq!(snapshot.status_for_file(project_path.join(B_TXT)), None);
1235 assert_eq!(snapshot.status_for_file(project_path.join(A_TXT)), None);
1236 });
1237
1238 git_reset(0, &repo);
1239 git_remove_index(Path::new(B_TXT), &repo);
1240 git_stash(&mut repo);
1241 std::fs::write(work_dir.join(E_TXT), "eeee").unwrap();
1242 std::fs::write(work_dir.join(BUILD_FILE), "this should be ignored").unwrap();
1243 tree.flush_fs_events(cx).await;
1244 deterministic.run_until_parked();
1245
1246 // Check that more complex repo changes are tracked
1247 tree.read_with(cx, |tree, _cx| {
1248 let snapshot = tree.snapshot();
1249
1250 assert_eq!(snapshot.status_for_file(project_path.join(A_TXT)), None);
1251 assert_eq!(
1252 snapshot.status_for_file(project_path.join(B_TXT)),
1253 Some(GitFileStatus::Added)
1254 );
1255 assert_eq!(
1256 snapshot.status_for_file(project_path.join(E_TXT)),
1257 Some(GitFileStatus::Modified)
1258 );
1259 });
1260
1261 std::fs::remove_file(work_dir.join(B_TXT)).unwrap();
1262 std::fs::remove_dir_all(work_dir.join("c")).unwrap();
1263 std::fs::write(
1264 work_dir.join(DOTGITIGNORE),
1265 [IGNORE_RULE, "f.txt"].join("\n"),
1266 )
1267 .unwrap();
1268
1269 git_add(Path::new(DOTGITIGNORE), &repo);
1270 git_commit("Committing modified git ignore", &repo);
1271
1272 tree.flush_fs_events(cx).await;
1273 deterministic.run_until_parked();
1274
1275 let mut renamed_dir_name = "first_directory/second_directory";
1276 const RENAMED_FILE: &'static str = "rf.txt";
1277
1278 std::fs::create_dir_all(work_dir.join(renamed_dir_name)).unwrap();
1279 std::fs::write(
1280 work_dir.join(renamed_dir_name).join(RENAMED_FILE),
1281 "new-contents",
1282 )
1283 .unwrap();
1284
1285 tree.flush_fs_events(cx).await;
1286 deterministic.run_until_parked();
1287
1288 tree.read_with(cx, |tree, _cx| {
1289 let snapshot = tree.snapshot();
1290 assert_eq!(
1291 snapshot.status_for_file(&project_path.join(renamed_dir_name).join(RENAMED_FILE)),
1292 Some(GitFileStatus::Added)
1293 );
1294 });
1295
1296 renamed_dir_name = "new_first_directory/second_directory";
1297
1298 std::fs::rename(
1299 work_dir.join("first_directory"),
1300 work_dir.join("new_first_directory"),
1301 )
1302 .unwrap();
1303
1304 tree.flush_fs_events(cx).await;
1305 deterministic.run_until_parked();
1306
1307 tree.read_with(cx, |tree, _cx| {
1308 let snapshot = tree.snapshot();
1309
1310 assert_eq!(
1311 snapshot.status_for_file(
1312 project_path
1313 .join(Path::new(renamed_dir_name))
1314 .join(RENAMED_FILE)
1315 ),
1316 Some(GitFileStatus::Added)
1317 );
1318 });
1319}
1320
1321#[gpui::test]
1322async fn test_propagate_git_statuses(cx: &mut TestAppContext) {
1323 let fs = FakeFs::new(cx.background());
1324 fs.insert_tree(
1325 "/root",
1326 json!({
1327 ".git": {},
1328 "a": {
1329 "b": {
1330 "c1.txt": "",
1331 "c2.txt": "",
1332 },
1333 "d": {
1334 "e1.txt": "",
1335 "e2.txt": "",
1336 "e3.txt": "",
1337 }
1338 },
1339 "f": {
1340 "no-status.txt": ""
1341 },
1342 "g": {
1343 "h1.txt": "",
1344 "h2.txt": ""
1345 },
1346
1347 }),
1348 )
1349 .await;
1350
1351 fs.set_status_for_repo_via_git_operation(
1352 &Path::new("/root/.git"),
1353 &[
1354 (Path::new("a/b/c1.txt"), GitFileStatus::Added),
1355 (Path::new("a/d/e2.txt"), GitFileStatus::Modified),
1356 (Path::new("g/h2.txt"), GitFileStatus::Conflict),
1357 ],
1358 );
1359
1360 let http_client = FakeHttpClient::with_404_response();
1361 let client = cx.read(|cx| Client::new(http_client, cx));
1362 let tree = Worktree::local(
1363 client,
1364 Path::new("/root"),
1365 true,
1366 fs.clone(),
1367 Default::default(),
1368 &mut cx.to_async(),
1369 )
1370 .await
1371 .unwrap();
1372
1373 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1374 .await;
1375
1376 cx.foreground().run_until_parked();
1377 let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
1378
1379 check_propagated_statuses(
1380 &snapshot,
1381 &[
1382 (Path::new(""), Some(GitFileStatus::Conflict)),
1383 (Path::new("a"), Some(GitFileStatus::Modified)),
1384 (Path::new("a/b"), Some(GitFileStatus::Added)),
1385 (Path::new("a/b/c1.txt"), Some(GitFileStatus::Added)),
1386 (Path::new("a/b/c2.txt"), None),
1387 (Path::new("a/d"), Some(GitFileStatus::Modified)),
1388 (Path::new("a/d/e2.txt"), Some(GitFileStatus::Modified)),
1389 (Path::new("f"), None),
1390 (Path::new("f/no-status.txt"), None),
1391 (Path::new("g"), Some(GitFileStatus::Conflict)),
1392 (Path::new("g/h2.txt"), Some(GitFileStatus::Conflict)),
1393 ],
1394 );
1395
1396 check_propagated_statuses(
1397 &snapshot,
1398 &[
1399 (Path::new("a/b"), Some(GitFileStatus::Added)),
1400 (Path::new("a/b/c1.txt"), Some(GitFileStatus::Added)),
1401 (Path::new("a/b/c2.txt"), None),
1402 (Path::new("a/d"), Some(GitFileStatus::Modified)),
1403 (Path::new("a/d/e1.txt"), None),
1404 (Path::new("a/d/e2.txt"), Some(GitFileStatus::Modified)),
1405 (Path::new("f"), None),
1406 (Path::new("f/no-status.txt"), None),
1407 (Path::new("g"), Some(GitFileStatus::Conflict)),
1408 ],
1409 );
1410
1411 check_propagated_statuses(
1412 &snapshot,
1413 &[
1414 (Path::new("a/b/c1.txt"), Some(GitFileStatus::Added)),
1415 (Path::new("a/b/c2.txt"), None),
1416 (Path::new("a/d/e1.txt"), None),
1417 (Path::new("a/d/e2.txt"), Some(GitFileStatus::Modified)),
1418 (Path::new("f/no-status.txt"), None),
1419 ],
1420 );
1421
1422 #[track_caller]
1423 fn check_propagated_statuses(
1424 snapshot: &Snapshot,
1425 expected_statuses: &[(&Path, Option<GitFileStatus>)],
1426 ) {
1427 let mut entries = expected_statuses
1428 .iter()
1429 .map(|(path, _)| snapshot.entry_for_path(path).unwrap().clone())
1430 .collect::<Vec<_>>();
1431 snapshot.propagate_git_statuses(&mut entries);
1432 assert_eq!(
1433 entries
1434 .iter()
1435 .map(|e| (e.path.as_ref(), e.git_status))
1436 .collect::<Vec<_>>(),
1437 expected_statuses
1438 );
1439 }
1440}
1441
1442#[track_caller]
1443fn git_init(path: &Path) -> git2::Repository {
1444 git2::Repository::init(path).expect("Failed to initialize git repository")
1445}
1446
1447#[track_caller]
1448fn git_add<P: AsRef<Path>>(path: P, repo: &git2::Repository) {
1449 let path = path.as_ref();
1450 let mut index = repo.index().expect("Failed to get index");
1451 index.add_path(path).expect("Failed to add a.txt");
1452 index.write().expect("Failed to write index");
1453}
1454
1455#[track_caller]
1456fn git_remove_index(path: &Path, repo: &git2::Repository) {
1457 let mut index = repo.index().expect("Failed to get index");
1458 index.remove_path(path).expect("Failed to add a.txt");
1459 index.write().expect("Failed to write index");
1460}
1461
1462#[track_caller]
1463fn git_commit(msg: &'static str, repo: &git2::Repository) {
1464 use git2::Signature;
1465
1466 let signature = Signature::now("test", "test@zed.dev").unwrap();
1467 let oid = repo.index().unwrap().write_tree().unwrap();
1468 let tree = repo.find_tree(oid).unwrap();
1469 if let Some(head) = repo.head().ok() {
1470 let parent_obj = head.peel(git2::ObjectType::Commit).unwrap();
1471
1472 let parent_commit = parent_obj.as_commit().unwrap();
1473
1474 repo.commit(
1475 Some("HEAD"),
1476 &signature,
1477 &signature,
1478 msg,
1479 &tree,
1480 &[parent_commit],
1481 )
1482 .expect("Failed to commit with parent");
1483 } else {
1484 repo.commit(Some("HEAD"), &signature, &signature, msg, &tree, &[])
1485 .expect("Failed to commit");
1486 }
1487}
1488
1489#[track_caller]
1490fn git_stash(repo: &mut git2::Repository) {
1491 use git2::Signature;
1492
1493 let signature = Signature::now("test", "test@zed.dev").unwrap();
1494 repo.stash_save(&signature, "N/A", None)
1495 .expect("Failed to stash");
1496}
1497
1498#[track_caller]
1499fn git_reset(offset: usize, repo: &git2::Repository) {
1500 let head = repo.head().expect("Couldn't get repo head");
1501 let object = head.peel(git2::ObjectType::Commit).unwrap();
1502 let commit = object.as_commit().unwrap();
1503 let new_head = commit
1504 .parents()
1505 .inspect(|parnet| {
1506 parnet.message();
1507 })
1508 .skip(offset)
1509 .next()
1510 .expect("Not enough history");
1511 repo.reset(&new_head.as_object(), git2::ResetType::Soft, None)
1512 .expect("Could not reset");
1513}
1514
1515#[allow(dead_code)]
1516#[track_caller]
1517fn git_status(repo: &git2::Repository) -> collections::HashMap<String, git2::Status> {
1518 repo.statuses(None)
1519 .unwrap()
1520 .iter()
1521 .map(|status| (status.path().unwrap().to_string(), status.status()))
1522 .collect()
1523}