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