1#![expect(missing_docs)]
16
17use std::collections::BTreeMap;
18use std::collections::HashSet;
19
20use itertools::Itertools as _;
21use thiserror::Error;
22
23use crate::backend::CommitId;
24use crate::index::Index;
25use crate::index::IndexResult;
26use crate::op_store;
27use crate::op_store::ABSENT_REMOTE_REF;
28use crate::op_store::LocalRemoteRefTarget;
29use crate::op_store::RefTarget;
30use crate::op_store::RefTargetOptionExt as _;
31use crate::op_store::RemoteRef;
32use crate::op_store::RemoteView;
33use crate::ref_name::GitRefName;
34use crate::ref_name::GitRefNameBuf;
35use crate::ref_name::RefName;
36use crate::ref_name::RemoteName;
37use crate::ref_name::RemoteRefSymbol;
38use crate::ref_name::WorkspaceName;
39use crate::ref_name::WorkspaceNameBuf;
40use crate::refs;
41use crate::refs::LocalAndRemoteRef;
42use crate::str_util::StringMatcher;
43
44#[derive(Eq, Debug, Clone)]
46pub struct View {
47 data: op_store::View,
48 head_normalized: bool,
49}
50
51impl PartialEq for View {
52 fn eq(&self, other: &Self) -> bool {
53 self.data == other.data
54 }
55}
56
57impl View {
58 pub fn new(op_store_view: op_store::View, head_normalized: bool) -> Self {
59 Self {
60 data: op_store_view,
61 head_normalized,
62 }
63 }
64
65 pub fn wc_commit_ids(&self) -> &BTreeMap<WorkspaceNameBuf, CommitId> {
66 &self.data.wc_commit_ids
67 }
68
69 pub fn get_wc_commit_id(&self, name: &WorkspaceName) -> Option<&CommitId> {
70 self.data.wc_commit_ids.get(name)
71 }
72
73 pub fn workspaces_for_wc_commit_id(&self, commit_id: &CommitId) -> Vec<WorkspaceNameBuf> {
74 let mut workspace_names = vec![];
75 for (name, wc_commit_id) in &self.data.wc_commit_ids {
76 if wc_commit_id == commit_id {
77 workspace_names.push(name.clone());
78 }
79 }
80 workspace_names
81 }
82
83 pub fn is_wc_commit_id(&self, commit_id: &CommitId) -> bool {
84 self.data.wc_commit_ids.values().contains(commit_id)
85 }
86
87 pub fn heads(&self) -> &HashSet<CommitId> {
88 &self.data.head_ids
89 }
90
91 pub fn bookmarks(&self) -> impl Iterator<Item = (&RefName, LocalRemoteRefTarget<'_>)> {
93 op_store::merge_join_ref_views(
94 &self.data.local_bookmarks,
95 &self.data.remote_views,
96 |view| &view.bookmarks,
97 )
98 }
99
100 pub fn tags(&self) -> impl Iterator<Item = (&RefName, LocalRemoteRefTarget<'_>)> {
102 op_store::merge_join_ref_views(&self.data.local_tags, &self.data.remote_views, |view| {
103 &view.tags
104 })
105 }
106
107 pub fn git_refs(&self) -> &BTreeMap<GitRefNameBuf, RefTarget> {
108 &self.data.git_refs
109 }
110
111 pub fn git_head(&self, workspace: &WorkspaceName) -> &RefTarget {
112 self.data.git_heads.get(workspace).flatten()
113 }
114
115 pub fn all_git_heads(&self) -> &BTreeMap<WorkspaceNameBuf, RefTarget> {
116 &self.data.git_heads
117 }
118
119 pub fn set_wc_commit(&mut self, name: WorkspaceNameBuf, commit_id: CommitId) {
120 self.data.wc_commit_ids.insert(name, commit_id);
121 }
122
123 pub fn remove_workspace(&mut self, name: &WorkspaceName) {
124 self.data.wc_commit_ids.remove(name);
125 self.data.git_heads.remove(name);
126 }
127
128 pub fn rename_workspace(
129 &mut self,
130 old_name: &WorkspaceName,
131 new_name: WorkspaceNameBuf,
132 ) -> Result<(), RenameWorkspaceError> {
133 if self.data.wc_commit_ids.contains_key(&new_name) {
134 return Err(RenameWorkspaceError::WorkspaceAlreadyExists {
135 name: new_name.clone(),
136 });
137 }
138 let wc_commit_id = self.data.wc_commit_ids.remove(old_name).ok_or_else(|| {
139 RenameWorkspaceError::WorkspaceDoesNotExist {
140 name: old_name.to_owned(),
141 }
142 })?;
143 self.data
144 .wc_commit_ids
145 .insert(new_name.clone(), wc_commit_id);
146 if let Some(git_head) = self.data.git_heads.remove(old_name) {
147 self.data.git_heads.insert(new_name, git_head);
148 }
149 Ok(())
150 }
151
152 pub fn add_head(&mut self, head_id: &CommitId) {
153 self.data.head_ids.insert(head_id.clone());
154 self.head_normalized = false;
155 }
156
157 pub fn remove_head(&mut self, head_id: &CommitId) {
158 self.data.head_ids.remove(head_id);
159 self.head_normalized = false;
160 }
161
162 pub fn replace_heads(&mut self, add_head_id: CommitId, remove_head_ids: &[CommitId]) {
169 self.data.head_ids.insert(add_head_id);
170 for head_id in remove_head_ids {
171 self.data.head_ids.remove(head_id);
172 }
173 }
174
175 pub fn local_bookmarks(&self) -> impl Iterator<Item = (&RefName, &RefTarget)> {
177 self.data
178 .local_bookmarks
179 .iter()
180 .map(|(name, target)| (name.as_ref(), target))
181 }
182
183 pub fn local_bookmarks_for_commit(
186 &self,
187 commit_id: &CommitId,
188 ) -> impl Iterator<Item = (&RefName, &RefTarget)> {
189 self.local_bookmarks()
190 .filter(|(_, target)| target.present_adds().contains(commit_id))
191 }
192
193 pub fn local_bookmarks_matching(
196 &self,
197 matcher: &StringMatcher,
198 ) -> impl Iterator<Item = (&RefName, &RefTarget)> {
199 matcher
200 .filter_btree_map_as_deref(&self.data.local_bookmarks)
201 .map(|(name, target)| (name.as_ref(), target))
202 }
203
204 pub fn get_local_bookmark(&self, name: &RefName) -> &RefTarget {
205 self.data.local_bookmarks.get(name).flatten()
206 }
207
208 pub fn set_local_bookmark_target(&mut self, name: &RefName, target: RefTarget) {
213 if target.is_present() {
214 self.data.local_bookmarks.insert(name.to_owned(), target);
215 } else {
216 self.data.local_bookmarks.remove(name);
217 for remote_view in self.data.remote_views.values_mut() {
218 let remote_refs = &mut remote_view.bookmarks;
219 if remote_refs.get(name).is_some_and(RemoteRef::is_absent) {
220 remote_refs.remove(name);
221 }
222 }
223 }
224 }
225
226 pub fn all_remote_bookmarks(&self) -> impl Iterator<Item = (RemoteRefSymbol<'_>, &RemoteRef)> {
229 op_store::flatten_remote_refs(&self.data.remote_views, |view| &view.bookmarks)
230 }
231
232 pub fn remote_bookmarks(
235 &self,
236 remote_name: &RemoteName,
237 ) -> impl Iterator<Item = (&RefName, &RemoteRef)> + use<'_> {
238 let maybe_remote_view = self.data.remote_views.get(remote_name);
239 maybe_remote_view
240 .map(|remote_view| {
241 remote_view
242 .bookmarks
243 .iter()
244 .map(|(name, remote_ref)| (name.as_ref(), remote_ref))
245 })
246 .into_iter()
247 .flatten()
248 }
249
250 pub fn remote_bookmarks_matching(
255 &self,
256 bookmark_matcher: &StringMatcher,
257 remote_matcher: &StringMatcher,
258 ) -> impl Iterator<Item = (RemoteRefSymbol<'_>, &RemoteRef)> {
259 remote_matcher
261 .filter_btree_map_as_deref(&self.data.remote_views)
262 .map(|(remote, remote_view)| {
263 bookmark_matcher
264 .filter_btree_map_as_deref(&remote_view.bookmarks)
265 .map(|(name, remote_ref)| (name.to_remote_symbol(remote), remote_ref))
266 })
267 .kmerge_by(|(symbol1, _), (symbol2, _)| symbol1 < symbol2)
268 }
269
270 pub fn get_remote_bookmark(&self, symbol: RemoteRefSymbol<'_>) -> &RemoteRef {
271 if let Some(remote_view) = self.data.remote_views.get(symbol.remote) {
272 remote_view.bookmarks.get(symbol.name).flatten()
273 } else {
274 &ABSENT_REMOTE_REF
275 }
276 }
277
278 pub fn set_remote_bookmark(&mut self, symbol: RemoteRefSymbol<'_>, remote_ref: RemoteRef) {
282 if remote_ref.is_present()
283 || (remote_ref.is_tracked() && self.get_local_bookmark(symbol.name).is_present())
284 {
285 let remote_view = self
286 .data
287 .remote_views
288 .entry(symbol.remote.to_owned())
289 .or_default();
290 remote_view
291 .bookmarks
292 .insert(symbol.name.to_owned(), remote_ref);
293 } else if let Some(remote_view) = self.data.remote_views.get_mut(symbol.remote) {
294 remote_view.bookmarks.remove(symbol.name);
295 }
296 }
297
298 pub fn local_remote_bookmarks(
306 &self,
307 remote_name: &RemoteName,
308 ) -> impl Iterator<Item = (&RefName, LocalAndRemoteRef<'_>)> + use<'_> {
309 refs::iter_named_local_remote_refs(
310 self.local_bookmarks(),
311 self.remote_bookmarks(remote_name),
312 )
313 .map(|(name, (local_target, remote_ref))| {
314 let targets = LocalAndRemoteRef {
315 local_target,
316 remote_ref,
317 };
318 (name, targets)
319 })
320 }
321
322 pub fn local_remote_bookmarks_matching<'a, 'b>(
332 &'a self,
333 bookmark_matcher: &'b StringMatcher,
334 remote_name: &RemoteName,
335 ) -> impl Iterator<Item = (&'a RefName, LocalAndRemoteRef<'a>)> + use<'a, 'b> {
336 let maybe_remote_view = self.data.remote_views.get(remote_name);
339 refs::iter_named_local_remote_refs(
340 bookmark_matcher.filter_btree_map_as_deref(&self.data.local_bookmarks),
341 maybe_remote_view
342 .map(|remote_view| {
343 bookmark_matcher.filter_btree_map_as_deref(&remote_view.bookmarks)
344 })
345 .into_iter()
346 .flatten(),
347 )
348 .map(|(name, (local_target, remote_ref))| {
349 let targets = LocalAndRemoteRef {
350 local_target,
351 remote_ref,
352 };
353 (name.as_ref(), targets)
354 })
355 }
356
357 pub fn remote_views(&self) -> impl Iterator<Item = (&RemoteName, &RemoteView)> {
359 self.data
360 .remote_views
361 .iter()
362 .map(|(name, view)| (name.as_ref(), view))
363 }
364
365 pub fn remote_views_matching(
367 &self,
368 matcher: &StringMatcher,
369 ) -> impl Iterator<Item = (&RemoteName, &RemoteView)> {
370 matcher
371 .filter_btree_map_as_deref(&self.data.remote_views)
372 .map(|(name, view)| (name.as_ref(), view))
373 }
374
375 pub fn get_remote_view(&self, name: &RemoteName) -> Option<&RemoteView> {
377 self.data.remote_views.get(name)
378 }
379
380 pub fn ensure_remote(&mut self, remote_name: &RemoteName) {
382 if self.data.remote_views.contains_key(remote_name) {
383 return;
384 }
385 self.data
386 .remote_views
387 .insert(remote_name.to_owned(), RemoteView::default());
388 }
389
390 pub fn remove_remote(&mut self, remote_name: &RemoteName) {
391 self.data.remote_views.remove(remote_name);
392 }
393
394 pub fn rename_remote(&mut self, old: &RemoteName, new: &RemoteName) {
395 if let Some(remote_view) = self.data.remote_views.remove(old) {
396 self.data.remote_views.insert(new.to_owned(), remote_view);
397 }
398 }
399
400 pub fn local_tags(&self) -> impl Iterator<Item = (&RefName, &RefTarget)> {
402 self.data
403 .local_tags
404 .iter()
405 .map(|(name, target)| (name.as_ref(), target))
406 }
407
408 pub fn get_local_tag(&self, name: &RefName) -> &RefTarget {
409 self.data.local_tags.get(name).flatten()
410 }
411
412 pub fn local_tags_matching(
415 &self,
416 matcher: &StringMatcher,
417 ) -> impl Iterator<Item = (&RefName, &RefTarget)> {
418 matcher
419 .filter_btree_map_as_deref(&self.data.local_tags)
420 .map(|(name, target)| (name.as_ref(), target))
421 }
422
423 pub fn set_local_tag_target(&mut self, name: &RefName, target: RefTarget) {
427 if target.is_present() {
428 self.data.local_tags.insert(name.to_owned(), target);
429 } else {
430 self.data.local_tags.remove(name);
431 for remote_view in self.data.remote_views.values_mut() {
432 let remote_refs = &mut remote_view.tags;
433 if remote_refs.get(name).is_some_and(RemoteRef::is_absent) {
434 remote_refs.remove(name);
435 }
436 }
437 }
438 }
439
440 pub fn all_remote_tags(&self) -> impl Iterator<Item = (RemoteRefSymbol<'_>, &RemoteRef)> {
443 op_store::flatten_remote_refs(&self.data.remote_views, |view| &view.tags)
444 }
445
446 pub fn remote_tags(
449 &self,
450 remote_name: &RemoteName,
451 ) -> impl Iterator<Item = (&RefName, &RemoteRef)> + use<'_> {
452 let maybe_remote_view = self.data.remote_views.get(remote_name);
453 maybe_remote_view
454 .map(|remote_view| {
455 remote_view
456 .tags
457 .iter()
458 .map(|(name, remote_ref)| (name.as_ref(), remote_ref))
459 })
460 .into_iter()
461 .flatten()
462 }
463
464 pub fn remote_tags_matching(
469 &self,
470 tag_matcher: &StringMatcher,
471 remote_matcher: &StringMatcher,
472 ) -> impl Iterator<Item = (RemoteRefSymbol<'_>, &RemoteRef)> {
473 remote_matcher
475 .filter_btree_map_as_deref(&self.data.remote_views)
476 .map(|(remote, remote_view)| {
477 tag_matcher
478 .filter_btree_map_as_deref(&remote_view.tags)
479 .map(|(name, remote_ref)| (name.to_remote_symbol(remote), remote_ref))
480 })
481 .kmerge_by(|(symbol1, _), (symbol2, _)| symbol1 < symbol2)
482 }
483
484 pub fn get_remote_tag(&self, symbol: RemoteRefSymbol<'_>) -> &RemoteRef {
486 if let Some(remote_view) = self.data.remote_views.get(symbol.remote) {
487 remote_view.tags.get(symbol.name).flatten()
488 } else {
489 &ABSENT_REMOTE_REF
490 }
491 }
492
493 pub fn set_remote_tag(&mut self, symbol: RemoteRefSymbol<'_>, remote_ref: RemoteRef) {
496 if remote_ref.is_present()
497 || (remote_ref.is_tracked() && self.get_local_tag(symbol.name).is_present())
498 {
499 let remote_view = self
500 .data
501 .remote_views
502 .entry(symbol.remote.to_owned())
503 .or_default();
504 remote_view.tags.insert(symbol.name.to_owned(), remote_ref);
505 } else if let Some(remote_view) = self.data.remote_views.get_mut(symbol.remote) {
506 remote_view.tags.remove(symbol.name);
507 }
508 }
509
510 pub fn local_remote_tags(
517 &self,
518 remote_name: &RemoteName,
519 ) -> impl Iterator<Item = (&RefName, LocalAndRemoteRef<'_>)> + use<'_> {
520 refs::iter_named_local_remote_refs(self.local_tags(), self.remote_tags(remote_name)).map(
521 |(name, (local_target, remote_ref))| {
522 let targets = LocalAndRemoteRef {
523 local_target,
524 remote_ref,
525 };
526 (name, targets)
527 },
528 )
529 }
530
531 pub fn local_remote_tags_matching<'a, 'b>(
541 &'a self,
542 tag_matcher: &'b StringMatcher,
543 remote_name: &RemoteName,
544 ) -> impl Iterator<Item = (&'a RefName, LocalAndRemoteRef<'a>)> + use<'a, 'b> {
545 let maybe_remote_view = self.data.remote_views.get(remote_name);
548 refs::iter_named_local_remote_refs(
549 tag_matcher.filter_btree_map_as_deref(&self.data.local_tags),
550 maybe_remote_view
551 .map(|remote_view| tag_matcher.filter_btree_map_as_deref(&remote_view.tags))
552 .into_iter()
553 .flatten(),
554 )
555 .map(|(name, (local_target, remote_ref))| {
556 let targets = LocalAndRemoteRef {
557 local_target,
558 remote_ref,
559 };
560 (name.as_ref(), targets)
561 })
562 }
563
564 pub fn get_git_ref(&self, name: &GitRefName) -> &RefTarget {
565 self.data.git_refs.get(name).flatten()
566 }
567
568 pub fn set_git_ref_target(&mut self, name: &GitRefName, target: RefTarget) {
571 if target.is_present() {
572 self.data.git_refs.insert(name.to_owned(), target);
573 } else {
574 self.data.git_refs.remove(name);
575 }
576 }
577
578 pub fn set_git_head_target(&mut self, workspace: &WorkspaceName, target: RefTarget) {
581 if target.is_present() {
582 self.data.git_heads.insert(workspace.to_owned(), target);
583 } else {
584 self.data.git_heads.remove(workspace);
585 }
586 }
587
588 pub fn all_referenced_commit_ids(&self) -> impl Iterator<Item = &CommitId> {
597 fn ref_target_ids(target: &RefTarget) -> impl Iterator<Item = &CommitId> {
600 target.iter().flatten()
601 }
602
603 let op_store::View {
606 head_ids,
607 local_bookmarks,
608 local_tags,
609 remote_views,
610 git_refs,
611 git_heads,
612 wc_commit_ids,
613 } = &self.data;
614 itertools::chain!(
615 head_ids,
616 local_bookmarks.values().flat_map(ref_target_ids),
617 local_tags.values().flat_map(ref_target_ids),
618 remote_views.values().flat_map(|remote_view| {
619 let op_store::RemoteView { bookmarks, tags } = remote_view;
620 itertools::chain(bookmarks.values(), tags.values())
621 .flat_map(|remote_ref| ref_target_ids(&remote_ref.target))
622 }),
623 git_refs.values().flat_map(ref_target_ids),
624 git_heads.values().flat_map(ref_target_ids),
625 wc_commit_ids.values()
626 )
627 }
628
629 pub fn set_view(&mut self, data: op_store::View, head_normalized: bool) {
630 self.data = data;
631 self.head_normalized = head_normalized;
632 }
633
634 pub fn store_view(&self) -> &op_store::View {
635 &self.data
636 }
637
638 pub fn store_view_mut(&mut self) -> &mut op_store::View {
639 &mut self.data
640 }
641
642 pub fn is_heads_normalized(&self) -> bool {
643 self.head_normalized
644 }
645
646 pub async fn normalize_heads(
647 &mut self,
648 index: &dyn Index,
649 root_commit_id: &CommitId,
650 ) -> IndexResult<()> {
651 if self.head_normalized {
652 return Ok(());
653 }
654 let view = self.store_view_mut();
655 if view.head_ids.is_empty() {
656 view.head_ids.insert(root_commit_id.clone());
657 } else if view.head_ids.len() > 1 {
658 view.head_ids.remove(root_commit_id);
661 view.head_ids = index
662 .heads(&mut view.head_ids.iter())
663 .await?
664 .into_iter()
665 .collect();
666 }
667 assert!(!view.head_ids.is_empty());
668 self.head_normalized = true;
669 Ok(())
670 }
671}
672
673#[derive(Debug, Error)]
675pub enum RenameWorkspaceError {
676 #[error("Workspace {} not found", name.as_symbol())]
677 WorkspaceDoesNotExist { name: WorkspaceNameBuf },
678
679 #[error("Workspace {} already exists", name.as_symbol())]
680 WorkspaceAlreadyExists { name: WorkspaceNameBuf },
681}
682
683#[cfg(test)]
684mod tests {
685 use super::*;
686 use crate::op_store::RemoteRefState;
687
688 fn remote_symbol<'a, N, M>(name: &'a N, remote: &'a M) -> RemoteRefSymbol<'a>
689 where
690 N: AsRef<RefName> + ?Sized,
691 M: AsRef<RemoteName> + ?Sized,
692 {
693 RemoteRefSymbol {
694 name: name.as_ref(),
695 remote: remote.as_ref(),
696 }
697 }
698
699 #[test]
700 fn test_absent_tracked_bookmarks() {
701 let mut view = View {
702 data: op_store::View::make_root(CommitId::from_hex("000000")),
703 head_normalized: true,
704 };
705 let absent_tracked_ref = RemoteRef {
706 target: RefTarget::absent(),
707 state: RemoteRefState::Tracked,
708 };
709 let present_tracked_ref = RemoteRef {
710 target: RefTarget::normal(CommitId::from_hex("111111")),
711 state: RemoteRefState::Tracked,
712 };
713
714 view.set_remote_bookmark(remote_symbol("foo", "new"), absent_tracked_ref.clone());
716 assert_eq!(
717 view.get_remote_bookmark(remote_symbol("foo", "new")),
718 &RemoteRef::absent()
719 );
720
721 view.set_remote_bookmark(remote_symbol("foo", "present"), present_tracked_ref.clone());
723 assert_eq!(
724 view.get_remote_bookmark(remote_symbol("foo", "present")),
725 &present_tracked_ref
726 );
727
728 view.set_local_bookmark_target(
730 "foo".as_ref(),
731 RefTarget::normal(CommitId::from_hex("222222")),
732 );
733 view.set_remote_bookmark(remote_symbol("foo", "new"), absent_tracked_ref.clone());
734 assert_eq!(
735 view.get_remote_bookmark(remote_symbol("foo", "new")),
736 &absent_tracked_ref
737 );
738
739 view.set_local_bookmark_target("foo".as_ref(), RefTarget::absent());
741 assert_eq!(
742 view.get_remote_bookmark(remote_symbol("foo", "new")),
743 &RemoteRef::absent()
744 );
745 assert_eq!(
746 view.get_remote_bookmark(remote_symbol("foo", "present")),
747 &present_tracked_ref
748 );
749 }
750
751 #[test]
752 fn test_absent_tracked_tags() {
753 let mut view = View {
754 data: op_store::View::make_root(CommitId::from_hex("000000")),
755 head_normalized: true,
756 };
757 let absent_tracked_ref = RemoteRef {
758 target: RefTarget::absent(),
759 state: RemoteRefState::Tracked,
760 };
761 let present_tracked_ref = RemoteRef {
762 target: RefTarget::normal(CommitId::from_hex("111111")),
763 state: RemoteRefState::Tracked,
764 };
765
766 view.set_remote_tag(remote_symbol("foo", "new"), absent_tracked_ref.clone());
768 assert_eq!(
769 view.get_remote_tag(remote_symbol("foo", "new")),
770 &RemoteRef::absent()
771 );
772
773 view.set_remote_tag(remote_symbol("foo", "present"), present_tracked_ref.clone());
775 assert_eq!(
776 view.get_remote_tag(remote_symbol("foo", "present")),
777 &present_tracked_ref
778 );
779
780 view.set_local_tag_target(
782 "foo".as_ref(),
783 RefTarget::normal(CommitId::from_hex("222222")),
784 );
785 view.set_remote_tag(remote_symbol("foo", "new"), absent_tracked_ref.clone());
786 assert_eq!(
787 view.get_remote_tag(remote_symbol("foo", "new")),
788 &absent_tracked_ref
789 );
790
791 view.set_local_tag_target("foo".as_ref(), RefTarget::absent());
793 assert_eq!(
794 view.get_remote_tag(remote_symbol("foo", "new")),
795 &RemoteRef::absent()
796 );
797 assert_eq!(
798 view.get_remote_tag(remote_symbol("foo", "present")),
799 &present_tracked_ref
800 );
801 }
802}