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::op_store;
25use crate::op_store::LocalRemoteRefTarget;
26use crate::op_store::RefTarget;
27use crate::op_store::RefTargetOptionExt as _;
28use crate::op_store::RemoteRef;
29use crate::op_store::RemoteView;
30use crate::ref_name::GitRefName;
31use crate::ref_name::GitRefNameBuf;
32use crate::ref_name::RefName;
33use crate::ref_name::RemoteName;
34use crate::ref_name::RemoteRefSymbol;
35use crate::ref_name::WorkspaceName;
36use crate::ref_name::WorkspaceNameBuf;
37use crate::refs;
38use crate::refs::LocalAndRemoteRef;
39use crate::str_util::StringMatcher;
40
41#[derive(PartialEq, Eq, Debug, Clone)]
43pub struct View {
44 data: op_store::View,
45}
46
47impl View {
48 pub fn new(op_store_view: op_store::View) -> Self {
49 Self {
50 data: op_store_view,
51 }
52 }
53
54 pub fn wc_commit_ids(&self) -> &BTreeMap<WorkspaceNameBuf, CommitId> {
55 &self.data.wc_commit_ids
56 }
57
58 pub fn get_wc_commit_id(&self, name: &WorkspaceName) -> Option<&CommitId> {
59 self.data.wc_commit_ids.get(name)
60 }
61
62 pub fn workspaces_for_wc_commit_id(&self, commit_id: &CommitId) -> Vec<WorkspaceNameBuf> {
63 let mut workspace_names = vec![];
64 for (name, wc_commit_id) in &self.data.wc_commit_ids {
65 if wc_commit_id == commit_id {
66 workspace_names.push(name.clone());
67 }
68 }
69 workspace_names
70 }
71
72 pub fn is_wc_commit_id(&self, commit_id: &CommitId) -> bool {
73 self.data.wc_commit_ids.values().contains(commit_id)
74 }
75
76 pub fn heads(&self) -> &HashSet<CommitId> {
77 &self.data.head_ids
78 }
79
80 pub fn bookmarks(&self) -> impl Iterator<Item = (&RefName, LocalRemoteRefTarget<'_>)> {
82 op_store::merge_join_ref_views(
83 &self.data.local_bookmarks,
84 &self.data.remote_views,
85 |view| &view.bookmarks,
86 )
87 }
88
89 pub fn tags(&self) -> impl Iterator<Item = (&RefName, LocalRemoteRefTarget<'_>)> {
91 op_store::merge_join_ref_views(&self.data.local_tags, &self.data.remote_views, |view| {
92 &view.tags
93 })
94 }
95
96 pub fn git_refs(&self) -> &BTreeMap<GitRefNameBuf, RefTarget> {
97 &self.data.git_refs
98 }
99
100 pub fn git_head(&self) -> &RefTarget {
101 &self.data.git_head
102 }
103
104 pub fn set_wc_commit(&mut self, name: WorkspaceNameBuf, commit_id: CommitId) {
105 self.data.wc_commit_ids.insert(name, commit_id);
106 }
107
108 pub fn remove_wc_commit(&mut self, name: &WorkspaceName) {
109 self.data.wc_commit_ids.remove(name);
110 }
111
112 pub fn rename_workspace(
113 &mut self,
114 old_name: &WorkspaceName,
115 new_name: WorkspaceNameBuf,
116 ) -> Result<(), RenameWorkspaceError> {
117 if self.data.wc_commit_ids.contains_key(&new_name) {
118 return Err(RenameWorkspaceError::WorkspaceAlreadyExists {
119 name: new_name.clone(),
120 });
121 }
122 let wc_commit_id = self.data.wc_commit_ids.remove(old_name).ok_or_else(|| {
123 RenameWorkspaceError::WorkspaceDoesNotExist {
124 name: old_name.to_owned(),
125 }
126 })?;
127 self.data.wc_commit_ids.insert(new_name, wc_commit_id);
128 Ok(())
129 }
130
131 pub fn add_head(&mut self, head_id: &CommitId) {
132 self.data.head_ids.insert(head_id.clone());
133 }
134
135 pub fn remove_head(&mut self, head_id: &CommitId) {
136 self.data.head_ids.remove(head_id);
137 }
138
139 pub fn local_bookmarks(&self) -> impl Iterator<Item = (&RefName, &RefTarget)> {
141 self.data
142 .local_bookmarks
143 .iter()
144 .map(|(name, target)| (name.as_ref(), target))
145 }
146
147 pub fn local_bookmarks_for_commit(
150 &self,
151 commit_id: &CommitId,
152 ) -> impl Iterator<Item = (&RefName, &RefTarget)> {
153 self.local_bookmarks()
154 .filter(|(_, target)| target.added_ids().contains(commit_id))
155 }
156
157 pub fn local_bookmarks_matching(
160 &self,
161 matcher: &StringMatcher,
162 ) -> impl Iterator<Item = (&RefName, &RefTarget)> {
163 matcher
164 .filter_btree_map_as_deref(&self.data.local_bookmarks)
165 .map(|(name, target)| (name.as_ref(), target))
166 }
167
168 pub fn get_local_bookmark(&self, name: &RefName) -> &RefTarget {
169 self.data.local_bookmarks.get(name).flatten()
170 }
171
172 pub fn set_local_bookmark_target(&mut self, name: &RefName, target: RefTarget) {
177 if target.is_present() {
178 self.data.local_bookmarks.insert(name.to_owned(), target);
179 } else {
180 self.data.local_bookmarks.remove(name);
181 for remote_view in self.data.remote_views.values_mut() {
182 let remote_refs = &mut remote_view.bookmarks;
183 if remote_refs.get(name).is_some_and(RemoteRef::is_absent) {
184 remote_refs.remove(name);
185 }
186 }
187 }
188 }
189
190 pub fn all_remote_bookmarks(&self) -> impl Iterator<Item = (RemoteRefSymbol<'_>, &RemoteRef)> {
193 op_store::flatten_remote_refs(&self.data.remote_views, |view| &view.bookmarks)
194 }
195
196 pub fn remote_bookmarks(
199 &self,
200 remote_name: &RemoteName,
201 ) -> impl Iterator<Item = (&RefName, &RemoteRef)> + use<'_> {
202 let maybe_remote_view = self.data.remote_views.get(remote_name);
203 maybe_remote_view
204 .map(|remote_view| {
205 remote_view
206 .bookmarks
207 .iter()
208 .map(|(name, remote_ref)| (name.as_ref(), remote_ref))
209 })
210 .into_iter()
211 .flatten()
212 }
213
214 pub fn remote_bookmarks_matching(
219 &self,
220 bookmark_matcher: &StringMatcher,
221 remote_matcher: &StringMatcher,
222 ) -> impl Iterator<Item = (RemoteRefSymbol<'_>, &RemoteRef)> {
223 remote_matcher
225 .filter_btree_map_as_deref(&self.data.remote_views)
226 .map(|(remote, remote_view)| {
227 bookmark_matcher
228 .filter_btree_map_as_deref(&remote_view.bookmarks)
229 .map(|(name, remote_ref)| (name.to_remote_symbol(remote), remote_ref))
230 })
231 .kmerge_by(|(symbol1, _), (symbol2, _)| symbol1 < symbol2)
232 }
233
234 pub fn get_remote_bookmark(&self, symbol: RemoteRefSymbol<'_>) -> &RemoteRef {
235 if let Some(remote_view) = self.data.remote_views.get(symbol.remote) {
236 remote_view.bookmarks.get(symbol.name).flatten()
237 } else {
238 RemoteRef::absent_ref()
239 }
240 }
241
242 pub fn set_remote_bookmark(&mut self, symbol: RemoteRefSymbol<'_>, remote_ref: RemoteRef) {
246 if remote_ref.is_present()
247 || (remote_ref.is_tracked() && self.get_local_bookmark(symbol.name).is_present())
248 {
249 let remote_view = self
250 .data
251 .remote_views
252 .entry(symbol.remote.to_owned())
253 .or_default();
254 remote_view
255 .bookmarks
256 .insert(symbol.name.to_owned(), remote_ref);
257 } else if let Some(remote_view) = self.data.remote_views.get_mut(symbol.remote) {
258 remote_view.bookmarks.remove(symbol.name);
259 }
260 }
261
262 pub fn local_remote_bookmarks(
270 &self,
271 remote_name: &RemoteName,
272 ) -> impl Iterator<Item = (&RefName, LocalAndRemoteRef<'_>)> + use<'_> {
273 refs::iter_named_local_remote_refs(
274 self.local_bookmarks(),
275 self.remote_bookmarks(remote_name),
276 )
277 .map(|(name, (local_target, remote_ref))| {
278 let targets = LocalAndRemoteRef {
279 local_target,
280 remote_ref,
281 };
282 (name, targets)
283 })
284 }
285
286 pub fn local_remote_bookmarks_matching<'a, 'b>(
296 &'a self,
297 bookmark_matcher: &'b StringMatcher,
298 remote_name: &RemoteName,
299 ) -> impl Iterator<Item = (&'a RefName, LocalAndRemoteRef<'a>)> + use<'a, 'b> {
300 let maybe_remote_view = self.data.remote_views.get(remote_name);
303 refs::iter_named_local_remote_refs(
304 bookmark_matcher.filter_btree_map_as_deref(&self.data.local_bookmarks),
305 maybe_remote_view
306 .map(|remote_view| {
307 bookmark_matcher.filter_btree_map_as_deref(&remote_view.bookmarks)
308 })
309 .into_iter()
310 .flatten(),
311 )
312 .map(|(name, (local_target, remote_ref))| {
313 let targets = LocalAndRemoteRef {
314 local_target,
315 remote_ref,
316 };
317 (name.as_ref(), targets)
318 })
319 }
320
321 pub fn remote_views(&self) -> impl Iterator<Item = (&RemoteName, &RemoteView)> {
323 self.data
324 .remote_views
325 .iter()
326 .map(|(name, view)| (name.as_ref(), view))
327 }
328
329 pub fn remote_views_matching(
331 &self,
332 matcher: &StringMatcher,
333 ) -> impl Iterator<Item = (&RemoteName, &RemoteView)> {
334 matcher
335 .filter_btree_map_as_deref(&self.data.remote_views)
336 .map(|(name, view)| (name.as_ref(), view))
337 }
338
339 pub fn get_remote_view(&self, name: &RemoteName) -> Option<&RemoteView> {
341 self.data.remote_views.get(name)
342 }
343
344 pub fn ensure_remote(&mut self, remote_name: &RemoteName) {
346 if self.data.remote_views.contains_key(remote_name) {
347 return;
348 }
349 self.data
350 .remote_views
351 .insert(remote_name.to_owned(), RemoteView::default());
352 }
353
354 pub fn remove_remote(&mut self, remote_name: &RemoteName) {
355 self.data.remote_views.remove(remote_name);
356 }
357
358 pub fn rename_remote(&mut self, old: &RemoteName, new: &RemoteName) {
359 if let Some(remote_view) = self.data.remote_views.remove(old) {
360 self.data.remote_views.insert(new.to_owned(), remote_view);
361 }
362 }
363
364 pub fn local_tags(&self) -> impl Iterator<Item = (&RefName, &RefTarget)> {
366 self.data
367 .local_tags
368 .iter()
369 .map(|(name, target)| (name.as_ref(), target))
370 }
371
372 pub fn get_local_tag(&self, name: &RefName) -> &RefTarget {
373 self.data.local_tags.get(name).flatten()
374 }
375
376 pub fn local_tags_matching(
379 &self,
380 matcher: &StringMatcher,
381 ) -> impl Iterator<Item = (&RefName, &RefTarget)> {
382 matcher
383 .filter_btree_map_as_deref(&self.data.local_tags)
384 .map(|(name, target)| (name.as_ref(), target))
385 }
386
387 pub fn set_local_tag_target(&mut self, name: &RefName, target: RefTarget) {
391 if target.is_present() {
392 self.data.local_tags.insert(name.to_owned(), target);
393 } else {
394 self.data.local_tags.remove(name);
395 for remote_view in self.data.remote_views.values_mut() {
396 let remote_refs = &mut remote_view.tags;
397 if remote_refs.get(name).is_some_and(RemoteRef::is_absent) {
398 remote_refs.remove(name);
399 }
400 }
401 }
402 }
403
404 pub fn all_remote_tags(&self) -> impl Iterator<Item = (RemoteRefSymbol<'_>, &RemoteRef)> {
407 op_store::flatten_remote_refs(&self.data.remote_views, |view| &view.tags)
408 }
409
410 pub fn remote_tags(
413 &self,
414 remote_name: &RemoteName,
415 ) -> impl Iterator<Item = (&RefName, &RemoteRef)> + use<'_> {
416 let maybe_remote_view = self.data.remote_views.get(remote_name);
417 maybe_remote_view
418 .map(|remote_view| {
419 remote_view
420 .tags
421 .iter()
422 .map(|(name, remote_ref)| (name.as_ref(), remote_ref))
423 })
424 .into_iter()
425 .flatten()
426 }
427
428 pub fn remote_tags_matching(
433 &self,
434 tag_matcher: &StringMatcher,
435 remote_matcher: &StringMatcher,
436 ) -> impl Iterator<Item = (RemoteRefSymbol<'_>, &RemoteRef)> {
437 remote_matcher
439 .filter_btree_map_as_deref(&self.data.remote_views)
440 .map(|(remote, remote_view)| {
441 tag_matcher
442 .filter_btree_map_as_deref(&remote_view.tags)
443 .map(|(name, remote_ref)| (name.to_remote_symbol(remote), remote_ref))
444 })
445 .kmerge_by(|(symbol1, _), (symbol2, _)| symbol1 < symbol2)
446 }
447
448 pub fn get_remote_tag(&self, symbol: RemoteRefSymbol<'_>) -> &RemoteRef {
450 if let Some(remote_view) = self.data.remote_views.get(symbol.remote) {
451 remote_view.tags.get(symbol.name).flatten()
452 } else {
453 RemoteRef::absent_ref()
454 }
455 }
456
457 pub fn set_remote_tag(&mut self, symbol: RemoteRefSymbol<'_>, remote_ref: RemoteRef) {
460 if remote_ref.is_present()
461 || (remote_ref.is_tracked() && self.get_local_tag(symbol.name).is_present())
462 {
463 let remote_view = self
464 .data
465 .remote_views
466 .entry(symbol.remote.to_owned())
467 .or_default();
468 remote_view.tags.insert(symbol.name.to_owned(), remote_ref);
469 } else if let Some(remote_view) = self.data.remote_views.get_mut(symbol.remote) {
470 remote_view.tags.remove(symbol.name);
471 }
472 }
473
474 pub fn local_remote_tags(
481 &self,
482 remote_name: &RemoteName,
483 ) -> impl Iterator<Item = (&RefName, LocalAndRemoteRef<'_>)> + use<'_> {
484 refs::iter_named_local_remote_refs(self.local_tags(), self.remote_tags(remote_name)).map(
485 |(name, (local_target, remote_ref))| {
486 let targets = LocalAndRemoteRef {
487 local_target,
488 remote_ref,
489 };
490 (name, targets)
491 },
492 )
493 }
494
495 pub fn local_remote_tags_matching<'a, 'b>(
505 &'a self,
506 tag_matcher: &'b StringMatcher,
507 remote_name: &RemoteName,
508 ) -> impl Iterator<Item = (&'a RefName, LocalAndRemoteRef<'a>)> + use<'a, 'b> {
509 let maybe_remote_view = self.data.remote_views.get(remote_name);
512 refs::iter_named_local_remote_refs(
513 tag_matcher.filter_btree_map_as_deref(&self.data.local_tags),
514 maybe_remote_view
515 .map(|remote_view| tag_matcher.filter_btree_map_as_deref(&remote_view.tags))
516 .into_iter()
517 .flatten(),
518 )
519 .map(|(name, (local_target, remote_ref))| {
520 let targets = LocalAndRemoteRef {
521 local_target,
522 remote_ref,
523 };
524 (name.as_ref(), targets)
525 })
526 }
527
528 pub fn get_git_ref(&self, name: &GitRefName) -> &RefTarget {
529 self.data.git_refs.get(name).flatten()
530 }
531
532 pub fn set_git_ref_target(&mut self, name: &GitRefName, target: RefTarget) {
535 if target.is_present() {
536 self.data.git_refs.insert(name.to_owned(), target);
537 } else {
538 self.data.git_refs.remove(name);
539 }
540 }
541
542 pub fn set_git_head_target(&mut self, target: RefTarget) {
545 self.data.git_head = target;
546 }
547
548 pub fn all_referenced_commit_ids(&self) -> impl Iterator<Item = &CommitId> {
557 fn ref_target_ids(target: &RefTarget) -> impl Iterator<Item = &CommitId> {
560 target.as_merge().iter().flatten()
561 }
562
563 let op_store::View {
566 head_ids,
567 local_bookmarks,
568 local_tags,
569 remote_views,
570 git_refs,
571 git_head,
572 wc_commit_ids,
573 } = &self.data;
574 itertools::chain!(
575 head_ids,
576 local_bookmarks.values().flat_map(ref_target_ids),
577 local_tags.values().flat_map(ref_target_ids),
578 remote_views.values().flat_map(|remote_view| {
579 let op_store::RemoteView { bookmarks, tags } = remote_view;
580 itertools::chain(bookmarks.values(), tags.values())
581 .flat_map(|remote_ref| ref_target_ids(&remote_ref.target))
582 }),
583 git_refs.values().flat_map(ref_target_ids),
584 ref_target_ids(git_head),
585 wc_commit_ids.values()
586 )
587 }
588
589 pub fn set_view(&mut self, data: op_store::View) {
590 self.data = data;
591 }
592
593 pub fn store_view(&self) -> &op_store::View {
594 &self.data
595 }
596
597 pub fn store_view_mut(&mut self) -> &mut op_store::View {
598 &mut self.data
599 }
600}
601
602#[derive(Debug, Error)]
604pub enum RenameWorkspaceError {
605 #[error("Workspace {} not found", name.as_symbol())]
606 WorkspaceDoesNotExist { name: WorkspaceNameBuf },
607
608 #[error("Workspace {} already exists", name.as_symbol())]
609 WorkspaceAlreadyExists { name: WorkspaceNameBuf },
610}
611
612#[cfg(test)]
613mod tests {
614 use super::*;
615 use crate::op_store::RemoteRefState;
616
617 fn remote_symbol<'a, N, M>(name: &'a N, remote: &'a M) -> RemoteRefSymbol<'a>
618 where
619 N: AsRef<RefName> + ?Sized,
620 M: AsRef<RemoteName> + ?Sized,
621 {
622 RemoteRefSymbol {
623 name: name.as_ref(),
624 remote: remote.as_ref(),
625 }
626 }
627
628 #[test]
629 fn test_absent_tracked_bookmarks() {
630 let mut view = View {
631 data: op_store::View::make_root(CommitId::from_hex("000000")),
632 };
633 let absent_tracked_ref = RemoteRef {
634 target: RefTarget::absent(),
635 state: RemoteRefState::Tracked,
636 };
637 let present_tracked_ref = RemoteRef {
638 target: RefTarget::normal(CommitId::from_hex("111111")),
639 state: RemoteRefState::Tracked,
640 };
641
642 view.set_remote_bookmark(remote_symbol("foo", "new"), absent_tracked_ref.clone());
644 assert_eq!(
645 view.get_remote_bookmark(remote_symbol("foo", "new")),
646 RemoteRef::absent_ref()
647 );
648
649 view.set_remote_bookmark(remote_symbol("foo", "present"), present_tracked_ref.clone());
651 assert_eq!(
652 view.get_remote_bookmark(remote_symbol("foo", "present")),
653 &present_tracked_ref
654 );
655
656 view.set_local_bookmark_target(
658 "foo".as_ref(),
659 RefTarget::normal(CommitId::from_hex("222222")),
660 );
661 view.set_remote_bookmark(remote_symbol("foo", "new"), absent_tracked_ref.clone());
662 assert_eq!(
663 view.get_remote_bookmark(remote_symbol("foo", "new")),
664 &absent_tracked_ref
665 );
666
667 view.set_local_bookmark_target("foo".as_ref(), RefTarget::absent());
669 assert_eq!(
670 view.get_remote_bookmark(remote_symbol("foo", "new")),
671 RemoteRef::absent_ref()
672 );
673 assert_eq!(
674 view.get_remote_bookmark(remote_symbol("foo", "present")),
675 &present_tracked_ref
676 );
677 }
678
679 #[test]
680 fn test_absent_tracked_tags() {
681 let mut view = View {
682 data: op_store::View::make_root(CommitId::from_hex("000000")),
683 };
684 let absent_tracked_ref = RemoteRef {
685 target: RefTarget::absent(),
686 state: RemoteRefState::Tracked,
687 };
688 let present_tracked_ref = RemoteRef {
689 target: RefTarget::normal(CommitId::from_hex("111111")),
690 state: RemoteRefState::Tracked,
691 };
692
693 view.set_remote_tag(remote_symbol("foo", "new"), absent_tracked_ref.clone());
695 assert_eq!(
696 view.get_remote_tag(remote_symbol("foo", "new")),
697 RemoteRef::absent_ref()
698 );
699
700 view.set_remote_tag(remote_symbol("foo", "present"), present_tracked_ref.clone());
702 assert_eq!(
703 view.get_remote_tag(remote_symbol("foo", "present")),
704 &present_tracked_ref
705 );
706
707 view.set_local_tag_target(
709 "foo".as_ref(),
710 RefTarget::normal(CommitId::from_hex("222222")),
711 );
712 view.set_remote_tag(remote_symbol("foo", "new"), absent_tracked_ref.clone());
713 assert_eq!(
714 view.get_remote_tag(remote_symbol("foo", "new")),
715 &absent_tracked_ref
716 );
717
718 view.set_local_tag_target("foo".as_ref(), RefTarget::absent());
720 assert_eq!(
721 view.get_remote_tag(remote_symbol("foo", "new")),
722 RemoteRef::absent_ref()
723 );
724 assert_eq!(
725 view.get_remote_tag(remote_symbol("foo", "present")),
726 &present_tracked_ref
727 );
728 }
729}