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_matching(
323 &self,
324 matcher: &StringMatcher,
325 ) -> impl Iterator<Item = (&RemoteName, &RemoteView)> {
326 matcher
327 .filter_btree_map_as_deref(&self.data.remote_views)
328 .map(|(name, view)| (name.as_ref(), view))
329 }
330
331 pub fn ensure_remote(&mut self, remote_name: &RemoteName) {
333 if self.data.remote_views.contains_key(remote_name) {
334 return;
335 }
336 self.data
337 .remote_views
338 .insert(remote_name.to_owned(), RemoteView::default());
339 }
340
341 pub fn remove_remote(&mut self, remote_name: &RemoteName) {
342 self.data.remote_views.remove(remote_name);
343 }
344
345 pub fn rename_remote(&mut self, old: &RemoteName, new: &RemoteName) {
346 if let Some(remote_view) = self.data.remote_views.remove(old) {
347 self.data.remote_views.insert(new.to_owned(), remote_view);
348 }
349 }
350
351 pub fn local_tags(&self) -> impl Iterator<Item = (&RefName, &RefTarget)> {
353 self.data
354 .local_tags
355 .iter()
356 .map(|(name, target)| (name.as_ref(), target))
357 }
358
359 pub fn get_local_tag(&self, name: &RefName) -> &RefTarget {
360 self.data.local_tags.get(name).flatten()
361 }
362
363 pub fn local_tags_matching(
366 &self,
367 matcher: &StringMatcher,
368 ) -> impl Iterator<Item = (&RefName, &RefTarget)> {
369 matcher
370 .filter_btree_map_as_deref(&self.data.local_tags)
371 .map(|(name, target)| (name.as_ref(), target))
372 }
373
374 pub fn set_local_tag_target(&mut self, name: &RefName, target: RefTarget) {
378 if target.is_present() {
379 self.data.local_tags.insert(name.to_owned(), target);
380 } else {
381 self.data.local_tags.remove(name);
382 for remote_view in self.data.remote_views.values_mut() {
383 let remote_refs = &mut remote_view.tags;
384 if remote_refs.get(name).is_some_and(RemoteRef::is_absent) {
385 remote_refs.remove(name);
386 }
387 }
388 }
389 }
390
391 pub fn all_remote_tags(&self) -> impl Iterator<Item = (RemoteRefSymbol<'_>, &RemoteRef)> {
394 op_store::flatten_remote_refs(&self.data.remote_views, |view| &view.tags)
395 }
396
397 pub fn remote_tags(
400 &self,
401 remote_name: &RemoteName,
402 ) -> impl Iterator<Item = (&RefName, &RemoteRef)> + use<'_> {
403 let maybe_remote_view = self.data.remote_views.get(remote_name);
404 maybe_remote_view
405 .map(|remote_view| {
406 remote_view
407 .tags
408 .iter()
409 .map(|(name, remote_ref)| (name.as_ref(), remote_ref))
410 })
411 .into_iter()
412 .flatten()
413 }
414
415 pub fn remote_tags_matching(
420 &self,
421 tag_matcher: &StringMatcher,
422 remote_matcher: &StringMatcher,
423 ) -> impl Iterator<Item = (RemoteRefSymbol<'_>, &RemoteRef)> {
424 remote_matcher
426 .filter_btree_map_as_deref(&self.data.remote_views)
427 .map(|(remote, remote_view)| {
428 tag_matcher
429 .filter_btree_map_as_deref(&remote_view.tags)
430 .map(|(name, remote_ref)| (name.to_remote_symbol(remote), remote_ref))
431 })
432 .kmerge_by(|(symbol1, _), (symbol2, _)| symbol1 < symbol2)
433 }
434
435 pub fn get_remote_tag(&self, symbol: RemoteRefSymbol<'_>) -> &RemoteRef {
437 if let Some(remote_view) = self.data.remote_views.get(symbol.remote) {
438 remote_view.tags.get(symbol.name).flatten()
439 } else {
440 RemoteRef::absent_ref()
441 }
442 }
443
444 pub fn set_remote_tag(&mut self, symbol: RemoteRefSymbol<'_>, remote_ref: RemoteRef) {
447 if remote_ref.is_present()
448 || (remote_ref.is_tracked() && self.get_local_tag(symbol.name).is_present())
449 {
450 let remote_view = self
451 .data
452 .remote_views
453 .entry(symbol.remote.to_owned())
454 .or_default();
455 remote_view.tags.insert(symbol.name.to_owned(), remote_ref);
456 } else if let Some(remote_view) = self.data.remote_views.get_mut(symbol.remote) {
457 remote_view.tags.remove(symbol.name);
458 }
459 }
460
461 pub fn local_remote_tags(
468 &self,
469 remote_name: &RemoteName,
470 ) -> impl Iterator<Item = (&RefName, LocalAndRemoteRef<'_>)> + use<'_> {
471 refs::iter_named_local_remote_refs(self.local_tags(), self.remote_tags(remote_name)).map(
472 |(name, (local_target, remote_ref))| {
473 let targets = LocalAndRemoteRef {
474 local_target,
475 remote_ref,
476 };
477 (name, targets)
478 },
479 )
480 }
481
482 pub fn get_git_ref(&self, name: &GitRefName) -> &RefTarget {
483 self.data.git_refs.get(name).flatten()
484 }
485
486 pub fn set_git_ref_target(&mut self, name: &GitRefName, target: RefTarget) {
489 if target.is_present() {
490 self.data.git_refs.insert(name.to_owned(), target);
491 } else {
492 self.data.git_refs.remove(name);
493 }
494 }
495
496 pub fn set_git_head_target(&mut self, target: RefTarget) {
499 self.data.git_head = target;
500 }
501
502 pub fn all_referenced_commit_ids(&self) -> impl Iterator<Item = &CommitId> {
511 fn ref_target_ids(target: &RefTarget) -> impl Iterator<Item = &CommitId> {
514 target.as_merge().iter().flatten()
515 }
516
517 let op_store::View {
520 head_ids,
521 local_bookmarks,
522 local_tags,
523 remote_views,
524 git_refs,
525 git_head,
526 wc_commit_ids,
527 } = &self.data;
528 itertools::chain!(
529 head_ids,
530 local_bookmarks.values().flat_map(ref_target_ids),
531 local_tags.values().flat_map(ref_target_ids),
532 remote_views.values().flat_map(|remote_view| {
533 let op_store::RemoteView { bookmarks, tags } = remote_view;
534 itertools::chain(bookmarks.values(), tags.values())
535 .flat_map(|remote_ref| ref_target_ids(&remote_ref.target))
536 }),
537 git_refs.values().flat_map(ref_target_ids),
538 ref_target_ids(git_head),
539 wc_commit_ids.values()
540 )
541 }
542
543 pub fn set_view(&mut self, data: op_store::View) {
544 self.data = data;
545 }
546
547 pub fn store_view(&self) -> &op_store::View {
548 &self.data
549 }
550
551 pub fn store_view_mut(&mut self) -> &mut op_store::View {
552 &mut self.data
553 }
554}
555
556#[derive(Debug, Error)]
558pub enum RenameWorkspaceError {
559 #[error("Workspace {} not found", name.as_symbol())]
560 WorkspaceDoesNotExist { name: WorkspaceNameBuf },
561
562 #[error("Workspace {} already exists", name.as_symbol())]
563 WorkspaceAlreadyExists { name: WorkspaceNameBuf },
564}
565
566#[cfg(test)]
567mod tests {
568 use super::*;
569 use crate::op_store::RemoteRefState;
570
571 fn remote_symbol<'a, N, M>(name: &'a N, remote: &'a M) -> RemoteRefSymbol<'a>
572 where
573 N: AsRef<RefName> + ?Sized,
574 M: AsRef<RemoteName> + ?Sized,
575 {
576 RemoteRefSymbol {
577 name: name.as_ref(),
578 remote: remote.as_ref(),
579 }
580 }
581
582 #[test]
583 fn test_absent_tracked_bookmarks() {
584 let mut view = View {
585 data: op_store::View::make_root(CommitId::from_hex("000000")),
586 };
587 let absent_tracked_ref = RemoteRef {
588 target: RefTarget::absent(),
589 state: RemoteRefState::Tracked,
590 };
591 let present_tracked_ref = RemoteRef {
592 target: RefTarget::normal(CommitId::from_hex("111111")),
593 state: RemoteRefState::Tracked,
594 };
595
596 view.set_remote_bookmark(remote_symbol("foo", "new"), absent_tracked_ref.clone());
598 assert_eq!(
599 view.get_remote_bookmark(remote_symbol("foo", "new")),
600 RemoteRef::absent_ref()
601 );
602
603 view.set_remote_bookmark(remote_symbol("foo", "present"), present_tracked_ref.clone());
605 assert_eq!(
606 view.get_remote_bookmark(remote_symbol("foo", "present")),
607 &present_tracked_ref
608 );
609
610 view.set_local_bookmark_target(
612 "foo".as_ref(),
613 RefTarget::normal(CommitId::from_hex("222222")),
614 );
615 view.set_remote_bookmark(remote_symbol("foo", "new"), absent_tracked_ref.clone());
616 assert_eq!(
617 view.get_remote_bookmark(remote_symbol("foo", "new")),
618 &absent_tracked_ref
619 );
620
621 view.set_local_bookmark_target("foo".as_ref(), RefTarget::absent());
623 assert_eq!(
624 view.get_remote_bookmark(remote_symbol("foo", "new")),
625 RemoteRef::absent_ref()
626 );
627 assert_eq!(
628 view.get_remote_bookmark(remote_symbol("foo", "present")),
629 &present_tracked_ref
630 );
631 }
632
633 #[test]
634 fn test_absent_tracked_tags() {
635 let mut view = View {
636 data: op_store::View::make_root(CommitId::from_hex("000000")),
637 };
638 let absent_tracked_ref = RemoteRef {
639 target: RefTarget::absent(),
640 state: RemoteRefState::Tracked,
641 };
642 let present_tracked_ref = RemoteRef {
643 target: RefTarget::normal(CommitId::from_hex("111111")),
644 state: RemoteRefState::Tracked,
645 };
646
647 view.set_remote_tag(remote_symbol("foo", "new"), absent_tracked_ref.clone());
649 assert_eq!(
650 view.get_remote_tag(remote_symbol("foo", "new")),
651 RemoteRef::absent_ref()
652 );
653
654 view.set_remote_tag(remote_symbol("foo", "present"), present_tracked_ref.clone());
656 assert_eq!(
657 view.get_remote_tag(remote_symbol("foo", "present")),
658 &present_tracked_ref
659 );
660
661 view.set_local_tag_target(
663 "foo".as_ref(),
664 RefTarget::normal(CommitId::from_hex("222222")),
665 );
666 view.set_remote_tag(remote_symbol("foo", "new"), absent_tracked_ref.clone());
667 assert_eq!(
668 view.get_remote_tag(remote_symbol("foo", "new")),
669 &absent_tracked_ref
670 );
671
672 view.set_local_tag_target("foo".as_ref(), RefTarget::absent());
674 assert_eq!(
675 view.get_remote_tag(remote_symbol("foo", "new")),
676 RemoteRef::absent_ref()
677 );
678 assert_eq!(
679 view.get_remote_tag(remote_symbol("foo", "present")),
680 &present_tracked_ref
681 );
682 }
683}