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#![allow(missing_docs)]
16
17use std::collections::BTreeMap;
18use std::collections::HashMap;
19use std::collections::HashSet;
20
21use itertools::Itertools;
22use thiserror::Error;
23
24use crate::backend::CommitId;
25use crate::op_store;
26use crate::op_store::BookmarkTarget;
27use crate::op_store::RefTarget;
28use crate::op_store::RefTargetOptionExt as _;
29use crate::op_store::RemoteRef;
30use crate::op_store::WorkspaceId;
31use crate::refs;
32use crate::refs::LocalAndRemoteRef;
33use crate::refs::RemoteRefSymbol;
34use crate::str_util::StringPattern;
35
36/// A wrapper around [`op_store::View`] that defines additional methods.
37#[derive(PartialEq, Eq, Debug, Clone)]
38pub struct View {
39    data: op_store::View,
40}
41
42impl View {
43    pub fn new(op_store_view: op_store::View) -> Self {
44        View {
45            data: op_store_view,
46        }
47    }
48
49    pub fn wc_commit_ids(&self) -> &HashMap<WorkspaceId, CommitId> {
50        &self.data.wc_commit_ids
51    }
52
53    pub fn get_wc_commit_id(&self, workspace_id: &WorkspaceId) -> Option<&CommitId> {
54        self.data.wc_commit_ids.get(workspace_id)
55    }
56
57    pub fn workspaces_for_wc_commit_id(&self, commit_id: &CommitId) -> Vec<WorkspaceId> {
58        let mut workspaces_ids = vec![];
59        for (workspace_id, wc_commit_id) in &self.data.wc_commit_ids {
60            if wc_commit_id == commit_id {
61                workspaces_ids.push(workspace_id.clone());
62            }
63        }
64        workspaces_ids
65    }
66
67    pub fn is_wc_commit_id(&self, commit_id: &CommitId) -> bool {
68        self.data.wc_commit_ids.values().contains(commit_id)
69    }
70
71    pub fn heads(&self) -> &HashSet<CommitId> {
72        &self.data.head_ids
73    }
74
75    /// Iterates pair of local and remote bookmarks by bookmark name.
76    pub fn bookmarks(&self) -> impl Iterator<Item = (&str, BookmarkTarget<'_>)> {
77        op_store::merge_join_bookmark_views(&self.data.local_bookmarks, &self.data.remote_views)
78    }
79
80    pub fn tags(&self) -> &BTreeMap<String, RefTarget> {
81        &self.data.tags
82    }
83
84    pub fn git_refs(&self) -> &BTreeMap<String, RefTarget> {
85        &self.data.git_refs
86    }
87
88    pub fn git_head(&self) -> &RefTarget {
89        &self.data.git_head
90    }
91
92    pub fn set_wc_commit(&mut self, workspace_id: WorkspaceId, commit_id: CommitId) {
93        self.data.wc_commit_ids.insert(workspace_id, commit_id);
94    }
95
96    pub fn remove_wc_commit(&mut self, workspace_id: &WorkspaceId) {
97        self.data.wc_commit_ids.remove(workspace_id);
98    }
99
100    pub fn rename_workspace(
101        &mut self,
102        old_workspace_id: &WorkspaceId,
103        new_workspace_id: WorkspaceId,
104    ) -> Result<(), RenameWorkspaceError> {
105        if self.data.wc_commit_ids.contains_key(&new_workspace_id) {
106            return Err(RenameWorkspaceError::WorkspaceAlreadyExists {
107                workspace_id: new_workspace_id.as_str().to_owned(),
108            });
109        }
110        let wc_commit_id = self
111            .data
112            .wc_commit_ids
113            .remove(old_workspace_id)
114            .ok_or_else(|| RenameWorkspaceError::WorkspaceDoesNotExist {
115                workspace_id: old_workspace_id.as_str().to_owned(),
116            })?;
117        self.data
118            .wc_commit_ids
119            .insert(new_workspace_id, 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 = (&str, &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, 'b>(
142        &'a self,
143        commit_id: &'b CommitId,
144    ) -> impl Iterator<Item = (&'a str, &'a RefTarget)> + '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, 'b>(
152        &'a self,
153        pattern: &'b StringPattern,
154    ) -> impl Iterator<Item = (&'a str, &'a RefTarget)> + 'b {
155        pattern
156            .filter_btree_map(&self.data.local_bookmarks)
157            .map(|(name, target)| (name.as_ref(), target))
158    }
159
160    pub fn get_local_bookmark(&self, name: &str) -> &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: &str, 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_bookmarks(&self.data.remote_views)
179            .map(|((name, remote), remote_ref)| (RemoteRefSymbol { name, remote }, remote_ref))
180    }
181
182    /// Iterates over `(name, remote_ref)`s for all remote bookmarks of the
183    /// specified remote in lexicographical order.
184    pub fn remote_bookmarks(&self, remote_name: &str) -> impl Iterator<Item = (&str, &RemoteRef)> {
185        let maybe_remote_view = self.data.remote_views.get(remote_name);
186        maybe_remote_view
187            .map(|remote_view| {
188                remote_view
189                    .bookmarks
190                    .iter()
191                    .map(|(name, remote_ref)| (name.as_ref(), remote_ref))
192            })
193            .into_iter()
194            .flatten()
195    }
196
197    /// Iterates over `(symbol, remote_ref)`s for all remote bookmarks of the
198    /// specified remote that match the given pattern.
199    ///
200    /// Entries are sorted by `symbol`, which is `(name, remote)`.
201    pub fn remote_bookmarks_matching<'a: 'b, 'b>(
202        &'a self,
203        bookmark_pattern: &'b StringPattern,
204        remote_pattern: &'b StringPattern,
205    ) -> impl Iterator<Item = (RemoteRefSymbol<'a>, &'a RemoteRef)> + 'b {
206        // Use kmerge instead of flat_map for consistency with all_remote_bookmarks().
207        remote_pattern
208            .filter_btree_map(&self.data.remote_views)
209            .map(|(remote, remote_view)| {
210                bookmark_pattern
211                    .filter_btree_map(&remote_view.bookmarks)
212                    .map(|(name, remote_ref)| {
213                        let symbol = RemoteRefSymbol { name, remote };
214                        (symbol, remote_ref)
215                    })
216            })
217            .kmerge_by(|(symbol1, _), (symbol2, _)| symbol1 < symbol2)
218    }
219
220    pub fn get_remote_bookmark(&self, symbol: RemoteRefSymbol<'_>) -> &RemoteRef {
221        if let Some(remote_view) = self.data.remote_views.get(symbol.remote) {
222            remote_view.bookmarks.get(symbol.name).flatten()
223        } else {
224            RemoteRef::absent_ref()
225        }
226    }
227
228    /// Sets remote-tracking bookmark to the given target and state. If the
229    /// target is absent, the bookmark will be removed.
230    pub fn set_remote_bookmark(&mut self, symbol: RemoteRefSymbol<'_>, remote_ref: RemoteRef) {
231        if remote_ref.is_present() {
232            let remote_view = self
233                .data
234                .remote_views
235                .entry(symbol.remote.to_owned())
236                .or_default();
237            remote_view
238                .bookmarks
239                .insert(symbol.name.to_owned(), remote_ref);
240        } else if let Some(remote_view) = self.data.remote_views.get_mut(symbol.remote) {
241            remote_view.bookmarks.remove(symbol.name);
242        }
243    }
244
245    /// Iterates over `(name, {local_ref, remote_ref})`s for every bookmark
246    /// present locally and/or on the specified remote, in lexicographical
247    /// order.
248    ///
249    /// Note that this does *not* take into account whether the local bookmark
250    /// tracks the remote bookmark or not. Missing values are represented as
251    /// RefTarget::absent_ref() or RemoteRef::absent_ref().
252    pub fn local_remote_bookmarks<'a>(
253        &'a self,
254        remote_name: &str,
255    ) -> impl Iterator<Item = (&'a str, LocalAndRemoteRef<'a>)> + 'a {
256        refs::iter_named_local_remote_refs(
257            self.local_bookmarks(),
258            self.remote_bookmarks(remote_name),
259        )
260        .map(|(name, (local_target, remote_ref))| {
261            let targets = LocalAndRemoteRef {
262                local_target,
263                remote_ref,
264            };
265            (name, targets)
266        })
267    }
268
269    /// Iterates over `(name, TrackingRefPair {local_ref, remote_ref})`s for
270    /// every bookmark with a name that matches the given pattern, and that is
271    /// present locally and/or on the specified remote.
272    ///
273    /// Entries are sorted by `name`.
274    ///
275    /// Note that this does *not* take into account whether the local bookmark
276    /// tracks the remote bookmark or not. Missing values are represented as
277    /// RefTarget::absent_ref() or RemoteRef::absent_ref().
278    pub fn local_remote_bookmarks_matching<'a: 'b, 'b>(
279        &'a self,
280        bookmark_pattern: &'b StringPattern,
281        remote_name: &str,
282    ) -> impl Iterator<Item = (&'a str, LocalAndRemoteRef<'a>)> + 'b {
283        // Change remote_name to StringPattern if needed, but merge-join adapter won't
284        // be usable.
285        let maybe_remote_view = self.data.remote_views.get(remote_name);
286        refs::iter_named_local_remote_refs(
287            bookmark_pattern.filter_btree_map(&self.data.local_bookmarks),
288            maybe_remote_view
289                .map(|remote_view| bookmark_pattern.filter_btree_map(&remote_view.bookmarks))
290                .into_iter()
291                .flatten(),
292        )
293        .map(|(name, (local_target, remote_ref))| {
294            let targets = LocalAndRemoteRef {
295                local_target,
296                remote_ref,
297            };
298            (name.as_ref(), targets)
299        })
300    }
301
302    pub fn remove_remote(&mut self, remote_name: &str) {
303        self.data.remote_views.remove(remote_name);
304    }
305
306    pub fn rename_remote(&mut self, old: &str, new: &str) {
307        if let Some(remote_view) = self.data.remote_views.remove(old) {
308            self.data.remote_views.insert(new.to_owned(), remote_view);
309        }
310    }
311
312    pub fn get_tag(&self, name: &str) -> &RefTarget {
313        self.data.tags.get(name).flatten()
314    }
315
316    /// Iterates tags `(name, target)`s matching the given pattern. Entries
317    /// are sorted by `name`.
318    pub fn tags_matching<'a: 'b, 'b>(
319        &'a self,
320        pattern: &'b StringPattern,
321    ) -> impl Iterator<Item = (&'a str, &'a RefTarget)> + 'b {
322        pattern
323            .filter_btree_map(&self.data.tags)
324            .map(|(name, target)| (name.as_ref(), target))
325    }
326
327    /// Sets tag to point to the given target. If the target is absent, the tag
328    /// will be removed.
329    pub fn set_tag_target(&mut self, name: &str, target: RefTarget) {
330        if target.is_present() {
331            self.data.tags.insert(name.to_owned(), target);
332        } else {
333            self.data.tags.remove(name);
334        }
335    }
336
337    pub fn get_git_ref(&self, name: &str) -> &RefTarget {
338        self.data.git_refs.get(name).flatten()
339    }
340
341    /// Sets the last imported Git ref to point to the given target. If the
342    /// target is absent, the reference will be removed.
343    pub fn set_git_ref_target(&mut self, name: &str, target: RefTarget) {
344        if target.is_present() {
345            self.data.git_refs.insert(name.to_owned(), target);
346        } else {
347            self.data.git_refs.remove(name);
348        }
349    }
350
351    /// Sets Git HEAD to point to the given target. If the target is absent, the
352    /// reference will be cleared.
353    pub fn set_git_head_target(&mut self, target: RefTarget) {
354        self.data.git_head = target;
355    }
356
357    /// Iterates all commit ids referenced by this view.
358    ///
359    /// This can include hidden commits referenced by remote bookmarks, previous
360    /// positions of conflicted bookmarks, etc. The ancestors and predecessors
361    /// of the returned commits should be considered reachable from the
362    /// view. Use this to build commit index from scratch.
363    ///
364    /// The iteration order is unspecified, and may include duplicated entries.
365    pub fn all_referenced_commit_ids(&self) -> impl Iterator<Item = &CommitId> {
366        // Include both added/removed ids since ancestry information of old
367        // references will be needed while merging views.
368        fn ref_target_ids(target: &RefTarget) -> impl Iterator<Item = &CommitId> {
369            target.as_merge().iter().flatten()
370        }
371
372        // Some of the fields (e.g. wc_commit_ids) would be redundant, but let's
373        // not be smart here. Callers will build a larger set of commits anyway.
374        let op_store::View {
375            head_ids,
376            local_bookmarks,
377            tags,
378            remote_views,
379            git_refs,
380            git_head,
381            wc_commit_ids,
382        } = &self.data;
383        itertools::chain!(
384            head_ids,
385            local_bookmarks.values().flat_map(ref_target_ids),
386            tags.values().flat_map(ref_target_ids),
387            remote_views.values().flat_map(|remote_view| {
388                let op_store::RemoteView { bookmarks } = remote_view;
389                bookmarks
390                    .values()
391                    .flat_map(|remote_ref| ref_target_ids(&remote_ref.target))
392            }),
393            git_refs.values().flat_map(ref_target_ids),
394            ref_target_ids(git_head),
395            wc_commit_ids.values()
396        )
397    }
398
399    pub fn set_view(&mut self, data: op_store::View) {
400        self.data = data;
401    }
402
403    pub fn store_view(&self) -> &op_store::View {
404        &self.data
405    }
406
407    pub fn store_view_mut(&mut self) -> &mut op_store::View {
408        &mut self.data
409    }
410}
411
412/// Error from attempts to rename a workspace
413#[derive(Debug, Error)]
414pub enum RenameWorkspaceError {
415    #[error("Workspace {workspace_id} not found")]
416    WorkspaceDoesNotExist { workspace_id: String },
417
418    #[error("Workspace {workspace_id} already exists")]
419    WorkspaceAlreadyExists { workspace_id: String },
420}