Skip to main content

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::index::Index;
25use crate::index::IndexResult;
26use crate::op_store;
27use crate::op_store::LocalRemoteRefTarget;
28use crate::op_store::RefTarget;
29use crate::op_store::RefTargetOptionExt as _;
30use crate::op_store::RemoteRef;
31use crate::op_store::RemoteView;
32use crate::ref_name::GitRefName;
33use crate::ref_name::GitRefNameBuf;
34use crate::ref_name::RefName;
35use crate::ref_name::RemoteName;
36use crate::ref_name::RemoteRefSymbol;
37use crate::ref_name::WorkspaceName;
38use crate::ref_name::WorkspaceNameBuf;
39use crate::refs;
40use crate::refs::LocalAndRemoteRef;
41use crate::str_util::StringMatcher;
42
43/// A wrapper around [`op_store::View`] that defines additional methods.
44#[derive(Eq, Debug, Clone)]
45pub struct View {
46    data: op_store::View,
47    head_normalized: bool,
48}
49
50impl PartialEq for View {
51    fn eq(&self, other: &Self) -> bool {
52        self.data == other.data
53    }
54}
55
56impl View {
57    pub fn new(op_store_view: op_store::View, head_normalized: bool) -> Self {
58        Self {
59            data: op_store_view,
60            head_normalized,
61        }
62    }
63
64    pub fn wc_commit_ids(&self) -> &BTreeMap<WorkspaceNameBuf, CommitId> {
65        &self.data.wc_commit_ids
66    }
67
68    pub fn get_wc_commit_id(&self, name: &WorkspaceName) -> Option<&CommitId> {
69        self.data.wc_commit_ids.get(name)
70    }
71
72    pub fn workspaces_for_wc_commit_id(&self, commit_id: &CommitId) -> Vec<WorkspaceNameBuf> {
73        let mut workspace_names = vec![];
74        for (name, wc_commit_id) in &self.data.wc_commit_ids {
75            if wc_commit_id == commit_id {
76                workspace_names.push(name.clone());
77            }
78        }
79        workspace_names
80    }
81
82    pub fn is_wc_commit_id(&self, commit_id: &CommitId) -> bool {
83        self.data.wc_commit_ids.values().contains(commit_id)
84    }
85
86    pub fn heads(&self) -> &HashSet<CommitId> {
87        &self.data.head_ids
88    }
89
90    /// Iterates pair of local and remote bookmarks by bookmark name.
91    pub fn bookmarks(&self) -> impl Iterator<Item = (&RefName, LocalRemoteRefTarget<'_>)> {
92        op_store::merge_join_ref_views(
93            &self.data.local_bookmarks,
94            &self.data.remote_views,
95            |view| &view.bookmarks,
96        )
97    }
98
99    /// Iterates pair of local and remote tags by tag name.
100    pub fn tags(&self) -> impl Iterator<Item = (&RefName, LocalRemoteRefTarget<'_>)> {
101        op_store::merge_join_ref_views(&self.data.local_tags, &self.data.remote_views, |view| {
102            &view.tags
103        })
104    }
105
106    pub fn git_refs(&self) -> &BTreeMap<GitRefNameBuf, RefTarget> {
107        &self.data.git_refs
108    }
109
110    pub fn git_head(&self, workspace: &WorkspaceName) -> &RefTarget {
111        self.data.git_heads.get(workspace).flatten()
112    }
113
114    pub fn all_git_heads(&self) -> &BTreeMap<WorkspaceNameBuf, RefTarget> {
115        &self.data.git_heads
116    }
117
118    pub fn set_wc_commit(&mut self, name: WorkspaceNameBuf, commit_id: CommitId) {
119        self.data.wc_commit_ids.insert(name, commit_id);
120    }
121
122    pub fn remove_workspace(&mut self, name: &WorkspaceName) {
123        self.data.wc_commit_ids.remove(name);
124        self.data.git_heads.remove(name);
125    }
126
127    pub fn rename_workspace(
128        &mut self,
129        old_name: &WorkspaceName,
130        new_name: WorkspaceNameBuf,
131    ) -> Result<(), RenameWorkspaceError> {
132        if self.data.wc_commit_ids.contains_key(&new_name) {
133            return Err(RenameWorkspaceError::WorkspaceAlreadyExists {
134                name: new_name.clone(),
135            });
136        }
137        let wc_commit_id = self.data.wc_commit_ids.remove(old_name).ok_or_else(|| {
138            RenameWorkspaceError::WorkspaceDoesNotExist {
139                name: old_name.to_owned(),
140            }
141        })?;
142        self.data
143            .wc_commit_ids
144            .insert(new_name.clone(), wc_commit_id);
145        if let Some(git_head) = self.data.git_heads.remove(old_name) {
146            self.data.git_heads.insert(new_name, git_head);
147        }
148        Ok(())
149    }
150
151    pub fn add_head(&mut self, head_id: &CommitId) {
152        self.data.head_ids.insert(head_id.clone());
153        self.head_normalized = false;
154    }
155
156    pub fn remove_head(&mut self, head_id: &CommitId) {
157        self.data.head_ids.remove(head_id);
158        self.head_normalized = false;
159    }
160
161    /// Inserts and removes the provided head ids without flipping
162    /// the `head_normalized` bit. This serves as an optimization
163    /// for the common incremental update case. Normalization guarantees
164    /// is up to callers.
165    ///
166    /// `add_head_id` must be a descendant of `remove_head_ids`.
167    pub fn replace_heads(&mut self, add_head_id: CommitId, remove_head_ids: &[CommitId]) {
168        self.data.head_ids.insert(add_head_id);
169        for head_id in remove_head_ids {
170            self.data.head_ids.remove(head_id);
171        }
172    }
173
174    /// Iterates local bookmark `(name, target)`s in lexicographical order.
175    pub fn local_bookmarks(&self) -> impl Iterator<Item = (&RefName, &RefTarget)> {
176        self.data
177            .local_bookmarks
178            .iter()
179            .map(|(name, target)| (name.as_ref(), target))
180    }
181
182    /// Iterates local bookmarks `(name, target)` in lexicographical order where
183    /// the target adds `commit_id`.
184    pub fn local_bookmarks_for_commit(
185        &self,
186        commit_id: &CommitId,
187    ) -> impl Iterator<Item = (&RefName, &RefTarget)> {
188        self.local_bookmarks()
189            .filter(|(_, target)| target.added_ids().contains(commit_id))
190    }
191
192    /// Iterates local bookmark `(name, target)`s matching the given pattern.
193    /// Entries are sorted by `name`.
194    pub fn local_bookmarks_matching(
195        &self,
196        matcher: &StringMatcher,
197    ) -> impl Iterator<Item = (&RefName, &RefTarget)> {
198        matcher
199            .filter_btree_map_as_deref(&self.data.local_bookmarks)
200            .map(|(name, target)| (name.as_ref(), target))
201    }
202
203    pub fn get_local_bookmark(&self, name: &RefName) -> &RefTarget {
204        self.data.local_bookmarks.get(name).flatten()
205    }
206
207    /// Sets local bookmark to point to the given target. If the target is
208    /// absent, the local bookmark will be removed. If there are absent remote
209    /// bookmarks tracked by the newly-absent local bookmark, they will also be
210    /// removed.
211    pub fn set_local_bookmark_target(&mut self, name: &RefName, target: RefTarget) {
212        if target.is_present() {
213            self.data.local_bookmarks.insert(name.to_owned(), target);
214        } else {
215            self.data.local_bookmarks.remove(name);
216            for remote_view in self.data.remote_views.values_mut() {
217                let remote_refs = &mut remote_view.bookmarks;
218                if remote_refs.get(name).is_some_and(RemoteRef::is_absent) {
219                    remote_refs.remove(name);
220                }
221            }
222        }
223    }
224
225    /// Iterates over `(symbol, remote_ref)` for all remote bookmarks in
226    /// lexicographical order.
227    pub fn all_remote_bookmarks(&self) -> impl Iterator<Item = (RemoteRefSymbol<'_>, &RemoteRef)> {
228        op_store::flatten_remote_refs(&self.data.remote_views, |view| &view.bookmarks)
229    }
230
231    /// Iterates over `(name, remote_ref)`s for all remote bookmarks of the
232    /// specified remote in lexicographical order.
233    pub fn remote_bookmarks(
234        &self,
235        remote_name: &RemoteName,
236    ) -> impl Iterator<Item = (&RefName, &RemoteRef)> + use<'_> {
237        let maybe_remote_view = self.data.remote_views.get(remote_name);
238        maybe_remote_view
239            .map(|remote_view| {
240                remote_view
241                    .bookmarks
242                    .iter()
243                    .map(|(name, remote_ref)| (name.as_ref(), remote_ref))
244            })
245            .into_iter()
246            .flatten()
247    }
248
249    /// Iterates over `(symbol, remote_ref)`s for all remote bookmarks of the
250    /// specified remote that match the given pattern.
251    ///
252    /// Entries are sorted by `symbol`, which is `(name, remote)`.
253    pub fn remote_bookmarks_matching(
254        &self,
255        bookmark_matcher: &StringMatcher,
256        remote_matcher: &StringMatcher,
257    ) -> impl Iterator<Item = (RemoteRefSymbol<'_>, &RemoteRef)> {
258        // Use kmerge instead of flat_map for consistency with all_remote_bookmarks().
259        remote_matcher
260            .filter_btree_map_as_deref(&self.data.remote_views)
261            .map(|(remote, remote_view)| {
262                bookmark_matcher
263                    .filter_btree_map_as_deref(&remote_view.bookmarks)
264                    .map(|(name, remote_ref)| (name.to_remote_symbol(remote), remote_ref))
265            })
266            .kmerge_by(|(symbol1, _), (symbol2, _)| symbol1 < symbol2)
267    }
268
269    pub fn get_remote_bookmark(&self, symbol: RemoteRefSymbol<'_>) -> &RemoteRef {
270        if let Some(remote_view) = self.data.remote_views.get(symbol.remote) {
271            remote_view.bookmarks.get(symbol.name).flatten()
272        } else {
273            RemoteRef::absent_ref()
274        }
275    }
276
277    /// Sets remote-tracking bookmark to the given target and state. If the
278    /// target is absent and if no tracking local bookmark exists, the bookmark
279    /// will be removed.
280    pub fn set_remote_bookmark(&mut self, symbol: RemoteRefSymbol<'_>, remote_ref: RemoteRef) {
281        if remote_ref.is_present()
282            || (remote_ref.is_tracked() && self.get_local_bookmark(symbol.name).is_present())
283        {
284            let remote_view = self
285                .data
286                .remote_views
287                .entry(symbol.remote.to_owned())
288                .or_default();
289            remote_view
290                .bookmarks
291                .insert(symbol.name.to_owned(), remote_ref);
292        } else if let Some(remote_view) = self.data.remote_views.get_mut(symbol.remote) {
293            remote_view.bookmarks.remove(symbol.name);
294        }
295    }
296
297    /// Iterates over `(name, {local_ref, remote_ref})`s for every bookmark
298    /// present locally and/or on the specified remote, in lexicographical
299    /// order.
300    ///
301    /// Note that this does *not* take into account whether the local bookmark
302    /// tracks the remote bookmark or not. Missing values are represented as
303    /// RefTarget::absent_ref() or RemoteRef::absent_ref().
304    pub fn local_remote_bookmarks(
305        &self,
306        remote_name: &RemoteName,
307    ) -> impl Iterator<Item = (&RefName, LocalAndRemoteRef<'_>)> + use<'_> {
308        refs::iter_named_local_remote_refs(
309            self.local_bookmarks(),
310            self.remote_bookmarks(remote_name),
311        )
312        .map(|(name, (local_target, remote_ref))| {
313            let targets = LocalAndRemoteRef {
314                local_target,
315                remote_ref,
316            };
317            (name, targets)
318        })
319    }
320
321    /// Iterates over `(name, TrackingRefPair {local_ref, remote_ref})`s for
322    /// every bookmark with a name that matches the given pattern, and that is
323    /// present locally and/or on the specified remote.
324    ///
325    /// Entries are sorted by `name`.
326    ///
327    /// Note that this does *not* take into account whether the local bookmark
328    /// tracks the remote bookmark or not. Missing values are represented as
329    /// RefTarget::absent_ref() or RemoteRef::absent_ref().
330    pub fn local_remote_bookmarks_matching<'a, 'b>(
331        &'a self,
332        bookmark_matcher: &'b StringMatcher,
333        remote_name: &RemoteName,
334    ) -> impl Iterator<Item = (&'a RefName, LocalAndRemoteRef<'a>)> + use<'a, 'b> {
335        // Change remote_name to StringMatcher if needed, but merge-join adapter won't
336        // be usable.
337        let maybe_remote_view = self.data.remote_views.get(remote_name);
338        refs::iter_named_local_remote_refs(
339            bookmark_matcher.filter_btree_map_as_deref(&self.data.local_bookmarks),
340            maybe_remote_view
341                .map(|remote_view| {
342                    bookmark_matcher.filter_btree_map_as_deref(&remote_view.bookmarks)
343                })
344                .into_iter()
345                .flatten(),
346        )
347        .map(|(name, (local_target, remote_ref))| {
348            let targets = LocalAndRemoteRef {
349                local_target,
350                remote_ref,
351            };
352            (name.as_ref(), targets)
353        })
354    }
355
356    /// Iterates remote `(name, view)`s in lexicographical order.
357    pub fn remote_views(&self) -> impl Iterator<Item = (&RemoteName, &RemoteView)> {
358        self.data
359            .remote_views
360            .iter()
361            .map(|(name, view)| (name.as_ref(), view))
362    }
363
364    /// Iterates matching remote `(name, view)`s in lexicographical order.
365    pub fn remote_views_matching(
366        &self,
367        matcher: &StringMatcher,
368    ) -> impl Iterator<Item = (&RemoteName, &RemoteView)> {
369        matcher
370            .filter_btree_map_as_deref(&self.data.remote_views)
371            .map(|(name, view)| (name.as_ref(), view))
372    }
373
374    /// Returns the remote view for `name`.
375    pub fn get_remote_view(&self, name: &RemoteName) -> Option<&RemoteView> {
376        self.data.remote_views.get(name)
377    }
378
379    /// Adds remote view if it doesn't exist.
380    pub fn ensure_remote(&mut self, remote_name: &RemoteName) {
381        if self.data.remote_views.contains_key(remote_name) {
382            return;
383        }
384        self.data
385            .remote_views
386            .insert(remote_name.to_owned(), RemoteView::default());
387    }
388
389    pub fn remove_remote(&mut self, remote_name: &RemoteName) {
390        self.data.remote_views.remove(remote_name);
391    }
392
393    pub fn rename_remote(&mut self, old: &RemoteName, new: &RemoteName) {
394        if let Some(remote_view) = self.data.remote_views.remove(old) {
395            self.data.remote_views.insert(new.to_owned(), remote_view);
396        }
397    }
398
399    /// Iterates local tag `(name, target)`s in lexicographical order.
400    pub fn local_tags(&self) -> impl Iterator<Item = (&RefName, &RefTarget)> {
401        self.data
402            .local_tags
403            .iter()
404            .map(|(name, target)| (name.as_ref(), target))
405    }
406
407    pub fn get_local_tag(&self, name: &RefName) -> &RefTarget {
408        self.data.local_tags.get(name).flatten()
409    }
410
411    /// Iterates local tag `(name, target)`s matching the given pattern. Entries
412    /// are sorted by `name`.
413    pub fn local_tags_matching(
414        &self,
415        matcher: &StringMatcher,
416    ) -> impl Iterator<Item = (&RefName, &RefTarget)> {
417        matcher
418            .filter_btree_map_as_deref(&self.data.local_tags)
419            .map(|(name, target)| (name.as_ref(), target))
420    }
421
422    /// Sets local tag to point to the given target. If the target is absent,
423    /// the local tag will be removed. If there are absent remote tags tracked
424    /// by the newly-absent local tag, they will also be removed.
425    pub fn set_local_tag_target(&mut self, name: &RefName, target: RefTarget) {
426        if target.is_present() {
427            self.data.local_tags.insert(name.to_owned(), target);
428        } else {
429            self.data.local_tags.remove(name);
430            for remote_view in self.data.remote_views.values_mut() {
431                let remote_refs = &mut remote_view.tags;
432                if remote_refs.get(name).is_some_and(RemoteRef::is_absent) {
433                    remote_refs.remove(name);
434                }
435            }
436        }
437    }
438
439    /// Iterates over `(symbol, remote_ref)` for all remote tags in
440    /// lexicographical order.
441    pub fn all_remote_tags(&self) -> impl Iterator<Item = (RemoteRefSymbol<'_>, &RemoteRef)> {
442        op_store::flatten_remote_refs(&self.data.remote_views, |view| &view.tags)
443    }
444
445    /// Iterates over `(name, remote_ref)`s for all remote tags of the specified
446    /// remote in lexicographical order.
447    pub fn remote_tags(
448        &self,
449        remote_name: &RemoteName,
450    ) -> impl Iterator<Item = (&RefName, &RemoteRef)> + use<'_> {
451        let maybe_remote_view = self.data.remote_views.get(remote_name);
452        maybe_remote_view
453            .map(|remote_view| {
454                remote_view
455                    .tags
456                    .iter()
457                    .map(|(name, remote_ref)| (name.as_ref(), remote_ref))
458            })
459            .into_iter()
460            .flatten()
461    }
462
463    /// Iterates over `(symbol, remote_ref)`s for all remote tags of the
464    /// specified remote that match the given pattern.
465    ///
466    /// Entries are sorted by `symbol`, which is `(name, remote)`.
467    pub fn remote_tags_matching(
468        &self,
469        tag_matcher: &StringMatcher,
470        remote_matcher: &StringMatcher,
471    ) -> impl Iterator<Item = (RemoteRefSymbol<'_>, &RemoteRef)> {
472        // Use kmerge instead of flat_map for consistency with all_remote_tags().
473        remote_matcher
474            .filter_btree_map_as_deref(&self.data.remote_views)
475            .map(|(remote, remote_view)| {
476                tag_matcher
477                    .filter_btree_map_as_deref(&remote_view.tags)
478                    .map(|(name, remote_ref)| (name.to_remote_symbol(remote), remote_ref))
479            })
480            .kmerge_by(|(symbol1, _), (symbol2, _)| symbol1 < symbol2)
481    }
482
483    /// Returns remote-tracking tag target and state specified by `symbol`.
484    pub fn get_remote_tag(&self, symbol: RemoteRefSymbol<'_>) -> &RemoteRef {
485        if let Some(remote_view) = self.data.remote_views.get(symbol.remote) {
486            remote_view.tags.get(symbol.name).flatten()
487        } else {
488            RemoteRef::absent_ref()
489        }
490    }
491
492    /// Sets remote-tracking tag to the given target and state. If the target is
493    /// absent and if no tracking local tag exists, the tag will be removed.
494    pub fn set_remote_tag(&mut self, symbol: RemoteRefSymbol<'_>, remote_ref: RemoteRef) {
495        if remote_ref.is_present()
496            || (remote_ref.is_tracked() && self.get_local_tag(symbol.name).is_present())
497        {
498            let remote_view = self
499                .data
500                .remote_views
501                .entry(symbol.remote.to_owned())
502                .or_default();
503            remote_view.tags.insert(symbol.name.to_owned(), remote_ref);
504        } else if let Some(remote_view) = self.data.remote_views.get_mut(symbol.remote) {
505            remote_view.tags.remove(symbol.name);
506        }
507    }
508
509    /// Iterates over `(name, {local_ref, remote_ref})`s for every tag present
510    /// locally and/or on the specified remote, in lexicographical order.
511    ///
512    /// Note that this does *not* take into account whether the local tag tracks
513    /// the remote tag or not. Missing values are represented as
514    /// [`RefTarget::absent_ref()`] or [`RemoteRef::absent_ref()`].
515    pub fn local_remote_tags(
516        &self,
517        remote_name: &RemoteName,
518    ) -> impl Iterator<Item = (&RefName, LocalAndRemoteRef<'_>)> + use<'_> {
519        refs::iter_named_local_remote_refs(self.local_tags(), self.remote_tags(remote_name)).map(
520            |(name, (local_target, remote_ref))| {
521                let targets = LocalAndRemoteRef {
522                    local_target,
523                    remote_ref,
524                };
525                (name, targets)
526            },
527        )
528    }
529
530    /// Iterates over `(name, TrackingRefPair {local_ref, remote_ref})`s for
531    /// every tag with a name that matches the given pattern, and that is
532    /// present locally and/or on the specified remote.
533    ///
534    /// Entries are sorted by `name`.
535    ///
536    /// Note that this does *not* take into account whether the local tag tracks
537    /// the remote tag or not. Missing values are represented as
538    /// RefTarget::absent_ref() or RemoteRef::absent_ref().
539    pub fn local_remote_tags_matching<'a, 'b>(
540        &'a self,
541        tag_matcher: &'b StringMatcher,
542        remote_name: &RemoteName,
543    ) -> impl Iterator<Item = (&'a RefName, LocalAndRemoteRef<'a>)> + use<'a, 'b> {
544        // Change remote_name to StringMatcher if needed, but merge-join adapter won't
545        // be usable.
546        let maybe_remote_view = self.data.remote_views.get(remote_name);
547        refs::iter_named_local_remote_refs(
548            tag_matcher.filter_btree_map_as_deref(&self.data.local_tags),
549            maybe_remote_view
550                .map(|remote_view| tag_matcher.filter_btree_map_as_deref(&remote_view.tags))
551                .into_iter()
552                .flatten(),
553        )
554        .map(|(name, (local_target, remote_ref))| {
555            let targets = LocalAndRemoteRef {
556                local_target,
557                remote_ref,
558            };
559            (name.as_ref(), targets)
560        })
561    }
562
563    pub fn get_git_ref(&self, name: &GitRefName) -> &RefTarget {
564        self.data.git_refs.get(name).flatten()
565    }
566
567    /// Sets the last imported Git ref to point to the given target. If the
568    /// target is absent, the reference will be removed.
569    pub fn set_git_ref_target(&mut self, name: &GitRefName, target: RefTarget) {
570        if target.is_present() {
571            self.data.git_refs.insert(name.to_owned(), target);
572        } else {
573            self.data.git_refs.remove(name);
574        }
575    }
576
577    /// Sets Git HEAD for the given workspace to point to the given target. If
578    /// the target is absent, the entry will be removed.
579    pub fn set_git_head_target(&mut self, workspace: &WorkspaceName, target: RefTarget) {
580        if target.is_present() {
581            self.data.git_heads.insert(workspace.to_owned(), target);
582        } else {
583            self.data.git_heads.remove(workspace);
584        }
585    }
586
587    /// Iterates all commit ids referenced by this view.
588    ///
589    /// This can include hidden commits referenced by remote bookmarks, previous
590    /// positions of conflicted bookmarks, etc. The ancestors of the returned
591    /// commits should be considered reachable from the view. Use this to build
592    /// commit index from scratch.
593    ///
594    /// The iteration order is unspecified, and may include duplicated entries.
595    pub fn all_referenced_commit_ids(&self) -> impl Iterator<Item = &CommitId> {
596        // Include both added/removed ids since ancestry information of old
597        // references will be needed while merging views.
598        fn ref_target_ids(target: &RefTarget) -> impl Iterator<Item = &CommitId> {
599            target.as_merge().iter().flatten()
600        }
601
602        // Some of the fields (e.g. wc_commit_ids) would be redundant, but let's
603        // not be smart here. Callers will build a larger set of commits anyway.
604        let op_store::View {
605            head_ids,
606            local_bookmarks,
607            local_tags,
608            remote_views,
609            git_refs,
610            git_heads,
611            wc_commit_ids,
612        } = &self.data;
613        itertools::chain!(
614            head_ids,
615            local_bookmarks.values().flat_map(ref_target_ids),
616            local_tags.values().flat_map(ref_target_ids),
617            remote_views.values().flat_map(|remote_view| {
618                let op_store::RemoteView { bookmarks, tags } = remote_view;
619                itertools::chain(bookmarks.values(), tags.values())
620                    .flat_map(|remote_ref| ref_target_ids(&remote_ref.target))
621            }),
622            git_refs.values().flat_map(ref_target_ids),
623            git_heads.values().flat_map(ref_target_ids),
624            wc_commit_ids.values()
625        )
626    }
627
628    pub fn set_view(&mut self, data: op_store::View, head_normalized: bool) {
629        self.data = data;
630        self.head_normalized = head_normalized;
631    }
632
633    pub fn store_view(&self) -> &op_store::View {
634        &self.data
635    }
636
637    pub fn store_view_mut(&mut self) -> &mut op_store::View {
638        &mut self.data
639    }
640
641    pub fn is_heads_normalized(&self) -> bool {
642        self.head_normalized
643    }
644
645    pub async fn normalize_heads(
646        &mut self,
647        index: &dyn Index,
648        root_commit_id: &CommitId,
649    ) -> IndexResult<()> {
650        if self.head_normalized {
651            return Ok(());
652        }
653        let view = self.store_view_mut();
654        if view.head_ids.is_empty() {
655            view.head_ids.insert(root_commit_id.clone());
656        } else if view.head_ids.len() > 1 {
657            // An empty head_ids set is padded with the root_commit_id, but the
658            // root id is unwanted during the heads resolution.
659            view.head_ids.remove(root_commit_id);
660            view.head_ids = index
661                .heads(&mut view.head_ids.iter())
662                .await?
663                .into_iter()
664                .collect();
665        }
666        assert!(!view.head_ids.is_empty());
667        self.head_normalized = true;
668        Ok(())
669    }
670}
671
672/// Error from attempts to rename a workspace
673#[derive(Debug, Error)]
674pub enum RenameWorkspaceError {
675    #[error("Workspace {} not found", name.as_symbol())]
676    WorkspaceDoesNotExist { name: WorkspaceNameBuf },
677
678    #[error("Workspace {} already exists", name.as_symbol())]
679    WorkspaceAlreadyExists { name: WorkspaceNameBuf },
680}
681
682#[cfg(test)]
683mod tests {
684    use super::*;
685    use crate::op_store::RemoteRefState;
686
687    fn remote_symbol<'a, N, M>(name: &'a N, remote: &'a M) -> RemoteRefSymbol<'a>
688    where
689        N: AsRef<RefName> + ?Sized,
690        M: AsRef<RemoteName> + ?Sized,
691    {
692        RemoteRefSymbol {
693            name: name.as_ref(),
694            remote: remote.as_ref(),
695        }
696    }
697
698    #[test]
699    fn test_absent_tracked_bookmarks() {
700        let mut view = View {
701            data: op_store::View::make_root(CommitId::from_hex("000000")),
702            head_normalized: true,
703        };
704        let absent_tracked_ref = RemoteRef {
705            target: RefTarget::absent(),
706            state: RemoteRefState::Tracked,
707        };
708        let present_tracked_ref = RemoteRef {
709            target: RefTarget::normal(CommitId::from_hex("111111")),
710            state: RemoteRefState::Tracked,
711        };
712
713        // Absent remote ref cannot be tracked by absent local ref
714        view.set_remote_bookmark(remote_symbol("foo", "new"), absent_tracked_ref.clone());
715        assert_eq!(
716            view.get_remote_bookmark(remote_symbol("foo", "new")),
717            RemoteRef::absent_ref()
718        );
719
720        // Present remote ref can be tracked by absent local ref
721        view.set_remote_bookmark(remote_symbol("foo", "present"), present_tracked_ref.clone());
722        assert_eq!(
723            view.get_remote_bookmark(remote_symbol("foo", "present")),
724            &present_tracked_ref
725        );
726
727        // Absent remote ref can be tracked by present local ref
728        view.set_local_bookmark_target(
729            "foo".as_ref(),
730            RefTarget::normal(CommitId::from_hex("222222")),
731        );
732        view.set_remote_bookmark(remote_symbol("foo", "new"), absent_tracked_ref.clone());
733        assert_eq!(
734            view.get_remote_bookmark(remote_symbol("foo", "new")),
735            &absent_tracked_ref
736        );
737
738        // Absent remote ref should be removed if local ref becomes absent
739        view.set_local_bookmark_target("foo".as_ref(), RefTarget::absent());
740        assert_eq!(
741            view.get_remote_bookmark(remote_symbol("foo", "new")),
742            RemoteRef::absent_ref()
743        );
744        assert_eq!(
745            view.get_remote_bookmark(remote_symbol("foo", "present")),
746            &present_tracked_ref
747        );
748    }
749
750    #[test]
751    fn test_absent_tracked_tags() {
752        let mut view = View {
753            data: op_store::View::make_root(CommitId::from_hex("000000")),
754            head_normalized: true,
755        };
756        let absent_tracked_ref = RemoteRef {
757            target: RefTarget::absent(),
758            state: RemoteRefState::Tracked,
759        };
760        let present_tracked_ref = RemoteRef {
761            target: RefTarget::normal(CommitId::from_hex("111111")),
762            state: RemoteRefState::Tracked,
763        };
764
765        // Absent remote ref cannot be tracked by absent local ref
766        view.set_remote_tag(remote_symbol("foo", "new"), absent_tracked_ref.clone());
767        assert_eq!(
768            view.get_remote_tag(remote_symbol("foo", "new")),
769            RemoteRef::absent_ref()
770        );
771
772        // Present remote ref can be tracked by absent local ref
773        view.set_remote_tag(remote_symbol("foo", "present"), present_tracked_ref.clone());
774        assert_eq!(
775            view.get_remote_tag(remote_symbol("foo", "present")),
776            &present_tracked_ref
777        );
778
779        // Absent remote ref can be tracked by present local ref
780        view.set_local_tag_target(
781            "foo".as_ref(),
782            RefTarget::normal(CommitId::from_hex("222222")),
783        );
784        view.set_remote_tag(remote_symbol("foo", "new"), absent_tracked_ref.clone());
785        assert_eq!(
786            view.get_remote_tag(remote_symbol("foo", "new")),
787            &absent_tracked_ref
788        );
789
790        // Absent remote ref should be removed if local ref becomes absent
791        view.set_local_tag_target("foo".as_ref(), RefTarget::absent());
792        assert_eq!(
793            view.get_remote_tag(remote_symbol("foo", "new")),
794            RemoteRef::absent_ref()
795        );
796        assert_eq!(
797            view.get_remote_tag(remote_symbol("foo", "present")),
798            &present_tracked_ref
799        );
800    }
801}