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