1#![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#[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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 pub fn set_git_head_target(&mut self, target: RefTarget) {
431 self.data.git_head = target;
432 }
433
434 pub fn all_referenced_commit_ids(&self) -> impl Iterator<Item = &CommitId> {
443 fn ref_target_ids(target: &RefTarget) -> impl Iterator<Item = &CommitId> {
446 target.as_merge().iter().flatten()
447 }
448
449 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#[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}