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