jj_lib/
view.rs

1// Copyright 2020 The Jujutsu Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15#![expect(missing_docs)]
16
17use std::collections::BTreeMap;
18use std::collections::HashSet;
19
20use itertools::Itertools as _;
21use thiserror::Error;
22
23use crate::backend::CommitId;
24use crate::op_store;
25use crate::op_store::LocalRemoteRefTarget;
26use crate::op_store::RefTarget;
27use crate::op_store::RefTargetOptionExt as _;
28use crate::op_store::RemoteRef;
29use crate::ref_name::GitRefName;
30use crate::ref_name::GitRefNameBuf;
31use crate::ref_name::RefName;
32use crate::ref_name::RemoteName;
33use crate::ref_name::RemoteRefSymbol;
34use crate::ref_name::WorkspaceName;
35use crate::ref_name::WorkspaceNameBuf;
36use crate::refs;
37use crate::refs::LocalAndRemoteRef;
38use crate::str_util::StringPattern;
39
40/// A wrapper around [`op_store::View`] that defines additional methods.
41#[derive(PartialEq, Eq, Debug, Clone)]
42pub struct View {
43    data: op_store::View,
44}
45
46impl View {
47    pub fn new(op_store_view: op_store::View) -> Self {
48        Self {
49            data: op_store_view,
50        }
51    }
52
53    pub fn wc_commit_ids(&self) -> &BTreeMap<WorkspaceNameBuf, CommitId> {
54        &self.data.wc_commit_ids
55    }
56
57    pub fn get_wc_commit_id(&self, name: &WorkspaceName) -> Option<&CommitId> {
58        self.data.wc_commit_ids.get(name)
59    }
60
61    pub fn workspaces_for_wc_commit_id(&self, commit_id: &CommitId) -> Vec<WorkspaceNameBuf> {
62        let mut workspace_names = vec![];
63        for (name, wc_commit_id) in &self.data.wc_commit_ids {
64            if wc_commit_id == commit_id {
65                workspace_names.push(name.clone());
66            }
67        }
68        workspace_names
69    }
70
71    pub fn is_wc_commit_id(&self, commit_id: &CommitId) -> bool {
72        self.data.wc_commit_ids.values().contains(commit_id)
73    }
74
75    pub fn heads(&self) -> &HashSet<CommitId> {
76        &self.data.head_ids
77    }
78
79    /// Iterates pair of local and remote bookmarks by bookmark name.
80    pub fn bookmarks(&self) -> impl Iterator<Item = (&RefName, LocalRemoteRefTarget<'_>)> {
81        op_store::merge_join_ref_views(
82            &self.data.local_bookmarks,
83            &self.data.remote_views,
84            |view| &view.bookmarks,
85        )
86    }
87
88    pub fn git_refs(&self) -> &BTreeMap<GitRefNameBuf, RefTarget> {
89        &self.data.git_refs
90    }
91
92    pub fn git_head(&self) -> &RefTarget {
93        &self.data.git_head
94    }
95
96    pub fn set_wc_commit(&mut self, name: WorkspaceNameBuf, commit_id: CommitId) {
97        self.data.wc_commit_ids.insert(name, commit_id);
98    }
99
100    pub fn remove_wc_commit(&mut self, name: &WorkspaceName) {
101        self.data.wc_commit_ids.remove(name);
102    }
103
104    pub fn rename_workspace(
105        &mut self,
106        old_name: &WorkspaceName,
107        new_name: WorkspaceNameBuf,
108    ) -> Result<(), RenameWorkspaceError> {
109        if self.data.wc_commit_ids.contains_key(&new_name) {
110            return Err(RenameWorkspaceError::WorkspaceAlreadyExists {
111                name: new_name.clone(),
112            });
113        }
114        let wc_commit_id = self.data.wc_commit_ids.remove(old_name).ok_or_else(|| {
115            RenameWorkspaceError::WorkspaceDoesNotExist {
116                name: old_name.to_owned(),
117            }
118        })?;
119        self.data.wc_commit_ids.insert(new_name, wc_commit_id);
120        Ok(())
121    }
122
123    pub fn add_head(&mut self, head_id: &CommitId) {
124        self.data.head_ids.insert(head_id.clone());
125    }
126
127    pub fn remove_head(&mut self, head_id: &CommitId) {
128        self.data.head_ids.remove(head_id);
129    }
130
131    /// Iterates local bookmark `(name, target)`s in lexicographical order.
132    pub fn local_bookmarks(&self) -> impl Iterator<Item = (&RefName, &RefTarget)> {
133        self.data
134            .local_bookmarks
135            .iter()
136            .map(|(name, target)| (name.as_ref(), target))
137    }
138
139    /// Iterates local bookmarks `(name, target)` in lexicographical order where
140    /// the target adds `commit_id`.
141    pub fn local_bookmarks_for_commit<'a, 'b>(
142        &'a self,
143        commit_id: &'b CommitId,
144    ) -> impl Iterator<Item = (&'a RefName, &'a RefTarget)> + use<'a, 'b> {
145        self.local_bookmarks()
146            .filter(|(_, target)| target.added_ids().contains(commit_id))
147    }
148
149    /// Iterates local bookmark `(name, target)`s matching the given pattern.
150    /// Entries are sorted by `name`.
151    pub fn local_bookmarks_matching<'a, 'b>(
152        &'a self,
153        pattern: &'b StringPattern,
154    ) -> impl Iterator<Item = (&'a RefName, &'a RefTarget)> + use<'a, 'b> {
155        pattern
156            .filter_btree_map_as_deref(&self.data.local_bookmarks)
157            .map(|(name, target)| (name.as_ref(), target))
158    }
159
160    pub fn get_local_bookmark(&self, name: &RefName) -> &RefTarget {
161        self.data.local_bookmarks.get(name).flatten()
162    }
163
164    /// Sets local bookmark to point to the given target. If the target is
165    /// absent, and if no associated remote bookmarks exist, the bookmark
166    /// will be removed.
167    pub fn set_local_bookmark_target(&mut self, name: &RefName, target: RefTarget) {
168        if target.is_present() {
169            self.data.local_bookmarks.insert(name.to_owned(), target);
170        } else {
171            self.data.local_bookmarks.remove(name);
172        }
173    }
174
175    /// Iterates over `(symbol, remote_ref)` for all remote bookmarks in
176    /// lexicographical order.
177    pub fn all_remote_bookmarks(&self) -> impl Iterator<Item = (RemoteRefSymbol<'_>, &RemoteRef)> {
178        op_store::flatten_remote_refs(&self.data.remote_views, |view| &view.bookmarks)
179    }
180
181    /// Iterates over `(name, remote_ref)`s for all remote bookmarks of the
182    /// specified remote in lexicographical order.
183    pub fn remote_bookmarks(
184        &self,
185        remote_name: &RemoteName,
186    ) -> impl Iterator<Item = (&RefName, &RemoteRef)> + use<'_> {
187        let maybe_remote_view = self.data.remote_views.get(remote_name);
188        maybe_remote_view
189            .map(|remote_view| {
190                remote_view
191                    .bookmarks
192                    .iter()
193                    .map(|(name, remote_ref)| (name.as_ref(), remote_ref))
194            })
195            .into_iter()
196            .flatten()
197    }
198
199    /// Iterates over `(symbol, remote_ref)`s for all remote bookmarks of the
200    /// specified remote that match the given pattern.
201    ///
202    /// Entries are sorted by `symbol`, which is `(name, remote)`.
203    pub fn remote_bookmarks_matching<'a, 'b>(
204        &'a self,
205        bookmark_pattern: &'b StringPattern,
206        remote_pattern: &'b StringPattern,
207    ) -> impl Iterator<Item = (RemoteRefSymbol<'a>, &'a RemoteRef)> + use<'a, 'b> {
208        // Use kmerge instead of flat_map for consistency with all_remote_bookmarks().
209        remote_pattern
210            .filter_btree_map_as_deref(&self.data.remote_views)
211            .map(|(remote, remote_view)| {
212                bookmark_pattern
213                    .filter_btree_map_as_deref(&remote_view.bookmarks)
214                    .map(|(name, remote_ref)| (name.to_remote_symbol(remote), remote_ref))
215            })
216            .kmerge_by(|(symbol1, _), (symbol2, _)| symbol1 < symbol2)
217    }
218
219    pub fn get_remote_bookmark(&self, symbol: RemoteRefSymbol<'_>) -> &RemoteRef {
220        if let Some(remote_view) = self.data.remote_views.get(symbol.remote) {
221            remote_view.bookmarks.get(symbol.name).flatten()
222        } else {
223            RemoteRef::absent_ref()
224        }
225    }
226
227    /// Sets remote-tracking bookmark to the given target and state. If the
228    /// target is absent, the bookmark will be removed.
229    pub fn set_remote_bookmark(&mut self, symbol: RemoteRefSymbol<'_>, remote_ref: RemoteRef) {
230        if remote_ref.is_present() {
231            let remote_view = self
232                .data
233                .remote_views
234                .entry(symbol.remote.to_owned())
235                .or_default();
236            remote_view
237                .bookmarks
238                .insert(symbol.name.to_owned(), remote_ref);
239        } else if let Some(remote_view) = self.data.remote_views.get_mut(symbol.remote) {
240            remote_view.bookmarks.remove(symbol.name);
241        }
242    }
243
244    /// Iterates over `(name, {local_ref, remote_ref})`s for every bookmark
245    /// present locally and/or on the specified remote, in lexicographical
246    /// order.
247    ///
248    /// Note that this does *not* take into account whether the local bookmark
249    /// tracks the remote bookmark or not. Missing values are represented as
250    /// RefTarget::absent_ref() or RemoteRef::absent_ref().
251    pub fn local_remote_bookmarks(
252        &self,
253        remote_name: &RemoteName,
254    ) -> impl Iterator<Item = (&RefName, LocalAndRemoteRef<'_>)> + use<'_> {
255        refs::iter_named_local_remote_refs(
256            self.local_bookmarks(),
257            self.remote_bookmarks(remote_name),
258        )
259        .map(|(name, (local_target, remote_ref))| {
260            let targets = LocalAndRemoteRef {
261                local_target,
262                remote_ref,
263            };
264            (name, targets)
265        })
266    }
267
268    /// Iterates over `(name, TrackingRefPair {local_ref, remote_ref})`s for
269    /// every bookmark with a name that matches the given pattern, and that is
270    /// present locally and/or on the specified remote.
271    ///
272    /// Entries are sorted by `name`.
273    ///
274    /// Note that this does *not* take into account whether the local bookmark
275    /// tracks the remote bookmark or not. Missing values are represented as
276    /// RefTarget::absent_ref() or RemoteRef::absent_ref().
277    pub fn local_remote_bookmarks_matching<'a, 'b>(
278        &'a self,
279        bookmark_pattern: &'b StringPattern,
280        remote_name: &RemoteName,
281    ) -> impl Iterator<Item = (&'a RefName, LocalAndRemoteRef<'a>)> + use<'a, 'b> {
282        // Change remote_name to StringPattern if needed, but merge-join adapter won't
283        // be usable.
284        let maybe_remote_view = self.data.remote_views.get(remote_name);
285        refs::iter_named_local_remote_refs(
286            bookmark_pattern.filter_btree_map_as_deref(&self.data.local_bookmarks),
287            maybe_remote_view
288                .map(|remote_view| {
289                    bookmark_pattern.filter_btree_map_as_deref(&remote_view.bookmarks)
290                })
291                .into_iter()
292                .flatten(),
293        )
294        .map(|(name, (local_target, remote_ref))| {
295            let targets = LocalAndRemoteRef {
296                local_target,
297                remote_ref,
298            };
299            (name.as_ref(), targets)
300        })
301    }
302
303    pub fn remove_remote(&mut self, remote_name: &RemoteName) {
304        self.data.remote_views.remove(remote_name);
305    }
306
307    pub fn rename_remote(&mut self, old: &RemoteName, new: &RemoteName) {
308        if let Some(remote_view) = self.data.remote_views.remove(old) {
309            self.data.remote_views.insert(new.to_owned(), remote_view);
310        }
311    }
312
313    /// Iterates local tag `(name, target)`s in lexicographical order.
314    pub fn local_tags(&self) -> impl Iterator<Item = (&RefName, &RefTarget)> {
315        self.data
316            .local_tags
317            .iter()
318            .map(|(name, target)| (name.as_ref(), target))
319    }
320
321    pub fn get_local_tag(&self, name: &RefName) -> &RefTarget {
322        self.data.local_tags.get(name).flatten()
323    }
324
325    /// Iterates local tag `(name, target)`s matching the given pattern. Entries
326    /// are sorted by `name`.
327    pub fn local_tags_matching<'a, 'b>(
328        &'a self,
329        pattern: &'b StringPattern,
330    ) -> impl Iterator<Item = (&'a RefName, &'a RefTarget)> + use<'a, 'b> {
331        pattern
332            .filter_btree_map_as_deref(&self.data.local_tags)
333            .map(|(name, target)| (name.as_ref(), target))
334    }
335
336    /// Sets local tag to point to the given target. If the target is absent,
337    /// the local tag will be removed.
338    pub fn set_local_tag_target(&mut self, name: &RefName, target: RefTarget) {
339        if target.is_present() {
340            self.data.local_tags.insert(name.to_owned(), target);
341        } else {
342            self.data.local_tags.remove(name);
343        }
344    }
345
346    /// Iterates over `(symbol, remote_ref)` for all remote tags in
347    /// lexicographical order.
348    pub fn all_remote_tags(&self) -> impl Iterator<Item = (RemoteRefSymbol<'_>, &RemoteRef)> {
349        op_store::flatten_remote_refs(&self.data.remote_views, |view| &view.tags)
350    }
351
352    /// Iterates over `(name, remote_ref)`s for all remote tags of the specified
353    /// remote in lexicographical order.
354    pub fn remote_tags(
355        &self,
356        remote_name: &RemoteName,
357    ) -> impl Iterator<Item = (&RefName, &RemoteRef)> + use<'_> {
358        let maybe_remote_view = self.data.remote_views.get(remote_name);
359        maybe_remote_view
360            .map(|remote_view| {
361                remote_view
362                    .tags
363                    .iter()
364                    .map(|(name, remote_ref)| (name.as_ref(), remote_ref))
365            })
366            .into_iter()
367            .flatten()
368    }
369
370    /// Iterates over `(symbol, remote_ref)`s for all remote tags of the
371    /// specified remote that match the given pattern.
372    ///
373    /// Entries are sorted by `symbol`, which is `(name, remote)`.
374    pub fn remote_tags_matching<'a, 'b>(
375        &'a self,
376        tag_pattern: &'b StringPattern,
377        remote_pattern: &'b StringPattern,
378    ) -> impl Iterator<Item = (RemoteRefSymbol<'a>, &'a RemoteRef)> + use<'a, 'b> {
379        // Use kmerge instead of flat_map for consistency with all_remote_tags().
380        remote_pattern
381            .filter_btree_map_as_deref(&self.data.remote_views)
382            .map(|(remote, remote_view)| {
383                tag_pattern
384                    .filter_btree_map_as_deref(&remote_view.tags)
385                    .map(|(name, remote_ref)| (name.to_remote_symbol(remote), remote_ref))
386            })
387            .kmerge_by(|(symbol1, _), (symbol2, _)| symbol1 < symbol2)
388    }
389
390    /// Returns remote-tracking tag target and state specified by `symbol`.
391    pub fn get_remote_tag(&self, symbol: RemoteRefSymbol<'_>) -> &RemoteRef {
392        if let Some(remote_view) = self.data.remote_views.get(symbol.remote) {
393            remote_view.tags.get(symbol.name).flatten()
394        } else {
395            RemoteRef::absent_ref()
396        }
397    }
398
399    /// Sets remote-tracking tag to the given target and state. If the target is
400    /// absent, the tag will be removed.
401    pub fn set_remote_tag(&mut self, symbol: RemoteRefSymbol<'_>, remote_ref: RemoteRef) {
402        if remote_ref.is_present() {
403            let remote_view = self
404                .data
405                .remote_views
406                .entry(symbol.remote.to_owned())
407                .or_default();
408            remote_view.tags.insert(symbol.name.to_owned(), remote_ref);
409        } else if let Some(remote_view) = self.data.remote_views.get_mut(symbol.remote) {
410            remote_view.tags.remove(symbol.name);
411        }
412    }
413
414    pub fn get_git_ref(&self, name: &GitRefName) -> &RefTarget {
415        self.data.git_refs.get(name).flatten()
416    }
417
418    /// Sets the last imported Git ref to point to the given target. If the
419    /// target is absent, the reference will be removed.
420    pub fn set_git_ref_target(&mut self, name: &GitRefName, target: RefTarget) {
421        if target.is_present() {
422            self.data.git_refs.insert(name.to_owned(), target);
423        } else {
424            self.data.git_refs.remove(name);
425        }
426    }
427
428    /// Sets Git HEAD to point to the given target. If the target is absent, the
429    /// reference will be cleared.
430    pub fn set_git_head_target(&mut self, target: RefTarget) {
431        self.data.git_head = target;
432    }
433
434    /// Iterates all commit ids referenced by this view.
435    ///
436    /// This can include hidden commits referenced by remote bookmarks, previous
437    /// positions of conflicted bookmarks, etc. The ancestors of the returned
438    /// commits should be considered reachable from the view. Use this to build
439    /// commit index from scratch.
440    ///
441    /// The iteration order is unspecified, and may include duplicated entries.
442    pub fn all_referenced_commit_ids(&self) -> impl Iterator<Item = &CommitId> {
443        // Include both added/removed ids since ancestry information of old
444        // references will be needed while merging views.
445        fn ref_target_ids(target: &RefTarget) -> impl Iterator<Item = &CommitId> {
446            target.as_merge().iter().flatten()
447        }
448
449        // Some of the fields (e.g. wc_commit_ids) would be redundant, but let's
450        // not be smart here. Callers will build a larger set of commits anyway.
451        let op_store::View {
452            head_ids,
453            local_bookmarks,
454            local_tags,
455            remote_views,
456            git_refs,
457            git_head,
458            wc_commit_ids,
459        } = &self.data;
460        itertools::chain!(
461            head_ids,
462            local_bookmarks.values().flat_map(ref_target_ids),
463            local_tags.values().flat_map(ref_target_ids),
464            remote_views.values().flat_map(|remote_view| {
465                let op_store::RemoteView { bookmarks, tags } = remote_view;
466                itertools::chain(bookmarks.values(), tags.values())
467                    .flat_map(|remote_ref| ref_target_ids(&remote_ref.target))
468            }),
469            git_refs.values().flat_map(ref_target_ids),
470            ref_target_ids(git_head),
471            wc_commit_ids.values()
472        )
473    }
474
475    pub fn set_view(&mut self, data: op_store::View) {
476        self.data = data;
477    }
478
479    pub fn store_view(&self) -> &op_store::View {
480        &self.data
481    }
482
483    pub fn store_view_mut(&mut self) -> &mut op_store::View {
484        &mut self.data
485    }
486}
487
488/// Error from attempts to rename a workspace
489#[derive(Debug, Error)]
490pub enum RenameWorkspaceError {
491    #[error("Workspace {} not found", name.as_symbol())]
492    WorkspaceDoesNotExist { name: WorkspaceNameBuf },
493
494    #[error("Workspace {} already exists", name.as_symbol())]
495    WorkspaceAlreadyExists { name: WorkspaceNameBuf },
496}