Skip to main content

triblespace_core/
repo.rs

1//! This module provides a high-level API for storing and retrieving data from repositories.
2//! The design is inspired by Git, but with a focus on object/content-addressed storage.
3//! It separates storage concerns from the data model, and reduces the mutable state of the repository,
4//! to an absolute minimum, making it easier to reason about and allowing for different storage backends.
5//!
6//! Blob repositories are collections of blobs that can be content-addressed by their hash.
7#![allow(clippy::type_complexity)]
8//! This is typically local `.pile` file or a S3 bucket or a similar service.
9//! On their own they have no notion of branches or commits, or other stateful constructs.
10//! As such they also don't have a notion of time, order or history,
11//! massively relaxing the constraints on storage.
12//! This makes it possible to use a wide range of storage services, including those that don't support
13//! atomic transactions or have other limitations.
14//!
15//! Branch repositories on the other hand are a stateful construct that can be used to represent a branch pointing to a specific commit.
16//! They are stored in a separate repository, typically a  local `.pile` file, a database or an S3 compatible service with a compare-and-swap operation,
17//! and can be used to represent the state of a repository at a specific point in time.
18//!
19//! Technically, branches are just a mapping from a branch id to a blob hash,
20//! But because TribleSets are themselves easily stored in a blob, and because
21//! trible commit histories are an append-only chain of TribleSet metadata,
22//! the hash of the head is sufficient to represent the entire history of a branch.
23//!
24//! ## Basic usage
25//!
26//! ```rust,ignore
27//! use ed25519_dalek::SigningKey;
28//! use rand::rngs::OsRng;
29//! use triblespace::prelude::*;
30//! use triblespace::prelude::valueschemas::{GenId, ShortString};
31//! use triblespace::repo::{memoryrepo::MemoryRepo, Repository};
32//!
33//! let storage = MemoryRepo::default();
34//! let mut repo = Repository::new(storage, SigningKey::generate(&mut OsRng));
35//! let branch_id = repo.create_branch("main", None).expect("create branch");
36//! let mut ws = repo.pull(*branch_id).expect("pull branch");
37//!
38//! attributes! {
39//!     "8F180883F9FD5F787E9E0AF0DF5866B9" as pub author: GenId;
40//!     "0DBB530B37B966D137C50B943700EDB2" as pub firstname: ShortString;
41//!     "6BAA463FD4EAF45F6A103DB9433E4545" as pub lastname: ShortString;
42//! }
43//! let author = fucid();
44//! ws.commit(
45//!     entity!{ &author @
46//!         literature::firstname: "Frank",
47//!         literature::lastname: "Herbert",
48//!      },
49//!     None,
50//!     Some("initial commit"),
51//! );
52//!
53//! // Single-attempt push: `try_push` uploads local blobs and attempts a
54//! // single CAS update. On conflict it returns a workspace containing the
55//! // new branch state which you should merge into before retrying.
56//! match repo.try_push(&mut ws).expect("try_push") {
57//!     None => {}
58//!     Some(_) => panic!("unexpected conflict"),
59//! }
60//! ```
61//!
62//! `create_branch` registers a new branch and returns an `ExclusiveId` guard.
63//! `pull` creates a new workspace from an existing branch while
64//! `branch_from` can be used to start a new branch from a specific commit
65//! handle. See `examples/workspace.rs` for a more complete example.
66//!
67//! ## Handling conflicts
68//!
69//! The single-attempt primitive is [`Repository::try_push`]. It returns
70//! `Ok(None)` on success or `Ok(Some(conflict_ws))` when the branch advanced
71//! concurrently. Callers that want explicit conflict handling may use this
72//! form:
73//!
74//! ```rust,ignore
75//! while let Some(mut other) = repo.try_push(&mut ws)? {
76//!     // Merge our staged changes into the incoming workspace and retry.
77//!     other.merge(&mut ws)?;
78//!     ws = other;
79//! }
80//! ```
81//!
82//! For convenience `Repository::push` is provided as a retrying wrapper that
83//! performs the merge-and-retry loop for you. Call `push` when you prefer the
84//! repository to handle conflicts automatically; call `try_push` when you need
85//! to inspect or control the intermediate conflict workspace yourself.
86//!
87//! `push` performs a compare‐and‐swap (CAS) update on the branch metadata.
88//! This optimistic concurrency control keeps branches consistent without
89//! locking and can be emulated by many storage systems (for example by
90//! using conditional writes on S3).
91//!
92//! ## Git parallels
93//!
94//! The API deliberately mirrors concepts from Git to make its usage familiar:
95//!
96//! - A `Repository` stores commits and branch metadata similar to a remote.
97//! - `Workspace` is akin to a working directory combined with an index. It
98//!   tracks changes against a branch head until you `push` them.
99//! - `create_branch` and `branch_from` correspond to creating new branches from
100//!   scratch or from a specific commit, respectively.
101//! - `push` updates the repository atomically. If the branch advanced in the
102//!   meantime, you receive a conflict workspace which can be merged before
103//!   retrying the push.
104//! - `pull` is similar to cloning a branch into a new workspace.
105//!
106//! `pull` uses the repository's default signing key for new commits. If you
107//! need to work with a different identity, the `_with_key` variants allow providing
108//! an explicit key when creating branches or pulling workspaces.
109//!
110//! These parallels should help readers leverage their Git knowledge when
111//! working with trible repositories.
112//!
113pub mod branch;
114pub mod commit;
115pub mod hybridstore;
116pub mod memoryrepo;
117pub mod objectstore;
118pub mod pile;
119
120/// Trait for storage backends that require explicit close/cleanup.
121///
122/// Not all storage backends need to implement this; implementations that have
123/// nothing to do on close may return Ok(()) or use `Infallible` as the error
124/// type.
125pub trait StorageClose {
126    /// Error type returned by `close`.
127    type Error: std::error::Error;
128
129    /// Consume the storage and perform any necessary cleanup.
130    fn close(self) -> Result<(), Self::Error>;
131}
132
133// Convenience impl for repositories whose storage supports explicit close.
134impl<Storage> Repository<Storage>
135where
136    Storage: BlobStore<Blake3> + BranchStore<Blake3> + StorageClose,
137{
138    /// Close the repository's underlying storage if it supports explicit
139    /// close operations.
140    ///
141    /// This method is only available when the storage type implements
142    /// [`StorageClose`]. It consumes the repository and delegates to the
143    /// storage's `close` implementation, returning any error produced.
144    pub fn close(self) -> Result<(), <Storage as StorageClose>::Error> {
145        self.storage.close()
146    }
147}
148
149use crate::macros::pattern;
150use std::collections::{HashSet, VecDeque};
151use std::convert::Infallible;
152use std::error::Error;
153use std::fmt::Debug;
154use std::fmt::{self};
155
156use commit::commit_metadata;
157use hifitime::Epoch;
158use itertools::Itertools;
159
160use crate::blob::schemas::simplearchive::UnarchiveError;
161use crate::blob::schemas::UnknownBlob;
162use crate::blob::Blob;
163use crate::blob::BlobSchema;
164use crate::blob::MemoryBlobStore;
165use crate::blob::ToBlob;
166use crate::blob::TryFromBlob;
167use crate::find;
168use crate::id::genid;
169use crate::id::Id;
170use crate::patch::Entry;
171use crate::patch::IdentitySchema;
172use crate::patch::PATCH;
173use crate::prelude::valueschemas::GenId;
174use crate::repo::branch::branch_metadata;
175use crate::trible::TribleSet;
176use crate::value::schemas::hash::Handle;
177use crate::value::schemas::hash::HashProtocol;
178use crate::value::Value;
179use crate::value::ValueSchema;
180use crate::value::VALUE_LEN;
181use ed25519_dalek::SigningKey;
182
183use crate::blob::schemas::longstring::LongString;
184use crate::blob::schemas::simplearchive::SimpleArchive;
185use crate::prelude::*;
186use crate::value::schemas::ed25519 as ed;
187use crate::value::schemas::hash::Blake3;
188use crate::value::schemas::shortstring::ShortString;
189use crate::value::schemas::time::NsTAIInterval;
190
191attributes! {
192    /// The actual data of the commit.
193    "4DD4DDD05CC31734B03ABB4E43188B1F" as pub content: Handle<Blake3, SimpleArchive>;
194    /// Metadata describing the commit content.
195    "88B59BD497540AC5AECDB7518E737C87" as pub metadata: Handle<Blake3, SimpleArchive>;
196    /// A commit that this commit is based on.
197    "317044B612C690000D798CA660ECFD2A" as pub parent: Handle<Blake3, SimpleArchive>;
198    /// A (potentially long) message describing the commit.
199    "B59D147839100B6ED4B165DF76EDF3BB" as pub message: Handle<Blake3, LongString>;
200    /// A short message describing the commit.
201    "12290C0BE0E9207E324F24DDE0D89300" as pub short_message: ShortString;
202    /// The hash of the first commit in the commit chain of the branch.
203    "272FBC56108F336C4D2E17289468C35F" as pub head: Handle<Blake3, SimpleArchive>;
204    /// An id used to track the branch.
205    "8694CC73AF96A5E1C7635C677D1B928A" as pub branch: GenId;
206    /// Timestamp range when this commit was created.
207    "71FF566AB4E3119FC2C5E66A18979586" as pub timestamp: NsTAIInterval;
208    /// The author of the signature identified by their ed25519 public key.
209    "ADB4FFAD247C886848161297EFF5A05B" as pub signed_by: ed::ED25519PublicKey;
210    /// The `r` part of a ed25519 signature.
211    "9DF34F84959928F93A3C40AEB6E9E499" as pub signature_r: ed::ED25519RComponent;
212    /// The `s` part of a ed25519 signature.
213    "1ACE03BF70242B289FDF00E4327C3BC6" as pub signature_s: ed::ED25519SComponent;
214}
215
216/// The `ListBlobs` trait is used to list all blobs in a repository.
217pub trait BlobStoreList<H: HashProtocol> {
218    type Iter<'a>: Iterator<Item = Result<Value<Handle<H, UnknownBlob>>, Self::Err>>
219    where
220        Self: 'a;
221    type Err: Error + Debug + Send + Sync + 'static;
222
223    /// Lists all blobs in the repository.
224    fn blobs<'a>(&'a self) -> Self::Iter<'a>;
225}
226
227/// Metadata about a blob in a repository.
228#[derive(Debug, Clone)]
229pub struct BlobMetadata {
230    /// Timestamp in milliseconds since UNIX epoch when the blob was created/stored.
231    pub timestamp: u64,
232    /// Length of the blob in bytes.
233    pub length: u64,
234}
235
236/// Trait exposing metadata lookup for blobs available in a repository reader.
237pub trait BlobStoreMeta<H: HashProtocol> {
238    /// Error type returned by metadata calls.
239    type MetaError: std::error::Error + Send + Sync + 'static;
240
241    fn metadata<S>(
242        &self,
243        handle: Value<Handle<H, S>>,
244    ) -> Result<Option<BlobMetadata>, Self::MetaError>
245    where
246        S: BlobSchema + 'static,
247        Handle<H, S>: ValueSchema;
248}
249
250/// Trait exposing a monotonic "forget" operation.
251///
252/// Forget is idempotent and monotonic: it removes materialization from a
253/// particular repository but does not semantically delete derived facts.
254pub trait BlobStoreForget<H: HashProtocol> {
255    type ForgetError: std::error::Error + Send + Sync + 'static;
256
257    fn forget<S>(&mut self, handle: Value<Handle<H, S>>) -> Result<(), Self::ForgetError>
258    where
259        S: BlobSchema + 'static,
260        Handle<H, S>: ValueSchema;
261}
262
263/// The `GetBlob` trait is used to retrieve blobs from a repository.
264pub trait BlobStoreGet<H: HashProtocol> {
265    type GetError<E: std::error::Error>: Error;
266
267    /// Retrieves a blob from the repository by its handle.
268    /// The handle is a unique identifier for the blob, and is used to retrieve it from the repository.
269    /// The blob is returned as a `Blob` object, which contains the raw bytes of the blob,
270    /// which can be deserialized via the appropriate schema type, which is specified by the `T` type parameter.
271    ///
272    /// # Errors
273    /// Returns an error if the blob could not be found in the repository.
274    /// The error type is specified by the `Err` associated type.
275    fn get<T, S>(
276        &self,
277        handle: Value<Handle<H, S>>,
278    ) -> Result<T, Self::GetError<<T as TryFromBlob<S>>::Error>>
279    where
280        S: BlobSchema + 'static,
281        T: TryFromBlob<S>,
282        Handle<H, S>: ValueSchema;
283}
284
285/// The `PutBlob` trait is used to store blobs in a repository.
286pub trait BlobStorePut<H: HashProtocol> {
287    type PutError: Error + Debug + Send + Sync + 'static;
288
289    fn put<S, T>(&mut self, item: T) -> Result<Value<Handle<H, S>>, Self::PutError>
290    where
291        S: BlobSchema + 'static,
292        T: ToBlob<S>,
293        Handle<H, S>: ValueSchema;
294}
295
296pub trait BlobStore<H: HashProtocol>: BlobStorePut<H> {
297    type Reader: BlobStoreGet<H> + BlobStoreList<H> + Clone + Send + PartialEq + Eq + 'static;
298    type ReaderError: Error + Debug + Send + Sync + 'static;
299    fn reader(&mut self) -> Result<Self::Reader, Self::ReaderError>;
300}
301
302/// Trait for blob stores that can retain a supplied set of handles.
303pub trait BlobStoreKeep<H: HashProtocol> {
304    /// Retain only the blobs identified by `handles`.
305    fn keep<I>(&mut self, handles: I)
306    where
307        I: IntoIterator<Item = Value<Handle<H, UnknownBlob>>>;
308}
309
310#[derive(Debug)]
311pub enum PushResult<H>
312where
313    H: HashProtocol,
314{
315    Success(),
316    Conflict(Option<Value<Handle<H, SimpleArchive>>>),
317}
318
319pub trait BranchStore<H: HashProtocol> {
320    type BranchesError: Error + Debug + Send + Sync + 'static;
321    type HeadError: Error + Debug + Send + Sync + 'static;
322    type UpdateError: Error + Debug + Send + Sync + 'static;
323
324    type ListIter<'a>: Iterator<Item = Result<Id, Self::BranchesError>>
325    where
326        Self: 'a;
327
328    /// Lists all branches in the repository.
329    /// This function returns a stream of branch ids.
330    fn branches<'a>(&'a mut self) -> Result<Self::ListIter<'a>, Self::BranchesError>;
331
332    // NOTE: keep the API lean — callers may call `branches()` and handle the
333    // fallible iterator directly; we avoid adding an extra helper here.
334
335    /// Retrieves a branch from the repository by its id.
336    /// The id is a unique identifier for the branch, and is used to retrieve it from the repository.
337    ///
338    /// # Errors
339    /// Returns an error if the branch could not be found in the repository.
340    ///
341    /// # Parameters
342    /// * `id` - The id of the branch to retrieve.
343    ///
344    /// # Returns
345    /// * A future that resolves to the handle of the branch.
346    /// * The handle is a unique identifier for the branch, and is used to retrieve it from the repository.
347    fn head(&mut self, id: Id) -> Result<Option<Value<Handle<H, SimpleArchive>>>, Self::HeadError>;
348
349    /// Puts a branch on the repository, creating or updating it.
350    ///
351    /// # Parameters
352    /// * `old` - Expected current value of the branch (None if creating new)
353    /// * `new` - Value to update the branch to (None deletes the branch)
354    ///
355    /// # Returns
356    /// * `Success` - Push completed successfully
357    /// * `Conflict(current)` - Failed because the branch's current value doesn't match `old`
358    ///   (contains the actual current value for conflict resolution)
359    fn update(
360        &mut self,
361        id: Id,
362        old: Option<Value<Handle<H, SimpleArchive>>>,
363        new: Option<Value<Handle<H, SimpleArchive>>>,
364    ) -> Result<PushResult<H>, Self::UpdateError>;
365}
366
367#[derive(Debug)]
368pub enum TransferError<ListErr, LoadErr, StoreErr> {
369    List(ListErr),
370    Load(LoadErr),
371    Store(StoreErr),
372}
373
374impl<ListErr, LoadErr, StoreErr> fmt::Display for TransferError<ListErr, LoadErr, StoreErr> {
375    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
376        write!(f, "failed to transfer blob")
377    }
378}
379
380impl<ListErr, LoadErr, StoreErr> Error for TransferError<ListErr, LoadErr, StoreErr>
381where
382    ListErr: Debug + Error + 'static,
383    LoadErr: Debug + Error + 'static,
384    StoreErr: Debug + Error + 'static,
385{
386    fn source(&self) -> Option<&(dyn Error + 'static)> {
387        match self {
388            Self::List(e) => Some(e),
389            Self::Load(e) => Some(e),
390            Self::Store(e) => Some(e),
391        }
392    }
393}
394
395/// Copies the specified blob handles from `source` into `target`.
396pub fn transfer<'a, BS, BT, HS, HT, Handles>(
397    source: &'a BS,
398    target: &'a mut BT,
399    handles: Handles,
400) -> impl Iterator<
401    Item = Result<
402        (
403            Value<Handle<HS, UnknownBlob>>,
404            Value<Handle<HT, UnknownBlob>>,
405        ),
406        TransferError<
407            Infallible,
408            <BS as BlobStoreGet<HS>>::GetError<Infallible>,
409            <BT as BlobStorePut<HT>>::PutError,
410        >,
411    >,
412> + 'a
413where
414    BS: BlobStoreGet<HS> + 'a,
415    BT: BlobStorePut<HT> + 'a,
416    HS: 'static + HashProtocol,
417    HT: 'static + HashProtocol,
418    Handles: IntoIterator<Item = Value<Handle<HS, UnknownBlob>>> + 'a,
419    Handles::IntoIter: 'a,
420{
421    handles.into_iter().map(move |source_handle| {
422        let blob: Blob<UnknownBlob> = source.get(source_handle).map_err(TransferError::Load)?;
423
424        Ok((
425            source_handle,
426            (target.put(blob).map_err(TransferError::Store)?),
427        ))
428    })
429}
430
431/// Iterator that visits every blob handle reachable from a set of roots.
432pub struct ReachableHandles<'a, BS, H>
433where
434    BS: BlobStoreGet<H>,
435    H: 'static + HashProtocol,
436{
437    source: &'a BS,
438    queue: VecDeque<Value<Handle<H, UnknownBlob>>>,
439    visited: HashSet<[u8; VALUE_LEN]>,
440}
441
442impl<'a, BS, H> ReachableHandles<'a, BS, H>
443where
444    BS: BlobStoreGet<H>,
445    H: 'static + HashProtocol,
446{
447    fn new(source: &'a BS, roots: impl IntoIterator<Item = Value<Handle<H, UnknownBlob>>>) -> Self {
448        let mut queue = VecDeque::new();
449        for handle in roots {
450            queue.push_back(handle);
451        }
452
453        Self {
454            source,
455            queue,
456            visited: HashSet::new(),
457        }
458    }
459
460    fn enqueue_from_blob(&mut self, blob: &Blob<UnknownBlob>) {
461        let bytes = blob.bytes.as_ref();
462        let mut offset = 0usize;
463
464        while offset + VALUE_LEN <= bytes.len() {
465            let mut raw = [0u8; VALUE_LEN];
466            raw.copy_from_slice(&bytes[offset..offset + VALUE_LEN]);
467
468            if !self.visited.contains(&raw) {
469                let candidate = Value::<Handle<H, UnknownBlob>>::new(raw);
470                if self
471                    .source
472                    .get::<anybytes::Bytes, UnknownBlob>(candidate)
473                    .is_ok()
474                {
475                    self.queue.push_back(candidate);
476                }
477            }
478
479            offset += VALUE_LEN;
480        }
481    }
482}
483
484impl<'a, BS, H> Iterator for ReachableHandles<'a, BS, H>
485where
486    BS: BlobStoreGet<H>,
487    H: 'static + HashProtocol,
488{
489    type Item = Value<Handle<H, UnknownBlob>>;
490
491    fn next(&mut self) -> Option<Self::Item> {
492        while let Some(handle) = self.queue.pop_front() {
493            let raw = handle.raw;
494
495            if !self.visited.insert(raw) {
496                continue;
497            }
498
499            if let Ok(blob) = self.source.get(handle) {
500                self.enqueue_from_blob(&blob);
501            }
502
503            return Some(handle);
504        }
505
506        None
507    }
508}
509
510/// Create a breadth-first iterator over blob handles reachable from `roots`.
511pub fn reachable<'a, BS, H>(
512    source: &'a BS,
513    roots: impl IntoIterator<Item = Value<Handle<H, UnknownBlob>>>,
514) -> ReachableHandles<'a, BS, H>
515where
516    BS: BlobStoreGet<H>,
517    H: 'static + HashProtocol,
518{
519    ReachableHandles::new(source, roots)
520}
521
522/// Iterate over every 32-byte candidate in the value column of a [`TribleSet`].
523///
524/// This is a conservative conversion used when scanning metadata for potential
525/// blob handles. Each 32-byte chunk is treated as a `Handle<H, UnknownBlob>`.
526/// Callers can feed the resulting iterator into [`BlobStoreKeep::keep`] or other
527/// helpers that accept collections of handles.
528pub fn potential_handles<'a, H>(
529    set: &'a TribleSet,
530) -> impl Iterator<Item = Value<Handle<H, UnknownBlob>>> + 'a
531where
532    H: HashProtocol,
533{
534    set.vae.iter().map(|raw| {
535        let mut value = [0u8; VALUE_LEN];
536        value.copy_from_slice(&raw[0..VALUE_LEN]);
537        Value::<Handle<H, UnknownBlob>>::new(value)
538    })
539}
540
541/// An error that can occur when creating a commit.
542/// This error can be caused by a failure to store the content or metadata blobs.
543#[derive(Debug)]
544pub enum CreateCommitError<BlobErr: Error + Debug + Send + Sync + 'static> {
545    /// Failed to store the content blob.
546    ContentStorageError(BlobErr),
547    /// Failed to store the commit metadata blob.
548    CommitStorageError(BlobErr),
549}
550
551impl<BlobErr: Error + Debug + Send + Sync + 'static> fmt::Display for CreateCommitError<BlobErr> {
552    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
553        match self {
554            CreateCommitError::ContentStorageError(e) => write!(f, "Content storage failed: {e}"),
555            CreateCommitError::CommitStorageError(e) => {
556                write!(f, "Commit metadata storage failed: {e}")
557            }
558        }
559    }
560}
561
562impl<BlobErr: Error + Debug + Send + Sync + 'static> Error for CreateCommitError<BlobErr> {
563    fn source(&self) -> Option<&(dyn Error + 'static)> {
564        match self {
565            CreateCommitError::ContentStorageError(e) => Some(e),
566            CreateCommitError::CommitStorageError(e) => Some(e),
567        }
568    }
569}
570
571#[derive(Debug)]
572pub enum MergeError {
573    /// The merge failed because the workspaces have different base repos.
574    DifferentRepos(),
575}
576
577#[derive(Debug)]
578pub enum PushError<Storage: BranchStore<Blake3> + BlobStore<Blake3>> {
579    /// An error occurred while enumerating the branch storage branches.
580    StorageBranches(Storage::BranchesError),
581    /// An error occurred while creating a blob reader.
582    StorageReader(<Storage as BlobStore<Blake3>>::ReaderError),
583    /// An error occurred while reading metadata blobs.
584    StorageGet(
585        <<Storage as BlobStore<Blake3>>::Reader as BlobStoreGet<Blake3>>::GetError<UnarchiveError>,
586    ),
587    /// An error occurred while transferring blobs to the repository.
588    StoragePut(<Storage as BlobStorePut<Blake3>>::PutError),
589    /// An error occurred while updating the branch storage.
590    BranchUpdate(Storage::UpdateError),
591    /// Malformed branch metadata.
592    BadBranchMetadata(),
593    /// Merge failed while retrying a push.
594    MergeError(MergeError),
595}
596
597// Allow using the `?` operator to convert MergeError into PushError in
598// contexts where PushError is the function error type. This keeps call sites
599// succinct by avoiding manual mapping closures like
600// `.map_err(|e| PushError::MergeError(e))?`.
601impl<Storage> From<MergeError> for PushError<Storage>
602where
603    Storage: BranchStore<Blake3> + BlobStore<Blake3>,
604{
605    fn from(e: MergeError) -> Self {
606        PushError::MergeError(e)
607    }
608}
609
610// Note: we intentionally avoid generic `From` impls for storage-associated
611// error types because they can overlap with other blanket implementations
612// and lead to coherence conflicts. Call sites use explicit mapping via the
613// enum variant constructors (e.g. `map_err(PushError::StoragePut)`) where
614// needed which keeps conversions explicit and stable.
615
616#[derive(Debug)]
617pub enum BranchError<Storage>
618where
619    Storage: BranchStore<Blake3> + BlobStore<Blake3>,
620{
621    /// An error occurred while creating a blob reader.
622    StorageReader(<Storage as BlobStore<Blake3>>::ReaderError),
623    /// An error occurred while reading metadata blobs.
624    StorageGet(
625        <<Storage as BlobStore<Blake3>>::Reader as BlobStoreGet<Blake3>>::GetError<UnarchiveError>,
626    ),
627    /// An error occurred while storing blobs.
628    StoragePut(<Storage as BlobStorePut<Blake3>>::PutError),
629    /// An error occurred while retrieving branch heads.
630    BranchHead(Storage::HeadError),
631    /// An error occurred while updating the branch storage.
632    BranchUpdate(Storage::UpdateError),
633    /// The branch already exists.
634    AlreadyExists(),
635    /// The referenced base branch does not exist.
636    BranchNotFound(Id),
637}
638
639#[derive(Debug)]
640pub enum LookupError<Storage>
641where
642    Storage: BranchStore<Blake3> + BlobStore<Blake3>,
643{
644    StorageBranches(Storage::BranchesError),
645    BranchHead(Storage::HeadError),
646    StorageReader(<Storage as BlobStore<Blake3>>::ReaderError),
647    StorageGet(
648        <<Storage as BlobStore<Blake3>>::Reader as BlobStoreGet<Blake3>>::GetError<UnarchiveError>,
649    ),
650    /// Multiple branches were found with the given name.
651    NameConflict(Vec<Id>),
652    BadBranchMetadata(),
653}
654
655/// High-level wrapper combining a blob store and branch store into a usable
656/// repository API.
657///
658/// The `Repository` type exposes convenience methods for creating branches,
659/// committing data and pushing changes while delegating actual storage to the
660/// given `BlobStore` and `BranchStore` implementations.
661pub struct Repository<Storage: BlobStore<Blake3> + BranchStore<Blake3>> {
662    storage: Storage,
663    signing_key: SigningKey,
664    default_metadata: Option<MetadataHandle>,
665}
666
667pub enum PullError<BranchStorageErr, BlobReaderErr, BlobStorageErr>
668where
669    BranchStorageErr: Error,
670    BlobReaderErr: Error,
671    BlobStorageErr: Error,
672{
673    /// The branch does not exist in the repository.
674    BranchNotFound(Id),
675    /// An error occurred while accessing the branch storage.
676    BranchStorage(BranchStorageErr),
677    /// An error occurred while creating a blob reader.
678    BlobReader(BlobReaderErr),
679    /// An error occurred while accessing the blob storage.
680    BlobStorage(BlobStorageErr),
681    /// The branch metadata is malformed or does not contain the expected fields.
682    BadBranchMetadata(),
683}
684
685impl<B, R, C> fmt::Debug for PullError<B, R, C>
686where
687    B: Error + fmt::Debug,
688    R: Error + fmt::Debug,
689    C: Error + fmt::Debug,
690{
691    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
692        match self {
693            PullError::BranchNotFound(id) => f.debug_tuple("BranchNotFound").field(id).finish(),
694            PullError::BranchStorage(e) => f.debug_tuple("BranchStorage").field(e).finish(),
695            PullError::BlobReader(e) => f.debug_tuple("BlobReader").field(e).finish(),
696            PullError::BlobStorage(e) => f.debug_tuple("BlobStorage").field(e).finish(),
697            PullError::BadBranchMetadata() => f.debug_tuple("BadBranchMetadata").finish(),
698        }
699    }
700}
701
702impl<Storage> Repository<Storage>
703where
704    Storage: BlobStore<Blake3> + BranchStore<Blake3>,
705{
706    /// Creates a new repository with the given blob and branch repositories.
707    /// The blob repository is used to store the actual data of the repository,
708    /// while the branch repository is used to store the state of the repository.
709    /// The hash protocol is used to hash the blobs and branches in the repository.
710    ///
711    /// # Parameters
712    /// * `blobs` - The blob repository to use for storing blobs.
713    /// * `branches` - The branch repository to use for storing branches.
714    /// # Returns
715    /// * A new `Repo` object that can be used to store and retrieve blobs and branches.
716    pub fn new(storage: Storage, signing_key: SigningKey) -> Self {
717        Self {
718            storage,
719            signing_key,
720            default_metadata: None,
721        }
722    }
723
724    /// Consume the repository and return the underlying storage backend.
725    ///
726    /// This is useful for callers that need to take ownership of the storage
727    /// (for example to call `close()` on a `Pile`) instead of letting the
728    /// repository drop it implicitly.
729    pub fn into_storage(self) -> Storage {
730        self.storage
731    }
732
733    /// Borrow the underlying storage backend.
734    pub fn storage(&self) -> &Storage {
735        &self.storage
736    }
737
738    /// Borrow the underlying storage backend mutably.
739    pub fn storage_mut(&mut self) -> &mut Storage {
740        &mut self.storage
741    }
742
743    /// Replace the repository signing key.
744    pub fn set_signing_key(&mut self, signing_key: SigningKey) {
745        self.signing_key = signing_key;
746    }
747
748    /// Sets the repository default metadata for new workspaces.
749    /// The metadata blob is stored in the repository's blob store.
750    pub fn set_default_metadata(
751        &mut self,
752        metadata_set: TribleSet,
753    ) -> Result<MetadataHandle, <Storage as BlobStorePut<Blake3>>::PutError> {
754        let handle = self.storage.put(metadata_set)?;
755        self.default_metadata = Some(handle);
756        Ok(handle)
757    }
758
759    /// Clears the repository default metadata.
760    pub fn clear_default_metadata(&mut self) {
761        self.default_metadata = None;
762    }
763
764    /// Returns the repository default metadata handle, if configured.
765    pub fn default_metadata(&self) -> Option<MetadataHandle> {
766        self.default_metadata
767    }
768
769    /// Initializes a new branch in the repository.
770    /// Branches are the only mutable state in the repository,
771    /// and are used to represent the state of a commit chain at a specific point in time.
772    /// A branch must always point to a commit, and this function can be used to create a new branch.
773    ///
774    /// Creates a new branch in the repository.
775    /// This branch is a pointer to a specific commit in the repository.
776    /// The branch is created with name and is initialized to point to the opionally given commit.
777    /// The branch is signed by the branch signing key.
778    ///
779    /// # Parameters
780    /// * `branch_name` - Name of the new branch.
781    /// * `commit` - Commit to initialize the branch from.
782    pub fn create_branch(
783        &mut self,
784        branch_name: &str,
785        commit: Option<CommitHandle>,
786    ) -> Result<ExclusiveId, BranchError<Storage>> {
787        self.create_branch_with_key(branch_name, commit, self.signing_key.clone())
788    }
789
790    /// Same as [`branch_from`] but uses the provided signing key.
791    pub fn create_branch_with_key(
792        &mut self,
793        branch_name: &str,
794        commit: Option<CommitHandle>,
795        signing_key: SigningKey,
796    ) -> Result<ExclusiveId, BranchError<Storage>> {
797        let branch_id = genid();
798        let name_blob = branch_name.to_owned().to_blob();
799        let name_handle = name_blob.get_handle::<Blake3>();
800        self.storage
801            .put(name_blob)
802            .map_err(|e| BranchError::StoragePut(e))?;
803
804        let branch_set = if let Some(commit) = commit {
805            let reader = self
806                .storage
807                .reader()
808                .map_err(|e| BranchError::StorageReader(e))?;
809            let set: TribleSet = reader.get(commit).map_err(|e| BranchError::StorageGet(e))?;
810
811            branch::branch_metadata(&signing_key, *branch_id, name_handle, Some(set.to_blob()))
812        } else {
813            branch::branch_unsigned(*branch_id, name_handle, None)
814        };
815
816        let branch_blob = branch_set.to_blob();
817        let branch_handle = self
818            .storage
819            .put(branch_blob)
820            .map_err(|e| BranchError::StoragePut(e))?;
821        let push_result = self
822            .storage
823            .update(*branch_id, None, Some(branch_handle))
824            .map_err(|e| BranchError::BranchUpdate(e))?;
825
826        match push_result {
827            PushResult::Success() => Ok(branch_id),
828            PushResult::Conflict(_) => Err(BranchError::AlreadyExists()),
829        }
830    }
831
832    /// Pulls an existing branch using the repository's signing key.
833    /// The workspace inherits the repository default metadata if configured.
834    pub fn pull(
835        &mut self,
836        branch_id: Id,
837    ) -> Result<
838        Workspace<Storage>,
839        PullError<
840            Storage::HeadError,
841            Storage::ReaderError,
842            <Storage::Reader as BlobStoreGet<Blake3>>::GetError<UnarchiveError>,
843        >,
844    > {
845        self.pull_with_key(branch_id, self.signing_key.clone())
846    }
847
848    /// Same as [`pull`] but overrides the signing key.
849    pub fn pull_with_key(
850        &mut self,
851        branch_id: Id,
852        signing_key: SigningKey,
853    ) -> Result<
854        Workspace<Storage>,
855        PullError<
856            Storage::HeadError,
857            Storage::ReaderError,
858            <Storage::Reader as BlobStoreGet<Blake3>>::GetError<UnarchiveError>,
859        >,
860    > {
861        // 1. Get the branch metadata head from the branch store.
862        let base_branch_meta_handle = match self.storage.head(branch_id) {
863            Ok(Some(handle)) => handle,
864            Ok(None) => return Err(PullError::BranchNotFound(branch_id)),
865            Err(e) => return Err(PullError::BranchStorage(e)),
866        };
867        // 2. Get the current commit from the branch metadata.
868        let reader = self.storage.reader().map_err(PullError::BlobReader)?;
869        let base_branch_meta: TribleSet = match reader.get(base_branch_meta_handle) {
870            Ok(meta_set) => meta_set,
871            Err(e) => return Err(PullError::BlobStorage(e)),
872        };
873
874        let head_ = match find!(
875            (head_: Value<_>),
876            pattern!(&base_branch_meta, [{ head: ?head_ }])
877        )
878        .at_most_one()
879        {
880            Ok(Some((h,))) => Some(h),
881            Ok(None) => None,
882            Err(_) => return Err(PullError::BadBranchMetadata()),
883        };
884        // Create workspace with the current commit and base blobs.
885        let base_blobs = self.storage.reader().map_err(PullError::BlobReader)?;
886        Ok(Workspace {
887            base_blobs,
888            local_blobs: MemoryBlobStore::new(),
889            head: head_,
890            base_head: head_,
891            base_branch_id: branch_id,
892            base_branch_meta: base_branch_meta_handle,
893            signing_key,
894            default_metadata: self.default_metadata,
895        })
896    }
897
898    /// Pulls an existing branch and overrides the workspace default metadata.
899    pub fn pull_with_metadata(
900        &mut self,
901        branch_id: Id,
902        metadata_set: TribleSet,
903    ) -> Result<
904        Workspace<Storage>,
905        PullError<
906            Storage::HeadError,
907            Storage::ReaderError,
908            <Storage::Reader as BlobStoreGet<Blake3>>::GetError<UnarchiveError>,
909        >,
910    > {
911        let mut workspace = self.pull_with_key(branch_id, self.signing_key.clone())?;
912        workspace.set_default_metadata(metadata_set);
913        Ok(workspace)
914    }
915
916    /// Pushes the workspace's new blobs and commit to the persistent repository.
917    /// This syncs the local BlobSet with the repository's BlobStore and performs
918    /// an atomic branch update (using the stored base_branch_meta).
919    pub fn push(&mut self, workspace: &mut Workspace<Storage>) -> Result<(), PushError<Storage>> {
920        // Retrying push: attempt a single push and, on conflict, merge the
921        // local workspace into the returned conflict workspace and retry.
922        // This implements the common push-merge-retry loop as a convenience
923        // wrapper around `try_push`.
924        while let Some(mut conflict_ws) = self.try_push(workspace)? {
925            // Keep the previous merge order: merge the caller's staged
926            // changes into the incoming conflict workspace. This preserves
927            // the semantic ordering of parents used in the merge commit.
928            conflict_ws.merge(workspace)?;
929
930            // Move the merged incoming workspace into the caller's workspace
931            // so the next try_push operates against the fresh branch state.
932            // Using assignment here is equivalent to `swap` but avoids
933            // retaining the previous `workspace` contents in the temp var.
934            *workspace = conflict_ws;
935        }
936
937        Ok(())
938    }
939
940    /// Single-attempt push: upload local blobs and try to update the branch
941    /// head once. Returns `Ok(None)` on success, or `Ok(Some(conflict_ws))`
942    /// when the branch was updated concurrently and the caller should merge.
943    pub fn try_push(
944        &mut self,
945        workspace: &mut Workspace<Storage>,
946    ) -> Result<Option<Workspace<Storage>>, PushError<Storage>> {
947        // 1. Sync `workspace.local_blobs` to repository's BlobStore.
948        let workspace_reader = workspace.local_blobs.reader().unwrap();
949        for handle in workspace_reader.blobs() {
950            let handle = handle.expect("infallible blob enumeration");
951            let blob: Blob<UnknownBlob> =
952                workspace_reader.get(handle).expect("infallible blob read");
953            self.storage.put(blob).map_err(PushError::StoragePut)?;
954        }
955
956        // 1.5 If the workspace's head did not change since the workspace was
957        // created, there's no commit to reference and therefore no branch
958        // metadata update is required. This avoids touching the branch store
959        // in the common case where only blobs were staged or nothing changed.
960        if workspace.base_head == workspace.head {
961            return Ok(None);
962        }
963
964        // 2. Create a new branch meta blob referencing the new workspace head.
965        let repo_reader = self.storage.reader().map_err(PushError::StorageReader)?;
966        let base_branch_meta: TribleSet = repo_reader
967            .get(workspace.base_branch_meta)
968            .map_err(PushError::StorageGet)?;
969
970        let Ok((branch_name,)) = find!(
971            (name: Value<Handle<Blake3, LongString>>),
972            pattern!(base_branch_meta, [{ crate::metadata::name: ?name }])
973        )
974        .exactly_one() else {
975            return Err(PushError::BadBranchMetadata());
976        };
977
978        let head_handle = workspace.head.ok_or(PushError::BadBranchMetadata())?;
979        let head_: TribleSet = repo_reader
980            .get(head_handle)
981            .map_err(PushError::StorageGet)?;
982
983        let branch_meta = branch_metadata(
984            &workspace.signing_key,
985            workspace.base_branch_id,
986            branch_name,
987            Some(head_.to_blob()),
988        );
989
990        let branch_meta_handle = self
991            .storage
992            .put(branch_meta)
993            .map_err(PushError::StoragePut)?;
994
995        // 3. Use CAS (comparing against workspace.base_branch_meta) to update the branch pointer.
996        let result = self
997            .storage
998            .update(
999                workspace.base_branch_id,
1000                Some(workspace.base_branch_meta),
1001                Some(branch_meta_handle),
1002            )
1003            .map_err(PushError::BranchUpdate)?;
1004
1005        match result {
1006            PushResult::Success() => {
1007                // Update workspace base pointers so subsequent pushes can detect
1008                // that the workspace is already synchronized and avoid re-upload.
1009                workspace.base_branch_meta = branch_meta_handle;
1010                workspace.base_head = workspace.head;
1011                // Refresh the workspace base blob reader to ensure newly
1012                // uploaded blobs are visible to subsequent checkout operations.
1013                workspace.base_blobs = self.storage.reader().map_err(PushError::StorageReader)?;
1014                // Clear staged local blobs now that they have been uploaded and
1015                // the branch metadata updated. This frees memory and prevents
1016                // repeated uploads of the same staged blobs on subsequent pushes.
1017                workspace.local_blobs = MemoryBlobStore::new();
1018                Ok(None)
1019            }
1020            PushResult::Conflict(conflicting_meta) => {
1021                let conflicting_meta = conflicting_meta.ok_or(PushError::BadBranchMetadata())?;
1022
1023                let repo_reader = self.storage.reader().map_err(PushError::StorageReader)?;
1024                let branch_meta: TribleSet = repo_reader
1025                    .get(conflicting_meta)
1026                    .map_err(PushError::StorageGet)?;
1027
1028                let head_ = match find!((head_: Value<_>),
1029                    pattern!(&branch_meta, [{ head: ?head_ }])
1030                )
1031                .at_most_one()
1032                {
1033                    Ok(Some((h,))) => Some(h),
1034                    Ok(None) => None,
1035                    Err(_) => return Err(PushError::BadBranchMetadata()),
1036                };
1037
1038                let conflict_ws = Workspace {
1039                    base_blobs: self.storage.reader().map_err(PushError::StorageReader)?,
1040                    local_blobs: MemoryBlobStore::new(),
1041                    head: head_,
1042                    base_head: head_,
1043                    base_branch_id: workspace.base_branch_id,
1044                    base_branch_meta: conflicting_meta,
1045                    signing_key: workspace.signing_key.clone(),
1046                    default_metadata: workspace.default_metadata,
1047                };
1048
1049                Ok(Some(conflict_ws))
1050            }
1051        }
1052    }
1053}
1054
1055type CommitHandle = Value<Handle<Blake3, SimpleArchive>>;
1056type MetadataHandle = Value<Handle<Blake3, SimpleArchive>>;
1057type CommitSet = PATCH<VALUE_LEN, IdentitySchema, ()>;
1058type BranchMetaHandle = Value<Handle<Blake3, SimpleArchive>>;
1059
1060/// The Workspace represents the mutable working area or "staging" state.
1061/// It was formerly known as `Head`. It is sent to worker threads,
1062/// modified (via commits, merges, etc.), and then merged back into the Repository.
1063pub struct Workspace<Blobs: BlobStore<Blake3>> {
1064    /// A local BlobStore that holds any new blobs (commits, trees, deltas) before they are synced.
1065    local_blobs: MemoryBlobStore<Blake3>,
1066    /// The blob storage base for the workspace.
1067    base_blobs: Blobs::Reader,
1068    /// The branch id this workspace is tracking; None for a detached workspace.
1069    base_branch_id: Id,
1070    /// The meta-handle corresponding to the base branch state used for CAS.
1071    base_branch_meta: BranchMetaHandle,
1072    /// Handle to the current commit in the working branch. `None` for an empty branch.
1073    head: Option<CommitHandle>,
1074    /// The branch head snapshot when this workspace was created (pull time).
1075    ///
1076    /// This allows `try_push` to cheaply detect whether the commit head has
1077    /// advanced since the workspace was created without querying the remote
1078    /// branch store.
1079    base_head: Option<CommitHandle>,
1080    /// Signing key used for commit/branch signing.
1081    signing_key: SigningKey,
1082    /// Optional default metadata handle for commits created in this workspace.
1083    default_metadata: Option<MetadataHandle>,
1084}
1085
1086impl<Blobs> fmt::Debug for Workspace<Blobs>
1087where
1088    Blobs: BlobStore<Blake3>,
1089    Blobs::Reader: fmt::Debug,
1090{
1091    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1092        f.debug_struct("Workspace")
1093            .field("local_blobs", &self.local_blobs)
1094            .field("base_blobs", &self.base_blobs)
1095            .field("base_branch_id", &self.base_branch_id)
1096            .field("base_branch_meta", &self.base_branch_meta)
1097            .field("base_head", &self.base_head)
1098            .field("head", &self.head)
1099            .field("default_metadata", &self.default_metadata)
1100            .finish()
1101    }
1102}
1103
1104/// Helper trait for [`Workspace::checkout`] specifying commit handles or ranges.
1105pub trait CommitSelector<Blobs: BlobStore<Blake3>> {
1106    fn select(
1107        self,
1108        ws: &mut Workspace<Blobs>,
1109    ) -> Result<
1110        CommitSet,
1111        WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet<Blake3>>::GetError<UnarchiveError>>,
1112    >;
1113}
1114
1115/// Selector that returns a commit along with all of its ancestors.
1116pub struct Ancestors(pub CommitHandle);
1117
1118/// Convenience function to create an [`Ancestors`] selector.
1119pub fn ancestors(commit: CommitHandle) -> Ancestors {
1120    Ancestors(commit)
1121}
1122
1123/// Selector that returns the Nth ancestor along the first-parent chain.
1124pub struct NthAncestor(pub CommitHandle, pub usize);
1125
1126/// Convenience function to create an [`NthAncestor`] selector.
1127pub fn nth_ancestor(commit: CommitHandle, n: usize) -> NthAncestor {
1128    NthAncestor(commit, n)
1129}
1130
1131/// Selector that returns the direct parents of a commit.
1132pub struct Parents(pub CommitHandle);
1133
1134/// Convenience function to create a [`Parents`] selector.
1135pub fn parents(commit: CommitHandle) -> Parents {
1136    Parents(commit)
1137}
1138
1139/// Selector that returns commits reachable from either of two commits but not
1140/// both.
1141pub struct SymmetricDiff(pub CommitHandle, pub CommitHandle);
1142
1143/// Convenience function to create a [`SymmetricDiff`] selector.
1144pub fn symmetric_diff(a: CommitHandle, b: CommitHandle) -> SymmetricDiff {
1145    SymmetricDiff(a, b)
1146}
1147
1148/// Selector that returns the union of commits returned by two selectors.
1149pub struct Union<A, B> {
1150    left: A,
1151    right: B,
1152}
1153
1154/// Convenience function to create a [`Union`] selector.
1155pub fn union<A, B>(left: A, right: B) -> Union<A, B> {
1156    Union { left, right }
1157}
1158
1159/// Selector that returns the intersection of commits returned by two selectors.
1160pub struct Intersect<A, B> {
1161    left: A,
1162    right: B,
1163}
1164
1165/// Convenience function to create an [`Intersect`] selector.
1166pub fn intersect<A, B>(left: A, right: B) -> Intersect<A, B> {
1167    Intersect { left, right }
1168}
1169
1170/// Selector that returns commits from the left selector that are not also
1171/// returned by the right selector.
1172pub struct Difference<A, B> {
1173    left: A,
1174    right: B,
1175}
1176
1177/// Convenience function to create a [`Difference`] selector.
1178pub fn difference<A, B>(left: A, right: B) -> Difference<A, B> {
1179    Difference { left, right }
1180}
1181
1182/// Selector that returns commits with timestamps in the given inclusive range.
1183pub struct TimeRange(pub Epoch, pub Epoch);
1184
1185/// Convenience function to create a [`TimeRange`] selector.
1186pub fn time_range(start: Epoch, end: Epoch) -> TimeRange {
1187    TimeRange(start, end)
1188}
1189
1190/// Selector that filters commits returned by another selector.
1191pub struct Filter<S, F> {
1192    selector: S,
1193    filter: F,
1194}
1195
1196/// Convenience function to create a [`Filter`] selector.
1197pub fn filter<S, F>(selector: S, filter: F) -> Filter<S, F> {
1198    Filter { selector, filter }
1199}
1200
1201impl<Blobs> CommitSelector<Blobs> for CommitHandle
1202where
1203    Blobs: BlobStore<Blake3>,
1204{
1205    fn select(
1206        self,
1207        _ws: &mut Workspace<Blobs>,
1208    ) -> Result<
1209        CommitSet,
1210        WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet<Blake3>>::GetError<UnarchiveError>>,
1211    > {
1212        let mut patch = CommitSet::new();
1213        patch.insert(&Entry::new(&self.raw));
1214        Ok(patch)
1215    }
1216}
1217
1218impl<Blobs> CommitSelector<Blobs> for Vec<CommitHandle>
1219where
1220    Blobs: BlobStore<Blake3>,
1221{
1222    fn select(
1223        self,
1224        _ws: &mut Workspace<Blobs>,
1225    ) -> Result<
1226        CommitSet,
1227        WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet<Blake3>>::GetError<UnarchiveError>>,
1228    > {
1229        let mut patch = CommitSet::new();
1230        for handle in self {
1231            patch.insert(&Entry::new(&handle.raw));
1232        }
1233        Ok(patch)
1234    }
1235}
1236
1237impl<Blobs> CommitSelector<Blobs> for &[CommitHandle]
1238where
1239    Blobs: BlobStore<Blake3>,
1240{
1241    fn select(
1242        self,
1243        _ws: &mut Workspace<Blobs>,
1244    ) -> Result<
1245        CommitSet,
1246        WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet<Blake3>>::GetError<UnarchiveError>>,
1247    > {
1248        let mut patch = CommitSet::new();
1249        for handle in self {
1250            patch.insert(&Entry::new(&handle.raw));
1251        }
1252        Ok(patch)
1253    }
1254}
1255
1256impl<Blobs> CommitSelector<Blobs> for Option<CommitHandle>
1257where
1258    Blobs: BlobStore<Blake3>,
1259{
1260    fn select(
1261        self,
1262        _ws: &mut Workspace<Blobs>,
1263    ) -> Result<
1264        CommitSet,
1265        WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet<Blake3>>::GetError<UnarchiveError>>,
1266    > {
1267        let mut patch = CommitSet::new();
1268        if let Some(handle) = self {
1269            patch.insert(&Entry::new(&handle.raw));
1270        }
1271        Ok(patch)
1272    }
1273}
1274
1275impl<Blobs> CommitSelector<Blobs> for Ancestors
1276where
1277    Blobs: BlobStore<Blake3>,
1278{
1279    fn select(
1280        self,
1281        ws: &mut Workspace<Blobs>,
1282    ) -> Result<
1283        CommitSet,
1284        WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet<Blake3>>::GetError<UnarchiveError>>,
1285    > {
1286        collect_reachable(ws, self.0)
1287    }
1288}
1289
1290impl<Blobs> CommitSelector<Blobs> for NthAncestor
1291where
1292    Blobs: BlobStore<Blake3>,
1293{
1294    fn select(
1295        self,
1296        ws: &mut Workspace<Blobs>,
1297    ) -> Result<
1298        CommitSet,
1299        WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet<Blake3>>::GetError<UnarchiveError>>,
1300    > {
1301        let mut current = self.0;
1302        let mut remaining = self.1;
1303
1304        while remaining > 0 {
1305            let meta: TribleSet = ws.get(current).map_err(WorkspaceCheckoutError::Storage)?;
1306            let mut parents = find!((p: Value<_>), pattern!(&meta, [{ parent: ?p }]));
1307            let Some((p,)) = parents.next() else {
1308                return Ok(CommitSet::new());
1309            };
1310            current = p;
1311            remaining -= 1;
1312        }
1313
1314        let mut patch = CommitSet::new();
1315        patch.insert(&Entry::new(&current.raw));
1316        Ok(patch)
1317    }
1318}
1319
1320impl<Blobs> CommitSelector<Blobs> for Parents
1321where
1322    Blobs: BlobStore<Blake3>,
1323{
1324    fn select(
1325        self,
1326        ws: &mut Workspace<Blobs>,
1327    ) -> Result<
1328        CommitSet,
1329        WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet<Blake3>>::GetError<UnarchiveError>>,
1330    > {
1331        let meta: TribleSet = ws.get(self.0).map_err(WorkspaceCheckoutError::Storage)?;
1332        let mut result = CommitSet::new();
1333        for (p,) in find!((p: Value<_>), pattern!(&meta, [{ parent: ?p }])) {
1334            result.insert(&Entry::new(&p.raw));
1335        }
1336        Ok(result)
1337    }
1338}
1339
1340impl<Blobs> CommitSelector<Blobs> for SymmetricDiff
1341where
1342    Blobs: BlobStore<Blake3>,
1343{
1344    fn select(
1345        self,
1346        ws: &mut Workspace<Blobs>,
1347    ) -> Result<
1348        CommitSet,
1349        WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet<Blake3>>::GetError<UnarchiveError>>,
1350    > {
1351        let a = collect_reachable(ws, self.0)?;
1352        let b = collect_reachable(ws, self.1)?;
1353        let inter = a.intersect(&b);
1354        let mut union = a;
1355        union.union(b);
1356        Ok(union.difference(&inter))
1357    }
1358}
1359
1360impl<A, B, Blobs> CommitSelector<Blobs> for Union<A, B>
1361where
1362    A: CommitSelector<Blobs>,
1363    B: CommitSelector<Blobs>,
1364    Blobs: BlobStore<Blake3>,
1365{
1366    fn select(
1367        self,
1368        ws: &mut Workspace<Blobs>,
1369    ) -> Result<
1370        CommitSet,
1371        WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet<Blake3>>::GetError<UnarchiveError>>,
1372    > {
1373        let mut left = self.left.select(ws)?;
1374        let right = self.right.select(ws)?;
1375        left.union(right);
1376        Ok(left)
1377    }
1378}
1379
1380impl<A, B, Blobs> CommitSelector<Blobs> for Intersect<A, B>
1381where
1382    A: CommitSelector<Blobs>,
1383    B: CommitSelector<Blobs>,
1384    Blobs: BlobStore<Blake3>,
1385{
1386    fn select(
1387        self,
1388        ws: &mut Workspace<Blobs>,
1389    ) -> Result<
1390        CommitSet,
1391        WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet<Blake3>>::GetError<UnarchiveError>>,
1392    > {
1393        let left = self.left.select(ws)?;
1394        let right = self.right.select(ws)?;
1395        Ok(left.intersect(&right))
1396    }
1397}
1398
1399impl<A, B, Blobs> CommitSelector<Blobs> for Difference<A, B>
1400where
1401    A: CommitSelector<Blobs>,
1402    B: CommitSelector<Blobs>,
1403    Blobs: BlobStore<Blake3>,
1404{
1405    fn select(
1406        self,
1407        ws: &mut Workspace<Blobs>,
1408    ) -> Result<
1409        CommitSet,
1410        WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet<Blake3>>::GetError<UnarchiveError>>,
1411    > {
1412        let left = self.left.select(ws)?;
1413        let right = self.right.select(ws)?;
1414        Ok(left.difference(&right))
1415    }
1416}
1417
1418impl<S, F, Blobs> CommitSelector<Blobs> for Filter<S, F>
1419where
1420    Blobs: BlobStore<Blake3>,
1421    S: CommitSelector<Blobs>,
1422    F: for<'x, 'y> Fn(&'x TribleSet, &'y TribleSet) -> bool,
1423{
1424    fn select(
1425        self,
1426        ws: &mut Workspace<Blobs>,
1427    ) -> Result<
1428        CommitSet,
1429        WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet<Blake3>>::GetError<UnarchiveError>>,
1430    > {
1431        let patch = self.selector.select(ws)?;
1432        let mut result = CommitSet::new();
1433        let filter = self.filter;
1434        for raw in patch.iter() {
1435            let handle = Value::new(*raw);
1436            let meta: TribleSet = ws.get(handle).map_err(WorkspaceCheckoutError::Storage)?;
1437
1438            let Ok((content_handle,)) = find!(
1439                (c: Value<_>),
1440                pattern!(&meta, [{ content: ?c }])
1441            )
1442            .exactly_one() else {
1443                return Err(WorkspaceCheckoutError::BadCommitMetadata());
1444            };
1445
1446            let payload: TribleSet = ws
1447                .get(content_handle)
1448                .map_err(WorkspaceCheckoutError::Storage)?;
1449
1450            if filter(&meta, &payload) {
1451                result.insert(&Entry::new(raw));
1452            }
1453        }
1454        Ok(result)
1455    }
1456}
1457
1458/// Selector that yields commits touching a specific entity.
1459pub struct HistoryOf(pub Id);
1460
1461/// Convenience function to create a [`HistoryOf`] selector.
1462pub fn history_of(entity: Id) -> HistoryOf {
1463    HistoryOf(entity)
1464}
1465
1466impl<Blobs> CommitSelector<Blobs> for HistoryOf
1467where
1468    Blobs: BlobStore<Blake3>,
1469{
1470    fn select(
1471        self,
1472        ws: &mut Workspace<Blobs>,
1473    ) -> Result<
1474        CommitSet,
1475        WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet<Blake3>>::GetError<UnarchiveError>>,
1476    > {
1477        let Some(head_) = ws.head else {
1478            return Ok(CommitSet::new());
1479        };
1480        let entity = self.0;
1481        filter(
1482            ancestors(head_),
1483            move |_: &TribleSet, payload: &TribleSet| payload.iter().any(|t| t.e() == &entity),
1484        )
1485        .select(ws)
1486    }
1487}
1488
1489// Generic range selectors: allow any selector type to be used as a range
1490// endpoint. We still walk the history reachable from the end selector but now
1491// stop descending a branch as soon as we encounter a commit produced by the
1492// start selector. This keeps the mechanics explicit—`start..end` literally
1493// walks from `end` until it hits `start`—while continuing to support selectors
1494// such as `Ancestors(...)` at either boundary.
1495
1496fn collect_reachable_from_patch<Blobs: BlobStore<Blake3>>(
1497    ws: &mut Workspace<Blobs>,
1498    patch: CommitSet,
1499) -> Result<
1500    CommitSet,
1501    WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet<Blake3>>::GetError<UnarchiveError>>,
1502> {
1503    let mut result = CommitSet::new();
1504    for raw in patch.iter() {
1505        let handle = Value::new(*raw);
1506        let reach = collect_reachable(ws, handle)?;
1507        result.union(reach);
1508    }
1509    Ok(result)
1510}
1511
1512fn collect_reachable_from_patch_until<Blobs: BlobStore<Blake3>>(
1513    ws: &mut Workspace<Blobs>,
1514    seeds: CommitSet,
1515    stop: &CommitSet,
1516) -> Result<
1517    CommitSet,
1518    WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet<Blake3>>::GetError<UnarchiveError>>,
1519> {
1520    let mut visited = HashSet::new();
1521    let mut stack: Vec<CommitHandle> = seeds.iter().map(|raw| Value::new(*raw)).collect();
1522    let mut result = CommitSet::new();
1523
1524    while let Some(commit) = stack.pop() {
1525        if !visited.insert(commit) {
1526            continue;
1527        }
1528
1529        if stop.get(&commit.raw).is_some() {
1530            continue;
1531        }
1532
1533        result.insert(&Entry::new(&commit.raw));
1534
1535        let meta: TribleSet = ws
1536            .local_blobs
1537            .reader()
1538            .unwrap()
1539            .get(commit)
1540            .or_else(|_| ws.base_blobs.get(commit))
1541            .map_err(WorkspaceCheckoutError::Storage)?;
1542
1543        for (p,) in find!((p: Value<_>,), pattern!(&meta, [{ parent: ?p }])) {
1544            stack.push(p);
1545        }
1546    }
1547
1548    Ok(result)
1549}
1550
1551impl<T, Blobs> CommitSelector<Blobs> for std::ops::Range<T>
1552where
1553    T: CommitSelector<Blobs>,
1554    Blobs: BlobStore<Blake3>,
1555{
1556    fn select(
1557        self,
1558        ws: &mut Workspace<Blobs>,
1559    ) -> Result<
1560        CommitSet,
1561        WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet<Blake3>>::GetError<UnarchiveError>>,
1562    > {
1563        let end_patch = self.end.select(ws)?;
1564        let start_patch = self.start.select(ws)?;
1565
1566        collect_reachable_from_patch_until(ws, end_patch, &start_patch)
1567    }
1568}
1569
1570impl<T, Blobs> CommitSelector<Blobs> for std::ops::RangeFrom<T>
1571where
1572    T: CommitSelector<Blobs>,
1573    Blobs: BlobStore<Blake3>,
1574{
1575    fn select(
1576        self,
1577        ws: &mut Workspace<Blobs>,
1578    ) -> Result<
1579        CommitSet,
1580        WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet<Blake3>>::GetError<UnarchiveError>>,
1581    > {
1582        let Some(head_) = ws.head else {
1583            return Ok(CommitSet::new());
1584        };
1585        let exclude_patch = self.start.select(ws)?;
1586
1587        let mut head_patch = CommitSet::new();
1588        head_patch.insert(&Entry::new(&head_.raw));
1589
1590        collect_reachable_from_patch_until(ws, head_patch, &exclude_patch)
1591    }
1592}
1593
1594impl<T, Blobs> CommitSelector<Blobs> for std::ops::RangeTo<T>
1595where
1596    T: CommitSelector<Blobs>,
1597    Blobs: BlobStore<Blake3>,
1598{
1599    fn select(
1600        self,
1601        ws: &mut Workspace<Blobs>,
1602    ) -> Result<
1603        CommitSet,
1604        WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet<Blake3>>::GetError<UnarchiveError>>,
1605    > {
1606        let end_patch = self.end.select(ws)?;
1607        collect_reachable_from_patch(ws, end_patch)
1608    }
1609}
1610
1611impl<Blobs> CommitSelector<Blobs> for std::ops::RangeFull
1612where
1613    Blobs: BlobStore<Blake3>,
1614{
1615    fn select(
1616        self,
1617        ws: &mut Workspace<Blobs>,
1618    ) -> Result<
1619        CommitSet,
1620        WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet<Blake3>>::GetError<UnarchiveError>>,
1621    > {
1622        let Some(head_) = ws.head else {
1623            return Ok(CommitSet::new());
1624        };
1625        collect_reachable(ws, head_)
1626    }
1627}
1628
1629impl<Blobs> CommitSelector<Blobs> for TimeRange
1630where
1631    Blobs: BlobStore<Blake3>,
1632{
1633    fn select(
1634        self,
1635        ws: &mut Workspace<Blobs>,
1636    ) -> Result<
1637        CommitSet,
1638        WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet<Blake3>>::GetError<UnarchiveError>>,
1639    > {
1640        let Some(head_) = ws.head else {
1641            return Ok(CommitSet::new());
1642        };
1643        let start = self.0;
1644        let end = self.1;
1645        filter(
1646            ancestors(head_),
1647            move |meta: &TribleSet, _payload: &TribleSet| {
1648                if let Ok(Some((ts,))) =
1649                    find!((t: Value<_>), pattern!(meta, [{ timestamp: ?t }])).at_most_one()
1650                {
1651                    let (ts_start, ts_end): (Epoch, Epoch) =
1652                        crate::value::FromValue::from_value(&ts);
1653                    ts_start <= end && ts_end >= start
1654                } else {
1655                    false
1656                }
1657            },
1658        )
1659        .select(ws)
1660    }
1661}
1662
1663impl<Blobs: BlobStore<Blake3>> Workspace<Blobs> {
1664    /// Returns the branch id associated with this workspace.
1665    pub fn branch_id(&self) -> Id {
1666        self.base_branch_id
1667    }
1668
1669    /// Returns the current commit handle if one exists.
1670    pub fn head(&self) -> Option<CommitHandle> {
1671        self.head
1672    }
1673
1674    /// Sets the default metadata for commits created in this workspace.
1675    /// The metadata blob is stored in the workspace's local blob store.
1676    pub fn set_default_metadata(&mut self, metadata_set: TribleSet) -> MetadataHandle {
1677        let handle = self
1678            .local_blobs
1679            .put(metadata_set)
1680            .expect("infallible metadata blob put");
1681        self.default_metadata = Some(handle);
1682        handle
1683    }
1684
1685    /// Clears the default metadata for commits created in this workspace.
1686    pub fn clear_default_metadata(&mut self) {
1687        self.default_metadata = None;
1688    }
1689
1690    /// Returns the default metadata handle, if configured.
1691    pub fn default_metadata(&self) -> Option<MetadataHandle> {
1692        self.default_metadata
1693    }
1694
1695    /// Adds a blob to the workspace's local blob store.
1696    /// Mirrors [`BlobStorePut::put`](crate::repo::BlobStorePut) for ease of use.
1697    pub fn put<S, T>(&mut self, item: T) -> Value<Handle<Blake3, S>>
1698    where
1699        S: BlobSchema + 'static,
1700        T: ToBlob<S>,
1701        Handle<Blake3, S>: ValueSchema,
1702    {
1703        self.local_blobs.put(item).expect("infallible blob put")
1704    }
1705
1706    /// Retrieves a blob from the workspace.
1707    ///
1708    /// The method first checks the workspace's local blob store and falls back
1709    /// to the base blob store if the blob is not found locally.
1710    pub fn get<T, S>(
1711        &mut self,
1712        handle: Value<Handle<Blake3, S>>,
1713    ) -> Result<T, <Blobs::Reader as BlobStoreGet<Blake3>>::GetError<<T as TryFromBlob<S>>::Error>>
1714    where
1715        S: BlobSchema + 'static,
1716        T: TryFromBlob<S>,
1717        Handle<Blake3, S>: ValueSchema,
1718    {
1719        self.local_blobs
1720            .reader()
1721            .unwrap()
1722            .get(handle)
1723            .or_else(|_| self.base_blobs.get(handle))
1724    }
1725
1726    /// Performs a commit in the workspace.
1727    /// This method creates a new commit blob (stored in the local blobset)
1728    /// and updates the current commit handle.
1729    /// If `metadata` is `None`, a configured default metadata handle is attached
1730    /// automatically. Supplying metadata does not change the workspace default.
1731    pub fn commit(
1732        &mut self,
1733        content_: impl Into<TribleSet>,
1734        metadata_: Option<TribleSet>,
1735        message_: Option<&str>,
1736    ) {
1737        let content_ = content_.into();
1738        let metadata_handle = match metadata_ {
1739            Some(metadata_set) => Some(
1740                self.local_blobs
1741                    .put(metadata_set)
1742                    .expect("infallible metadata blob put"),
1743            ),
1744            None => self.default_metadata,
1745        };
1746        self.commit_internal(content_, metadata_handle, message_);
1747    }
1748
1749    fn commit_internal(
1750        &mut self,
1751        content_: TribleSet,
1752        metadata_handle: Option<MetadataHandle>,
1753        message_: Option<&str>,
1754    ) {
1755        // 1. Create a commit blob from the current head, content, metadata and the commit message.
1756        let content_blob = content_.to_blob();
1757        // If a message is provided, store it as a LongString blob and pass the handle.
1758        let message_handle = message_.map(|m| self.put(m.to_string()));
1759        let parents = self.head.iter().copied();
1760
1761        let commit_set = crate::repo::commit::commit_metadata(
1762            &self.signing_key,
1763            parents,
1764            message_handle,
1765            Some(content_blob.clone()),
1766            metadata_handle,
1767        );
1768        // 2. Store the content and commit blobs in `self.local_blobs`.
1769        let _ = self
1770            .local_blobs
1771            .put(content_blob)
1772            .expect("failed to put content blob");
1773        let commit_handle = self
1774            .local_blobs
1775            .put(commit_set)
1776            .expect("failed to put commit blob");
1777        // 3. Update `self.head` to point to the new commit.
1778        self.head = Some(commit_handle);
1779    }
1780
1781    /// Merge another workspace (or its commit state) into this one.
1782    ///
1783    /// Notes on semantics
1784    /// - This operation will copy the *staged* blobs created in `other`
1785    ///   (i.e., `other.local_blobs`) into `self.local_blobs`, then create a
1786    ///   merge commit whose parents are `self.head` and `other.head`.
1787    /// - The merge does *not* automatically import the entire base history
1788    ///   reachable from `other`'s head. If the incoming parent commits
1789    ///   reference blobs that do not exist in this repository's storage,
1790    ///   reading those commits later will fail until the missing blobs are
1791    ///   explicitly imported (for example via `repo::transfer(reachable(...))`).
1792    /// - This design keeps merge permissive and leaves cross-repository blob
1793    ///   import as an explicit user action.
1794    pub fn merge(&mut self, other: &mut Workspace<Blobs>) -> Result<CommitHandle, MergeError> {
1795        // 1. Transfer all blobs from the other workspace to self.local_blobs.
1796        let other_local = other.local_blobs.reader().unwrap();
1797        for r in other_local.blobs() {
1798            let handle = r.expect("infallible blob enumeration");
1799            let blob: Blob<UnknownBlob> = other_local.get(handle).expect("infallible blob read");
1800
1801            // Store the blob in the local workspace's blob store.
1802            self.local_blobs.put(blob).expect("infallible blob put");
1803        }
1804        // 2. Compute a merge commit from self.current_commit and other.current_commit.
1805        let parents = self.head.iter().copied().chain(other.head.iter().copied());
1806        let merge_commit = commit_metadata(
1807            &self.signing_key,
1808            parents,
1809            None, // No message for the merge commit
1810            None, // No content blob for the merge commit
1811            None, // No metadata blob for the merge commit
1812        );
1813        // 3. Store the merge commit in self.local_blobs.
1814        let commit_handle = self
1815            .local_blobs
1816            .put(merge_commit)
1817            .expect("failed to put merge commit blob");
1818        self.head = Some(commit_handle);
1819
1820        Ok(commit_handle)
1821    }
1822
1823    /// Create a merge commit that ties this workspace's current head and an
1824    /// arbitrary other commit (already present in the underlying blob store)
1825    /// together without requiring another `Workspace` instance.
1826    ///
1827    /// This does not attach any content to the merge commit.
1828    pub fn merge_commit(
1829        &mut self,
1830        other: Value<Handle<Blake3, SimpleArchive>>,
1831    ) -> Result<CommitHandle, MergeError> {
1832        // Validate that `other` can be loaded from either local or base blobs.
1833        // If it cannot be loaded we still proceed with the merge; dereference
1834        // failures will surface later when reading history. Callers should
1835        // ensure the reachable blobs were imported beforehand (e.g. by
1836        // combining `reachable` with `transfer`).
1837
1838        let parents = self.head.iter().copied().chain(Some(other));
1839        let merge_commit = commit_metadata(&self.signing_key, parents, None, None, None);
1840        let commit_handle = self
1841            .local_blobs
1842            .put(merge_commit)
1843            .expect("failed to put merge commit blob");
1844        self.head = Some(commit_handle);
1845        Ok(commit_handle)
1846    }
1847
1848    /// Returns the combined [`TribleSet`] for the specified commits.
1849    ///
1850    /// Each commit handle must reference a commit blob stored either in the
1851    /// workspace's local blob store or the repository's base store. The
1852    /// associated content blobs are loaded and unioned together. An error is
1853    /// returned if any commit or content blob is missing or malformed.
1854    fn checkout_commits<I>(
1855        &mut self,
1856        commits: I,
1857    ) -> Result<
1858        TribleSet,
1859        WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet<Blake3>>::GetError<UnarchiveError>>,
1860    >
1861    where
1862        I: IntoIterator<Item = CommitHandle>,
1863    {
1864        let local = self.local_blobs.reader().unwrap();
1865        let mut result = TribleSet::new();
1866        for commit in commits {
1867            let meta: TribleSet = local
1868                .get(commit)
1869                .or_else(|_| self.base_blobs.get(commit))
1870                .map_err(WorkspaceCheckoutError::Storage)?;
1871
1872            // Some commits (for example merge commits) intentionally do not
1873            // carry a content blob. Treat those as no-ops during checkout so
1874            // callers can request ancestor ranges without failing when a
1875            // merge commit is encountered.
1876            let content_opt =
1877                match find!((c: Value<_>), pattern!(&meta, [{ content: ?c }])).at_most_one() {
1878                    Ok(Some((c,))) => Some(c),
1879                    Ok(None) => None,
1880                    Err(_) => return Err(WorkspaceCheckoutError::BadCommitMetadata()),
1881                };
1882
1883            if let Some(c) = content_opt {
1884                let set: TribleSet = local
1885                    .get(c)
1886                    .or_else(|_| self.base_blobs.get(c))
1887                    .map_err(WorkspaceCheckoutError::Storage)?;
1888                result.union(set);
1889            } else {
1890                // No content for this commit (e.g. merge-only commit); skip it.
1891                continue;
1892            }
1893        }
1894        Ok(result)
1895    }
1896
1897    fn checkout_commits_metadata<I>(
1898        &mut self,
1899        commits: I,
1900    ) -> Result<
1901        TribleSet,
1902        WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet<Blake3>>::GetError<UnarchiveError>>,
1903    >
1904    where
1905        I: IntoIterator<Item = CommitHandle>,
1906    {
1907        let local = self.local_blobs.reader().unwrap();
1908        let mut result = TribleSet::new();
1909        for commit in commits {
1910            let meta: TribleSet = local
1911                .get(commit)
1912                .or_else(|_| self.base_blobs.get(commit))
1913                .map_err(WorkspaceCheckoutError::Storage)?;
1914
1915            let metadata_opt =
1916                match find!((c: Value<_>), pattern!(&meta, [{ metadata: ?c }])).at_most_one() {
1917                    Ok(Some((c,))) => Some(c),
1918                    Ok(None) => None,
1919                    Err(_) => return Err(WorkspaceCheckoutError::BadCommitMetadata()),
1920                };
1921
1922            if let Some(c) = metadata_opt {
1923                let set: TribleSet = local
1924                    .get(c)
1925                    .or_else(|_| self.base_blobs.get(c))
1926                    .map_err(WorkspaceCheckoutError::Storage)?;
1927                result.union(set);
1928            }
1929        }
1930        Ok(result)
1931    }
1932
1933    fn checkout_commits_with_metadata<I>(
1934        &mut self,
1935        commits: I,
1936    ) -> Result<
1937        (TribleSet, TribleSet),
1938        WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet<Blake3>>::GetError<UnarchiveError>>,
1939    >
1940    where
1941        I: IntoIterator<Item = CommitHandle>,
1942    {
1943        let local = self.local_blobs.reader().unwrap();
1944        let mut data = TribleSet::new();
1945        let mut metadata_set = TribleSet::new();
1946        for commit in commits {
1947            let meta: TribleSet = local
1948                .get(commit)
1949                .or_else(|_| self.base_blobs.get(commit))
1950                .map_err(WorkspaceCheckoutError::Storage)?;
1951
1952            let content_opt =
1953                match find!((c: Value<_>), pattern!(&meta, [{ content: ?c }])).at_most_one() {
1954                    Ok(Some((c,))) => Some(c),
1955                    Ok(None) => None,
1956                    Err(_) => return Err(WorkspaceCheckoutError::BadCommitMetadata()),
1957                };
1958
1959            if let Some(c) = content_opt {
1960                let set: TribleSet = local
1961                    .get(c)
1962                    .or_else(|_| self.base_blobs.get(c))
1963                    .map_err(WorkspaceCheckoutError::Storage)?;
1964                data.union(set);
1965            }
1966
1967            let metadata_opt =
1968                match find!((c: Value<_>), pattern!(&meta, [{ metadata: ?c }])).at_most_one() {
1969                    Ok(Some((c,))) => Some(c),
1970                    Ok(None) => None,
1971                    Err(_) => return Err(WorkspaceCheckoutError::BadCommitMetadata()),
1972                };
1973
1974            if let Some(c) = metadata_opt {
1975                let set: TribleSet = local
1976                    .get(c)
1977                    .or_else(|_| self.base_blobs.get(c))
1978                    .map_err(WorkspaceCheckoutError::Storage)?;
1979                metadata_set.union(set);
1980            }
1981        }
1982        Ok((data, metadata_set))
1983    }
1984
1985    /// Returns the combined [`TribleSet`] for the specified commits or commit
1986    /// ranges. `spec` can be a single [`CommitHandle`], an iterator of handles
1987    /// or any of the standard range types over `CommitHandle`.
1988    pub fn checkout<R>(
1989        &mut self,
1990        spec: R,
1991    ) -> Result<
1992        TribleSet,
1993        WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet<Blake3>>::GetError<UnarchiveError>>,
1994    >
1995    where
1996        R: CommitSelector<Blobs>,
1997    {
1998        let patch = spec.select(self)?;
1999        let commits = patch.iter().map(|raw| Value::new(*raw));
2000        self.checkout_commits(commits)
2001    }
2002
2003    /// Returns the combined metadata [`TribleSet`] for the specified commits.
2004    /// Commits without metadata handles contribute an empty set.
2005    pub fn checkout_metadata<R>(
2006        &mut self,
2007        spec: R,
2008    ) -> Result<
2009        TribleSet,
2010        WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet<Blake3>>::GetError<UnarchiveError>>,
2011    >
2012    where
2013        R: CommitSelector<Blobs>,
2014    {
2015        let patch = spec.select(self)?;
2016        let commits = patch.iter().map(|raw| Value::new(*raw));
2017        self.checkout_commits_metadata(commits)
2018    }
2019
2020    /// Returns the combined data and metadata [`TribleSet`] for the specified commits.
2021    /// Metadata is loaded from each commit's `metadata` handle, when present.
2022    pub fn checkout_with_metadata<R>(
2023        &mut self,
2024        spec: R,
2025    ) -> Result<
2026        (TribleSet, TribleSet),
2027        WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet<Blake3>>::GetError<UnarchiveError>>,
2028    >
2029    where
2030        R: CommitSelector<Blobs>,
2031    {
2032        let patch = spec.select(self)?;
2033        let commits = patch.iter().map(|raw| Value::new(*raw));
2034        self.checkout_commits_with_metadata(commits)
2035    }
2036}
2037
2038#[derive(Debug)]
2039pub enum WorkspaceCheckoutError<GetErr: Error> {
2040    /// Error retrieving blobs from storage.
2041    Storage(GetErr),
2042    /// Commit metadata is malformed or ambiguous.
2043    BadCommitMetadata(),
2044}
2045
2046impl<E: Error + fmt::Debug> fmt::Display for WorkspaceCheckoutError<E> {
2047    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2048        match self {
2049            WorkspaceCheckoutError::Storage(e) => write!(f, "storage error: {e}"),
2050            WorkspaceCheckoutError::BadCommitMetadata() => {
2051                write!(f, "commit metadata malformed")
2052            }
2053        }
2054    }
2055}
2056
2057impl<E: Error + fmt::Debug> Error for WorkspaceCheckoutError<E> {}
2058
2059fn collect_reachable<Blobs: BlobStore<Blake3>>(
2060    ws: &mut Workspace<Blobs>,
2061    from: CommitHandle,
2062) -> Result<
2063    CommitSet,
2064    WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet<Blake3>>::GetError<UnarchiveError>>,
2065> {
2066    let mut visited = HashSet::new();
2067    let mut stack = vec![from];
2068    let mut result = CommitSet::new();
2069
2070    while let Some(commit) = stack.pop() {
2071        if !visited.insert(commit) {
2072            continue;
2073        }
2074        result.insert(&Entry::new(&commit.raw));
2075
2076        let meta: TribleSet = ws
2077            .local_blobs
2078            .reader()
2079            .unwrap()
2080            .get(commit)
2081            .or_else(|_| ws.base_blobs.get(commit))
2082            .map_err(WorkspaceCheckoutError::Storage)?;
2083
2084        for (p,) in find!((p: Value<_>,), pattern!(&meta, [{ parent: ?p }])) {
2085            stack.push(p);
2086        }
2087    }
2088
2089    Ok(result)
2090}