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