1use collections::HashMap;
2use git::status::GitSummary;
3use std::{ops::Deref, path::Path};
4use sum_tree::Cursor;
5use text::Bias;
6use worktree::{Entry, PathProgress, PathTarget, Traversal};
7
8use super::{RepositoryId, RepositorySnapshot, StatusEntry};
9
10/// Walks the worktree entries and their associated git statuses.
11pub struct GitTraversal<'a> {
12 traversal: Traversal<'a>,
13 current_entry_summary: Option<GitSummary>,
14 repo_snapshots: &'a HashMap<RepositoryId, RepositorySnapshot>,
15 repo_location: Option<(RepositoryId, Cursor<'a, StatusEntry, PathProgress<'a>>)>,
16}
17
18impl<'a> GitTraversal<'a> {
19 pub fn new(
20 repo_snapshots: &'a HashMap<RepositoryId, RepositorySnapshot>,
21 traversal: Traversal<'a>,
22 ) -> GitTraversal<'a> {
23 let mut this = GitTraversal {
24 traversal,
25 repo_snapshots,
26 current_entry_summary: None,
27 repo_location: None,
28 };
29 this.synchronize_statuses(true);
30 this
31 }
32
33 fn synchronize_statuses(&mut self, reset: bool) {
34 self.current_entry_summary = None;
35
36 let Some(entry) = self.entry() else {
37 return;
38 };
39
40 let Ok(abs_path) = self.traversal.snapshot().absolutize(&entry.path) else {
41 self.repo_location = None;
42 return;
43 };
44
45 let Some((repo, repo_path)) = self
46 .repo_snapshots
47 .values()
48 .filter_map(|repo_snapshot| {
49 let repo_path = repo_snapshot.abs_path_to_repo_path(&abs_path)?;
50 Some((repo_snapshot, repo_path))
51 })
52 .max_by_key(|(repo, _)| repo.work_directory_abs_path.clone())
53 else {
54 self.repo_location = None;
55 return;
56 };
57
58 // Update our state if we changed repositories.
59 if reset
60 || self
61 .repo_location
62 .as_ref()
63 .map(|(prev_repo_id, _)| *prev_repo_id)
64 != Some(repo.id)
65 {
66 self.repo_location = Some((repo.id, repo.statuses_by_path.cursor::<PathProgress>(&())));
67 }
68
69 let Some((_, statuses)) = &mut self.repo_location else {
70 return;
71 };
72
73 if entry.is_dir() {
74 let mut statuses = statuses.clone();
75 statuses.seek_forward(&PathTarget::Path(repo_path.as_ref()), Bias::Left, &());
76 let summary =
77 statuses.summary(&PathTarget::Successor(repo_path.as_ref()), Bias::Left, &());
78
79 self.current_entry_summary = Some(summary);
80 } else if entry.is_file() {
81 // For a file entry, park the cursor on the corresponding status
82 if statuses.seek_forward(&PathTarget::Path(repo_path.as_ref()), Bias::Left, &()) {
83 // TODO: Investigate statuses.item() being None here.
84 self.current_entry_summary = statuses.item().map(|item| item.status.into());
85 } else {
86 self.current_entry_summary = Some(GitSummary::UNCHANGED);
87 }
88 }
89 }
90
91 pub fn advance(&mut self) -> bool {
92 self.advance_by(1)
93 }
94
95 pub fn advance_by(&mut self, count: usize) -> bool {
96 let found = self.traversal.advance_by(count);
97 self.synchronize_statuses(false);
98 found
99 }
100
101 pub fn advance_to_sibling(&mut self) -> bool {
102 let found = self.traversal.advance_to_sibling();
103 self.synchronize_statuses(false);
104 found
105 }
106
107 pub fn back_to_parent(&mut self) -> bool {
108 let found = self.traversal.back_to_parent();
109 self.synchronize_statuses(true);
110 found
111 }
112
113 pub fn start_offset(&self) -> usize {
114 self.traversal.start_offset()
115 }
116
117 pub fn end_offset(&self) -> usize {
118 self.traversal.end_offset()
119 }
120
121 pub fn entry(&self) -> Option<GitEntryRef<'a>> {
122 let entry = self.traversal.entry()?;
123 let git_summary = self.current_entry_summary.unwrap_or(GitSummary::UNCHANGED);
124 Some(GitEntryRef { entry, git_summary })
125 }
126}
127
128impl<'a> Iterator for GitTraversal<'a> {
129 type Item = GitEntryRef<'a>;
130
131 fn next(&mut self) -> Option<Self::Item> {
132 if let Some(item) = self.entry() {
133 self.advance();
134 Some(item)
135 } else {
136 None
137 }
138 }
139}
140
141pub struct ChildEntriesGitIter<'a> {
142 parent_path: &'a Path,
143 traversal: GitTraversal<'a>,
144}
145
146impl<'a> ChildEntriesGitIter<'a> {
147 pub fn new(
148 repo_snapshots: &'a HashMap<RepositoryId, RepositorySnapshot>,
149 worktree_snapshot: &'a worktree::Snapshot,
150 parent_path: &'a Path,
151 ) -> Self {
152 let mut traversal = GitTraversal::new(
153 repo_snapshots,
154 worktree_snapshot.traverse_from_path(true, true, true, parent_path),
155 );
156 traversal.advance();
157 ChildEntriesGitIter {
158 parent_path,
159 traversal,
160 }
161 }
162}
163
164impl<'a> Iterator for ChildEntriesGitIter<'a> {
165 type Item = GitEntryRef<'a>;
166
167 fn next(&mut self) -> Option<Self::Item> {
168 if let Some(item) = self.traversal.entry() {
169 if item.path.starts_with(self.parent_path) {
170 self.traversal.advance_to_sibling();
171 return Some(item);
172 }
173 }
174 None
175 }
176}
177
178#[derive(Debug, Clone, Copy)]
179pub struct GitEntryRef<'a> {
180 pub entry: &'a Entry,
181 pub git_summary: GitSummary,
182}
183
184impl GitEntryRef<'_> {
185 pub fn to_owned(&self) -> GitEntry {
186 GitEntry {
187 entry: self.entry.clone(),
188 git_summary: self.git_summary,
189 }
190 }
191}
192
193impl Deref for GitEntryRef<'_> {
194 type Target = Entry;
195
196 fn deref(&self) -> &Self::Target {
197 &self.entry
198 }
199}
200
201impl AsRef<Entry> for GitEntryRef<'_> {
202 fn as_ref(&self) -> &Entry {
203 self.entry
204 }
205}
206
207#[derive(Debug, Clone, PartialEq, Eq)]
208pub struct GitEntry {
209 pub entry: Entry,
210 pub git_summary: GitSummary,
211}
212
213impl GitEntry {
214 pub fn to_ref(&self) -> GitEntryRef {
215 GitEntryRef {
216 entry: &self.entry,
217 git_summary: self.git_summary,
218 }
219 }
220}
221
222impl Deref for GitEntry {
223 type Target = Entry;
224
225 fn deref(&self) -> &Self::Target {
226 &self.entry
227 }
228}
229
230impl AsRef<Entry> for GitEntry {
231 fn as_ref(&self) -> &Entry {
232 &self.entry
233 }
234}
235
236#[cfg(test)]
237mod tests {
238 use std::time::Duration;
239
240 use crate::Project;
241
242 use super::*;
243 use fs::FakeFs;
244 use git::status::{FileStatus, StatusCode, TrackedSummary, UnmergedStatus, UnmergedStatusCode};
245 use gpui::TestAppContext;
246 use serde_json::json;
247 use settings::{Settings as _, SettingsStore};
248 use util::path;
249 use worktree::WorktreeSettings;
250
251 const CONFLICT: FileStatus = FileStatus::Unmerged(UnmergedStatus {
252 first_head: UnmergedStatusCode::Updated,
253 second_head: UnmergedStatusCode::Updated,
254 });
255 const ADDED: GitSummary = GitSummary {
256 index: TrackedSummary::ADDED,
257 count: 1,
258 ..GitSummary::UNCHANGED
259 };
260 const MODIFIED: GitSummary = GitSummary {
261 index: TrackedSummary::MODIFIED,
262 count: 1,
263 ..GitSummary::UNCHANGED
264 };
265
266 #[gpui::test]
267 async fn test_git_traversal_with_one_repo(cx: &mut TestAppContext) {
268 init_test(cx);
269 let fs = FakeFs::new(cx.background_executor.clone());
270 fs.insert_tree(
271 path!("/root"),
272 json!({
273 "x": {
274 ".git": {},
275 "x1.txt": "foo",
276 "x2.txt": "bar",
277 "y": {
278 ".git": {},
279 "y1.txt": "baz",
280 "y2.txt": "qux"
281 },
282 "z.txt": "sneaky..."
283 },
284 "z": {
285 ".git": {},
286 "z1.txt": "quux",
287 "z2.txt": "quuux"
288 }
289 }),
290 )
291 .await;
292
293 fs.set_status_for_repo(
294 Path::new(path!("/root/x/.git")),
295 &[
296 (Path::new("x2.txt"), StatusCode::Modified.index()),
297 (Path::new("z.txt"), StatusCode::Added.index()),
298 ],
299 );
300 fs.set_status_for_repo(
301 Path::new(path!("/root/x/y/.git")),
302 &[(Path::new("y1.txt"), CONFLICT)],
303 );
304 fs.set_status_for_repo(
305 Path::new(path!("/root/z/.git")),
306 &[(Path::new("z2.txt"), StatusCode::Added.index())],
307 );
308
309 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
310 cx.executor().run_until_parked();
311
312 let (repo_snapshots, worktree_snapshot) = project.read_with(cx, |project, cx| {
313 (
314 project.git_store().read(cx).repo_snapshots(cx),
315 project.worktrees(cx).next().unwrap().read(cx).snapshot(),
316 )
317 });
318
319 let traversal = GitTraversal::new(
320 &repo_snapshots,
321 worktree_snapshot.traverse_from_path(true, false, true, Path::new("x")),
322 );
323 let entries = traversal
324 .map(|entry| (entry.path.clone(), entry.git_summary))
325 .collect::<Vec<_>>();
326 pretty_assertions::assert_eq!(
327 entries,
328 [
329 (Path::new("x/x1.txt").into(), GitSummary::UNCHANGED),
330 (Path::new("x/x2.txt").into(), MODIFIED),
331 (Path::new("x/y/y1.txt").into(), GitSummary::CONFLICT),
332 (Path::new("x/y/y2.txt").into(), GitSummary::UNCHANGED),
333 (Path::new("x/z.txt").into(), ADDED),
334 (Path::new("z/z1.txt").into(), GitSummary::UNCHANGED),
335 (Path::new("z/z2.txt").into(), ADDED),
336 ]
337 )
338 }
339
340 #[gpui::test]
341 async fn test_git_traversal_with_nested_repos(cx: &mut TestAppContext) {
342 init_test(cx);
343 let fs = FakeFs::new(cx.background_executor.clone());
344 fs.insert_tree(
345 path!("/root"),
346 json!({
347 "x": {
348 ".git": {},
349 "x1.txt": "foo",
350 "x2.txt": "bar",
351 "y": {
352 ".git": {},
353 "y1.txt": "baz",
354 "y2.txt": "qux"
355 },
356 "z.txt": "sneaky..."
357 },
358 "z": {
359 ".git": {},
360 "z1.txt": "quux",
361 "z2.txt": "quuux"
362 }
363 }),
364 )
365 .await;
366
367 fs.set_status_for_repo(
368 Path::new(path!("/root/x/.git")),
369 &[
370 (Path::new("x2.txt"), StatusCode::Modified.index()),
371 (Path::new("z.txt"), StatusCode::Added.index()),
372 ],
373 );
374 fs.set_status_for_repo(
375 Path::new(path!("/root/x/y/.git")),
376 &[(Path::new("y1.txt"), CONFLICT)],
377 );
378
379 fs.set_status_for_repo(
380 Path::new(path!("/root/z/.git")),
381 &[(Path::new("z2.txt"), StatusCode::Added.index())],
382 );
383
384 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
385 cx.executor().run_until_parked();
386
387 let (repo_snapshots, worktree_snapshot) = project.read_with(cx, |project, cx| {
388 (
389 project.git_store().read(cx).repo_snapshots(cx),
390 project.worktrees(cx).next().unwrap().read(cx).snapshot(),
391 )
392 });
393
394 // Sanity check the propagation for x/y and z
395 check_git_statuses(
396 &repo_snapshots,
397 &worktree_snapshot,
398 &[
399 (Path::new("x/y"), GitSummary::CONFLICT),
400 (Path::new("x/y/y1.txt"), GitSummary::CONFLICT),
401 (Path::new("x/y/y2.txt"), GitSummary::UNCHANGED),
402 ],
403 );
404 check_git_statuses(
405 &repo_snapshots,
406 &worktree_snapshot,
407 &[
408 (Path::new("z"), ADDED),
409 (Path::new("z/z1.txt"), GitSummary::UNCHANGED),
410 (Path::new("z/z2.txt"), ADDED),
411 ],
412 );
413
414 // Test one of the fundamental cases of propagation blocking, the transition from one git repository to another
415 check_git_statuses(
416 &repo_snapshots,
417 &worktree_snapshot,
418 &[
419 (Path::new("x"), MODIFIED + ADDED),
420 (Path::new("x/y"), GitSummary::CONFLICT),
421 (Path::new("x/y/y1.txt"), GitSummary::CONFLICT),
422 ],
423 );
424
425 // Sanity check everything around it
426 check_git_statuses(
427 &repo_snapshots,
428 &worktree_snapshot,
429 &[
430 (Path::new("x"), MODIFIED + ADDED),
431 (Path::new("x/x1.txt"), GitSummary::UNCHANGED),
432 (Path::new("x/x2.txt"), MODIFIED),
433 (Path::new("x/y"), GitSummary::CONFLICT),
434 (Path::new("x/y/y1.txt"), GitSummary::CONFLICT),
435 (Path::new("x/y/y2.txt"), GitSummary::UNCHANGED),
436 (Path::new("x/z.txt"), ADDED),
437 ],
438 );
439
440 // Test the other fundamental case, transitioning from git repository to non-git repository
441 check_git_statuses(
442 &repo_snapshots,
443 &worktree_snapshot,
444 &[
445 (Path::new(""), GitSummary::UNCHANGED),
446 (Path::new("x"), MODIFIED + ADDED),
447 (Path::new("x/x1.txt"), GitSummary::UNCHANGED),
448 ],
449 );
450
451 // And all together now
452 check_git_statuses(
453 &repo_snapshots,
454 &worktree_snapshot,
455 &[
456 (Path::new(""), GitSummary::UNCHANGED),
457 (Path::new("x"), MODIFIED + ADDED),
458 (Path::new("x/x1.txt"), GitSummary::UNCHANGED),
459 (Path::new("x/x2.txt"), MODIFIED),
460 (Path::new("x/y"), GitSummary::CONFLICT),
461 (Path::new("x/y/y1.txt"), GitSummary::CONFLICT),
462 (Path::new("x/y/y2.txt"), GitSummary::UNCHANGED),
463 (Path::new("x/z.txt"), ADDED),
464 (Path::new("z"), ADDED),
465 (Path::new("z/z1.txt"), GitSummary::UNCHANGED),
466 (Path::new("z/z2.txt"), ADDED),
467 ],
468 );
469 }
470
471 #[gpui::test]
472 async fn test_git_traversal_simple(cx: &mut TestAppContext) {
473 init_test(cx);
474 let fs = FakeFs::new(cx.background_executor.clone());
475 fs.insert_tree(
476 path!("/root"),
477 json!({
478 ".git": {},
479 "a": {
480 "b": {
481 "c1.txt": "",
482 "c2.txt": "",
483 },
484 "d": {
485 "e1.txt": "",
486 "e2.txt": "",
487 "e3.txt": "",
488 }
489 },
490 "f": {
491 "no-status.txt": ""
492 },
493 "g": {
494 "h1.txt": "",
495 "h2.txt": ""
496 },
497 }),
498 )
499 .await;
500
501 fs.set_status_for_repo(
502 Path::new(path!("/root/.git")),
503 &[
504 (Path::new("a/b/c1.txt"), StatusCode::Added.index()),
505 (Path::new("a/d/e2.txt"), StatusCode::Modified.index()),
506 (Path::new("g/h2.txt"), CONFLICT),
507 ],
508 );
509
510 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
511 cx.executor().run_until_parked();
512
513 let (repo_snapshots, worktree_snapshot) = project.read_with(cx, |project, cx| {
514 (
515 project.git_store().read(cx).repo_snapshots(cx),
516 project.worktrees(cx).next().unwrap().read(cx).snapshot(),
517 )
518 });
519
520 check_git_statuses(
521 &repo_snapshots,
522 &worktree_snapshot,
523 &[
524 (Path::new(""), GitSummary::CONFLICT + MODIFIED + ADDED),
525 (Path::new("g"), GitSummary::CONFLICT),
526 (Path::new("g/h2.txt"), GitSummary::CONFLICT),
527 ],
528 );
529
530 check_git_statuses(
531 &repo_snapshots,
532 &worktree_snapshot,
533 &[
534 (Path::new(""), GitSummary::CONFLICT + ADDED + MODIFIED),
535 (Path::new("a"), ADDED + MODIFIED),
536 (Path::new("a/b"), ADDED),
537 (Path::new("a/b/c1.txt"), ADDED),
538 (Path::new("a/b/c2.txt"), GitSummary::UNCHANGED),
539 (Path::new("a/d"), MODIFIED),
540 (Path::new("a/d/e2.txt"), MODIFIED),
541 (Path::new("f"), GitSummary::UNCHANGED),
542 (Path::new("f/no-status.txt"), GitSummary::UNCHANGED),
543 (Path::new("g"), GitSummary::CONFLICT),
544 (Path::new("g/h2.txt"), GitSummary::CONFLICT),
545 ],
546 );
547
548 check_git_statuses(
549 &repo_snapshots,
550 &worktree_snapshot,
551 &[
552 (Path::new("a/b"), ADDED),
553 (Path::new("a/b/c1.txt"), ADDED),
554 (Path::new("a/b/c2.txt"), GitSummary::UNCHANGED),
555 (Path::new("a/d"), MODIFIED),
556 (Path::new("a/d/e1.txt"), GitSummary::UNCHANGED),
557 (Path::new("a/d/e2.txt"), MODIFIED),
558 (Path::new("f"), GitSummary::UNCHANGED),
559 (Path::new("f/no-status.txt"), GitSummary::UNCHANGED),
560 (Path::new("g"), GitSummary::CONFLICT),
561 ],
562 );
563
564 check_git_statuses(
565 &repo_snapshots,
566 &worktree_snapshot,
567 &[
568 (Path::new("a/b/c1.txt"), ADDED),
569 (Path::new("a/b/c2.txt"), GitSummary::UNCHANGED),
570 (Path::new("a/d/e1.txt"), GitSummary::UNCHANGED),
571 (Path::new("a/d/e2.txt"), MODIFIED),
572 (Path::new("f/no-status.txt"), GitSummary::UNCHANGED),
573 ],
574 );
575 }
576
577 #[gpui::test]
578 async fn test_git_traversal_with_repos_under_project(cx: &mut TestAppContext) {
579 init_test(cx);
580 let fs = FakeFs::new(cx.background_executor.clone());
581 fs.insert_tree(
582 path!("/root"),
583 json!({
584 "x": {
585 ".git": {},
586 "x1.txt": "foo",
587 "x2.txt": "bar"
588 },
589 "y": {
590 ".git": {},
591 "y1.txt": "baz",
592 "y2.txt": "qux"
593 },
594 "z": {
595 ".git": {},
596 "z1.txt": "quux",
597 "z2.txt": "quuux"
598 }
599 }),
600 )
601 .await;
602
603 fs.set_status_for_repo(
604 Path::new(path!("/root/x/.git")),
605 &[(Path::new("x1.txt"), StatusCode::Added.index())],
606 );
607 fs.set_status_for_repo(
608 Path::new(path!("/root/y/.git")),
609 &[
610 (Path::new("y1.txt"), CONFLICT),
611 (Path::new("y2.txt"), StatusCode::Modified.index()),
612 ],
613 );
614 fs.set_status_for_repo(
615 Path::new(path!("/root/z/.git")),
616 &[(Path::new("z2.txt"), StatusCode::Modified.index())],
617 );
618
619 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
620 cx.executor().run_until_parked();
621
622 let (repo_snapshots, worktree_snapshot) = project.read_with(cx, |project, cx| {
623 (
624 project.git_store().read(cx).repo_snapshots(cx),
625 project.worktrees(cx).next().unwrap().read(cx).snapshot(),
626 )
627 });
628
629 check_git_statuses(
630 &repo_snapshots,
631 &worktree_snapshot,
632 &[(Path::new("x"), ADDED), (Path::new("x/x1.txt"), ADDED)],
633 );
634
635 check_git_statuses(
636 &repo_snapshots,
637 &worktree_snapshot,
638 &[
639 (Path::new("y"), GitSummary::CONFLICT + MODIFIED),
640 (Path::new("y/y1.txt"), GitSummary::CONFLICT),
641 (Path::new("y/y2.txt"), MODIFIED),
642 ],
643 );
644
645 check_git_statuses(
646 &repo_snapshots,
647 &worktree_snapshot,
648 &[
649 (Path::new("z"), MODIFIED),
650 (Path::new("z/z2.txt"), MODIFIED),
651 ],
652 );
653
654 check_git_statuses(
655 &repo_snapshots,
656 &worktree_snapshot,
657 &[(Path::new("x"), ADDED), (Path::new("x/x1.txt"), ADDED)],
658 );
659
660 check_git_statuses(
661 &repo_snapshots,
662 &worktree_snapshot,
663 &[
664 (Path::new("x"), ADDED),
665 (Path::new("x/x1.txt"), ADDED),
666 (Path::new("x/x2.txt"), GitSummary::UNCHANGED),
667 (Path::new("y"), GitSummary::CONFLICT + MODIFIED),
668 (Path::new("y/y1.txt"), GitSummary::CONFLICT),
669 (Path::new("y/y2.txt"), MODIFIED),
670 (Path::new("z"), MODIFIED),
671 (Path::new("z/z1.txt"), GitSummary::UNCHANGED),
672 (Path::new("z/z2.txt"), MODIFIED),
673 ],
674 );
675 }
676
677 fn init_test(cx: &mut gpui::TestAppContext) {
678 if std::env::var("RUST_LOG").is_ok() {
679 env_logger::try_init().ok();
680 }
681
682 cx.update(|cx| {
683 let settings_store = SettingsStore::test(cx);
684 cx.set_global(settings_store);
685 WorktreeSettings::register(cx);
686 });
687 }
688
689 #[gpui::test]
690 async fn test_bump_mtime_of_git_repo_workdir(cx: &mut TestAppContext) {
691 init_test(cx);
692
693 // Create a worktree with a git directory.
694 let fs = FakeFs::new(cx.background_executor.clone());
695 fs.insert_tree(
696 path!("/root"),
697 json!({
698 ".git": {},
699 "a.txt": "",
700 "b": {
701 "c.txt": "",
702 },
703 }),
704 )
705 .await;
706 fs.set_head_and_index_for_repo(
707 path!("/root/.git").as_ref(),
708 &[("a.txt".into(), "".into()), ("b/c.txt".into(), "".into())],
709 );
710 cx.run_until_parked();
711
712 let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
713 cx.executor().run_until_parked();
714
715 let (old_entry_ids, old_mtimes) = project.read_with(cx, |project, cx| {
716 let tree = project.worktrees(cx).next().unwrap().read(cx);
717 (
718 tree.entries(true, 0).map(|e| e.id).collect::<Vec<_>>(),
719 tree.entries(true, 0).map(|e| e.mtime).collect::<Vec<_>>(),
720 )
721 });
722
723 // Regression test: after the directory is scanned, touch the git repo's
724 // working directory, bumping its mtime. That directory keeps its project
725 // entry id after the directories are re-scanned.
726 fs.touch_path(path!("/root")).await;
727 cx.executor().run_until_parked();
728
729 let (new_entry_ids, new_mtimes) = project.read_with(cx, |project, cx| {
730 let tree = project.worktrees(cx).next().unwrap().read(cx);
731 (
732 tree.entries(true, 0).map(|e| e.id).collect::<Vec<_>>(),
733 tree.entries(true, 0).map(|e| e.mtime).collect::<Vec<_>>(),
734 )
735 });
736 assert_eq!(new_entry_ids, old_entry_ids);
737 assert_ne!(new_mtimes, old_mtimes);
738
739 // Regression test: changes to the git repository should still be
740 // detected.
741 fs.set_head_for_repo(
742 path!("/root/.git").as_ref(),
743 &[
744 ("a.txt".into(), "".into()),
745 ("b/c.txt".into(), "something-else".into()),
746 ],
747 );
748 cx.executor().run_until_parked();
749 cx.executor().advance_clock(Duration::from_secs(1));
750
751 let (repo_snapshots, worktree_snapshot) = project.read_with(cx, |project, cx| {
752 (
753 project.git_store().read(cx).repo_snapshots(cx),
754 project.worktrees(cx).next().unwrap().read(cx).snapshot(),
755 )
756 });
757
758 check_git_statuses(
759 &repo_snapshots,
760 &worktree_snapshot,
761 &[
762 (Path::new(""), MODIFIED),
763 (Path::new("a.txt"), GitSummary::UNCHANGED),
764 (Path::new("b/c.txt"), MODIFIED),
765 ],
766 );
767 }
768
769 #[track_caller]
770 fn check_git_statuses(
771 repo_snapshots: &HashMap<RepositoryId, RepositorySnapshot>,
772 worktree_snapshot: &worktree::Snapshot,
773 expected_statuses: &[(&Path, GitSummary)],
774 ) {
775 let mut traversal = GitTraversal::new(
776 repo_snapshots,
777 worktree_snapshot.traverse_from_path(true, true, false, "".as_ref()),
778 );
779 let found_statuses = expected_statuses
780 .iter()
781 .map(|&(path, _)| {
782 let git_entry = traversal
783 .find(|git_entry| &*git_entry.path == path)
784 .unwrap_or_else(|| panic!("Traversal has no entry for {path:?}"));
785 (path, git_entry.git_summary)
786 })
787 .collect::<Vec<_>>();
788 pretty_assertions::assert_eq!(found_statuses, expected_statuses);
789 }
790}