jj_lib/
view.rs

1// Copyright 2020 The Jujutsu Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15#![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/// A wrapper around [`op_store::View`] that defines additional methods.
42#[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    /// Iterates pair of local and remote bookmarks by bookmark name.
81    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    /// Iterates pair of local and remote tags by tag name.
90    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    /// Iterates local bookmark `(name, target)`s in lexicographical order.
140    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    /// Iterates local bookmarks `(name, target)` in lexicographical order where
148    /// the target adds `commit_id`.
149    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    /// Iterates local bookmark `(name, target)`s matching the given pattern.
158    /// Entries are sorted by `name`.
159    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    /// Sets local bookmark to point to the given target. If the target is
173    /// absent, the local bookmark will be removed. If there are absent remote
174    /// bookmarks tracked by the newly-absent local bookmark, they will also be
175    /// removed.
176    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    /// Iterates over `(symbol, remote_ref)` for all remote bookmarks in
191    /// lexicographical order.
192    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    /// Iterates over `(name, remote_ref)`s for all remote bookmarks of the
197    /// specified remote in lexicographical order.
198    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    /// Iterates over `(symbol, remote_ref)`s for all remote bookmarks of the
215    /// specified remote that match the given pattern.
216    ///
217    /// Entries are sorted by `symbol`, which is `(name, remote)`.
218    pub fn remote_bookmarks_matching(
219        &self,
220        bookmark_matcher: &StringMatcher,
221        remote_matcher: &StringMatcher,
222    ) -> impl Iterator<Item = (RemoteRefSymbol<'_>, &RemoteRef)> {
223        // Use kmerge instead of flat_map for consistency with all_remote_bookmarks().
224        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    /// Sets remote-tracking bookmark to the given target and state. If the
243    /// target is absent and if no tracking local bookmark exists, the bookmark
244    /// will be removed.
245    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    /// Iterates over `(name, {local_ref, remote_ref})`s for every bookmark
263    /// present locally and/or on the specified remote, in lexicographical
264    /// order.
265    ///
266    /// Note that this does *not* take into account whether the local bookmark
267    /// tracks the remote bookmark or not. Missing values are represented as
268    /// RefTarget::absent_ref() or RemoteRef::absent_ref().
269    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    /// Iterates over `(name, TrackingRefPair {local_ref, remote_ref})`s for
287    /// every bookmark with a name that matches the given pattern, and that is
288    /// present locally and/or on the specified remote.
289    ///
290    /// Entries are sorted by `name`.
291    ///
292    /// Note that this does *not* take into account whether the local bookmark
293    /// tracks the remote bookmark or not. Missing values are represented as
294    /// RefTarget::absent_ref() or RemoteRef::absent_ref().
295    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        // Change remote_name to StringMatcher if needed, but merge-join adapter won't
301        // be usable.
302        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    /// Iterates remote `(name, view)`s in lexicographical order.
322    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    /// Adds remote view if it doesn't exist.
332    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    /// Iterates local tag `(name, target)`s in lexicographical order.
352    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    /// Iterates local tag `(name, target)`s matching the given pattern. Entries
364    /// are sorted by `name`.
365    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    /// Sets local tag to point to the given target. If the target is absent,
375    /// the local tag will be removed. If there are absent remote tags tracked
376    /// by the newly-absent local tag, they will also be removed.
377    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    /// Iterates over `(symbol, remote_ref)` for all remote tags in
392    /// lexicographical order.
393    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    /// Iterates over `(name, remote_ref)`s for all remote tags of the specified
398    /// remote in lexicographical order.
399    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    /// Iterates over `(symbol, remote_ref)`s for all remote tags of the
416    /// specified remote that match the given pattern.
417    ///
418    /// Entries are sorted by `symbol`, which is `(name, remote)`.
419    pub fn remote_tags_matching(
420        &self,
421        tag_matcher: &StringMatcher,
422        remote_matcher: &StringMatcher,
423    ) -> impl Iterator<Item = (RemoteRefSymbol<'_>, &RemoteRef)> {
424        // Use kmerge instead of flat_map for consistency with all_remote_tags().
425        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    /// Returns remote-tracking tag target and state specified by `symbol`.
436    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    /// Sets remote-tracking tag to the given target and state. If the target is
445    /// absent and if no tracking local tag exists, the tag will be removed.
446    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    /// Iterates over `(name, {local_ref, remote_ref})`s for every tag present
462    /// locally and/or on the specified remote, in lexicographical order.
463    ///
464    /// Note that this does *not* take into account whether the local tag tracks
465    /// the remote tag or not. Missing values are represented as
466    /// [`RefTarget::absent_ref()`] or [`RemoteRef::absent_ref()`].
467    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    /// Sets the last imported Git ref to point to the given target. If the
487    /// target is absent, the reference will be removed.
488    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    /// Sets Git HEAD to point to the given target. If the target is absent, the
497    /// reference will be cleared.
498    pub fn set_git_head_target(&mut self, target: RefTarget) {
499        self.data.git_head = target;
500    }
501
502    /// Iterates all commit ids referenced by this view.
503    ///
504    /// This can include hidden commits referenced by remote bookmarks, previous
505    /// positions of conflicted bookmarks, etc. The ancestors of the returned
506    /// commits should be considered reachable from the view. Use this to build
507    /// commit index from scratch.
508    ///
509    /// The iteration order is unspecified, and may include duplicated entries.
510    pub fn all_referenced_commit_ids(&self) -> impl Iterator<Item = &CommitId> {
511        // Include both added/removed ids since ancestry information of old
512        // references will be needed while merging views.
513        fn ref_target_ids(target: &RefTarget) -> impl Iterator<Item = &CommitId> {
514            target.as_merge().iter().flatten()
515        }
516
517        // Some of the fields (e.g. wc_commit_ids) would be redundant, but let's
518        // not be smart here. Callers will build a larger set of commits anyway.
519        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/// Error from attempts to rename a workspace
557#[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        // Absent remote ref cannot be tracked by absent local ref
597        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        // Present remote ref can be tracked by absent local ref
604        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        // Absent remote ref can be tracked by present local ref
611        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        // Absent remote ref should be removed if local ref becomes absent
622        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        // Absent remote ref cannot be tracked by absent local ref
648        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        // Present remote ref can be tracked by absent local ref
655        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        // Absent remote ref can be tracked by present local ref
662        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        // Absent remote ref should be removed if local ref becomes absent
673        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}