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