Skip to main content

triblespace_core/
repo.rs

1#![allow(clippy::type_complexity)]
2//! This module provides a high-level API for storing and retrieving data from repositories.
3//! The design is inspired by Git, but with a focus on object/content-addressed storage.
4//! It separates storage concerns from the data model, and reduces the mutable state of the repository,
5//! to an absolute minimum, making it easier to reason about and allowing for different storage backends.
6//!
7//! Blob repositories are collections of blobs that can be content-addressed by their hash.
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::inlineencodings::{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), TribleSet::new()).unwrap();
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//!     "initial commit",
50//! );
51//!
52//! // Single-attempt push: `try_push` uploads local blobs and attempts a
53//! // single CAS update. On conflict it returns a workspace containing the
54//! // new branch state which you should merge into before retrying.
55//! match repo.try_push(&mut ws).expect("try_push") {
56//!     None => {}
57//!     Some(_) => panic!("unexpected conflict"),
58//! }
59//! ```
60//!
61//! `create_branch` registers a new branch and returns an [`ExclusiveId`](crate::id::ExclusiveId) guard.
62//! `pull` creates a new workspace from an existing branch while
63//! `branch_from` can be used to start a new branch from a specific commit
64//! handle. See `examples/workspace.rs` for a more complete example.
65//!
66//! ## Handling conflicts
67//!
68//! The single-attempt primitive is [`Repository::try_push`](crate::repo::Repository::try_push). It returns
69//! `Ok(None)` on success or `Ok(Some(conflict_ws))` when the branch advanced
70//! concurrently. Callers that want explicit conflict handling may use this
71//! form:
72//!
73//! ```rust,ignore
74//! while let Some(mut other) = repo.try_push(&mut ws)? {
75//!     // Merge our staged changes into the incoming workspace and retry.
76//!     other.merge(&mut ws)?;
77//!     ws = other;
78//! }
79//! ```
80//!
81//! For convenience `Repository::push` is provided as a retrying wrapper that
82//! performs the merge-and-retry loop for you. Call `push` when you prefer the
83//! repository to handle conflicts automatically; call `try_push` when you need
84//! to inspect or control the intermediate conflict workspace yourself.
85//!
86//! `push` performs a compare‐and‐swap (CAS) update on the branch metadata.
87//! This optimistic concurrency control keeps branches consistent without
88//! locking and can be emulated by many storage systems (for example by
89//! using conditional writes on S3).
90//!
91//! ## Git parallels
92//!
93//! The API deliberately mirrors concepts from Git to make its usage familiar:
94//!
95//! - A [`Repository`](crate::repo::Repository) stores commits and branch metadata similar to a remote.
96//! - [`Workspace`](crate::repo::Workspace) is akin to a working directory combined with an index. It
97//!   tracks changes against a branch head until you `push` them.
98//! - `create_branch` and `branch_from` correspond to creating new branches from
99//!   scratch or from a specific commit, respectively.
100//! - `push` updates the repository atomically. If the branch advanced in the
101//!   meantime, you receive a conflict workspace which can be merged before
102//!   retrying the push.
103//! - `pull` is similar to cloning a branch into a new workspace.
104//!
105//! `pull` uses the repository's default signing key for new commits. If you
106//! need to work with a different identity, the `_with_key` variants allow providing
107//! an explicit key when creating branches or pulling workspaces.
108//!
109//! These parallels should help readers leverage their Git knowledge when
110//! working with trible repositories.
111//!
112/// Branch metadata construction and signature verification.
113pub mod branch;
114/// Capability-based authorization for triblespace networks.
115pub mod capability;
116/// Commit metadata construction and signature verification.
117pub mod commit;
118/// Storage adapter that delegates blobs and branches to separate backends.
119pub mod hybridstore;
120/// Fully in-memory repository implementation for tests and ephemeral use.
121pub mod memoryrepo;
122#[cfg(feature = "object-store")]
123/// Repository backed by an `object_store`-compatible remote (S3, local FS, etc.).
124pub mod objectstore;
125/// Local file-based pile storage backend.
126pub mod pile;
127
128/// Trait for storage backends that require explicit close/cleanup.
129///
130/// Not all storage backends need to implement this; implementations that have
131/// nothing to do on close may return Ok(()) or use `Infallible` as the error
132/// type.
133pub trait StorageClose {
134    /// Error type returned by `close`.
135    type Error: std::error::Error;
136
137    /// Consume the storage and perform any necessary cleanup.
138    fn close(self) -> Result<(), Self::Error>;
139}
140
141// Convenience impl for repositories whose storage supports explicit close.
142impl<Storage> Repository<Storage>
143where
144    Storage: BlobStore + BranchStore + StorageClose,
145{
146    /// Close the repository's underlying storage if it supports explicit
147    /// close operations.
148    ///
149    /// This method is only available when the storage type implements
150    /// [`StorageClose`]. It consumes the repository and delegates to the
151    /// storage's `close` implementation, returning any error produced.
152    pub fn close(self) -> Result<(), <Storage as StorageClose>::Error> {
153        self.storage.close()
154    }
155}
156
157use crate::macros::pattern;
158use std::collections::{HashSet, VecDeque};
159use std::convert::Infallible;
160use std::error::Error;
161use std::fmt::Debug;
162use std::fmt::{self};
163
164use commit::commit_metadata;
165use hifitime::Epoch;
166use itertools::Itertools;
167
168use crate::blob::encodings::simplearchive::UnarchiveError;
169use crate::blob::encodings::UnknownBlob;
170use crate::blob::Blob;
171use crate::blob::BlobEncoding;
172use crate::blob::MemoryBlobStore;
173use crate::blob::IntoBlob;
174use crate::blob::TryFromBlob;
175use crate::find;
176use crate::id::genid;
177use crate::id::Id;
178use crate::patch::Entry;
179use crate::patch::IdentitySchema;
180use crate::patch::PATCH;
181use crate::prelude::inlineencodings::GenId;
182use crate::repo::branch::branch_metadata;
183use crate::trible::TribleSet;
184use crate::inline::encodings::hash::Handle;
185use crate::inline::Inline;
186use crate::inline::InlineEncoding;
187use crate::inline::INLINE_LEN;
188use ed25519_dalek::SigningKey;
189
190use crate::blob::encodings::longstring::LongString;
191use crate::blob::encodings::simplearchive::SimpleArchive;
192use crate::blob::encodings::succinctarchive::SuccinctArchiveBlob;
193use crate::prelude::*;
194use crate::inline::encodings::ed25519 as ed;
195use crate::inline::encodings::shortstring::ShortString;
196
197attributes! {
198    /// The actual data of the commit.
199    "4DD4DDD05CC31734B03ABB4E43188B1F" as pub content: Handle<SimpleArchive>;
200    /// Metadata describing the commit content.
201    "88B59BD497540AC5AECDB7518E737C87" as pub metadata: Handle<SimpleArchive>;
202    /// A commit that this commit is based on.
203    "317044B612C690000D798CA660ECFD2A" as pub parent: Handle<SimpleArchive>;
204    /// A (potentially long) message describing the commit.
205    "B59D147839100B6ED4B165DF76EDF3BB" as pub message: Handle<LongString>;
206    /// A short message describing the commit.
207    "12290C0BE0E9207E324F24DDE0D89300" as pub short_message: ShortString;
208    /// The hash of the first commit in the commit chain of the branch.
209    "272FBC56108F336C4D2E17289468C35F" as pub head: Handle<SimpleArchive>;
210    /// An id used to track the branch.
211    "8694CC73AF96A5E1C7635C677D1B928A" as pub branch: GenId;
212    /// The author of the signature identified by their ed25519 public key.
213    "ADB4FFAD247C886848161297EFF5A05B" as pub signed_by: ed::ED25519PublicKey;
214    /// The `r` part of a ed25519 signature.
215    "9DF34F84959928F93A3C40AEB6E9E499" as pub signature_r: ed::ED25519RComponent;
216    /// The `s` part of a ed25519 signature.
217    "1ACE03BF70242B289FDF00E4327C3BC6" as pub signature_s: ed::ED25519SComponent;
218    /// Optional SuccinctArchive rollup of the branch HEAD's logical contents.
219    ///
220    /// Readers can fetch this blob via the repository's blob store to obtain
221    /// a compact, instantly-queryable representation of the branch's state
222    /// without having to materialise the TribleSet from the commit chain.
223    /// Absent on branches that haven't had a rollup built yet. Soft state:
224    /// the rollup is redundant with (and must agree with) whatever
225    /// `ws.checkout(..)` would return for the same HEAD.
226    "D7D14C6737AA27A51E1E08D380D13EF9" as pub rollup: Handle<SuccinctArchiveBlob>;
227}
228
229/// The `ListBlobs` trait is used to list all blobs in a repository.
230pub trait BlobStoreList {
231    /// Iterator over blob handles in the store.
232    type Iter<'a>: Iterator<Item = Result<Inline<Handle<UnknownBlob>>, Self::Err>>
233    where
234        Self: 'a;
235    /// Error type for listing operations.
236    type Err: Error + Debug + Send + Sync + 'static;
237
238    /// Lists all blobs in the repository.
239    fn blobs<'a>(&'a self) -> Self::Iter<'a>;
240
241    /// Lists blobs in `self` that are not in `old`.
242    ///
243    /// Backends with true snapshot semantics (e.g. [`Pile`],
244    /// where each [`Reader`](BlobStore::Reader) holds a frozen clone of the
245    /// in-memory blob index) compute the difference cheaply via the index's
246    /// own set-difference operation. Backends without snapshot semantics
247    /// (e.g. an object store, where the Reader is just a handle to the live
248    /// remote) fall back to the default implementation, which lists all
249    /// current blobs — over-eager but always correct.
250    ///
251    /// Use this for "what blobs are new since I last looked" patterns
252    /// (e.g. announcing newly-imported blobs to a DHT) where holding the
253    /// previous Reader as a baseline gives you the delta.
254    fn blobs_diff<'a>(&'a self, _old: &Self) -> Self::Iter<'a> {
255        self.blobs()
256    }
257}
258
259/// Metadata about a blob in a repository.
260#[derive(Debug, Clone)]
261pub struct BlobMetadata {
262    /// Timestamp in milliseconds since UNIX epoch when the blob was created/stored.
263    pub timestamp: u64,
264    /// Length of the blob in bytes.
265    pub length: u64,
266}
267
268/// Trait exposing metadata lookup for blobs available in a repository reader.
269pub trait BlobStoreMeta {
270    /// Error type returned by metadata calls.
271    type MetaError: std::error::Error + Send + Sync + 'static;
272
273    /// Returns metadata for the blob identified by `handle`, or `None` if
274    /// the blob is not present.
275    fn metadata<S>(
276        &self,
277        handle: Inline<Handle<S>>,
278    ) -> Result<Option<BlobMetadata>, Self::MetaError>
279    where
280        S: BlobEncoding + 'static,
281        Handle<S>: InlineEncoding;
282}
283
284/// Trait exposing a monotonic "forget" operation.
285///
286/// Forget is idempotent and monotonic: it removes materialization from a
287/// particular repository but does not semantically delete derived facts.
288pub trait BlobStoreForget {
289    /// Error type for forget operations.
290    type ForgetError: std::error::Error + Send + Sync + 'static;
291
292    /// Removes the materialized blob identified by `handle` from this store.
293    fn forget<S>(&mut self, handle: Inline<Handle<S>>) -> Result<(), Self::ForgetError>
294    where
295        S: BlobEncoding + 'static,
296        Handle<S>: InlineEncoding;
297}
298
299/// The `GetBlob` trait is used to retrieve blobs from a repository.
300pub trait BlobStoreGet {
301    /// Error type for get operations, parameterised by the deserialization error.
302    type GetError<E: std::error::Error + Send + Sync + 'static>: Error + Send + Sync + 'static;
303
304    /// Retrieves a blob from the repository by its handle.
305    /// The handle is a unique identifier for the blob, and is used to retrieve it from the repository.
306    /// The blob is returned as a [`Blob`] object, which contains the raw bytes of the blob,
307    /// which can be deserialized via the appropriate schema type, which is specified by the `T` type parameter.
308    ///
309    /// # Errors
310    /// Returns an error if the blob could not be found in the repository.
311    /// The error type is specified by the `Err` associated type.
312    fn get<T, S>(
313        &self,
314        handle: Inline<Handle<S>>,
315    ) -> Result<T, Self::GetError<<T as TryFromBlob<S>>::Error>>
316    where
317        S: BlobEncoding + 'static,
318        T: TryFromBlob<S>,
319        Handle<S>: InlineEncoding;
320}
321
322/// The `PutBlob` trait is used to store blobs in a repository.
323pub trait BlobStorePut {
324    /// Error type for put operations.
325    type PutError: Error + Debug + Send + Sync + 'static;
326
327    /// Serialises `item` as a blob, stores it, and returns its handle.
328    fn put<S, T>(&mut self, item: T) -> Result<Inline<Handle<S>>, Self::PutError>
329    where
330        S: BlobEncoding + 'static,
331        T: IntoBlob<S>,
332        Handle<S>: InlineEncoding;
333}
334
335/// Combined read/write blob storage.
336///
337/// Extends [`BlobStorePut`] with the ability to create a shareable
338/// [`Reader`](BlobStore::Reader) snapshot for concurrent reads.
339pub trait BlobStore: BlobStorePut {
340    /// A clonable reader handle for concurrent blob lookups.
341    type Reader: BlobStoreGet + BlobStoreList + Clone + Send + PartialEq + Eq + 'static;
342    /// Error type for creating a reader.
343    type ReaderError: Error + Debug + Send + Sync + 'static;
344    /// Creates a shareable reader snapshot of the current store state.
345    fn reader(&mut self) -> Result<Self::Reader, Self::ReaderError>;
346}
347
348/// Trait for blob stores that can retain a supplied set of handles.
349pub trait BlobStoreKeep {
350    /// Retain only the blobs identified by `handles`.
351    fn keep<I>(&mut self, handles: I)
352    where
353        I: IntoIterator<Item = Inline<Handle<UnknownBlob>>>;
354}
355
356/// Trait for stores that can enumerate a blob's child references.
357///
358/// "Children" are the 32-byte-aligned values in a blob that correspond
359/// to existing blobs in the store — the conservative set of references.
360///
361/// The default implementation scans the blob's bytes and checks each
362/// 32-byte chunk with [`BlobStoreGet::get`]. Backends with batch
363/// capabilities (e.g. a network store with a SYNC protocol) can
364/// override this for efficiency.
365pub trait BlobChildren: BlobStoreGet {
366    /// Return handles of blobs referenced by `handle` that exist in this store.
367    fn children(
368        &self,
369        handle: Inline<Handle<UnknownBlob>>,
370    ) -> Vec<Inline<Handle<UnknownBlob>>> {
371        let Ok(blob) = self.get::<Blob<UnknownBlob>, UnknownBlob>(handle) else {
372            return Vec::new();
373        };
374        let bytes = blob.bytes.as_ref();
375        let mut result = Vec::new();
376        let mut offset = 0usize;
377        while offset + INLINE_LEN <= bytes.len() {
378            let mut raw = [0u8; INLINE_LEN];
379            raw.copy_from_slice(&bytes[offset..offset + INLINE_LEN]);
380            let candidate = Inline::<Handle<UnknownBlob>>::new(raw);
381            if self.get::<anybytes::Bytes, UnknownBlob>(candidate).is_ok() {
382                result.push(candidate);
383            }
384            offset += INLINE_LEN;
385        }
386        result
387    }
388}
389
390// No blanket impl — types opt in explicitly so they can provide
391// optimized implementations (e.g. network stores with batch protocols).
392// Use `impl_blob_children_default!` for the scan-and-check fallback.
393
394/// Outcome of a compare-and-swap branch update.
395#[derive(Debug)]
396pub enum PushResult {
397    /// The CAS succeeded — the branch now points to the new commit.
398    Success(),
399    /// The CAS failed — the branch had advanced. Contains the current
400    /// head, or `None` if the branch was deleted concurrently.
401    Conflict(Option<Inline<Handle<SimpleArchive>>>),
402}
403
404/// Storage backend for branch metadata (branch-id → commit-handle mapping).
405///
406/// This is the stateful counterpart to [`BlobStore`]: blob stores are
407/// content-addressed and orderless, while branch stores track a single
408/// mutable pointer per branch. The update operation uses compare-and-swap
409/// semantics so multiple writers can coordinate without locks.
410pub trait BranchStore {
411    /// Error type for listing branches.
412    type BranchesError: Error + Debug + Send + Sync + 'static;
413    /// Error type for head lookups.
414    type HeadError: Error + Debug + Send + Sync + 'static;
415    /// Error type for CAS updates.
416    type UpdateError: Error + Debug + Send + Sync + 'static;
417
418    /// Iterator over branch IDs.
419    type ListIter<'a>: Iterator<Item = Result<Id, Self::BranchesError>>
420    where
421        Self: 'a;
422
423    /// Lists all branches in the repository.
424    /// This function returns a stream of branch ids.
425    fn branches<'a>(&'a mut self) -> Result<Self::ListIter<'a>, Self::BranchesError>;
426
427    // NOTE: keep the API lean — callers may call `branches()` and handle the
428    // fallible iterator directly; we avoid adding an extra helper here.
429
430    /// Retrieves a branch from the repository by its id.
431    /// The id is a unique identifier for the branch, and is used to retrieve it from the repository.
432    ///
433    /// # Errors
434    /// Returns an error if the branch could not be found in the repository.
435    ///
436    /// # Parameters
437    /// * `id` - The id of the branch to retrieve.
438    ///
439    /// # Returns
440    /// * A future that resolves to the handle of the branch.
441    /// * The handle is a unique identifier for the branch, and is used to retrieve it from the repository.
442    fn head(&mut self, id: Id) -> Result<Option<Inline<Handle<SimpleArchive>>>, Self::HeadError>;
443
444    /// Puts a branch on the repository, creating or updating it.
445    ///
446    /// # Parameters
447    /// * `old` - Expected current value of the branch (None if creating new)
448    /// * `new` - Inline to update the branch to (None deletes the branch)
449    ///
450    /// # Returns
451    /// * `Success` - Push completed successfully
452    /// * `Conflict(current)` - Failed because the branch's current value doesn't match `old`
453    ///   (contains the actual current value for conflict resolution)
454    fn update(
455        &mut self,
456        id: Id,
457        old: Option<Inline<Handle<SimpleArchive>>>,
458        new: Option<Inline<Handle<SimpleArchive>>>,
459    ) -> Result<PushResult, Self::UpdateError>;
460}
461
462/// Error returned by [`transfer`] when copying blobs between stores.
463#[derive(Debug)]
464pub enum TransferError<ListErr, LoadErr, StoreErr> {
465    /// Failed to list handles from the source.
466    List(ListErr),
467    /// Failed to load a blob from the source.
468    Load(LoadErr),
469    /// Failed to store a blob in the target.
470    Store(StoreErr),
471}
472
473impl<ListErr, LoadErr, StoreErr> fmt::Display for TransferError<ListErr, LoadErr, StoreErr> {
474    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
475        write!(f, "failed to transfer blob")
476    }
477}
478
479impl<ListErr, LoadErr, StoreErr> Error for TransferError<ListErr, LoadErr, StoreErr>
480where
481    ListErr: Debug + Error + 'static,
482    LoadErr: Debug + Error + 'static,
483    StoreErr: Debug + Error + 'static,
484{
485    fn source(&self) -> Option<&(dyn Error + 'static)> {
486        match self {
487            Self::List(e) => Some(e),
488            Self::Load(e) => Some(e),
489            Self::Store(e) => Some(e),
490        }
491    }
492}
493
494/// Copies the specified blob handles from `source` into `target`.
495pub fn transfer<'a, BS, BT, Handles>(
496    source: &'a BS,
497    target: &'a mut BT,
498    handles: Handles,
499) -> impl Iterator<
500    Item = Result<
501        (
502            Inline<Handle<UnknownBlob>>,
503            Inline<Handle<UnknownBlob>>,
504        ),
505        TransferError<
506            Infallible,
507            <BS as BlobStoreGet>::GetError<Infallible>,
508            <BT as BlobStorePut>::PutError,
509        >,
510    >,
511> + 'a
512where
513    BS: BlobStoreGet + 'a,
514    BT: BlobStorePut + 'a,
515    Handles: IntoIterator<Item = Inline<Handle<UnknownBlob>>> + 'a,
516    Handles::IntoIter: 'a,
517{
518    handles.into_iter().map(move |source_handle| {
519        let blob: Blob<UnknownBlob> = source.get(source_handle).map_err(TransferError::Load)?;
520
521        Ok((
522            source_handle,
523            (target.put(blob).map_err(TransferError::Store)?),
524        ))
525    })
526}
527
528/// Iterator that visits every blob handle reachable from a set of roots.
529///
530/// Uses [`BlobChildren`] to enumerate references at each level,
531/// so backends with batch capabilities get efficient traversal.
532pub struct ReachableHandles<'a, BS>
533where
534    BS: BlobChildren,
535{
536    source: &'a BS,
537    queue: VecDeque<Inline<Handle<UnknownBlob>>>,
538    visited: HashSet<[u8; INLINE_LEN]>,
539}
540
541impl<'a, BS> ReachableHandles<'a, BS>
542where
543    BS: BlobChildren,
544{
545    fn new(source: &'a BS, roots: impl IntoIterator<Item = Inline<Handle<UnknownBlob>>>) -> Self {
546        let mut queue = VecDeque::new();
547        for handle in roots {
548            queue.push_back(handle);
549        }
550
551        Self {
552            source,
553            queue,
554            visited: HashSet::new(),
555        }
556    }
557}
558
559impl<'a, BS> Iterator for ReachableHandles<'a, BS>
560where
561    BS: BlobChildren,
562{
563    type Item = Inline<Handle<UnknownBlob>>;
564
565    fn next(&mut self) -> Option<Self::Item> {
566        while let Some(handle) = self.queue.pop_front() {
567            let raw = handle.raw;
568
569            if !self.visited.insert(raw) {
570                continue;
571            }
572
573            // Use BlobChildren to get references — backends can override
574            // with batch-optimized implementations.
575            for child in self.source.children(handle) {
576                if !self.visited.contains(&child.raw) {
577                    self.queue.push_back(child);
578                }
579            }
580
581            return Some(handle);
582        }
583
584        None
585    }
586}
587
588/// Create a breadth-first iterator over blob handles reachable from `roots`.
589///
590/// Uses [`BlobChildren`] for reference enumeration, so network-backed
591/// stores can provide optimized batch implementations.
592pub fn reachable<'a, BS>(
593    source: &'a BS,
594    roots: impl IntoIterator<Item = Inline<Handle<UnknownBlob>>>,
595) -> ReachableHandles<'a, BS>
596where
597    BS: BlobChildren,
598{
599    ReachableHandles::new(source, roots)
600}
601
602/// Iterate over every 32-byte candidate in the value column of a [`TribleSet`].
603///
604/// This is a conservative conversion used when scanning metadata for potential
605/// blob handles. Each 32-byte chunk is treated as a `Handle<UnknownBlob>`.
606/// Callers can feed the resulting iterator into [`BlobStoreKeep::keep`] or other
607/// helpers that accept collections of handles.
608pub fn potential_handles<'a>(
609    set: &'a TribleSet,
610) -> impl Iterator<Item = Inline<Handle<UnknownBlob>>> + 'a {
611    set.vae.iter().map(|raw| {
612        let mut value = [0u8; INLINE_LEN];
613        value.copy_from_slice(&raw[0..INLINE_LEN]);
614        Inline::<Handle<UnknownBlob>>::new(value)
615    })
616}
617
618/// An error that can occur when creating a commit.
619/// This error can be caused by a failure to store the content or metadata blobs.
620#[derive(Debug)]
621pub enum CreateCommitError<BlobErr: Error + Debug + Send + Sync + 'static> {
622    /// Failed to store the content blob.
623    ContentStorageError(BlobErr),
624    /// Failed to store the commit metadata blob.
625    CommitStorageError(BlobErr),
626}
627
628impl<BlobErr: Error + Debug + Send + Sync + 'static> fmt::Display for CreateCommitError<BlobErr> {
629    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
630        match self {
631            CreateCommitError::ContentStorageError(e) => write!(f, "Content storage failed: {e}"),
632            CreateCommitError::CommitStorageError(e) => {
633                write!(f, "Commit metadata storage failed: {e}")
634            }
635        }
636    }
637}
638
639impl<BlobErr: Error + Debug + Send + Sync + 'static> Error for CreateCommitError<BlobErr> {
640    fn source(&self) -> Option<&(dyn Error + 'static)> {
641        match self {
642            CreateCommitError::ContentStorageError(e) => Some(e),
643            CreateCommitError::CommitStorageError(e) => Some(e),
644        }
645    }
646}
647
648/// Error returned by [`Workspace::merge`].
649#[derive(Debug)]
650pub enum MergeError {
651    /// The merge failed because the workspaces have different base repos.
652    DifferentRepos(),
653}
654
655/// Error returned by [`Repository::push`] and [`Repository::try_push`].
656/// Error type for [`Repository::compute_rollup`].
657#[derive(Debug)]
658pub enum RollupError<Storage: BranchStore + BlobStore> {
659    /// The branch was not found in the underlying storage.
660    UnknownBranch,
661    /// The branch is empty — no HEAD to roll up.
662    EmptyBranch,
663    /// The branch HEAD advanced between checkout and CAS-update. The
664    /// caller may retry (`compute_rollup` is content-addressed so repeat
665    /// calls dedupe against already-uploaded blobs).
666    HeadAdvanced,
667    /// Underlying push / storage error during the attach step.
668    Push(PushError<Storage>),
669    /// Could not pull the branch to obtain a workspace.
670    Pull(PullError<Storage::HeadError,
671                    <Storage as BlobStore>::ReaderError,
672                    <<Storage as BlobStore>::Reader as BlobStoreGet>::GetError<UnarchiveError>>),
673    /// Could not check out the branch state to build the archive.
674    Checkout(WorkspaceCheckoutError<
675        <<Storage as BlobStore>::Reader as BlobStoreGet>::GetError<UnarchiveError>>),
676}
677
678#[derive(Debug)]
679pub enum PushError<Storage: BranchStore + BlobStore> {
680    /// An error occurred while enumerating the branch storage branches.
681    StorageBranches(Storage::BranchesError),
682    /// An error occurred while creating a blob reader.
683    StorageReader(<Storage as BlobStore>::ReaderError),
684    /// An error occurred while reading metadata blobs.
685    StorageGet(
686        <<Storage as BlobStore>::Reader as BlobStoreGet>::GetError<UnarchiveError>,
687    ),
688    /// An error occurred while transferring blobs to the repository.
689    StoragePut(<Storage as BlobStorePut>::PutError),
690    /// An error occurred while updating the branch storage.
691    BranchUpdate(Storage::UpdateError),
692    /// Malformed branch metadata.
693    BadBranchMetadata(),
694    /// Merge failed while retrying a push.
695    MergeError(MergeError),
696}
697
698// Allow using the `?` operator to convert MergeError into PushError in
699// contexts where PushError is the function error type. This keeps call sites
700// succinct by avoiding manual mapping closures like
701// `.map_err(|e| PushError::MergeError(e))?`.
702impl<Storage> From<MergeError> for PushError<Storage>
703where
704    Storage: BranchStore + BlobStore,
705{
706    fn from(e: MergeError) -> Self {
707        PushError::MergeError(e)
708    }
709}
710
711// Note: we intentionally avoid generic `From` impls for storage-associated
712// error types because they can overlap with other blanket implementations
713// and lead to coherence conflicts. Call sites use explicit mapping via the
714// enum variant constructors (e.g. `map_err(PushError::StoragePut)`) where
715// needed which keeps conversions explicit and stable.
716
717/// Error returned by [`Repository::create_branch`] and related methods.
718#[derive(Debug)]
719pub enum BranchError<Storage>
720where
721    Storage: BranchStore + BlobStore,
722{
723    /// An error occurred while creating a blob reader.
724    StorageReader(<Storage as BlobStore>::ReaderError),
725    /// An error occurred while reading metadata blobs.
726    StorageGet(
727        <<Storage as BlobStore>::Reader as BlobStoreGet>::GetError<UnarchiveError>,
728    ),
729    /// An error occurred while storing blobs.
730    StoragePut(<Storage as BlobStorePut>::PutError),
731    /// An error occurred while retrieving branch heads.
732    BranchHead(Storage::HeadError),
733    /// An error occurred while updating the branch storage.
734    BranchUpdate(Storage::UpdateError),
735    /// The branch already exists.
736    AlreadyExists(),
737    /// The referenced base branch does not exist.
738    BranchNotFound(Id),
739}
740
741/// Error returned by [`Repository::lookup_branch`].
742#[derive(Debug)]
743pub enum LookupError<Storage>
744where
745    Storage: BranchStore + BlobStore,
746{
747    /// Failed to enumerate branches.
748    StorageBranches(Storage::BranchesError),
749    /// Failed to read a branch head.
750    BranchHead(Storage::HeadError),
751    /// Failed to create a blob reader.
752    StorageReader(<Storage as BlobStore>::ReaderError),
753    /// Failed to read a metadata blob.
754    StorageGet(
755        <<Storage as BlobStore>::Reader as BlobStoreGet>::GetError<UnarchiveError>,
756    ),
757    /// Multiple branches were found with the given name.
758    NameConflict(Vec<Id>),
759    /// Branch metadata is malformed.
760    BadBranchMetadata(),
761}
762
763/// Error returned by [`Repository::ensure_branch`].
764#[derive(Debug)]
765pub enum EnsureBranchError<Storage>
766where
767    Storage: BranchStore + BlobStore,
768{
769    /// Failed to look up the branch.
770    Lookup(LookupError<Storage>),
771    /// Failed to create the branch.
772    Create(BranchError<Storage>),
773}
774
775/// High-level wrapper combining a blob store and branch store into a usable
776/// repository API.
777///
778/// The [`Repository`] type exposes convenience methods for creating branches,
779/// committing data and pushing changes while delegating actual storage to the
780/// given [`BlobStore`] and [`BranchStore`] implementations.
781pub struct Repository<Storage: BlobStore + BranchStore> {
782    storage: Storage,
783    signing_key: SigningKey,
784    commit_metadata: MetadataHandle,
785}
786
787/// Error returned by [`Repository::pull`].
788pub enum PullError<BranchStorageErr, BlobReaderErr, BlobStorageErr>
789where
790    BranchStorageErr: Error,
791    BlobReaderErr: Error,
792    BlobStorageErr: Error,
793{
794    /// The branch does not exist in the repository.
795    BranchNotFound(Id),
796    /// An error occurred while accessing the branch storage.
797    BranchStorage(BranchStorageErr),
798    /// An error occurred while creating a blob reader.
799    BlobReader(BlobReaderErr),
800    /// An error occurred while accessing the blob storage.
801    BlobStorage(BlobStorageErr),
802    /// The branch metadata is malformed or does not contain the expected fields.
803    BadBranchMetadata(),
804}
805
806impl<B, R, C> fmt::Debug for PullError<B, R, C>
807where
808    B: Error + fmt::Debug,
809    R: Error + fmt::Debug,
810    C: Error + fmt::Debug,
811{
812    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
813        match self {
814            PullError::BranchNotFound(id) => f.debug_tuple("BranchNotFound").field(id).finish(),
815            PullError::BranchStorage(e) => f.debug_tuple("BranchStorage").field(e).finish(),
816            PullError::BlobReader(e) => f.debug_tuple("BlobReader").field(e).finish(),
817            PullError::BlobStorage(e) => f.debug_tuple("BlobStorage").field(e).finish(),
818            PullError::BadBranchMetadata() => f.debug_tuple("BadBranchMetadata").finish(),
819        }
820    }
821}
822
823impl<Storage> Repository<Storage>
824where
825    Storage: BlobStore + BranchStore,
826{
827    /// Creates a new repository with the given storage, signing key, and commit metadata.
828    ///
829    /// The `commit_metadata` TribleSet is stored as a blob in the repository and attached
830    /// to every commit created through this repository's workspaces.
831    pub fn new(
832        mut storage: Storage,
833        signing_key: SigningKey,
834        commit_metadata: TribleSet,
835    ) -> Result<Self, <Storage as BlobStorePut>::PutError> {
836        let commit_metadata = storage.put(commit_metadata)?;
837        Ok(Self {
838            storage,
839            signing_key,
840            commit_metadata,
841        })
842    }
843
844    /// Consume the repository and return the underlying storage backend.
845    ///
846    /// This is useful for callers that need to take ownership of the storage
847    /// (for example to call `close()` on a [`Pile`]) instead of letting the
848    /// repository drop it implicitly.
849    pub fn into_storage(self) -> Storage {
850        self.storage
851    }
852
853    /// Borrow the underlying storage backend.
854    pub fn storage(&self) -> &Storage {
855        &self.storage
856    }
857
858    /// Borrow the underlying storage backend mutably.
859    pub fn storage_mut(&mut self) -> &mut Storage {
860        &mut self.storage
861    }
862
863    /// Replace the repository signing key.
864    pub fn set_signing_key(&mut self, signing_key: SigningKey) {
865        self.signing_key = signing_key;
866    }
867
868    /// Returns the repository commit metadata handle.
869    pub fn commit_metadata(&self) -> MetadataHandle {
870        self.commit_metadata
871    }
872
873    /// Initializes a new branch in the repository.
874    /// Branches are the only mutable state in the repository,
875    /// and are used to represent the state of a commit chain at a specific point in time.
876    /// A branch must always point to a commit, and this function can be used to create a new branch.
877    ///
878    /// Creates a new branch in the repository.
879    /// This branch is a pointer to a specific commit in the repository.
880    /// The branch is created with name and is initialized to point to the opionally given commit.
881    /// The branch is signed by the branch signing key.
882    ///
883    /// # Parameters
884    /// * `branch_name` - Name of the new branch.
885    /// * `commit` - Commit to initialize the branch from.
886    pub fn create_branch(
887        &mut self,
888        branch_name: &str,
889        commit: Option<CommitHandle>,
890    ) -> Result<ExclusiveId, BranchError<Storage>> {
891        self.create_branch_with_key(branch_name, commit, self.signing_key.clone())
892    }
893
894    /// Same as [`Self::create_branch`] but uses the provided signing key.
895    pub fn create_branch_with_key(
896        &mut self,
897        branch_name: &str,
898        commit: Option<CommitHandle>,
899        signing_key: SigningKey,
900    ) -> Result<ExclusiveId, BranchError<Storage>> {
901        let branch_id = genid();
902        let name_blob: Blob<LongString> = branch_name.to_owned().to_blob();
903        let name_handle = name_blob.get_handle();
904        self.storage
905            .put::<LongString, _>(name_blob)
906            .map_err(|e| BranchError::StoragePut(e))?;
907
908        let branch_set = if let Some(commit) = commit {
909            let reader = self
910                .storage
911                .reader()
912                .map_err(|e| BranchError::StorageReader(e))?;
913            let set: TribleSet = reader.get(commit).map_err(|e| BranchError::StorageGet(e))?;
914
915            branch::branch_metadata(
916                &signing_key,
917                *branch_id,
918                name_handle,
919                Some(set.to_blob()),
920                None,
921            )
922        } else {
923            branch::branch_unsigned(*branch_id, name_handle, None, None)
924        };
925
926        let branch_blob = branch_set.to_blob();
927        let branch_handle = self
928            .storage
929            .put(branch_blob)
930            .map_err(|e| BranchError::StoragePut(e))?;
931        let push_result = self
932            .storage
933            .update(*branch_id, None, Some(branch_handle))
934            .map_err(|e| BranchError::BranchUpdate(e))?;
935
936        match push_result {
937            PushResult::Success() => Ok(branch_id),
938            PushResult::Conflict(_) => Err(BranchError::AlreadyExists()),
939        }
940    }
941
942    /// Look up a branch by name.
943    ///
944    /// Iterates all branches, reads each one's metadata, and returns the ID
945    /// of the branch whose name matches. Returns `Ok(None)` if no branch has
946    /// that name, or `LookupError::NameConflict` if multiple branches share it.
947    pub fn lookup_branch(&mut self, name: &str) -> Result<Option<Id>, LookupError<Storage>> {
948        let branch_ids: Vec<Id> = self
949            .storage
950            .branches()
951            .map_err(LookupError::StorageBranches)?
952            .collect::<Result<Vec<_>, _>>()
953            .map_err(LookupError::StorageBranches)?;
954
955        let mut matches = Vec::new();
956
957        for branch_id in branch_ids {
958            let Some(meta_handle) = self
959                .storage
960                .head(branch_id)
961                .map_err(LookupError::BranchHead)?
962            else {
963                continue;
964            };
965
966            let reader = self.storage.reader().map_err(LookupError::StorageReader)?;
967            let meta_set: TribleSet = reader.get(meta_handle).map_err(LookupError::StorageGet)?;
968
969            let Ok((name_handle,)) = find!(
970                (n: Inline<Handle<LongString>>),
971                pattern!(&meta_set, [{ crate::metadata::name: ?n }])
972            )
973            .exactly_one() else {
974                continue;
975            };
976
977            let Ok(branch_name): Result<anybytes::View<str>, _> = reader.get(name_handle) else {
978                continue;
979            };
980
981            if branch_name.as_ref() == name {
982                matches.push(branch_id);
983            }
984        }
985
986        match matches.len() {
987            0 => Ok(None),
988            1 => Ok(Some(matches[0])),
989            _ => Err(LookupError::NameConflict(matches)),
990        }
991    }
992
993    /// Ensure a branch with the given name exists, creating it if necessary.
994    ///
995    /// If a branch named `name` already exists, returns its ID.
996    /// If no such branch exists, creates a new one (optionally from the given
997    /// commit) and returns its ID.
998    ///
999    /// Errors if multiple branches share the same name (ambiguous).
1000    pub fn ensure_branch(
1001        &mut self,
1002        name: &str,
1003        commit: Option<CommitHandle>,
1004    ) -> Result<Id, EnsureBranchError<Storage>> {
1005        match self
1006            .lookup_branch(name)
1007            .map_err(EnsureBranchError::Lookup)?
1008        {
1009            Some(id) => Ok(id),
1010            None => {
1011                let id = self
1012                    .create_branch(name, commit)
1013                    .map_err(EnsureBranchError::Create)?;
1014                Ok(*id)
1015            }
1016        }
1017    }
1018
1019    /// Pulls an existing branch using the repository's signing key.
1020    /// The workspace inherits the repository default metadata if configured.
1021    pub fn pull(
1022        &mut self,
1023        branch_id: Id,
1024    ) -> Result<
1025        Workspace<Storage>,
1026        PullError<
1027            Storage::HeadError,
1028            Storage::ReaderError,
1029            <Storage::Reader as BlobStoreGet>::GetError<UnarchiveError>,
1030        >,
1031    > {
1032        self.pull_with_key(branch_id, self.signing_key.clone())
1033    }
1034
1035    /// Same as [`Self::pull`] but overrides the signing key.
1036    pub fn pull_with_key(
1037        &mut self,
1038        branch_id: Id,
1039        signing_key: SigningKey,
1040    ) -> Result<
1041        Workspace<Storage>,
1042        PullError<
1043            Storage::HeadError,
1044            Storage::ReaderError,
1045            <Storage::Reader as BlobStoreGet>::GetError<UnarchiveError>,
1046        >,
1047    > {
1048        // 1. Get the branch metadata head from the branch store.
1049        let base_branch_meta_handle = match self.storage.head(branch_id) {
1050            Ok(Some(handle)) => handle,
1051            Ok(None) => return Err(PullError::BranchNotFound(branch_id)),
1052            Err(e) => return Err(PullError::BranchStorage(e)),
1053        };
1054        // 2. Get the current commit from the branch metadata.
1055        let reader = self.storage.reader().map_err(PullError::BlobReader)?;
1056        let base_branch_meta: TribleSet = match reader.get(base_branch_meta_handle) {
1057            Ok(meta_set) => meta_set,
1058            Err(e) => return Err(PullError::BlobStorage(e)),
1059        };
1060
1061        let head_ = match find!(
1062            (head_: Inline<_>),
1063            pattern!(&base_branch_meta, [{ head: ?head_ }])
1064        )
1065        .at_most_one()
1066        {
1067            Ok(Some((h,))) => Some(h),
1068            Ok(None) => None,
1069            Err(_) => return Err(PullError::BadBranchMetadata()),
1070        };
1071        // Create workspace with the current commit and base blobs.
1072        let base_blobs = self.storage.reader().map_err(PullError::BlobReader)?;
1073        Ok(Workspace {
1074            base_blobs,
1075            staged: MemoryBlobStore::new(),
1076            head: head_,
1077            base_head: head_,
1078            base_branch_id: branch_id,
1079            base_branch_meta: base_branch_meta_handle,
1080            signing_key,
1081            commit_metadata: self.commit_metadata,
1082        })
1083    }
1084
1085    /// Pushes the workspace's new blobs and commit to the persistent repository.
1086    /// This syncs the local BlobSet with the repository's BlobStore and performs
1087    /// an atomic branch update (using the stored base_branch_meta).
1088    pub fn push(&mut self, workspace: &mut Workspace<Storage>) -> Result<(), PushError<Storage>> {
1089        // Retrying push: attempt a single push and, on conflict, merge the
1090        // local workspace into the returned conflict workspace and retry.
1091        // This implements the common push-merge-retry loop as a convenience
1092        // wrapper around `try_push`.
1093        while let Some(mut conflict_ws) = self.try_push(workspace)? {
1094            // Keep the previous merge order: merge the caller's staged
1095            // changes into the incoming conflict workspace. This preserves
1096            // the semantic ordering of parents used in the merge commit.
1097            conflict_ws.merge(workspace)?;
1098
1099            // Move the merged incoming workspace into the caller's workspace
1100            // so the next try_push operates against the fresh branch state.
1101            // Using assignment here is equivalent to `swap` but avoids
1102            // retaining the previous `workspace` contents in the temp var.
1103            *workspace = conflict_ws;
1104        }
1105
1106        Ok(())
1107    }
1108
1109    /// Single-attempt push: upload local blobs and try to update the branch
1110    /// head once. Returns `Ok(None)` on success, or `Ok(Some(conflict_ws))`
1111    /// when the branch was updated concurrently and the caller should merge.
1112    pub fn try_push(
1113        &mut self,
1114        workspace: &mut Workspace<Storage>,
1115    ) -> Result<Option<Workspace<Storage>>, PushError<Storage>> {
1116        // 1. Sync `workspace.staged` to repository's BlobStore.
1117        let workspace_reader = workspace.staged.reader().unwrap();
1118        for handle in workspace_reader.blobs() {
1119            let handle = handle.expect("infallible blob enumeration");
1120            let blob: Blob<UnknownBlob> =
1121                workspace_reader.get(handle).expect("infallible blob read");
1122            self.storage
1123                .put::<UnknownBlob, _>(blob)
1124                .map_err(PushError::StoragePut)?;
1125        }
1126
1127        // 1.5 If the workspace's head did not change since the workspace was
1128        // created, there's no commit to reference and therefore no branch
1129        // metadata update is required. This avoids touching the branch store
1130        // in the common case where only blobs were staged or nothing changed.
1131        if workspace.base_head == workspace.head {
1132            return Ok(None);
1133        }
1134
1135        // 2. Create a new branch meta blob referencing the new workspace head.
1136        let repo_reader = self.storage.reader().map_err(PushError::StorageReader)?;
1137        let base_branch_meta: TribleSet = repo_reader
1138            .get(workspace.base_branch_meta)
1139            .map_err(PushError::StorageGet)?;
1140
1141        let Ok((branch_name,)) = find!(
1142            (name: Inline<Handle<LongString>>),
1143            pattern!(base_branch_meta, [{ crate::metadata::name: ?name }])
1144        )
1145        .exactly_one() else {
1146            return Err(PushError::BadBranchMetadata());
1147        };
1148
1149        let head_handle = workspace.head.ok_or(PushError::BadBranchMetadata())?;
1150        let head_: TribleSet = repo_reader
1151            .get(head_handle)
1152            .map_err(PushError::StorageGet)?;
1153
1154        let branch_meta = branch_metadata(
1155            &workspace.signing_key,
1156            workspace.base_branch_id,
1157            branch_name,
1158            Some(head_.to_blob()),
1159            // A fresh commit invalidates any prior rollup (it was computed
1160            // against the old HEAD). Readers fall back to checkout until
1161            // `compute_rollup` runs against the new HEAD.
1162            None,
1163        );
1164
1165        let branch_meta_handle = self
1166            .storage
1167            .put(branch_meta)
1168            .map_err(PushError::StoragePut)?;
1169
1170        // 3. Use CAS (comparing against workspace.base_branch_meta) to update the branch pointer.
1171        let result = self
1172            .storage
1173            .update(
1174                workspace.base_branch_id,
1175                Some(workspace.base_branch_meta),
1176                Some(branch_meta_handle),
1177            )
1178            .map_err(PushError::BranchUpdate)?;
1179
1180        match result {
1181            PushResult::Success() => {
1182                // Update workspace base pointers so subsequent pushes can detect
1183                // that the workspace is already synchronized and avoid re-upload.
1184                workspace.base_branch_meta = branch_meta_handle;
1185                workspace.base_head = workspace.head;
1186                // Refresh the workspace base blob reader to ensure newly
1187                // uploaded blobs are visible to subsequent checkout operations.
1188                workspace.base_blobs = self.storage.reader().map_err(PushError::StorageReader)?;
1189                // Clear staged local blobs now that they have been uploaded and
1190                // the branch metadata updated. This frees memory and prevents
1191                // repeated uploads of the same staged blobs on subsequent pushes.
1192                workspace.staged = MemoryBlobStore::new();
1193                Ok(None)
1194            }
1195            PushResult::Conflict(conflicting_meta) => {
1196                let conflicting_meta = conflicting_meta.ok_or(PushError::BadBranchMetadata())?;
1197
1198                let repo_reader = self.storage.reader().map_err(PushError::StorageReader)?;
1199                let branch_meta: TribleSet = repo_reader
1200                    .get(conflicting_meta)
1201                    .map_err(PushError::StorageGet)?;
1202
1203                let head_ = match find!((head_: Inline<_>),
1204                    pattern!(&branch_meta, [{ head: ?head_ }])
1205                )
1206                .at_most_one()
1207                {
1208                    Ok(Some((h,))) => Some(h),
1209                    Ok(None) => None,
1210                    Err(_) => return Err(PushError::BadBranchMetadata()),
1211                };
1212
1213                let conflict_ws = Workspace {
1214                    base_blobs: self.storage.reader().map_err(PushError::StorageReader)?,
1215                    staged: MemoryBlobStore::new(),
1216                    head: head_,
1217                    base_head: head_,
1218                    base_branch_id: workspace.base_branch_id,
1219                    base_branch_meta: conflicting_meta,
1220                    signing_key: workspace.signing_key.clone(),
1221                    commit_metadata: workspace.commit_metadata,
1222                };
1223
1224                Ok(Some(conflict_ws))
1225            }
1226        }
1227    }
1228
1229    /// Builds a [`SuccinctArchive`](crate::blob::encodings::succinctarchive::SuccinctArchive) rollup of the branch's current HEAD,
1230    /// stores it as a blob in the underlying storage, and attaches the
1231    /// resulting handle to the branch metadata via CAS.
1232    ///
1233    /// Returns the new rollup handle on success.
1234    ///
1235    /// Returns [`RollupError::HeadAdvanced`] if the branch HEAD moved
1236    /// between `pull` and the CAS-update. The caller may retry — the
1237    /// archive blob is content-addressed, so subsequent calls dedupe
1238    /// against already-uploaded blobs.
1239    ///
1240    /// Returns [`RollupError::EmptyBranch`] if the branch has no HEAD
1241    /// commit yet (nothing to roll up).
1242    ///
1243    /// This is the sole public write-path for rollups. The companion
1244    /// read-path is [`Workspace::rollup`].
1245    pub fn compute_rollup(
1246        &mut self,
1247        branch_id: Id,
1248    ) -> Result<
1249        Inline<Handle<crate::blob::encodings::succinctarchive::SuccinctArchiveBlob>>,
1250        RollupError<Storage>,
1251    > {
1252        use crate::blob::encodings::succinctarchive::{OrderedUniverse, SuccinctArchive};
1253        use crate::blob::IntoBlob;
1254
1255        let mut ws = self.pull(branch_id).map_err(RollupError::Pull)?;
1256        let head_handle = ws.head().ok_or(RollupError::EmptyBranch)?;
1257
1258        // Materialise the branch state from its commit chain and build the
1259        // succinct index over it.
1260        let space = ws.checkout(..).map_err(RollupError::Checkout)?;
1261        let archive: SuccinctArchive<OrderedUniverse> = (&*space).into();
1262        drop(space);
1263
1264        // Upload the archive blob directly to storage — no workspace-local
1265        // staging needed; the CAS below references it by handle.
1266        let archive_blob = (&archive).to_blob();
1267        let handle: Inline<
1268            Handle<crate::blob::encodings::succinctarchive::SuccinctArchiveBlob>,
1269        > = self
1270            .storage
1271            .put(archive_blob)
1272            .map_err(|e| RollupError::Push(PushError::StoragePut(e)))?;
1273
1274        // Construct a fresh branch meta that carries the same head as
1275        // `base_branch_meta` plus the new rollup attribute.
1276        let reader = self
1277            .storage
1278            .reader()
1279            .map_err(|e| RollupError::Push(PushError::StorageReader(e)))?;
1280        let base_meta: TribleSet = reader
1281            .get(ws.base_branch_meta)
1282            .map_err(|e| RollupError::Push(PushError::StorageGet(e)))?;
1283        let (branch_name,) = find!(
1284            (name: Inline<Handle<LongString>>),
1285            pattern!(&base_meta, [{ crate::metadata::name: ?name }])
1286        )
1287        .exactly_one()
1288        .map_err(|_| RollupError::Push(PushError::BadBranchMetadata()))?;
1289        let head_blob: TribleSet = reader
1290            .get(head_handle)
1291            .map_err(|e| RollupError::Push(PushError::StorageGet(e)))?;
1292
1293        let new_meta = branch::branch_metadata(
1294            &ws.signing_key,
1295            branch_id,
1296            branch_name,
1297            Some(head_blob.to_blob()),
1298            Some(handle),
1299        );
1300        let new_meta_handle = self
1301            .storage
1302            .put(new_meta)
1303            .map_err(|e| RollupError::Push(PushError::StoragePut(e)))?;
1304
1305        // CAS: swap `base_branch_meta` for the new meta. On conflict, the
1306        // head advanced between our pull and this CAS — the rollup we built
1307        // is stale against the new head, so report upstream.
1308        let update_result = self
1309            .storage
1310            .update(branch_id, Some(ws.base_branch_meta), Some(new_meta_handle))
1311            .map_err(|e| RollupError::Push(PushError::BranchUpdate(e)))?;
1312        match update_result {
1313            PushResult::Success() => Ok(handle),
1314            PushResult::Conflict(_) => Err(RollupError::HeadAdvanced),
1315        }
1316    }
1317}
1318
1319/// A handle to a commit blob in the repository.
1320pub type CommitHandle = Inline<Handle<SimpleArchive>>;
1321type MetadataHandle = Inline<Handle<SimpleArchive>>;
1322/// A set of commit handles, used by [`CommitSelector`] and [`Checkout`].
1323pub type CommitSet = PATCH<INLINE_LEN, IdentitySchema, ()>;
1324type BranchMetaHandle = Inline<Handle<SimpleArchive>>;
1325
1326/// The result of a [`Workspace::checkout`] operation: a [`TribleSet`] paired
1327/// with the set of commits that produced it. Pass the commit set as the start
1328/// of a range selector to obtain incremental deltas on the next checkout.
1329///
1330/// [`Checkout`] dereferences to [`TribleSet`], so it can be used directly with
1331/// `find!`, `pattern!`, and `pattern_changes!`.
1332///
1333/// # Example: incremental updates
1334///
1335/// ```rust,ignore
1336/// let mut changed = repo.pull(branch_id)?.checkout(..)?;
1337/// let mut full = changed.facts().clone();
1338///
1339/// loop {
1340///     // full already includes changed
1341///     for result in pattern_changes!(&full, &changed, [{ ... }]) {
1342///         // process new results
1343///     }
1344///
1345///     // Advance — exclude exactly the commits we already processed.
1346///     changed = repo.pull(branch_id)?.checkout(changed.commits()..)?;
1347///     full += &changed;
1348/// }
1349/// ```
1350#[derive(Debug, Clone)]
1351pub struct Checkout {
1352    facts: TribleSet,
1353    commits: CommitSet,
1354}
1355
1356impl PartialEq<TribleSet> for Checkout {
1357    fn eq(&self, other: &TribleSet) -> bool {
1358        self.facts == *other
1359    }
1360}
1361
1362impl PartialEq<Checkout> for TribleSet {
1363    fn eq(&self, other: &Checkout) -> bool {
1364        *self == other.facts
1365    }
1366}
1367
1368impl Checkout {
1369    /// The checked-out tribles.
1370    pub fn facts(&self) -> &TribleSet {
1371        &self.facts
1372    }
1373
1374    /// The set of commits that produced this checkout. Use as the start of a
1375    /// range selector (`checkout.commits()..`) to exclude these commits
1376    /// on the next checkout and obtain only new data.
1377    pub fn commits(&self) -> CommitSet {
1378        self.commits.clone()
1379    }
1380
1381    /// Consume the checkout and return the inner TribleSet.
1382    pub fn into_facts(self) -> TribleSet {
1383        self.facts
1384    }
1385}
1386
1387impl std::ops::Deref for Checkout {
1388    type Target = TribleSet;
1389    fn deref(&self) -> &TribleSet {
1390        &self.facts
1391    }
1392}
1393
1394impl std::ops::AddAssign<&Checkout> for Checkout {
1395    fn add_assign(&mut self, rhs: &Checkout) {
1396        self.facts += rhs.facts.clone();
1397        self.commits.union(rhs.commits.clone());
1398    }
1399}
1400
1401impl std::ops::Add for Checkout {
1402    type Output = Self;
1403    fn add(mut self, rhs: Self) -> Self {
1404        self.facts += rhs.facts;
1405        self.commits.union(rhs.commits);
1406        self
1407    }
1408}
1409
1410impl std::ops::Add<&Checkout> for Checkout {
1411    type Output = Self;
1412    fn add(mut self, rhs: &Checkout) -> Self {
1413        self += rhs;
1414        self
1415    }
1416}
1417
1418/// The Workspace represents the mutable working area or "staging" state.
1419/// It was formerly known as `Head`. It is sent to worker threads,
1420/// modified (via commits, merges, etc.), and then merged back into the Repository.
1421pub struct Workspace<Blobs: BlobStore> {
1422    /// Staged blobs — added to this workspace but not yet pushed to
1423    /// the underlying repo. Analogous to git's staging area (the
1424    /// index): blobs accumulate here via `put` and friends, then
1425    /// `repo.push(&mut ws)` ships everything as one batch to the
1426    /// durable backend.
1427    pub staged: MemoryBlobStore,
1428    /// The blob storage base for the workspace.
1429    base_blobs: Blobs::Reader,
1430    /// The branch id this workspace is tracking; None for a detached workspace.
1431    base_branch_id: Id,
1432    /// The meta-handle corresponding to the base branch state used for CAS.
1433    base_branch_meta: BranchMetaHandle,
1434    /// Handle to the current commit in the working branch. `None` for an empty branch.
1435    head: Option<CommitHandle>,
1436    /// The branch head snapshot when this workspace was created (pull time).
1437    ///
1438    /// This allows `try_push` to cheaply detect whether the commit head has
1439    /// advanced since the workspace was created without querying the remote
1440    /// branch store.
1441    base_head: Option<CommitHandle>,
1442    /// Signing key used for commit/branch signing.
1443    signing_key: SigningKey,
1444    /// Metadata handle for commits created in this workspace.
1445    commit_metadata: MetadataHandle,
1446}
1447
1448impl<Blobs> fmt::Debug for Workspace<Blobs>
1449where
1450    Blobs: BlobStore,
1451    Blobs::Reader: fmt::Debug,
1452{
1453    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1454        f.debug_struct("Workspace")
1455            .field("staged", &self.staged)
1456            .field("base_blobs", &self.base_blobs)
1457            .field("base_branch_id", &self.base_branch_id)
1458            .field("base_branch_meta", &self.base_branch_meta)
1459            .field("base_head", &self.base_head)
1460            .field("head", &self.head)
1461            .field("commit_metadata", &self.commit_metadata)
1462            .finish()
1463    }
1464}
1465
1466/// Helper trait for [`Workspace::checkout`] specifying commit handles or ranges.
1467pub trait CommitSelector<Blobs: BlobStore> {
1468    fn select(
1469        self,
1470        ws: &mut Workspace<Blobs>,
1471    ) -> Result<
1472        CommitSet,
1473        WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet>::GetError<UnarchiveError>>,
1474    >;
1475}
1476
1477/// Selector that returns every commit reachable from a starting selector.
1478pub struct Ancestors<S>(pub S);
1479
1480/// Convenience function to create an [`Ancestors`] selector.
1481pub fn ancestors<S>(selector: S) -> Ancestors<S> {
1482    Ancestors(selector)
1483}
1484
1485/// Selector that walks every commit in the input set back N parent steps,
1486/// following all parent links (including merge parents). Returns the set
1487/// of all commits found at exactly depth N from the starting set.
1488///
1489/// This is a wavefront expansion: at each step, every commit in the current
1490/// frontier is replaced by all of its parents. After N steps the frontier
1491/// is the result.
1492pub struct NthAncestors<S>(pub S, pub usize);
1493
1494/// Walk `selector` back `n` parent steps through all parent links.
1495pub fn nth_ancestors<S>(selector: S, n: usize) -> NthAncestors<S> {
1496    NthAncestors(selector, n)
1497}
1498
1499/// Selector that returns the direct parents of commits from a starting selector.
1500pub struct Parents<S>(pub S);
1501
1502/// Convenience function to create a [`Parents`] selector.
1503pub fn parents<S>(selector: S) -> Parents<S> {
1504    Parents(selector)
1505}
1506
1507/// Selector that returns commits reachable from either of two selectors but
1508/// not both.
1509pub struct SymmetricDiff<A, B>(pub A, pub B);
1510
1511/// Convenience function to create a [`SymmetricDiff`] selector.
1512pub fn symmetric_diff<A, B>(a: A, b: B) -> SymmetricDiff<A, B> {
1513    SymmetricDiff(a, b)
1514}
1515
1516/// Selector that returns the union of commits returned by two selectors.
1517pub struct Union<A, B> {
1518    left: A,
1519    right: B,
1520}
1521
1522/// Convenience function to create a [`Union`] selector.
1523pub fn union<A, B>(left: A, right: B) -> Union<A, B> {
1524    Union { left, right }
1525}
1526
1527/// Selector that returns the intersection of commits returned by two selectors.
1528pub struct Intersect<A, B> {
1529    left: A,
1530    right: B,
1531}
1532
1533/// Convenience function to create an [`Intersect`] selector.
1534pub fn intersect<A, B>(left: A, right: B) -> Intersect<A, B> {
1535    Intersect { left, right }
1536}
1537
1538/// Selector that returns commits from the left selector that are not also
1539/// returned by the right selector.
1540pub struct Difference<A, B> {
1541    left: A,
1542    right: B,
1543}
1544
1545/// Convenience function to create a [`Difference`] selector.
1546pub fn difference<A, B>(left: A, right: B) -> Difference<A, B> {
1547    Difference { left, right }
1548}
1549
1550/// Selector that returns commits with timestamps in the given inclusive range.
1551pub struct TimeRange(pub Epoch, pub Epoch);
1552
1553/// Convenience function to create a [`TimeRange`] selector.
1554pub fn time_range(start: Epoch, end: Epoch) -> TimeRange {
1555    TimeRange(start, end)
1556}
1557
1558/// Selector that filters commits returned by another selector.
1559pub struct Filter<S, F> {
1560    selector: S,
1561    filter: F,
1562}
1563
1564/// Convenience function to create a [`Filter`] selector.
1565pub fn filter<S, F>(selector: S, filter: F) -> Filter<S, F> {
1566    Filter { selector, filter }
1567}
1568
1569impl<Blobs> CommitSelector<Blobs> for CommitHandle
1570where
1571    Blobs: BlobStore,
1572{
1573    fn select(
1574        self,
1575        _ws: &mut Workspace<Blobs>,
1576    ) -> Result<
1577        CommitSet,
1578        WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet>::GetError<UnarchiveError>>,
1579    > {
1580        let mut patch = CommitSet::new();
1581        patch.insert(&Entry::new(&self.raw));
1582        Ok(patch)
1583    }
1584}
1585
1586impl<Blobs> CommitSelector<Blobs> for CommitSet
1587where
1588    Blobs: BlobStore,
1589{
1590    fn select(
1591        self,
1592        _ws: &mut Workspace<Blobs>,
1593    ) -> Result<
1594        CommitSet,
1595        WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet>::GetError<UnarchiveError>>,
1596    > {
1597        Ok(self)
1598    }
1599}
1600
1601impl<Blobs> CommitSelector<Blobs> for Vec<CommitHandle>
1602where
1603    Blobs: BlobStore,
1604{
1605    fn select(
1606        self,
1607        _ws: &mut Workspace<Blobs>,
1608    ) -> Result<
1609        CommitSet,
1610        WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet>::GetError<UnarchiveError>>,
1611    > {
1612        let mut patch = CommitSet::new();
1613        for handle in self {
1614            patch.insert(&Entry::new(&handle.raw));
1615        }
1616        Ok(patch)
1617    }
1618}
1619
1620impl<Blobs> CommitSelector<Blobs> for &[CommitHandle]
1621where
1622    Blobs: BlobStore,
1623{
1624    fn select(
1625        self,
1626        _ws: &mut Workspace<Blobs>,
1627    ) -> Result<
1628        CommitSet,
1629        WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet>::GetError<UnarchiveError>>,
1630    > {
1631        let mut patch = CommitSet::new();
1632        for handle in self {
1633            patch.insert(&Entry::new(&handle.raw));
1634        }
1635        Ok(patch)
1636    }
1637}
1638
1639impl<Blobs> CommitSelector<Blobs> for Option<CommitHandle>
1640where
1641    Blobs: BlobStore,
1642{
1643    fn select(
1644        self,
1645        _ws: &mut Workspace<Blobs>,
1646    ) -> Result<
1647        CommitSet,
1648        WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet>::GetError<UnarchiveError>>,
1649    > {
1650        let mut patch = CommitSet::new();
1651        if let Some(handle) = self {
1652            patch.insert(&Entry::new(&handle.raw));
1653        }
1654        Ok(patch)
1655    }
1656}
1657
1658impl<S, Blobs> CommitSelector<Blobs> for Ancestors<S>
1659where
1660    S: CommitSelector<Blobs>,
1661    Blobs: BlobStore,
1662{
1663    fn select(
1664        self,
1665        ws: &mut Workspace<Blobs>,
1666    ) -> Result<
1667        CommitSet,
1668        WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet>::GetError<UnarchiveError>>,
1669    > {
1670        let seeds = self.0.select(ws)?;
1671        collect_reachable_from_patch(ws, seeds)
1672    }
1673}
1674
1675impl<Blobs, S> CommitSelector<Blobs> for NthAncestors<S>
1676where
1677    Blobs: BlobStore,
1678    S: CommitSelector<Blobs>,
1679{
1680    fn select(
1681        self,
1682        ws: &mut Workspace<Blobs>,
1683    ) -> Result<
1684        CommitSet,
1685        WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet>::GetError<UnarchiveError>>,
1686    > {
1687        let mut frontier = self.0.select(ws)?;
1688        let mut remaining = self.1;
1689
1690        while remaining > 0 && !frontier.is_empty() {
1691            // Collect current frontier keys before mutating.
1692            let keys: Vec<[u8; INLINE_LEN]> = frontier.iter().copied().collect();
1693            let mut next_frontier = CommitSet::new();
1694            for raw in keys {
1695                let handle = CommitHandle::new(raw);
1696                let meta: TribleSet = ws.get(handle).map_err(WorkspaceCheckoutError::Storage)?;
1697                for (p,) in find!((p: Inline<_>), pattern!(&meta, [{ parent: ?p }])) {
1698                    next_frontier.insert(&Entry::new(&p.raw));
1699                }
1700            }
1701            frontier = next_frontier;
1702            remaining -= 1;
1703        }
1704
1705        Ok(frontier)
1706    }
1707}
1708
1709impl<S, Blobs> CommitSelector<Blobs> for Parents<S>
1710where
1711    S: CommitSelector<Blobs>,
1712    Blobs: BlobStore,
1713{
1714    fn select(
1715        self,
1716        ws: &mut Workspace<Blobs>,
1717    ) -> Result<
1718        CommitSet,
1719        WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet>::GetError<UnarchiveError>>,
1720    > {
1721        let seeds = self.0.select(ws)?;
1722        let mut result = CommitSet::new();
1723        for raw in seeds.iter() {
1724            let handle = Inline::new(*raw);
1725            let meta: TribleSet = ws.get(handle).map_err(WorkspaceCheckoutError::Storage)?;
1726            for (p,) in find!((p: Inline<_>), pattern!(&meta, [{ parent: ?p }])) {
1727                result.insert(&Entry::new(&p.raw));
1728            }
1729        }
1730        Ok(result)
1731    }
1732}
1733
1734impl<A, B, Blobs> CommitSelector<Blobs> for SymmetricDiff<A, B>
1735where
1736    A: CommitSelector<Blobs>,
1737    B: CommitSelector<Blobs>,
1738    Blobs: BlobStore,
1739{
1740    fn select(
1741        self,
1742        ws: &mut Workspace<Blobs>,
1743    ) -> Result<
1744        CommitSet,
1745        WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet>::GetError<UnarchiveError>>,
1746    > {
1747        let seeds_a = self.0.select(ws)?;
1748        let seeds_b = self.1.select(ws)?;
1749        let a = collect_reachable_from_patch(ws, seeds_a)?;
1750        let b = collect_reachable_from_patch(ws, seeds_b)?;
1751        let inter = a.intersect(&b);
1752        let mut union = a;
1753        union.union(b);
1754        Ok(union.difference(&inter))
1755    }
1756}
1757
1758impl<A, B, Blobs> CommitSelector<Blobs> for Union<A, B>
1759where
1760    A: CommitSelector<Blobs>,
1761    B: CommitSelector<Blobs>,
1762    Blobs: BlobStore,
1763{
1764    fn select(
1765        self,
1766        ws: &mut Workspace<Blobs>,
1767    ) -> Result<
1768        CommitSet,
1769        WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet>::GetError<UnarchiveError>>,
1770    > {
1771        let mut left = self.left.select(ws)?;
1772        let right = self.right.select(ws)?;
1773        left.union(right);
1774        Ok(left)
1775    }
1776}
1777
1778impl<A, B, Blobs> CommitSelector<Blobs> for Intersect<A, B>
1779where
1780    A: CommitSelector<Blobs>,
1781    B: CommitSelector<Blobs>,
1782    Blobs: BlobStore,
1783{
1784    fn select(
1785        self,
1786        ws: &mut Workspace<Blobs>,
1787    ) -> Result<
1788        CommitSet,
1789        WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet>::GetError<UnarchiveError>>,
1790    > {
1791        let left = self.left.select(ws)?;
1792        let right = self.right.select(ws)?;
1793        Ok(left.intersect(&right))
1794    }
1795}
1796
1797impl<A, B, Blobs> CommitSelector<Blobs> for Difference<A, B>
1798where
1799    A: CommitSelector<Blobs>,
1800    B: CommitSelector<Blobs>,
1801    Blobs: BlobStore,
1802{
1803    fn select(
1804        self,
1805        ws: &mut Workspace<Blobs>,
1806    ) -> Result<
1807        CommitSet,
1808        WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet>::GetError<UnarchiveError>>,
1809    > {
1810        let left = self.left.select(ws)?;
1811        let right = self.right.select(ws)?;
1812        Ok(left.difference(&right))
1813    }
1814}
1815
1816impl<S, F, Blobs> CommitSelector<Blobs> for Filter<S, F>
1817where
1818    Blobs: BlobStore,
1819    S: CommitSelector<Blobs>,
1820    F: for<'x, 'y> Fn(&'x TribleSet, &'y TribleSet) -> bool,
1821{
1822    fn select(
1823        self,
1824        ws: &mut Workspace<Blobs>,
1825    ) -> Result<
1826        CommitSet,
1827        WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet>::GetError<UnarchiveError>>,
1828    > {
1829        let patch = self.selector.select(ws)?;
1830        let mut result = CommitSet::new();
1831        let filter = self.filter;
1832        for raw in patch.iter() {
1833            let handle = Inline::new(*raw);
1834            let meta: TribleSet = ws.get(handle).map_err(WorkspaceCheckoutError::Storage)?;
1835
1836            let Ok((content_handle,)) = find!(
1837                (c: Inline<_>),
1838                pattern!(&meta, [{ content: ?c }])
1839            )
1840            .exactly_one() else {
1841                return Err(WorkspaceCheckoutError::BadCommitMetadata());
1842            };
1843
1844            let payload: TribleSet = ws
1845                .get(content_handle)
1846                .map_err(WorkspaceCheckoutError::Storage)?;
1847
1848            if filter(&meta, &payload) {
1849                result.insert(&Entry::new(raw));
1850            }
1851        }
1852        Ok(result)
1853    }
1854}
1855
1856/// Selector that yields commits touching a specific entity.
1857pub struct HistoryOf(pub Id);
1858
1859/// Convenience function to create a [`HistoryOf`] selector.
1860pub fn history_of(entity: Id) -> HistoryOf {
1861    HistoryOf(entity)
1862}
1863
1864impl<Blobs> CommitSelector<Blobs> for HistoryOf
1865where
1866    Blobs: BlobStore,
1867{
1868    fn select(
1869        self,
1870        ws: &mut Workspace<Blobs>,
1871    ) -> Result<
1872        CommitSet,
1873        WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet>::GetError<UnarchiveError>>,
1874    > {
1875        let Some(head_) = ws.head else {
1876            return Ok(CommitSet::new());
1877        };
1878        let entity = self.0;
1879        filter(
1880            ancestors(head_),
1881            move |_: &TribleSet, payload: &TribleSet| payload.iter().any(|t| t.e() == &entity),
1882        )
1883        .select(ws)
1884    }
1885}
1886
1887// Generic range selectors: allow any selector type to be used as a range
1888// endpoint. We still walk the history reachable from the end selector but now
1889// stop descending a branch as soon as we encounter a commit produced by the
1890// start selector. This keeps the mechanics explicit—`start..end` literally
1891// walks from `end` until it hits `start`—while continuing to support selectors
1892// such as `Ancestors(...)` at either boundary.
1893
1894fn collect_reachable_from_patch<Blobs: BlobStore>(
1895    ws: &mut Workspace<Blobs>,
1896    patch: CommitSet,
1897) -> Result<
1898    CommitSet,
1899    WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet>::GetError<UnarchiveError>>,
1900> {
1901    let mut result = CommitSet::new();
1902    for raw in patch.iter() {
1903        let handle = Inline::new(*raw);
1904        let reach = collect_reachable(ws, handle)?;
1905        result.union(reach);
1906    }
1907    Ok(result)
1908}
1909
1910fn collect_reachable_from_patch_until<Blobs: BlobStore>(
1911    ws: &mut Workspace<Blobs>,
1912    seeds: CommitSet,
1913    stop: &CommitSet,
1914) -> Result<
1915    CommitSet,
1916    WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet>::GetError<UnarchiveError>>,
1917> {
1918    let mut visited = HashSet::new();
1919    let mut stack: Vec<CommitHandle> = seeds.iter().map(|raw| Inline::new(*raw)).collect();
1920    let mut result = CommitSet::new();
1921
1922    while let Some(commit) = stack.pop() {
1923        if !visited.insert(commit) {
1924            continue;
1925        }
1926
1927        if stop.get(&commit.raw).is_some() {
1928            continue;
1929        }
1930
1931        result.insert(&Entry::new(&commit.raw));
1932
1933        let meta: TribleSet = ws
1934            .staged
1935            .reader()
1936            .unwrap()
1937            .get(commit)
1938            .or_else(|_| ws.base_blobs.get(commit))
1939            .map_err(WorkspaceCheckoutError::Storage)?;
1940
1941        for (p,) in find!((p: Inline<_>,), pattern!(&meta, [{ parent: ?p }])) {
1942            stack.push(p);
1943        }
1944    }
1945
1946    Ok(result)
1947}
1948
1949impl<T, Blobs> CommitSelector<Blobs> for std::ops::Range<T>
1950where
1951    T: CommitSelector<Blobs>,
1952    Blobs: BlobStore,
1953{
1954    fn select(
1955        self,
1956        ws: &mut Workspace<Blobs>,
1957    ) -> Result<
1958        CommitSet,
1959        WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet>::GetError<UnarchiveError>>,
1960    > {
1961        let end_patch = self.end.select(ws)?;
1962        let start_patch = self.start.select(ws)?;
1963
1964        collect_reachable_from_patch_until(ws, end_patch, &start_patch)
1965    }
1966}
1967
1968impl<T, Blobs> CommitSelector<Blobs> for std::ops::RangeFrom<T>
1969where
1970    T: CommitSelector<Blobs>,
1971    Blobs: BlobStore,
1972{
1973    fn select(
1974        self,
1975        ws: &mut Workspace<Blobs>,
1976    ) -> Result<
1977        CommitSet,
1978        WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet>::GetError<UnarchiveError>>,
1979    > {
1980        let Some(head_) = ws.head else {
1981            return Ok(CommitSet::new());
1982        };
1983        let exclude_patch = self.start.select(ws)?;
1984
1985        let mut head_patch = CommitSet::new();
1986        head_patch.insert(&Entry::new(&head_.raw));
1987
1988        collect_reachable_from_patch_until(ws, head_patch, &exclude_patch)
1989    }
1990}
1991
1992impl<T, Blobs> CommitSelector<Blobs> for std::ops::RangeTo<T>
1993where
1994    T: CommitSelector<Blobs>,
1995    Blobs: BlobStore,
1996{
1997    fn select(
1998        self,
1999        ws: &mut Workspace<Blobs>,
2000    ) -> Result<
2001        CommitSet,
2002        WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet>::GetError<UnarchiveError>>,
2003    > {
2004        let end_patch = self.end.select(ws)?;
2005        collect_reachable_from_patch(ws, end_patch)
2006    }
2007}
2008
2009impl<Blobs> CommitSelector<Blobs> for std::ops::RangeFull
2010where
2011    Blobs: BlobStore,
2012{
2013    fn select(
2014        self,
2015        ws: &mut Workspace<Blobs>,
2016    ) -> Result<
2017        CommitSet,
2018        WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet>::GetError<UnarchiveError>>,
2019    > {
2020        let Some(head_) = ws.head else {
2021            return Ok(CommitSet::new());
2022        };
2023        collect_reachable(ws, head_)
2024    }
2025}
2026
2027impl<Blobs> CommitSelector<Blobs> for TimeRange
2028where
2029    Blobs: BlobStore,
2030{
2031    fn select(
2032        self,
2033        ws: &mut Workspace<Blobs>,
2034    ) -> Result<
2035        CommitSet,
2036        WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet>::GetError<UnarchiveError>>,
2037    > {
2038        let Some(head_) = ws.head else {
2039            return Ok(CommitSet::new());
2040        };
2041        let start = self.0;
2042        let end = self.1;
2043        filter(
2044            ancestors(head_),
2045            move |meta: &TribleSet, _payload: &TribleSet| {
2046                if let Ok(Some(((ts_start, ts_end),))) =
2047                    find!((t: (Epoch, Epoch)), pattern!(meta, [{ crate::metadata::created_at: ?t }])).at_most_one()
2048                {
2049                    ts_start <= end && ts_end >= start
2050                } else {
2051                    false
2052                }
2053            },
2054        )
2055        .select(ws)
2056    }
2057}
2058
2059impl<Blobs: BlobStore> Workspace<Blobs> {
2060    /// Returns the branch id associated with this workspace.
2061    pub fn branch_id(&self) -> Id {
2062        self.base_branch_id
2063    }
2064
2065    /// Returns the current commit handle if one exists.
2066    pub fn head(&self) -> Option<CommitHandle> {
2067        self.head
2068    }
2069
2070    /// Returns the workspace metadata handle.
2071    pub fn metadata(&self) -> MetadataHandle {
2072        self.commit_metadata
2073    }
2074
2075    /// Reads the rollup handle, if any, from the workspace's base branch
2076    /// metadata. Returns `None` if the branch has no rollup yet or if the
2077    /// metadata is missing the attribute. Readers can use this to fetch
2078    /// the archive blob directly and skip `checkout(..)` for warm queries:
2079    ///
2080    /// ```rust,ignore
2081    /// let mut ws = repo.pull(branch)?;
2082    /// match ws.rollup()? {
2083    ///     Some(h) => {
2084    ///         let archive: SuccinctArchive<_> = ws.get(h)?;
2085    ///         // query archive
2086    ///     }
2087    ///     None => {
2088    ///         let space = ws.checkout(..)?;
2089    ///         // query space (commit-chain materialisation)
2090    ///     }
2091    /// }
2092    /// ```
2093    ///
2094    /// Writers don't go through this — attach a rollup via
2095    /// [`Repository::compute_rollup`] instead.
2096    pub fn rollup(
2097        &mut self,
2098    ) -> Result<
2099        Option<Inline<Handle<SuccinctArchiveBlob>>>,
2100        <Blobs::Reader as BlobStoreGet>::GetError<UnarchiveError>,
2101    > {
2102        let base_meta: TribleSet = self.base_blobs.get(self.base_branch_meta)?;
2103        Ok(
2104            find!(
2105                (r: Inline<Handle<SuccinctArchiveBlob>>),
2106                pattern!(&base_meta, [{ rollup: ?r }])
2107            )
2108            .next()
2109            .map(|(r,)| r),
2110        )
2111    }
2112
2113    /// Adds a blob to the workspace's local blob store.
2114    /// Mirrors [`BlobStorePut::put`](crate::repo::BlobStorePut) for ease of use.
2115    pub fn put<S, T>(&mut self, item: T) -> Inline<Handle<S>>
2116    where
2117        S: BlobEncoding + 'static,
2118        T: IntoBlob<S>,
2119        Handle<S>: InlineEncoding,
2120    {
2121        self.staged.put(item).expect("infallible blob put")
2122    }
2123
2124
2125    /// Retrieves a blob from the workspace.
2126    ///
2127    /// The method first checks the workspace's local blob store and falls back
2128    /// to the base blob store if the blob is not found locally.
2129    pub fn get<T, S>(
2130        &mut self,
2131        handle: Inline<Handle<S>>,
2132    ) -> Result<T, <Blobs::Reader as BlobStoreGet>::GetError<<T as TryFromBlob<S>>::Error>>
2133    where
2134        S: BlobEncoding + 'static,
2135        T: TryFromBlob<S>,
2136        Handle<S>: InlineEncoding,
2137    {
2138        self.staged
2139            .reader()
2140            .unwrap()
2141            .get(handle)
2142            .or_else(|_| self.base_blobs.get(handle))
2143    }
2144
2145    /// Performs a commit in the workspace.
2146    ///
2147    /// Accepts anything that converts into a [`Fragment`] — either a
2148    /// raw [`TribleSet`] (auto-promoted to a Fragment with empty blob
2149    /// store), or a Fragment built up via `entity!{}` /
2150    /// `MetaDescribe::describe()` whose embedded blobs get absorbed
2151    /// into `self.staged` alongside the commit-content blob.
2152    /// This method creates a new commit blob (stored in the local
2153    /// blobset) and updates the current commit handle.
2154    pub fn commit(&mut self, content_: impl Into<Fragment>, message_: &str) {
2155        self.commit_internal(content_.into(), Some(self.commit_metadata), Some(message_));
2156    }
2157
2158    /// Like [`commit`](Self::commit) but attaches a one-off metadata handle
2159    /// instead of the repository default.
2160    pub fn commit_with_metadata(
2161        &mut self,
2162        content_: impl Into<Fragment>,
2163        metadata_: MetadataHandle,
2164        message_: &str,
2165    ) {
2166        self.commit_internal(content_.into(), Some(metadata_), Some(message_));
2167    }
2168
2169    fn commit_internal(
2170        &mut self,
2171        content_: Fragment,
2172        metadata_handle: Option<MetadataHandle>,
2173        message_: Option<&str>,
2174    ) {
2175        let (content_facts, content_blobs) = content_.into_facts_and_blobs();
2176        // 0. Absorb any blobs the Fragment carried with it into the
2177        //    staging area before producing the commit blob, so handles
2178        //    inside `content_facts` resolve against `self.staged`.
2179        self.staged.union(content_blobs);
2180        // 1. Create a commit blob from the current head, content, metadata and the commit message.
2181        let content_blob: Blob<SimpleArchive> = content_facts.to_blob();
2182        // If a message is provided, store it as a LongString blob and pass the handle.
2183        let message_handle = message_.map(|m| self.put(m.to_string()));
2184        let parents = self.head.iter().copied();
2185
2186        let commit_set = crate::repo::commit::commit_metadata(
2187            &self.signing_key,
2188            parents,
2189            message_handle,
2190            Some(content_blob.clone()),
2191            metadata_handle,
2192        );
2193        // 2. Store the content and commit blobs in `self.staged`.
2194        let _ = self
2195            .staged
2196            .put::<SimpleArchive, _>(content_blob)
2197            .expect("failed to put content blob");
2198        let commit_handle = self
2199            .staged
2200            .put(commit_set)
2201            .expect("failed to put commit blob");
2202        // 3. Update `self.head` to point to the new commit.
2203        self.head = Some(commit_handle);
2204    }
2205
2206    /// Merge another workspace into this one.
2207    ///
2208    /// Always copies the *staged* blobs from `other.staged` into
2209    /// `self.staged` (so standalone blobs that aren't referenced by any
2210    /// commit chain still come along — useful when the other workspace was
2211    /// being used to stage content).
2212    ///
2213    /// Then integrates `other.head` via [`merge_commit`](Self::merge_commit),
2214    /// which picks no-op / fast-forward / merge commit as appropriate.
2215    ///
2216    /// Returns the workspace's new head, or `None` if both workspaces were
2217    /// empty (nothing to merge into anything).
2218    ///
2219    /// Notes:
2220    /// - The merge does *not* automatically import the entire base history
2221    ///   reachable from `other`'s head. If the incoming parent commits
2222    ///   reference blobs that do not exist in this repository's storage,
2223    ///   reading those commits later will fail until the missing blobs are
2224    ///   explicitly imported (for example via `repo::transfer(reachable(...))`).
2225    /// - This design keeps merge permissive and leaves cross-repository blob
2226    ///   import as an explicit user action.
2227    pub fn merge(
2228        &mut self,
2229        other: &mut Workspace<Blobs>,
2230    ) -> Result<Option<CommitHandle>, MergeError> {
2231        // 1. Always transfer staged blobs from `other`. They may include
2232        //    standalone blobs (no commit referring to them yet) that the
2233        //    caller wanted to stash in the workspace independent of any
2234        //    branch state.
2235        let other_local = other.staged.reader().unwrap();
2236        for r in other_local.blobs() {
2237            let handle = r.expect("infallible blob enumeration");
2238            let blob: Blob<UnknownBlob> = other_local.get(handle).expect("infallible blob read");
2239            self.staged
2240                .put::<UnknownBlob, _>(blob)
2241                .expect("infallible blob put");
2242        }
2243
2244        // 2. Integrate `other`'s head via the smart merge_commit. If `other`
2245        //    has no head, there's nothing further to integrate — just return
2246        //    our current head (which may or may not exist).
2247        match other.head {
2248            Some(other_head) => Ok(Some(self.merge_commit(other_head)?)),
2249            None => Ok(self.head),
2250        }
2251    }
2252
2253    /// Integrate another commit into this workspace's history.
2254    ///
2255    /// Picks the cheapest correct strategy:
2256    ///
2257    /// - **No-op** if the workspace has no head and `other` *is* the head, or
2258    ///   if `other` is already in the current head's ancestry.
2259    /// - **Fast-forward** if the workspace has no head, or if the current head
2260    ///   is in `other`'s ancestry — `self.head` is set to `other` directly.
2261    /// - **Merge commit** otherwise — a new commit with `[current_head, other]`
2262    ///   as parents is created and `self.head` advances to it.
2263    ///
2264    /// Returns the workspace's new head in all cases.
2265    ///
2266    /// The ancestor checks are best-effort: if the relevant commit blobs are
2267    /// missing from the workspace's view, the function falls through to the
2268    /// always-correct merge-commit path. Callers that mirror remote chains
2269    /// should ensure reachable blobs were imported (e.g. via `reachable` +
2270    /// `transfer`) for the optimization to kick in.
2271    pub fn merge_commit(
2272        &mut self,
2273        other: Inline<Handle<SimpleArchive>>,
2274    ) -> Result<CommitHandle, MergeError> {
2275        // Trivial cases first.
2276        let local_head = match self.head {
2277            None => {
2278                // No local head — fast-forward to `other`.
2279                self.head = Some(other);
2280                return Ok(other);
2281            }
2282            Some(h) if h == other => {
2283                // Identical — no-op.
2284                return Ok(h);
2285            }
2286            Some(h) => h,
2287        };
2288
2289        // Best-effort ancestry checks. If the walks fail (missing blobs,
2290        // unreadable metadata), fall through to the always-correct merge.
2291        let remote_in_local = ancestors(local_head)
2292            .select(self)
2293            .ok()
2294            .map(|set| set.get(&other.raw).is_some())
2295            .unwrap_or(false);
2296        if remote_in_local {
2297            // `other` is already in our history → no-op.
2298            return Ok(local_head);
2299        }
2300
2301        let local_in_remote = ancestors(other)
2302            .select(self)
2303            .ok()
2304            .map(|set| set.get(&local_head.raw).is_some())
2305            .unwrap_or(false);
2306        if local_in_remote {
2307            // We're behind `other` → fast-forward.
2308            self.head = Some(other);
2309            return Ok(other);
2310        }
2311
2312        // Truly divergent — create a merge commit.
2313        let parents = self.head.iter().copied().chain(Some(other));
2314        let merge_commit = commit_metadata(&self.signing_key, parents, None, None, None);
2315        let commit_handle = self
2316            .staged
2317            .put(merge_commit)
2318            .expect("failed to put merge commit blob");
2319        self.head = Some(commit_handle);
2320        Ok(commit_handle)
2321    }
2322
2323    /// Move the workspace's head to `commit` without creating a new commit.
2324    ///
2325    /// This is the "fast-forward" case: when the new commit is a descendant
2326    /// of (or equal to) the current head, you can advance directly without
2327    /// a merge commit. The caller is responsible for verifying the
2328    /// descendancy relationship — typically via [`ancestors`] over `commit`.
2329    ///
2330    /// Use this in pull/sync flows to avoid spurious merge commits when one
2331    /// peer is simply behind the other.
2332    pub fn set_head(&mut self, commit: CommitHandle) {
2333        self.head = Some(commit);
2334    }
2335
2336    /// Returns the combined [`TribleSet`] for the specified commits.
2337    ///
2338    /// Each commit handle must reference a commit blob stored either in the
2339    /// workspace's local blob store or the repository's base store. The
2340    /// associated content blobs are loaded and unioned together. An error is
2341    /// returned if any commit or content blob is missing or malformed.
2342    fn checkout_commits<I>(
2343        &mut self,
2344        commits: I,
2345    ) -> Result<
2346        TribleSet,
2347        WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet>::GetError<UnarchiveError>>,
2348    >
2349    where
2350        I: IntoIterator<Item = CommitHandle>,
2351    {
2352        let local = self.staged.reader().unwrap();
2353        let mut result = TribleSet::new();
2354        for commit in commits {
2355            let meta: TribleSet = local
2356                .get(commit)
2357                .or_else(|_| self.base_blobs.get(commit))
2358                .map_err(WorkspaceCheckoutError::Storage)?;
2359
2360            // Some commits (for example merge commits) intentionally do not
2361            // carry a content blob. Treat those as no-ops during checkout so
2362            // callers can request ancestor ranges without failing when a
2363            // merge commit is encountered.
2364            let content_opt =
2365                match find!((c: Inline<_>), pattern!(&meta, [{ content: ?c }])).at_most_one() {
2366                    Ok(Some((c,))) => Some(c),
2367                    Ok(None) => None,
2368                    Err(_) => return Err(WorkspaceCheckoutError::BadCommitMetadata()),
2369                };
2370
2371            if let Some(c) = content_opt {
2372                let set: TribleSet = local
2373                    .get(c)
2374                    .or_else(|_| self.base_blobs.get(c))
2375                    .map_err(WorkspaceCheckoutError::Storage)?;
2376                result += set;
2377            } else {
2378                // No content for this commit (e.g. merge-only commit); skip it.
2379                continue;
2380            }
2381        }
2382        Ok(result)
2383    }
2384
2385    fn checkout_commits_metadata<I>(
2386        &mut self,
2387        commits: I,
2388    ) -> Result<
2389        TribleSet,
2390        WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet>::GetError<UnarchiveError>>,
2391    >
2392    where
2393        I: IntoIterator<Item = CommitHandle>,
2394    {
2395        let local = self.staged.reader().unwrap();
2396        let mut result = TribleSet::new();
2397        for commit in commits {
2398            let meta: TribleSet = local
2399                .get(commit)
2400                .or_else(|_| self.base_blobs.get(commit))
2401                .map_err(WorkspaceCheckoutError::Storage)?;
2402
2403            let metadata_opt =
2404                match find!((c: Inline<_>), pattern!(&meta, [{ metadata: ?c }])).at_most_one() {
2405                    Ok(Some((c,))) => Some(c),
2406                    Ok(None) => None,
2407                    Err(_) => return Err(WorkspaceCheckoutError::BadCommitMetadata()),
2408                };
2409
2410            if let Some(c) = metadata_opt {
2411                let set: TribleSet = local
2412                    .get(c)
2413                    .or_else(|_| self.base_blobs.get(c))
2414                    .map_err(WorkspaceCheckoutError::Storage)?;
2415                result += set;
2416            }
2417        }
2418        Ok(result)
2419    }
2420
2421    fn checkout_commits_with_metadata<I>(
2422        &mut self,
2423        commits: I,
2424    ) -> Result<
2425        (TribleSet, TribleSet),
2426        WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet>::GetError<UnarchiveError>>,
2427    >
2428    where
2429        I: IntoIterator<Item = CommitHandle>,
2430    {
2431        let local = self.staged.reader().unwrap();
2432        let mut data = TribleSet::new();
2433        let mut metadata_set = TribleSet::new();
2434        for commit in commits {
2435            let meta: TribleSet = local
2436                .get(commit)
2437                .or_else(|_| self.base_blobs.get(commit))
2438                .map_err(WorkspaceCheckoutError::Storage)?;
2439
2440            let content_opt =
2441                match find!((c: Inline<_>), pattern!(&meta, [{ content: ?c }])).at_most_one() {
2442                    Ok(Some((c,))) => Some(c),
2443                    Ok(None) => None,
2444                    Err(_) => return Err(WorkspaceCheckoutError::BadCommitMetadata()),
2445                };
2446
2447            if let Some(c) = content_opt {
2448                let set: TribleSet = local
2449                    .get(c)
2450                    .or_else(|_| self.base_blobs.get(c))
2451                    .map_err(WorkspaceCheckoutError::Storage)?;
2452                data += set;
2453            }
2454
2455            let metadata_opt =
2456                match find!((c: Inline<_>), pattern!(&meta, [{ metadata: ?c }])).at_most_one() {
2457                    Ok(Some((c,))) => Some(c),
2458                    Ok(None) => None,
2459                    Err(_) => return Err(WorkspaceCheckoutError::BadCommitMetadata()),
2460                };
2461
2462            if let Some(c) = metadata_opt {
2463                let set: TribleSet = local
2464                    .get(c)
2465                    .or_else(|_| self.base_blobs.get(c))
2466                    .map_err(WorkspaceCheckoutError::Storage)?;
2467                metadata_set += set;
2468            }
2469        }
2470        Ok((data, metadata_set))
2471    }
2472
2473    /// Returns the combined [`TribleSet`] for the specified commits or commit
2474    /// ranges. `spec` can be a single [`CommitHandle`], an iterator of handles
2475    /// or any of the standard range types over [`CommitHandle`].
2476    pub fn checkout<R>(
2477        &mut self,
2478        spec: R,
2479    ) -> Result<
2480        Checkout,
2481        WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet>::GetError<UnarchiveError>>,
2482    >
2483    where
2484        R: CommitSelector<Blobs>,
2485    {
2486        let commits = spec.select(self)?;
2487        let facts = self.checkout_commits(commits.iter().map(|raw| Inline::new(*raw)))?;
2488        Ok(Checkout { facts, commits })
2489    }
2490
2491    /// Returns the combined metadata [`TribleSet`] for the specified commits.
2492    /// Commits without metadata handles contribute an empty set.
2493    pub fn checkout_metadata<R>(
2494        &mut self,
2495        spec: R,
2496    ) -> Result<
2497        TribleSet,
2498        WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet>::GetError<UnarchiveError>>,
2499    >
2500    where
2501        R: CommitSelector<Blobs>,
2502    {
2503        let patch = spec.select(self)?;
2504        let commits = patch.iter().map(|raw| Inline::new(*raw));
2505        self.checkout_commits_metadata(commits)
2506    }
2507
2508    /// Returns the combined data and metadata [`TribleSet`] for the specified commits.
2509    /// Metadata is loaded from each commit's `metadata` handle, when present.
2510    pub fn checkout_with_metadata<R>(
2511        &mut self,
2512        spec: R,
2513    ) -> Result<
2514        (TribleSet, TribleSet),
2515        WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet>::GetError<UnarchiveError>>,
2516    >
2517    where
2518        R: CommitSelector<Blobs>,
2519    {
2520        let patch = spec.select(self)?;
2521        let commits = patch.iter().map(|raw| Inline::new(*raw));
2522        self.checkout_commits_with_metadata(commits)
2523    }
2524}
2525
2526#[derive(Debug)]
2527pub enum WorkspaceCheckoutError<GetErr: Error> {
2528    /// Error retrieving blobs from storage.
2529    Storage(GetErr),
2530    /// Commit metadata is malformed or ambiguous.
2531    BadCommitMetadata(),
2532}
2533
2534impl<E: Error + fmt::Debug> fmt::Display for WorkspaceCheckoutError<E> {
2535    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2536        match self {
2537            WorkspaceCheckoutError::Storage(e) => write!(f, "storage error: {e}"),
2538            WorkspaceCheckoutError::BadCommitMetadata() => {
2539                write!(f, "commit metadata malformed")
2540            }
2541        }
2542    }
2543}
2544
2545impl<E: Error + fmt::Debug> Error for WorkspaceCheckoutError<E> {}
2546
2547fn collect_reachable<Blobs: BlobStore>(
2548    ws: &mut Workspace<Blobs>,
2549    from: CommitHandle,
2550) -> Result<
2551    CommitSet,
2552    WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet>::GetError<UnarchiveError>>,
2553> {
2554    let mut visited = HashSet::new();
2555    let mut stack = vec![from];
2556    let mut result = CommitSet::new();
2557
2558    while let Some(commit) = stack.pop() {
2559        if !visited.insert(commit) {
2560            continue;
2561        }
2562        result.insert(&Entry::new(&commit.raw));
2563
2564        let meta: TribleSet = ws
2565            .staged
2566            .reader()
2567            .unwrap()
2568            .get(commit)
2569            .or_else(|_| ws.base_blobs.get(commit))
2570            .map_err(WorkspaceCheckoutError::Storage)?;
2571
2572        for (p,) in find!((p: Inline<_>,), pattern!(&meta, [{ parent: ?p }])) {
2573            stack.push(p);
2574        }
2575    }
2576
2577    Ok(result)
2578}