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) -> &RefTarget {
111 &self.data.git_head
112 }
113
114 pub fn set_wc_commit(&mut self, name: WorkspaceNameBuf, commit_id: CommitId) {
115 self.data.wc_commit_ids.insert(name, commit_id);
116 }
117
118 pub fn remove_wc_commit(&mut self, name: &WorkspaceName) {
119 self.data.wc_commit_ids.remove(name);
120 }
121
122 pub fn rename_workspace(
123 &mut self,
124 old_name: &WorkspaceName,
125 new_name: WorkspaceNameBuf,
126 ) -> Result<(), RenameWorkspaceError> {
127 if self.data.wc_commit_ids.contains_key(&new_name) {
128 return Err(RenameWorkspaceError::WorkspaceAlreadyExists {
129 name: new_name.clone(),
130 });
131 }
132 let wc_commit_id = self.data.wc_commit_ids.remove(old_name).ok_or_else(|| {
133 RenameWorkspaceError::WorkspaceDoesNotExist {
134 name: old_name.to_owned(),
135 }
136 })?;
137 self.data.wc_commit_ids.insert(new_name, wc_commit_id);
138 Ok(())
139 }
140
141 pub fn add_head(&mut self, head_id: &CommitId) {
142 self.data.head_ids.insert(head_id.clone());
143 self.head_normalized = false;
144 }
145
146 pub fn remove_head(&mut self, head_id: &CommitId) {
147 self.data.head_ids.remove(head_id);
148 self.head_normalized = false;
149 }
150
151 pub fn replace_heads(&mut self, add_head_id: CommitId, remove_head_ids: &[CommitId]) {
158 self.data.head_ids.insert(add_head_id);
159 for head_id in remove_head_ids {
160 self.data.head_ids.remove(head_id);
161 }
162 }
163
164 pub fn local_bookmarks(&self) -> impl Iterator<Item = (&RefName, &RefTarget)> {
166 self.data
167 .local_bookmarks
168 .iter()
169 .map(|(name, target)| (name.as_ref(), target))
170 }
171
172 pub fn local_bookmarks_for_commit(
175 &self,
176 commit_id: &CommitId,
177 ) -> impl Iterator<Item = (&RefName, &RefTarget)> {
178 self.local_bookmarks()
179 .filter(|(_, target)| target.added_ids().contains(commit_id))
180 }
181
182 pub fn local_bookmarks_matching(
185 &self,
186 matcher: &StringMatcher,
187 ) -> impl Iterator<Item = (&RefName, &RefTarget)> {
188 matcher
189 .filter_btree_map_as_deref(&self.data.local_bookmarks)
190 .map(|(name, target)| (name.as_ref(), target))
191 }
192
193 pub fn get_local_bookmark(&self, name: &RefName) -> &RefTarget {
194 self.data.local_bookmarks.get(name).flatten()
195 }
196
197 pub fn set_local_bookmark_target(&mut self, name: &RefName, target: RefTarget) {
202 if target.is_present() {
203 self.data.local_bookmarks.insert(name.to_owned(), target);
204 } else {
205 self.data.local_bookmarks.remove(name);
206 for remote_view in self.data.remote_views.values_mut() {
207 let remote_refs = &mut remote_view.bookmarks;
208 if remote_refs.get(name).is_some_and(RemoteRef::is_absent) {
209 remote_refs.remove(name);
210 }
211 }
212 }
213 }
214
215 pub fn all_remote_bookmarks(&self) -> impl Iterator<Item = (RemoteRefSymbol<'_>, &RemoteRef)> {
218 op_store::flatten_remote_refs(&self.data.remote_views, |view| &view.bookmarks)
219 }
220
221 pub fn remote_bookmarks(
224 &self,
225 remote_name: &RemoteName,
226 ) -> impl Iterator<Item = (&RefName, &RemoteRef)> + use<'_> {
227 let maybe_remote_view = self.data.remote_views.get(remote_name);
228 maybe_remote_view
229 .map(|remote_view| {
230 remote_view
231 .bookmarks
232 .iter()
233 .map(|(name, remote_ref)| (name.as_ref(), remote_ref))
234 })
235 .into_iter()
236 .flatten()
237 }
238
239 pub fn remote_bookmarks_matching(
244 &self,
245 bookmark_matcher: &StringMatcher,
246 remote_matcher: &StringMatcher,
247 ) -> impl Iterator<Item = (RemoteRefSymbol<'_>, &RemoteRef)> {
248 remote_matcher
250 .filter_btree_map_as_deref(&self.data.remote_views)
251 .map(|(remote, remote_view)| {
252 bookmark_matcher
253 .filter_btree_map_as_deref(&remote_view.bookmarks)
254 .map(|(name, remote_ref)| (name.to_remote_symbol(remote), remote_ref))
255 })
256 .kmerge_by(|(symbol1, _), (symbol2, _)| symbol1 < symbol2)
257 }
258
259 pub fn get_remote_bookmark(&self, symbol: RemoteRefSymbol<'_>) -> &RemoteRef {
260 if let Some(remote_view) = self.data.remote_views.get(symbol.remote) {
261 remote_view.bookmarks.get(symbol.name).flatten()
262 } else {
263 RemoteRef::absent_ref()
264 }
265 }
266
267 pub fn set_remote_bookmark(&mut self, symbol: RemoteRefSymbol<'_>, remote_ref: RemoteRef) {
271 if remote_ref.is_present()
272 || (remote_ref.is_tracked() && self.get_local_bookmark(symbol.name).is_present())
273 {
274 let remote_view = self
275 .data
276 .remote_views
277 .entry(symbol.remote.to_owned())
278 .or_default();
279 remote_view
280 .bookmarks
281 .insert(symbol.name.to_owned(), remote_ref);
282 } else if let Some(remote_view) = self.data.remote_views.get_mut(symbol.remote) {
283 remote_view.bookmarks.remove(symbol.name);
284 }
285 }
286
287 pub fn local_remote_bookmarks(
295 &self,
296 remote_name: &RemoteName,
297 ) -> impl Iterator<Item = (&RefName, LocalAndRemoteRef<'_>)> + use<'_> {
298 refs::iter_named_local_remote_refs(
299 self.local_bookmarks(),
300 self.remote_bookmarks(remote_name),
301 )
302 .map(|(name, (local_target, remote_ref))| {
303 let targets = LocalAndRemoteRef {
304 local_target,
305 remote_ref,
306 };
307 (name, targets)
308 })
309 }
310
311 pub fn local_remote_bookmarks_matching<'a, 'b>(
321 &'a self,
322 bookmark_matcher: &'b StringMatcher,
323 remote_name: &RemoteName,
324 ) -> impl Iterator<Item = (&'a RefName, LocalAndRemoteRef<'a>)> + use<'a, 'b> {
325 let maybe_remote_view = self.data.remote_views.get(remote_name);
328 refs::iter_named_local_remote_refs(
329 bookmark_matcher.filter_btree_map_as_deref(&self.data.local_bookmarks),
330 maybe_remote_view
331 .map(|remote_view| {
332 bookmark_matcher.filter_btree_map_as_deref(&remote_view.bookmarks)
333 })
334 .into_iter()
335 .flatten(),
336 )
337 .map(|(name, (local_target, remote_ref))| {
338 let targets = LocalAndRemoteRef {
339 local_target,
340 remote_ref,
341 };
342 (name.as_ref(), targets)
343 })
344 }
345
346 pub fn remote_views(&self) -> impl Iterator<Item = (&RemoteName, &RemoteView)> {
348 self.data
349 .remote_views
350 .iter()
351 .map(|(name, view)| (name.as_ref(), view))
352 }
353
354 pub fn remote_views_matching(
356 &self,
357 matcher: &StringMatcher,
358 ) -> impl Iterator<Item = (&RemoteName, &RemoteView)> {
359 matcher
360 .filter_btree_map_as_deref(&self.data.remote_views)
361 .map(|(name, view)| (name.as_ref(), view))
362 }
363
364 pub fn get_remote_view(&self, name: &RemoteName) -> Option<&RemoteView> {
366 self.data.remote_views.get(name)
367 }
368
369 pub fn ensure_remote(&mut self, remote_name: &RemoteName) {
371 if self.data.remote_views.contains_key(remote_name) {
372 return;
373 }
374 self.data
375 .remote_views
376 .insert(remote_name.to_owned(), RemoteView::default());
377 }
378
379 pub fn remove_remote(&mut self, remote_name: &RemoteName) {
380 self.data.remote_views.remove(remote_name);
381 }
382
383 pub fn rename_remote(&mut self, old: &RemoteName, new: &RemoteName) {
384 if let Some(remote_view) = self.data.remote_views.remove(old) {
385 self.data.remote_views.insert(new.to_owned(), remote_view);
386 }
387 }
388
389 pub fn local_tags(&self) -> impl Iterator<Item = (&RefName, &RefTarget)> {
391 self.data
392 .local_tags
393 .iter()
394 .map(|(name, target)| (name.as_ref(), target))
395 }
396
397 pub fn get_local_tag(&self, name: &RefName) -> &RefTarget {
398 self.data.local_tags.get(name).flatten()
399 }
400
401 pub fn local_tags_matching(
404 &self,
405 matcher: &StringMatcher,
406 ) -> impl Iterator<Item = (&RefName, &RefTarget)> {
407 matcher
408 .filter_btree_map_as_deref(&self.data.local_tags)
409 .map(|(name, target)| (name.as_ref(), target))
410 }
411
412 pub fn set_local_tag_target(&mut self, name: &RefName, target: RefTarget) {
416 if target.is_present() {
417 self.data.local_tags.insert(name.to_owned(), target);
418 } else {
419 self.data.local_tags.remove(name);
420 for remote_view in self.data.remote_views.values_mut() {
421 let remote_refs = &mut remote_view.tags;
422 if remote_refs.get(name).is_some_and(RemoteRef::is_absent) {
423 remote_refs.remove(name);
424 }
425 }
426 }
427 }
428
429 pub fn all_remote_tags(&self) -> impl Iterator<Item = (RemoteRefSymbol<'_>, &RemoteRef)> {
432 op_store::flatten_remote_refs(&self.data.remote_views, |view| &view.tags)
433 }
434
435 pub fn remote_tags(
438 &self,
439 remote_name: &RemoteName,
440 ) -> impl Iterator<Item = (&RefName, &RemoteRef)> + use<'_> {
441 let maybe_remote_view = self.data.remote_views.get(remote_name);
442 maybe_remote_view
443 .map(|remote_view| {
444 remote_view
445 .tags
446 .iter()
447 .map(|(name, remote_ref)| (name.as_ref(), remote_ref))
448 })
449 .into_iter()
450 .flatten()
451 }
452
453 pub fn remote_tags_matching(
458 &self,
459 tag_matcher: &StringMatcher,
460 remote_matcher: &StringMatcher,
461 ) -> impl Iterator<Item = (RemoteRefSymbol<'_>, &RemoteRef)> {
462 remote_matcher
464 .filter_btree_map_as_deref(&self.data.remote_views)
465 .map(|(remote, remote_view)| {
466 tag_matcher
467 .filter_btree_map_as_deref(&remote_view.tags)
468 .map(|(name, remote_ref)| (name.to_remote_symbol(remote), remote_ref))
469 })
470 .kmerge_by(|(symbol1, _), (symbol2, _)| symbol1 < symbol2)
471 }
472
473 pub fn get_remote_tag(&self, symbol: RemoteRefSymbol<'_>) -> &RemoteRef {
475 if let Some(remote_view) = self.data.remote_views.get(symbol.remote) {
476 remote_view.tags.get(symbol.name).flatten()
477 } else {
478 RemoteRef::absent_ref()
479 }
480 }
481
482 pub fn set_remote_tag(&mut self, symbol: RemoteRefSymbol<'_>, remote_ref: RemoteRef) {
485 if remote_ref.is_present()
486 || (remote_ref.is_tracked() && self.get_local_tag(symbol.name).is_present())
487 {
488 let remote_view = self
489 .data
490 .remote_views
491 .entry(symbol.remote.to_owned())
492 .or_default();
493 remote_view.tags.insert(symbol.name.to_owned(), remote_ref);
494 } else if let Some(remote_view) = self.data.remote_views.get_mut(symbol.remote) {
495 remote_view.tags.remove(symbol.name);
496 }
497 }
498
499 pub fn local_remote_tags(
506 &self,
507 remote_name: &RemoteName,
508 ) -> impl Iterator<Item = (&RefName, LocalAndRemoteRef<'_>)> + use<'_> {
509 refs::iter_named_local_remote_refs(self.local_tags(), self.remote_tags(remote_name)).map(
510 |(name, (local_target, remote_ref))| {
511 let targets = LocalAndRemoteRef {
512 local_target,
513 remote_ref,
514 };
515 (name, targets)
516 },
517 )
518 }
519
520 pub fn local_remote_tags_matching<'a, 'b>(
530 &'a self,
531 tag_matcher: &'b StringMatcher,
532 remote_name: &RemoteName,
533 ) -> impl Iterator<Item = (&'a RefName, LocalAndRemoteRef<'a>)> + use<'a, 'b> {
534 let maybe_remote_view = self.data.remote_views.get(remote_name);
537 refs::iter_named_local_remote_refs(
538 tag_matcher.filter_btree_map_as_deref(&self.data.local_tags),
539 maybe_remote_view
540 .map(|remote_view| tag_matcher.filter_btree_map_as_deref(&remote_view.tags))
541 .into_iter()
542 .flatten(),
543 )
544 .map(|(name, (local_target, remote_ref))| {
545 let targets = LocalAndRemoteRef {
546 local_target,
547 remote_ref,
548 };
549 (name.as_ref(), targets)
550 })
551 }
552
553 pub fn get_git_ref(&self, name: &GitRefName) -> &RefTarget {
554 self.data.git_refs.get(name).flatten()
555 }
556
557 pub fn set_git_ref_target(&mut self, name: &GitRefName, target: RefTarget) {
560 if target.is_present() {
561 self.data.git_refs.insert(name.to_owned(), target);
562 } else {
563 self.data.git_refs.remove(name);
564 }
565 }
566
567 pub fn set_git_head_target(&mut self, target: RefTarget) {
570 self.data.git_head = target;
571 }
572
573 pub fn all_referenced_commit_ids(&self) -> impl Iterator<Item = &CommitId> {
582 fn ref_target_ids(target: &RefTarget) -> impl Iterator<Item = &CommitId> {
585 target.as_merge().iter().flatten()
586 }
587
588 let op_store::View {
591 head_ids,
592 local_bookmarks,
593 local_tags,
594 remote_views,
595 git_refs,
596 git_head,
597 wc_commit_ids,
598 } = &self.data;
599 itertools::chain!(
600 head_ids,
601 local_bookmarks.values().flat_map(ref_target_ids),
602 local_tags.values().flat_map(ref_target_ids),
603 remote_views.values().flat_map(|remote_view| {
604 let op_store::RemoteView { bookmarks, tags } = remote_view;
605 itertools::chain(bookmarks.values(), tags.values())
606 .flat_map(|remote_ref| ref_target_ids(&remote_ref.target))
607 }),
608 git_refs.values().flat_map(ref_target_ids),
609 ref_target_ids(git_head),
610 wc_commit_ids.values()
611 )
612 }
613
614 pub fn set_view(&mut self, data: op_store::View, head_normalized: bool) {
615 self.data = data;
616 self.head_normalized = head_normalized;
617 }
618
619 pub fn store_view(&self) -> &op_store::View {
620 &self.data
621 }
622
623 pub fn store_view_mut(&mut self) -> &mut op_store::View {
624 &mut self.data
625 }
626
627 pub fn is_heads_normalized(&self) -> bool {
628 self.head_normalized
629 }
630
631 pub async fn normalize_heads(
632 &mut self,
633 index: &dyn Index,
634 root_commit_id: &CommitId,
635 ) -> IndexResult<()> {
636 if self.head_normalized {
637 return Ok(());
638 }
639 let view = self.store_view_mut();
640 if view.head_ids.is_empty() {
641 view.head_ids.insert(root_commit_id.clone());
642 } else if view.head_ids.len() > 1 {
643 view.head_ids.remove(root_commit_id);
646 view.head_ids = index
647 .heads(&mut view.head_ids.iter())
648 .await?
649 .into_iter()
650 .collect();
651 }
652 assert!(!view.head_ids.is_empty());
653 self.head_normalized = true;
654 Ok(())
655 }
656}
657
658#[derive(Debug, Error)]
660pub enum RenameWorkspaceError {
661 #[error("Workspace {} not found", name.as_symbol())]
662 WorkspaceDoesNotExist { name: WorkspaceNameBuf },
663
664 #[error("Workspace {} already exists", name.as_symbol())]
665 WorkspaceAlreadyExists { name: WorkspaceNameBuf },
666}
667
668#[cfg(test)]
669mod tests {
670 use super::*;
671 use crate::op_store::RemoteRefState;
672
673 fn remote_symbol<'a, N, M>(name: &'a N, remote: &'a M) -> RemoteRefSymbol<'a>
674 where
675 N: AsRef<RefName> + ?Sized,
676 M: AsRef<RemoteName> + ?Sized,
677 {
678 RemoteRefSymbol {
679 name: name.as_ref(),
680 remote: remote.as_ref(),
681 }
682 }
683
684 #[test]
685 fn test_absent_tracked_bookmarks() {
686 let mut view = View {
687 data: op_store::View::make_root(CommitId::from_hex("000000")),
688 head_normalized: true,
689 };
690 let absent_tracked_ref = RemoteRef {
691 target: RefTarget::absent(),
692 state: RemoteRefState::Tracked,
693 };
694 let present_tracked_ref = RemoteRef {
695 target: RefTarget::normal(CommitId::from_hex("111111")),
696 state: RemoteRefState::Tracked,
697 };
698
699 view.set_remote_bookmark(remote_symbol("foo", "new"), absent_tracked_ref.clone());
701 assert_eq!(
702 view.get_remote_bookmark(remote_symbol("foo", "new")),
703 RemoteRef::absent_ref()
704 );
705
706 view.set_remote_bookmark(remote_symbol("foo", "present"), present_tracked_ref.clone());
708 assert_eq!(
709 view.get_remote_bookmark(remote_symbol("foo", "present")),
710 &present_tracked_ref
711 );
712
713 view.set_local_bookmark_target(
715 "foo".as_ref(),
716 RefTarget::normal(CommitId::from_hex("222222")),
717 );
718 view.set_remote_bookmark(remote_symbol("foo", "new"), absent_tracked_ref.clone());
719 assert_eq!(
720 view.get_remote_bookmark(remote_symbol("foo", "new")),
721 &absent_tracked_ref
722 );
723
724 view.set_local_bookmark_target("foo".as_ref(), RefTarget::absent());
726 assert_eq!(
727 view.get_remote_bookmark(remote_symbol("foo", "new")),
728 RemoteRef::absent_ref()
729 );
730 assert_eq!(
731 view.get_remote_bookmark(remote_symbol("foo", "present")),
732 &present_tracked_ref
733 );
734 }
735
736 #[test]
737 fn test_absent_tracked_tags() {
738 let mut view = View {
739 data: op_store::View::make_root(CommitId::from_hex("000000")),
740 head_normalized: true,
741 };
742 let absent_tracked_ref = RemoteRef {
743 target: RefTarget::absent(),
744 state: RemoteRefState::Tracked,
745 };
746 let present_tracked_ref = RemoteRef {
747 target: RefTarget::normal(CommitId::from_hex("111111")),
748 state: RemoteRefState::Tracked,
749 };
750
751 view.set_remote_tag(remote_symbol("foo", "new"), absent_tracked_ref.clone());
753 assert_eq!(
754 view.get_remote_tag(remote_symbol("foo", "new")),
755 RemoteRef::absent_ref()
756 );
757
758 view.set_remote_tag(remote_symbol("foo", "present"), present_tracked_ref.clone());
760 assert_eq!(
761 view.get_remote_tag(remote_symbol("foo", "present")),
762 &present_tracked_ref
763 );
764
765 view.set_local_tag_target(
767 "foo".as_ref(),
768 RefTarget::normal(CommitId::from_hex("222222")),
769 );
770 view.set_remote_tag(remote_symbol("foo", "new"), absent_tracked_ref.clone());
771 assert_eq!(
772 view.get_remote_tag(remote_symbol("foo", "new")),
773 &absent_tracked_ref
774 );
775
776 view.set_local_tag_target("foo".as_ref(), RefTarget::absent());
778 assert_eq!(
779 view.get_remote_tag(remote_symbol("foo", "new")),
780 RemoteRef::absent_ref()
781 );
782 assert_eq!(
783 view.get_remote_tag(remote_symbol("foo", "present")),
784 &present_tracked_ref
785 );
786 }
787}