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