1#![expect(missing_docs)]
16
17use std::any::Any;
18use std::collections::BTreeMap;
19use std::collections::HashMap;
20use std::collections::HashSet;
21use std::fmt::Debug;
22use std::iter;
23use std::sync::LazyLock;
24use std::time::SystemTime;
25
26use async_trait::async_trait;
27use itertools::Itertools as _;
28use thiserror::Error;
29
30use crate::backend::CommitId;
31use crate::backend::MillisSinceEpoch;
32use crate::backend::Timestamp;
33use crate::content_hash::ContentHash;
34use crate::merge::Merge;
35use crate::object_id::HexPrefix;
36use crate::object_id::ObjectId as _;
37use crate::object_id::PrefixResolution;
38use crate::object_id::id_type;
39use crate::ref_name::GitRefNameBuf;
40use crate::ref_name::RefName;
41use crate::ref_name::RefNameBuf;
42use crate::ref_name::RemoteName;
43use crate::ref_name::RemoteNameBuf;
44use crate::ref_name::RemoteRefSymbol;
45use crate::ref_name::WorkspaceNameBuf;
46
47id_type!(pub ViewId { hex() });
48id_type!(pub OperationId { hex() });
49
50#[derive(ContentHash, PartialEq, Eq, Hash, Clone, Debug, serde::Serialize)]
51#[serde(transparent)]
52pub struct RefTarget {
53 merge: Merge<Option<CommitId>>,
54}
55
56impl Default for RefTarget {
57 fn default() -> Self {
58 Self::absent()
59 }
60}
61
62impl RefTarget {
63 pub fn absent() -> Self {
65 Self::from_merge(Merge::absent())
66 }
67
68 pub fn absent_ref() -> &'static Self {
72 static TARGET: LazyLock<RefTarget> = LazyLock::new(RefTarget::absent);
73 &TARGET
74 }
75
76 pub fn resolved(maybe_id: Option<CommitId>) -> Self {
78 Self::from_merge(Merge::resolved(maybe_id))
79 }
80
81 pub fn normal(id: CommitId) -> Self {
83 Self::from_merge(Merge::normal(id))
84 }
85
86 pub fn from_legacy_form(
88 removed_ids: impl IntoIterator<Item = CommitId>,
89 added_ids: impl IntoIterator<Item = CommitId>,
90 ) -> Self {
91 Self::from_merge(Merge::from_legacy_form(removed_ids, added_ids))
92 }
93
94 pub fn from_merge(merge: Merge<Option<CommitId>>) -> Self {
95 Self { merge }
96 }
97
98 pub fn as_resolved(&self) -> Option<&Option<CommitId>> {
100 self.merge.as_resolved()
101 }
102
103 pub fn as_normal(&self) -> Option<&CommitId> {
105 self.merge.as_normal()
106 }
107
108 pub fn is_absent(&self) -> bool {
110 self.merge.is_absent()
111 }
112
113 pub fn is_present(&self) -> bool {
116 self.merge.is_present()
117 }
118
119 pub fn has_conflict(&self) -> bool {
121 !self.merge.is_resolved()
122 }
123
124 pub fn removed_ids(&self) -> impl Iterator<Item = &CommitId> {
125 self.merge.removes().flatten()
126 }
127
128 pub fn added_ids(&self) -> impl Iterator<Item = &CommitId> {
129 self.merge.adds().flatten()
130 }
131
132 pub fn as_merge(&self) -> &Merge<Option<CommitId>> {
133 &self.merge
134 }
135}
136
137#[derive(ContentHash, Clone, Debug, Eq, Hash, PartialEq)]
139pub struct RemoteRef {
140 pub target: RefTarget,
141 pub state: RemoteRefState,
142}
143
144impl RemoteRef {
145 pub fn absent() -> Self {
147 Self {
148 target: RefTarget::absent(),
149 state: RemoteRefState::New,
150 }
151 }
152
153 pub fn absent_ref() -> &'static Self {
157 static TARGET: LazyLock<RemoteRef> = LazyLock::new(RemoteRef::absent);
158 &TARGET
159 }
160
161 pub fn is_absent(&self) -> bool {
163 self.target.is_absent()
164 }
165
166 pub fn is_present(&self) -> bool {
168 self.target.is_present()
169 }
170
171 pub fn is_tracked(&self) -> bool {
173 self.state == RemoteRefState::Tracked
174 }
175
176 pub fn tracked_target(&self) -> &RefTarget {
181 if self.is_tracked() {
182 &self.target
183 } else {
184 RefTarget::absent_ref()
185 }
186 }
187}
188
189#[derive(ContentHash, Clone, Copy, Debug, Eq, Hash, PartialEq)]
191pub enum RemoteRefState {
192 New,
194 Tracked,
197}
198
199pub trait RefTargetOptionExt {
201 type Value;
202
203 fn flatten(self) -> Self::Value;
204}
205
206impl RefTargetOptionExt for Option<RefTarget> {
207 type Value = RefTarget;
208
209 fn flatten(self) -> Self::Value {
210 self.unwrap_or_else(RefTarget::absent)
211 }
212}
213
214impl<'a> RefTargetOptionExt for Option<&'a RefTarget> {
215 type Value = &'a RefTarget;
216
217 fn flatten(self) -> Self::Value {
218 self.unwrap_or_else(|| RefTarget::absent_ref())
219 }
220}
221
222impl RefTargetOptionExt for Option<RemoteRef> {
223 type Value = RemoteRef;
224
225 fn flatten(self) -> Self::Value {
226 self.unwrap_or_else(RemoteRef::absent)
227 }
228}
229
230impl<'a> RefTargetOptionExt for Option<&'a RemoteRef> {
231 type Value = &'a RemoteRef;
232
233 fn flatten(self) -> Self::Value {
234 self.unwrap_or_else(|| RemoteRef::absent_ref())
235 }
236}
237
238#[derive(PartialEq, Eq, Clone, Debug)]
240pub struct LocalRemoteRefTarget<'a> {
241 pub local_target: &'a RefTarget,
243 pub remote_refs: Vec<(&'a RemoteName, &'a RemoteRef)>,
245}
246
247#[derive(ContentHash, PartialEq, Eq, Clone, Debug)]
250pub struct View {
251 pub head_ids: HashSet<CommitId>,
253 pub local_bookmarks: BTreeMap<RefNameBuf, RefTarget>,
254 pub local_tags: BTreeMap<RefNameBuf, RefTarget>,
255 pub remote_views: BTreeMap<RemoteNameBuf, RemoteView>,
256 pub git_refs: BTreeMap<GitRefNameBuf, RefTarget>,
257 pub git_head: RefTarget,
261 pub wc_commit_ids: BTreeMap<WorkspaceNameBuf, CommitId>,
265}
266
267impl View {
268 pub fn make_root(root_commit_id: CommitId) -> Self {
270 Self {
271 head_ids: HashSet::from([root_commit_id]),
272 local_bookmarks: BTreeMap::new(),
273 local_tags: BTreeMap::new(),
274 remote_views: BTreeMap::new(),
275 git_refs: BTreeMap::new(),
276 git_head: RefTarget::absent(),
277 wc_commit_ids: BTreeMap::new(),
278 }
279 }
280}
281
282#[derive(ContentHash, Clone, Debug, Default, Eq, PartialEq)]
284pub struct RemoteView {
285 pub bookmarks: BTreeMap<RefNameBuf, RemoteRef>,
290 pub tags: BTreeMap<RefNameBuf, RemoteRef>,
291}
292
293pub(crate) fn merge_join_ref_views<'a>(
295 local_refs: &'a BTreeMap<RefNameBuf, RefTarget>,
296 remote_views: &'a BTreeMap<RemoteNameBuf, RemoteView>,
297 get_remote_refs: impl FnMut(&RemoteView) -> &BTreeMap<RefNameBuf, RemoteRef>,
298) -> impl Iterator<Item = (&'a RefName, LocalRemoteRefTarget<'a>)> {
299 let mut local_refs_iter = local_refs
300 .iter()
301 .map(|(name, target)| (&**name, target))
302 .peekable();
303 let mut remote_refs_iter = flatten_remote_refs(remote_views, get_remote_refs).peekable();
304
305 iter::from_fn(move || {
306 let (name, local_target) = if let Some((symbol, _)) = remote_refs_iter.peek() {
308 local_refs_iter
309 .next_if(|&(local_name, _)| local_name <= symbol.name)
310 .unwrap_or((symbol.name, RefTarget::absent_ref()))
311 } else {
312 local_refs_iter.next()?
313 };
314 let remote_refs = remote_refs_iter
315 .peeking_take_while(|(symbol, _)| symbol.name == name)
316 .map(|(symbol, remote_ref)| (symbol.remote, remote_ref))
317 .collect();
318 let local_remote_target = LocalRemoteRefTarget {
319 local_target,
320 remote_refs,
321 };
322 Some((name, local_remote_target))
323 })
324}
325
326pub(crate) fn flatten_remote_refs(
328 remote_views: &BTreeMap<RemoteNameBuf, RemoteView>,
329 mut get_remote_refs: impl FnMut(&RemoteView) -> &BTreeMap<RefNameBuf, RemoteRef>,
330) -> impl Iterator<Item = (RemoteRefSymbol<'_>, &RemoteRef)> {
331 remote_views
332 .iter()
333 .map(|(remote, remote_view)| {
334 get_remote_refs(remote_view)
335 .iter()
336 .map(move |(name, remote_ref)| (name.to_remote_symbol(remote), remote_ref))
337 })
338 .kmerge_by(|(symbol1, _), (symbol2, _)| symbol1 < symbol2)
339}
340
341#[derive(Clone, ContentHash, Debug, Eq, PartialEq, serde::Serialize)]
342pub struct TimestampRange {
343 pub start: Timestamp,
345 pub end: Timestamp,
346}
347
348#[derive(ContentHash, PartialEq, Eq, Clone, Debug, serde::Serialize)]
361pub struct Operation {
362 #[serde(skip)] pub view_id: ViewId,
364 pub parents: Vec<OperationId>,
365 #[serde(flatten)]
366 pub metadata: OperationMetadata,
367 #[serde(skip)] pub commit_predecessors: Option<BTreeMap<CommitId, Vec<CommitId>>>,
384}
385
386impl Operation {
387 pub fn make_root(root_view_id: ViewId) -> Self {
388 let timestamp = Timestamp {
389 timestamp: MillisSinceEpoch(0),
390 tz_offset: 0,
391 };
392 let metadata = OperationMetadata {
393 time: TimestampRange {
394 start: timestamp,
395 end: timestamp,
396 },
397 description: "".to_string(),
398 hostname: "".to_string(),
399 username: "".to_string(),
400 is_snapshot: false,
401 tags: HashMap::new(),
402 };
403 Self {
404 view_id: root_view_id,
405 parents: vec![],
406 metadata,
407 commit_predecessors: Some(BTreeMap::new()),
412 }
413 }
414}
415
416#[derive(ContentHash, PartialEq, Eq, Clone, Debug, serde::Serialize)]
417pub struct OperationMetadata {
418 pub time: TimestampRange,
419 pub description: String,
421 pub hostname: String,
422 pub username: String,
423 pub is_snapshot: bool,
426 pub tags: HashMap<String, String>,
427}
428
429#[derive(Clone, Debug)]
431pub struct RootOperationData {
432 pub root_commit_id: CommitId,
434}
435
436#[derive(Debug, Error)]
437pub enum OpStoreError {
438 #[error("Object {hash} of type {object_type} not found")]
439 ObjectNotFound {
440 object_type: String,
441 hash: String,
442 source: Box<dyn std::error::Error + Send + Sync>,
443 },
444 #[error("Error when reading object {hash} of type {object_type}")]
445 ReadObject {
446 object_type: String,
447 hash: String,
448 source: Box<dyn std::error::Error + Send + Sync>,
449 },
450 #[error("Could not write object of type {object_type}")]
451 WriteObject {
452 object_type: &'static str,
453 source: Box<dyn std::error::Error + Send + Sync>,
454 },
455 #[error(transparent)]
456 Other(Box<dyn std::error::Error + Send + Sync>),
457}
458
459pub type OpStoreResult<T> = Result<T, OpStoreError>;
460
461#[async_trait]
462pub trait OpStore: Any + Send + Sync + Debug {
463 fn name(&self) -> &str;
464
465 fn root_operation_id(&self) -> &OperationId;
466
467 async fn read_view(&self, id: &ViewId) -> OpStoreResult<View>;
468
469 async fn write_view(&self, contents: &View) -> OpStoreResult<ViewId>;
470
471 async fn read_operation(&self, id: &OperationId) -> OpStoreResult<Operation>;
472
473 async fn write_operation(&self, contents: &Operation) -> OpStoreResult<OperationId>;
474
475 async fn resolve_operation_id_prefix(
477 &self,
478 prefix: &HexPrefix,
479 ) -> OpStoreResult<PrefixResolution<OperationId>>;
480
481 fn gc(&self, head_ids: &[OperationId], keep_newer: SystemTime) -> OpStoreResult<()>;
489}
490
491impl dyn OpStore {
492 pub fn downcast_ref<T: OpStore>(&self) -> Option<&T> {
494 (self as &dyn Any).downcast_ref()
495 }
496}
497
498#[cfg(test)]
499mod tests {
500 use maplit::btreemap;
501
502 use super::*;
503
504 #[test]
505 fn test_merge_join_bookmark_views() {
506 let remote_ref = |target: &RefTarget| RemoteRef {
507 target: target.clone(),
508 state: RemoteRefState::Tracked, };
510 let local_bookmark1_target = RefTarget::normal(CommitId::from_hex("111111"));
511 let local_bookmark2_target = RefTarget::normal(CommitId::from_hex("222222"));
512 let git_bookmark1_remote_ref = remote_ref(&RefTarget::normal(CommitId::from_hex("333333")));
513 let git_bookmark2_remote_ref = remote_ref(&RefTarget::normal(CommitId::from_hex("444444")));
514 let remote1_bookmark1_remote_ref =
515 remote_ref(&RefTarget::normal(CommitId::from_hex("555555")));
516 let remote2_bookmark2_remote_ref =
517 remote_ref(&RefTarget::normal(CommitId::from_hex("666666")));
518
519 let local_bookmarks = btreemap! {
520 "bookmark1".into() => local_bookmark1_target.clone(),
521 "bookmark2".into() => local_bookmark2_target.clone(),
522 };
523 let remote_views = btreemap! {
524 "git".into() => RemoteView {
525 bookmarks: btreemap! {
526 "bookmark1".into() => git_bookmark1_remote_ref.clone(),
527 "bookmark2".into() => git_bookmark2_remote_ref.clone(),
528 },
529 tags: btreemap! {},
530 },
531 "remote1".into() => RemoteView {
532 bookmarks: btreemap! {
533 "bookmark1".into() => remote1_bookmark1_remote_ref.clone(),
534 },
535 tags: btreemap! {},
536 },
537 "remote2".into() => RemoteView {
538 bookmarks: btreemap! {
539 "bookmark2".into() => remote2_bookmark2_remote_ref.clone(),
540 },
541 tags: btreemap! {},
542 },
543 };
544 assert_eq!(
545 merge_join_ref_views(&local_bookmarks, &remote_views, |view| &view.bookmarks)
546 .collect_vec(),
547 vec![
548 (
549 "bookmark1".as_ref(),
550 LocalRemoteRefTarget {
551 local_target: &local_bookmark1_target,
552 remote_refs: vec![
553 ("git".as_ref(), &git_bookmark1_remote_ref),
554 ("remote1".as_ref(), &remote1_bookmark1_remote_ref),
555 ],
556 },
557 ),
558 (
559 "bookmark2".as_ref(),
560 LocalRemoteRefTarget {
561 local_target: &local_bookmark2_target.clone(),
562 remote_refs: vec![
563 ("git".as_ref(), &git_bookmark2_remote_ref),
564 ("remote2".as_ref(), &remote2_bookmark2_remote_ref),
565 ],
566 },
567 ),
568 ],
569 );
570
571 let local_bookmarks = btreemap! {
573 "bookmark1".into() => local_bookmark1_target.clone(),
574 };
575 let remote_views = btreemap! {};
576 assert_eq!(
577 merge_join_ref_views(&local_bookmarks, &remote_views, |view| &view.bookmarks)
578 .collect_vec(),
579 vec![(
580 "bookmark1".as_ref(),
581 LocalRemoteRefTarget {
582 local_target: &local_bookmark1_target,
583 remote_refs: vec![]
584 },
585 )],
586 );
587
588 let local_bookmarks = btreemap! {};
590 let remote_views = btreemap! {
591 "remote1".into() => RemoteView {
592 bookmarks: btreemap! {
593 "bookmark1".into() => remote1_bookmark1_remote_ref.clone(),
594 },
595 tags: btreemap! {},
596 },
597 };
598 assert_eq!(
599 merge_join_ref_views(&local_bookmarks, &remote_views, |view| &view.bookmarks)
600 .collect_vec(),
601 vec![(
602 "bookmark1".as_ref(),
603 LocalRemoteRefTarget {
604 local_target: RefTarget::absent_ref(),
605 remote_refs: vec![("remote1".as_ref(), &remote1_bookmark1_remote_ref)],
606 },
607 )],
608 );
609 }
610}