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