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(&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    /// Iterates matching remote `(name, view)`s in lexicographical order.
330    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    /// Returns the remote view for `name`.
340    pub fn get_remote_view(&self, name: &RemoteName) -> Option<&RemoteView> {
341        self.data.remote_views.get(name)
342    }
343
344    /// Adds remote view if it doesn't exist.
345    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    /// Iterates local tag `(name, target)`s in lexicographical order.
365    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    /// Iterates local tag `(name, target)`s matching the given pattern. Entries
377    /// are sorted by `name`.
378    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    /// Sets local tag to point to the given target. If the target is absent,
388    /// the local tag will be removed. If there are absent remote tags tracked
389    /// by the newly-absent local tag, they will also be removed.
390    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    /// Iterates over `(symbol, remote_ref)` for all remote tags in
405    /// lexicographical order.
406    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    /// Iterates over `(name, remote_ref)`s for all remote tags of the specified
411    /// remote in lexicographical order.
412    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    /// Iterates over `(symbol, remote_ref)`s for all remote tags of the
429    /// specified remote that match the given pattern.
430    ///
431    /// Entries are sorted by `symbol`, which is `(name, remote)`.
432    pub fn remote_tags_matching(
433        &self,
434        tag_matcher: &StringMatcher,
435        remote_matcher: &StringMatcher,
436    ) -> impl Iterator<Item = (RemoteRefSymbol<'_>, &RemoteRef)> {
437        // Use kmerge instead of flat_map for consistency with all_remote_tags().
438        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    /// Returns remote-tracking tag target and state specified by `symbol`.
449    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    /// Sets remote-tracking tag to the given target and state. If the target is
458    /// absent and if no tracking local tag exists, the tag will be removed.
459    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    /// Iterates over `(name, {local_ref, remote_ref})`s for every tag present
475    /// locally and/or on the specified remote, in lexicographical order.
476    ///
477    /// Note that this does *not* take into account whether the local tag tracks
478    /// the remote tag or not. Missing values are represented as
479    /// [`RefTarget::absent_ref()`] or [`RemoteRef::absent_ref()`].
480    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    /// Sets the last imported Git ref to point to the given target. If the
500    /// target is absent, the reference will be removed.
501    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    /// Sets Git HEAD to point to the given target. If the target is absent, the
510    /// reference will be cleared.
511    pub fn set_git_head_target(&mut self, target: RefTarget) {
512        self.data.git_head = target;
513    }
514
515    /// Iterates all commit ids referenced by this view.
516    ///
517    /// This can include hidden commits referenced by remote bookmarks, previous
518    /// positions of conflicted bookmarks, etc. The ancestors of the returned
519    /// commits should be considered reachable from the view. Use this to build
520    /// commit index from scratch.
521    ///
522    /// The iteration order is unspecified, and may include duplicated entries.
523    pub fn all_referenced_commit_ids(&self) -> impl Iterator<Item = &CommitId> {
524        // Include both added/removed ids since ancestry information of old
525        // references will be needed while merging views.
526        fn ref_target_ids(target: &RefTarget) -> impl Iterator<Item = &CommitId> {
527            target.as_merge().iter().flatten()
528        }
529
530        // Some of the fields (e.g. wc_commit_ids) would be redundant, but let's
531        // not be smart here. Callers will build a larger set of commits anyway.
532        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/// Error from attempts to rename a workspace
570#[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        // Absent remote ref cannot be tracked by absent local ref
610        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        // Present remote ref can be tracked by absent local ref
617        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        // Absent remote ref can be tracked by present local ref
624        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        // Absent remote ref should be removed if local ref becomes absent
635        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        // Absent remote ref cannot be tracked by absent local ref
661        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        // Present remote ref can be tracked by absent local ref
668        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        // Absent remote ref can be tracked by present local ref
675        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        // Absent remote ref should be removed if local ref becomes absent
686        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}