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 get_git_ref(&self, name: &GitRefName) -> &RefTarget {
496 self.data.git_refs.get(name).flatten()
497 }
498
499 pub fn set_git_ref_target(&mut self, name: &GitRefName, target: RefTarget) {
502 if target.is_present() {
503 self.data.git_refs.insert(name.to_owned(), target);
504 } else {
505 self.data.git_refs.remove(name);
506 }
507 }
508
509 pub fn set_git_head_target(&mut self, target: RefTarget) {
512 self.data.git_head = target;
513 }
514
515 pub fn all_referenced_commit_ids(&self) -> impl Iterator<Item = &CommitId> {
524 fn ref_target_ids(target: &RefTarget) -> impl Iterator<Item = &CommitId> {
527 target.as_merge().iter().flatten()
528 }
529
530 let op_store::View {
533 head_ids,
534 local_bookmarks,
535 local_tags,
536 remote_views,
537 git_refs,
538 git_head,
539 wc_commit_ids,
540 } = &self.data;
541 itertools::chain!(
542 head_ids,
543 local_bookmarks.values().flat_map(ref_target_ids),
544 local_tags.values().flat_map(ref_target_ids),
545 remote_views.values().flat_map(|remote_view| {
546 let op_store::RemoteView { bookmarks, tags } = remote_view;
547 itertools::chain(bookmarks.values(), tags.values())
548 .flat_map(|remote_ref| ref_target_ids(&remote_ref.target))
549 }),
550 git_refs.values().flat_map(ref_target_ids),
551 ref_target_ids(git_head),
552 wc_commit_ids.values()
553 )
554 }
555
556 pub fn set_view(&mut self, data: op_store::View) {
557 self.data = data;
558 }
559
560 pub fn store_view(&self) -> &op_store::View {
561 &self.data
562 }
563
564 pub fn store_view_mut(&mut self) -> &mut op_store::View {
565 &mut self.data
566 }
567}
568
569#[derive(Debug, Error)]
571pub enum RenameWorkspaceError {
572 #[error("Workspace {} not found", name.as_symbol())]
573 WorkspaceDoesNotExist { name: WorkspaceNameBuf },
574
575 #[error("Workspace {} already exists", name.as_symbol())]
576 WorkspaceAlreadyExists { name: WorkspaceNameBuf },
577}
578
579#[cfg(test)]
580mod tests {
581 use super::*;
582 use crate::op_store::RemoteRefState;
583
584 fn remote_symbol<'a, N, M>(name: &'a N, remote: &'a M) -> RemoteRefSymbol<'a>
585 where
586 N: AsRef<RefName> + ?Sized,
587 M: AsRef<RemoteName> + ?Sized,
588 {
589 RemoteRefSymbol {
590 name: name.as_ref(),
591 remote: remote.as_ref(),
592 }
593 }
594
595 #[test]
596 fn test_absent_tracked_bookmarks() {
597 let mut view = View {
598 data: op_store::View::make_root(CommitId::from_hex("000000")),
599 };
600 let absent_tracked_ref = RemoteRef {
601 target: RefTarget::absent(),
602 state: RemoteRefState::Tracked,
603 };
604 let present_tracked_ref = RemoteRef {
605 target: RefTarget::normal(CommitId::from_hex("111111")),
606 state: RemoteRefState::Tracked,
607 };
608
609 view.set_remote_bookmark(remote_symbol("foo", "new"), absent_tracked_ref.clone());
611 assert_eq!(
612 view.get_remote_bookmark(remote_symbol("foo", "new")),
613 RemoteRef::absent_ref()
614 );
615
616 view.set_remote_bookmark(remote_symbol("foo", "present"), present_tracked_ref.clone());
618 assert_eq!(
619 view.get_remote_bookmark(remote_symbol("foo", "present")),
620 &present_tracked_ref
621 );
622
623 view.set_local_bookmark_target(
625 "foo".as_ref(),
626 RefTarget::normal(CommitId::from_hex("222222")),
627 );
628 view.set_remote_bookmark(remote_symbol("foo", "new"), absent_tracked_ref.clone());
629 assert_eq!(
630 view.get_remote_bookmark(remote_symbol("foo", "new")),
631 &absent_tracked_ref
632 );
633
634 view.set_local_bookmark_target("foo".as_ref(), RefTarget::absent());
636 assert_eq!(
637 view.get_remote_bookmark(remote_symbol("foo", "new")),
638 RemoteRef::absent_ref()
639 );
640 assert_eq!(
641 view.get_remote_bookmark(remote_symbol("foo", "present")),
642 &present_tracked_ref
643 );
644 }
645
646 #[test]
647 fn test_absent_tracked_tags() {
648 let mut view = View {
649 data: op_store::View::make_root(CommitId::from_hex("000000")),
650 };
651 let absent_tracked_ref = RemoteRef {
652 target: RefTarget::absent(),
653 state: RemoteRefState::Tracked,
654 };
655 let present_tracked_ref = RemoteRef {
656 target: RefTarget::normal(CommitId::from_hex("111111")),
657 state: RemoteRefState::Tracked,
658 };
659
660 view.set_remote_tag(remote_symbol("foo", "new"), absent_tracked_ref.clone());
662 assert_eq!(
663 view.get_remote_tag(remote_symbol("foo", "new")),
664 RemoteRef::absent_ref()
665 );
666
667 view.set_remote_tag(remote_symbol("foo", "present"), present_tracked_ref.clone());
669 assert_eq!(
670 view.get_remote_tag(remote_symbol("foo", "present")),
671 &present_tracked_ref
672 );
673
674 view.set_local_tag_target(
676 "foo".as_ref(),
677 RefTarget::normal(CommitId::from_hex("222222")),
678 );
679 view.set_remote_tag(remote_symbol("foo", "new"), absent_tracked_ref.clone());
680 assert_eq!(
681 view.get_remote_tag(remote_symbol("foo", "new")),
682 &absent_tracked_ref
683 );
684
685 view.set_local_tag_target("foo".as_ref(), RefTarget::absent());
687 assert_eq!(
688 view.get_remote_tag(remote_symbol("foo", "new")),
689 RemoteRef::absent_ref()
690 );
691 assert_eq!(
692 view.get_remote_tag(remote_symbol("foo", "present")),
693 &present_tracked_ref
694 );
695 }
696}