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 + PinStore + 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 pin update (used by both the
395/// primitive `PinStore::update` and the higher-level
396/// `Repository::push` for content branches).
397#[derive(Debug)]
398pub enum PushResult {
399 /// The CAS succeeded — the pin now points to the new value.
400 Success(),
401 /// The CAS failed — the pin's head had advanced. Contains the
402 /// current head, or `None` if the pin was tombstoned concurrently.
403 Conflict(Option<Inline<Handle<SimpleArchive>>>),
404}
405
406/// Storage backend for pins: named, atomically-updatable handles to
407/// SimpleArchive blobs.
408///
409/// A *pin* is the storage primitive — a named cell holding a single
410/// `Inline<Handle<SimpleArchive>>`, updated via compare-and-swap. The
411/// pile's compaction sweep treats every pin head as a reachability
412/// root: blobs reachable from a pin survive; the rest are reclaimed.
413///
414/// Pins back several specialized use patterns, distinguished at
415/// higher layers via metadata markers:
416/// - A **branch** is a pin whose value resolves to a commit-chain
417/// head (Repository's content abstraction). Branch metadata
418/// carries `metadata::name` for human-readable lookup.
419/// - A **tracking pin** mirrors a remote peer's branch head and
420/// carries `tracking_remote_pin` + `remote_name`.
421/// - A **local-only pin** (renewal policy, pending requests,
422/// per-team cap holdings) carries `local_only_pin: <kind>` and is
423/// excluded from gossip publication.
424///
425/// `PinStore` itself doesn't know about these distinctions — it just
426/// provides the primitive: enumerate ids, read the current head, CAS
427/// an update. The two-level taxonomy lives at higher layers
428/// (decide#6de2dd95).
429///
430/// This trait is the stateful counterpart to [`BlobStore`]: blob
431/// stores are content-addressed and orderless; pin stores track a
432/// single mutable pointer per pin. The update operation uses
433/// compare-and-swap semantics so multiple writers can coordinate
434/// without locks.
435pub trait PinStore {
436 /// Error type for listing pins.
437 type PinsError: Error + Debug + Send + Sync + 'static;
438 /// Error type for head lookups.
439 type HeadError: Error + Debug + Send + Sync + 'static;
440 /// Error type for CAS updates.
441 type UpdateError: Error + Debug + Send + Sync + 'static;
442
443 /// Iterator over pin IDs.
444 type ListIter<'a>: Iterator<Item = Result<Id, Self::PinsError>>
445 where
446 Self: 'a;
447
448 /// Lists every pin in the store. Returns a fallible iterator over
449 /// pin ids (any role — branches, tracking pins, local-only pins).
450 /// Callers that want only content branches filter by checking for
451 /// the `metadata::name` attribute on each pin's head metadata.
452 fn pins<'a>(&'a mut self) -> Result<Self::ListIter<'a>, Self::PinsError>;
453
454 // NOTE: keep the API lean — callers may call `pins()` and handle
455 // the fallible iterator directly; we avoid adding an extra helper
456 // here.
457
458 /// Retrieves the current head of a pin by its id.
459 ///
460 /// Returns `Ok(Some(handle))` if the pin exists and has a head,
461 /// `Ok(None)` if the pin is tombstoned (deleted), and an error if
462 /// the underlying store failed to read.
463 ///
464 /// # Parameters
465 /// * `id` — The id of the pin to look up.
466 fn head(&mut self, id: Id) -> Result<Option<Inline<Handle<SimpleArchive>>>, Self::HeadError>;
467
468 /// Compare-and-swap update of a pin's head.
469 ///
470 /// Used to create a fresh pin, advance an existing one, or
471 /// tombstone (delete) one. The CAS guard (`old`) lets multiple
472 /// writers coordinate without locks: a stale writer's update
473 /// returns `PushResult::Conflict(current)` carrying the actual
474 /// current head for retry / merge.
475 ///
476 /// # Parameters
477 /// * `id` — The id of the pin to update.
478 /// * `old` — Expected current head (`None` when creating a fresh pin).
479 /// * `new` — New head (`None` tombstones the pin).
480 ///
481 /// # Returns
482 /// * `Success` — The pin now points at `new`.
483 /// * `Conflict(current)` — Some other writer advanced first; the
484 /// pin's current head is `current`.
485 fn update(
486 &mut self,
487 id: Id,
488 old: Option<Inline<Handle<SimpleArchive>>>,
489 new: Option<Inline<Handle<SimpleArchive>>>,
490 ) -> Result<PushResult, Self::UpdateError>;
491}
492
493/// Error returned by [`transfer`] when copying blobs between stores.
494#[derive(Debug)]
495pub enum TransferError<ListErr, LoadErr, StoreErr> {
496 /// Failed to list handles from the source.
497 List(ListErr),
498 /// Failed to load a blob from the source.
499 Load(LoadErr),
500 /// Failed to store a blob in the target.
501 Store(StoreErr),
502}
503
504impl<ListErr, LoadErr, StoreErr> fmt::Display for TransferError<ListErr, LoadErr, StoreErr> {
505 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
506 write!(f, "failed to transfer blob")
507 }
508}
509
510impl<ListErr, LoadErr, StoreErr> Error for TransferError<ListErr, LoadErr, StoreErr>
511where
512 ListErr: Debug + Error + 'static,
513 LoadErr: Debug + Error + 'static,
514 StoreErr: Debug + Error + 'static,
515{
516 fn source(&self) -> Option<&(dyn Error + 'static)> {
517 match self {
518 Self::List(e) => Some(e),
519 Self::Load(e) => Some(e),
520 Self::Store(e) => Some(e),
521 }
522 }
523}
524
525/// Copies the specified blob handles from `source` into `target`.
526pub fn transfer<'a, BS, BT, Handles>(
527 source: &'a BS,
528 target: &'a mut BT,
529 handles: Handles,
530) -> impl Iterator<
531 Item = Result<
532 (
533 Inline<Handle<UnknownBlob>>,
534 Inline<Handle<UnknownBlob>>,
535 ),
536 TransferError<
537 Infallible,
538 <BS as BlobStoreGet>::GetError<Infallible>,
539 <BT as BlobStorePut>::PutError,
540 >,
541 >,
542> + 'a
543where
544 BS: BlobStoreGet + 'a,
545 BT: BlobStorePut + 'a,
546 Handles: IntoIterator<Item = Inline<Handle<UnknownBlob>>> + 'a,
547 Handles::IntoIter: 'a,
548{
549 handles.into_iter().map(move |source_handle| {
550 let blob: Blob<UnknownBlob> = source.get(source_handle).map_err(TransferError::Load)?;
551
552 Ok((
553 source_handle,
554 (target.put(blob).map_err(TransferError::Store)?),
555 ))
556 })
557}
558
559/// Iterator that visits every blob handle reachable from a set of roots.
560///
561/// Uses [`BlobChildren`] to enumerate references at each level,
562/// so backends with batch capabilities get efficient traversal.
563pub struct ReachableHandles<'a, BS>
564where
565 BS: BlobChildren,
566{
567 source: &'a BS,
568 queue: VecDeque<Inline<Handle<UnknownBlob>>>,
569 visited: HashSet<[u8; INLINE_LEN]>,
570}
571
572impl<'a, BS> ReachableHandles<'a, BS>
573where
574 BS: BlobChildren,
575{
576 fn new(source: &'a BS, roots: impl IntoIterator<Item = Inline<Handle<UnknownBlob>>>) -> Self {
577 let mut queue = VecDeque::new();
578 for handle in roots {
579 queue.push_back(handle);
580 }
581
582 Self {
583 source,
584 queue,
585 visited: HashSet::new(),
586 }
587 }
588}
589
590impl<'a, BS> Iterator for ReachableHandles<'a, BS>
591where
592 BS: BlobChildren,
593{
594 type Item = Inline<Handle<UnknownBlob>>;
595
596 fn next(&mut self) -> Option<Self::Item> {
597 while let Some(handle) = self.queue.pop_front() {
598 let raw = handle.raw;
599
600 if !self.visited.insert(raw) {
601 continue;
602 }
603
604 // Use BlobChildren to get references — backends can override
605 // with batch-optimized implementations.
606 for child in self.source.children(handle) {
607 if !self.visited.contains(&child.raw) {
608 self.queue.push_back(child);
609 }
610 }
611
612 return Some(handle);
613 }
614
615 None
616 }
617}
618
619/// Create a breadth-first iterator over blob handles reachable from `roots`.
620///
621/// Uses [`BlobChildren`] for reference enumeration, so network-backed
622/// stores can provide optimized batch implementations.
623pub fn reachable<'a, BS>(
624 source: &'a BS,
625 roots: impl IntoIterator<Item = Inline<Handle<UnknownBlob>>>,
626) -> ReachableHandles<'a, BS>
627where
628 BS: BlobChildren,
629{
630 ReachableHandles::new(source, roots)
631}
632
633/// Iterate over every 32-byte candidate in the value column of a [`TribleSet`].
634///
635/// This is a conservative conversion used when scanning metadata for potential
636/// blob handles. Each 32-byte chunk is treated as a `Handle<UnknownBlob>`.
637/// Callers can feed the resulting iterator into [`BlobStoreKeep::keep`] or other
638/// helpers that accept collections of handles.
639pub fn potential_handles<'a>(
640 set: &'a TribleSet,
641) -> impl Iterator<Item = Inline<Handle<UnknownBlob>>> + 'a {
642 set.vae.iter().map(|raw| {
643 let mut value = [0u8; INLINE_LEN];
644 value.copy_from_slice(&raw[0..INLINE_LEN]);
645 Inline::<Handle<UnknownBlob>>::new(value)
646 })
647}
648
649/// An error that can occur when creating a commit.
650/// This error can be caused by a failure to store the content or metadata blobs.
651#[derive(Debug)]
652pub enum CreateCommitError<BlobErr: Error + Debug + Send + Sync + 'static> {
653 /// Failed to store the content blob.
654 ContentStorageError(BlobErr),
655 /// Failed to store the commit metadata blob.
656 CommitStorageError(BlobErr),
657}
658
659impl<BlobErr: Error + Debug + Send + Sync + 'static> fmt::Display for CreateCommitError<BlobErr> {
660 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
661 match self {
662 CreateCommitError::ContentStorageError(e) => write!(f, "Content storage failed: {e}"),
663 CreateCommitError::CommitStorageError(e) => {
664 write!(f, "Commit metadata storage failed: {e}")
665 }
666 }
667 }
668}
669
670impl<BlobErr: Error + Debug + Send + Sync + 'static> Error for CreateCommitError<BlobErr> {
671 fn source(&self) -> Option<&(dyn Error + 'static)> {
672 match self {
673 CreateCommitError::ContentStorageError(e) => Some(e),
674 CreateCommitError::CommitStorageError(e) => Some(e),
675 }
676 }
677}
678
679/// Error returned by [`Workspace::merge`].
680#[derive(Debug)]
681pub enum MergeError {
682 /// The merge failed because the workspaces have different base repos.
683 DifferentRepos(),
684 /// The ancestry walk failed because one or more commit blobs along the
685 /// chain weren't readable from the workspace's view. The merge refuses
686 /// to fall through to a divergent-merge in this case — creating a merge
687 /// commit referencing an unknown chain would leave a dangling parent in
688 /// the resulting branch, and the append-only pile keeps that corruption
689 /// forever.
690 ///
691 /// Callers should ensure both heads' full closures are locally present
692 /// (e.g. via `fetch_reachable`) before retrying. The contained string
693 /// is a human-readable description of the underlying read failure.
694 AncestryWalkFailed(String),
695}
696
697/// Error returned by [`Repository::push`] and [`Repository::try_push`].
698/// Error type for [`Repository::compute_rollup`].
699#[derive(Debug)]
700pub enum RollupError<Storage: PinStore + BlobStore> {
701 /// The branch was not found in the underlying storage.
702 UnknownBranch,
703 /// The branch is empty — no HEAD to roll up.
704 EmptyBranch,
705 /// The branch HEAD advanced between checkout and CAS-update. The
706 /// caller may retry (`compute_rollup` is content-addressed so repeat
707 /// calls dedupe against already-uploaded blobs).
708 HeadAdvanced,
709 /// Underlying push / storage error during the attach step.
710 Push(PushError<Storage>),
711 /// Could not pull the branch to obtain a workspace.
712 Pull(PullError<Storage::HeadError,
713 <Storage as BlobStore>::ReaderError,
714 <<Storage as BlobStore>::Reader as BlobStoreGet>::GetError<UnarchiveError>>),
715 /// Could not check out the branch state to build the archive.
716 Checkout(WorkspaceCheckoutError<
717 <<Storage as BlobStore>::Reader as BlobStoreGet>::GetError<UnarchiveError>>),
718}
719
720#[derive(Debug)]
721pub enum PushError<Storage: PinStore + BlobStore> {
722 /// An error occurred while enumerating the branch storage branches.
723 StorageBranches(Storage::PinsError),
724 /// An error occurred while creating a blob reader.
725 StorageReader(<Storage as BlobStore>::ReaderError),
726 /// An error occurred while reading metadata blobs.
727 StorageGet(
728 <<Storage as BlobStore>::Reader as BlobStoreGet>::GetError<UnarchiveError>,
729 ),
730 /// An error occurred while transferring blobs to the repository.
731 StoragePut(<Storage as BlobStorePut>::PutError),
732 /// An error occurred while updating the branch storage.
733 BranchUpdate(Storage::UpdateError),
734 /// Malformed branch metadata.
735 BadBranchMetadata(),
736 /// Merge failed while retrying a push.
737 MergeError(MergeError),
738}
739
740// Allow using the `?` operator to convert MergeError into PushError in
741// contexts where PushError is the function error type. This keeps call sites
742// succinct by avoiding manual mapping closures like
743// `.map_err(|e| PushError::MergeError(e))?`.
744impl<Storage> From<MergeError> for PushError<Storage>
745where
746 Storage: PinStore + BlobStore,
747{
748 fn from(e: MergeError) -> Self {
749 PushError::MergeError(e)
750 }
751}
752
753// Note: we intentionally avoid generic `From` impls for storage-associated
754// error types because they can overlap with other blanket implementations
755// and lead to coherence conflicts. Call sites use explicit mapping via the
756// enum variant constructors (e.g. `map_err(PushError::StoragePut)`) where
757// needed which keeps conversions explicit and stable.
758
759/// Error returned by [`Repository::create_branch`] and related methods.
760#[derive(Debug)]
761pub enum BranchError<Storage>
762where
763 Storage: PinStore + BlobStore,
764{
765 /// An error occurred while creating a blob reader.
766 StorageReader(<Storage as BlobStore>::ReaderError),
767 /// An error occurred while reading metadata blobs.
768 StorageGet(
769 <<Storage as BlobStore>::Reader as BlobStoreGet>::GetError<UnarchiveError>,
770 ),
771 /// An error occurred while storing blobs.
772 StoragePut(<Storage as BlobStorePut>::PutError),
773 /// An error occurred while retrieving branch heads.
774 BranchHead(Storage::HeadError),
775 /// An error occurred while updating the branch storage.
776 BranchUpdate(Storage::UpdateError),
777 /// The branch already exists.
778 AlreadyExists(),
779 /// The referenced base branch does not exist.
780 BranchNotFound(Id),
781}
782
783/// Error returned by [`Repository::lookup_branch`].
784#[derive(Debug)]
785pub enum LookupError<Storage>
786where
787 Storage: PinStore + BlobStore,
788{
789 /// Failed to enumerate branches.
790 StorageBranches(Storage::PinsError),
791 /// Failed to read a branch head.
792 BranchHead(Storage::HeadError),
793 /// Failed to create a blob reader.
794 StorageReader(<Storage as BlobStore>::ReaderError),
795 /// Failed to read a metadata blob.
796 StorageGet(
797 <<Storage as BlobStore>::Reader as BlobStoreGet>::GetError<UnarchiveError>,
798 ),
799 /// Multiple branches were found with the given name.
800 NameConflict(Vec<Id>),
801 /// Branch metadata is malformed.
802 BadBranchMetadata(),
803}
804
805/// Error returned by [`Repository::ensure_branch`].
806#[derive(Debug)]
807pub enum EnsureBranchError<Storage>
808where
809 Storage: PinStore + BlobStore,
810{
811 /// Failed to look up the branch.
812 Lookup(LookupError<Storage>),
813 /// Failed to create the branch.
814 Create(BranchError<Storage>),
815}
816
817/// High-level wrapper combining a blob store and branch store into a usable
818/// repository API.
819///
820/// The [`Repository`] type exposes convenience methods for creating branches,
821/// committing data and pushing changes while delegating actual storage to the
822/// given [`BlobStore`] and [`PinStore`] implementations.
823pub struct Repository<Storage: BlobStore + PinStore> {
824 storage: Storage,
825 signing_key: SigningKey,
826 commit_metadata: MetadataHandle,
827}
828
829/// Error returned by [`Repository::pull`].
830pub enum PullError<BranchStorageErr, BlobReaderErr, BlobStorageErr>
831where
832 BranchStorageErr: Error,
833 BlobReaderErr: Error,
834 BlobStorageErr: Error,
835{
836 /// The branch does not exist in the repository.
837 BranchNotFound(Id),
838 /// An error occurred while accessing the branch storage.
839 BranchStorage(BranchStorageErr),
840 /// An error occurred while creating a blob reader.
841 BlobReader(BlobReaderErr),
842 /// An error occurred while accessing the blob storage.
843 BlobStorage(BlobStorageErr),
844 /// The branch metadata is malformed or does not contain the expected fields.
845 BadBranchMetadata(),
846}
847
848impl<B, R, C> fmt::Debug for PullError<B, R, C>
849where
850 B: Error + fmt::Debug,
851 R: Error + fmt::Debug,
852 C: Error + fmt::Debug,
853{
854 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
855 match self {
856 PullError::BranchNotFound(id) => f.debug_tuple("BranchNotFound").field(id).finish(),
857 PullError::BranchStorage(e) => f.debug_tuple("BranchStorage").field(e).finish(),
858 PullError::BlobReader(e) => f.debug_tuple("BlobReader").field(e).finish(),
859 PullError::BlobStorage(e) => f.debug_tuple("BlobStorage").field(e).finish(),
860 PullError::BadBranchMetadata() => f.debug_tuple("BadBranchMetadata").finish(),
861 }
862 }
863}
864
865impl<Storage> Repository<Storage>
866where
867 Storage: BlobStore + PinStore,
868{
869 /// Creates a new repository with the given storage, signing key, and
870 /// repo-wide commit metadata.
871 ///
872 /// `commit_metadata` accepts anything convertible into a [`Fragment`] —
873 /// either a raw [`TribleSet`] (auto-promoted with empty blob store via
874 /// `impl From<TribleSet> for Fragment`), or a Fragment built up via
875 /// `entity!{}` / `attributes!::describe()` that carries auxiliary blobs
876 /// (e.g. `Handle<LongString>` doc strings). The Fragment's blobs are
877 /// absorbed into storage so handles referenced by the metadata facts
878 /// stay resolvable for any downstream reader that pulls a commit and
879 /// calls [`Workspace::checkout_metadata`].
880 ///
881 /// The resulting metadata blob is referenced from every commit produced
882 /// by workspaces of this repository.
883 pub fn new<F: Into<crate::trible::Fragment>>(
884 mut storage: Storage,
885 signing_key: SigningKey,
886 commit_metadata: F,
887 ) -> Result<Self, <Storage as BlobStorePut>::PutError> {
888 let (facts, mut blobs) = commit_metadata.into().into_facts_and_blobs();
889 // Persist any blobs the Fragment carried — typically `Handle<LongString>`
890 // doc strings or other handle-referenced payloads. They're stored as
891 // `UnknownBlob` (raw bytes) because the storage layer is encoding-agnostic;
892 // readers recover the schema via the handle's declared encoding.
893 let reader = blobs
894 .reader()
895 .expect("MemoryBlobStore::reader is infallible");
896 for (_handle, blob) in reader {
897 storage.put::<UnknownBlob, _>(blob)?;
898 }
899 let commit_metadata = storage.put(facts)?;
900 Ok(Self {
901 storage,
902 signing_key,
903 commit_metadata,
904 })
905 }
906
907 /// Consume the repository and return the underlying storage backend.
908 ///
909 /// This is useful for callers that need to take ownership of the storage
910 /// (for example to call `close()` on a [`Pile`]) instead of letting the
911 /// repository drop it implicitly.
912 pub fn into_storage(self) -> Storage {
913 self.storage
914 }
915
916 /// Borrow the underlying storage backend.
917 pub fn storage(&self) -> &Storage {
918 &self.storage
919 }
920
921 /// Borrow the underlying storage backend mutably.
922 pub fn storage_mut(&mut self) -> &mut Storage {
923 &mut self.storage
924 }
925
926 /// Replace the repository signing key.
927 pub fn set_signing_key(&mut self, signing_key: SigningKey) {
928 self.signing_key = signing_key;
929 }
930
931 /// Returns the repository commit metadata handle.
932 pub fn commit_metadata(&self) -> MetadataHandle {
933 self.commit_metadata
934 }
935
936 /// Initializes a new branch in the repository.
937 /// Branches are the only mutable state in the repository,
938 /// and are used to represent the state of a commit chain at a specific point in time.
939 /// A branch must always point to a commit, and this function can be used to create a new branch.
940 ///
941 /// Creates a new branch in the repository.
942 /// This branch is a pointer to a specific commit in the repository.
943 /// The branch is created with name and is initialized to point to the opionally given commit.
944 /// The branch is signed by the branch signing key.
945 ///
946 /// # Parameters
947 /// * `branch_name` - Name of the new branch.
948 /// * `commit` - Commit to initialize the branch from.
949 pub fn create_branch(
950 &mut self,
951 branch_name: &str,
952 commit: Option<CommitHandle>,
953 ) -> Result<ExclusiveId, BranchError<Storage>> {
954 self.create_branch_with_key(branch_name, commit, self.signing_key.clone())
955 }
956
957 /// Same as [`Self::create_branch`] but uses the provided signing key.
958 pub fn create_branch_with_key(
959 &mut self,
960 branch_name: &str,
961 commit: Option<CommitHandle>,
962 signing_key: SigningKey,
963 ) -> Result<ExclusiveId, BranchError<Storage>> {
964 let branch_id = genid();
965 let name_blob: Blob<LongString> = branch_name.to_owned().to_blob();
966 let name_handle = name_blob.get_handle();
967 self.storage
968 .put::<LongString, _>(name_blob)
969 .map_err(|e| BranchError::StoragePut(e))?;
970
971 let branch_set = if let Some(commit) = commit {
972 let reader = self
973 .storage
974 .reader()
975 .map_err(|e| BranchError::StorageReader(e))?;
976 let set: TribleSet = reader.get(commit).map_err(|e| BranchError::StorageGet(e))?;
977
978 branch::branch_metadata(
979 &signing_key,
980 *branch_id,
981 name_handle,
982 Some(set.to_blob()),
983 None,
984 )
985 } else {
986 branch::branch_unsigned(*branch_id, name_handle, None, None)
987 };
988
989 let branch_blob = branch_set.to_blob();
990 let branch_handle = self
991 .storage
992 .put(branch_blob)
993 .map_err(|e| BranchError::StoragePut(e))?;
994 let push_result = self
995 .storage
996 .update(*branch_id, None, Some(branch_handle))
997 .map_err(|e| BranchError::BranchUpdate(e))?;
998
999 match push_result {
1000 PushResult::Success() => Ok(branch_id),
1001 PushResult::Conflict(_) => Err(BranchError::AlreadyExists()),
1002 }
1003 }
1004
1005 /// Look up a branch by name.
1006 ///
1007 /// Iterates all branches, reads each one's metadata, and returns the ID
1008 /// of the branch whose name matches. Returns `Ok(None)` if no branch has
1009 /// that name, or `LookupError::NameConflict` if multiple branches share it.
1010 pub fn lookup_branch(&mut self, name: &str) -> Result<Option<Id>, LookupError<Storage>> {
1011 let branch_ids: Vec<Id> = self
1012 .storage
1013 .pins()
1014 .map_err(LookupError::StorageBranches)?
1015 .collect::<Result<Vec<_>, _>>()
1016 .map_err(LookupError::StorageBranches)?;
1017
1018 let mut matches = Vec::new();
1019
1020 for branch_id in branch_ids {
1021 let Some(meta_handle) = self
1022 .storage
1023 .head(branch_id)
1024 .map_err(LookupError::BranchHead)?
1025 else {
1026 continue;
1027 };
1028
1029 let reader = self.storage.reader().map_err(LookupError::StorageReader)?;
1030 let meta_set: TribleSet = reader.get(meta_handle).map_err(LookupError::StorageGet)?;
1031
1032 let Ok((name_handle,)) = find!(
1033 (n: Inline<Handle<LongString>>),
1034 pattern!(&meta_set, [{ crate::metadata::name: ?n }])
1035 )
1036 .exactly_one() else {
1037 continue;
1038 };
1039
1040 let Ok(branch_name): Result<anybytes::View<str>, _> = reader.get(name_handle) else {
1041 continue;
1042 };
1043
1044 if branch_name.as_ref() == name {
1045 matches.push(branch_id);
1046 }
1047 }
1048
1049 match matches.len() {
1050 0 => Ok(None),
1051 1 => Ok(Some(matches[0])),
1052 _ => Err(LookupError::NameConflict(matches)),
1053 }
1054 }
1055
1056 /// Ensure a branch with the given name exists, creating it if necessary.
1057 ///
1058 /// If a branch named `name` already exists, returns its ID.
1059 /// If no such branch exists, creates a new one (optionally from the given
1060 /// commit) and returns its ID.
1061 ///
1062 /// Errors if multiple branches share the same name (ambiguous).
1063 pub fn ensure_branch(
1064 &mut self,
1065 name: &str,
1066 commit: Option<CommitHandle>,
1067 ) -> Result<Id, EnsureBranchError<Storage>> {
1068 match self
1069 .lookup_branch(name)
1070 .map_err(EnsureBranchError::Lookup)?
1071 {
1072 Some(id) => Ok(id),
1073 None => {
1074 let id = self
1075 .create_branch(name, commit)
1076 .map_err(EnsureBranchError::Create)?;
1077 Ok(*id)
1078 }
1079 }
1080 }
1081
1082 /// Pulls an existing branch using the repository's signing key.
1083 /// The workspace inherits the repository default metadata if configured.
1084 pub fn pull(
1085 &mut self,
1086 branch_id: Id,
1087 ) -> Result<
1088 Workspace<Storage>,
1089 PullError<
1090 Storage::HeadError,
1091 Storage::ReaderError,
1092 <Storage::Reader as BlobStoreGet>::GetError<UnarchiveError>,
1093 >,
1094 > {
1095 self.pull_with_key(branch_id, self.signing_key.clone())
1096 }
1097
1098 /// Same as [`Self::pull`] but overrides the signing key.
1099 pub fn pull_with_key(
1100 &mut self,
1101 branch_id: Id,
1102 signing_key: SigningKey,
1103 ) -> Result<
1104 Workspace<Storage>,
1105 PullError<
1106 Storage::HeadError,
1107 Storage::ReaderError,
1108 <Storage::Reader as BlobStoreGet>::GetError<UnarchiveError>,
1109 >,
1110 > {
1111 // 1. Get the branch metadata head from the branch store.
1112 let base_branch_meta_handle = match self.storage.head(branch_id) {
1113 Ok(Some(handle)) => handle,
1114 Ok(None) => return Err(PullError::BranchNotFound(branch_id)),
1115 Err(e) => return Err(PullError::BranchStorage(e)),
1116 };
1117 // 2. Get the current commit from the branch metadata.
1118 let reader = self.storage.reader().map_err(PullError::BlobReader)?;
1119 let base_branch_meta: TribleSet = match reader.get(base_branch_meta_handle) {
1120 Ok(meta_set) => meta_set,
1121 Err(e) => return Err(PullError::BlobStorage(e)),
1122 };
1123
1124 let head_ = match find!(
1125 (head_: Inline<_>),
1126 pattern!(&base_branch_meta, [{ head: ?head_ }])
1127 )
1128 .at_most_one()
1129 {
1130 Ok(Some((h,))) => Some(h),
1131 Ok(None) => None,
1132 Err(_) => return Err(PullError::BadBranchMetadata()),
1133 };
1134 // Create workspace with the current commit and base blobs.
1135 let base_blobs = self.storage.reader().map_err(PullError::BlobReader)?;
1136 Ok(Workspace {
1137 base_blobs,
1138 staged: MemoryBlobStore::new(),
1139 head: head_,
1140 base_head: head_,
1141 base_branch_id: branch_id,
1142 base_branch_meta: base_branch_meta_handle,
1143 signing_key,
1144 commit_metadata: self.commit_metadata,
1145 })
1146 }
1147
1148 /// Pushes the workspace's new blobs and commit to the persistent repository.
1149 /// This syncs the local BlobSet with the repository's BlobStore and performs
1150 /// an atomic branch update (using the stored base_branch_meta).
1151 pub fn push(&mut self, workspace: &mut Workspace<Storage>) -> Result<(), PushError<Storage>> {
1152 // Retrying push: attempt a single push and, on conflict, merge the
1153 // local workspace into the returned conflict workspace and retry.
1154 // This implements the common push-merge-retry loop as a convenience
1155 // wrapper around `try_push`.
1156 while let Some(mut conflict_ws) = self.try_push(workspace)? {
1157 // Keep the previous merge order: merge the caller's staged
1158 // changes into the incoming conflict workspace. This preserves
1159 // the semantic ordering of parents used in the merge commit.
1160 conflict_ws.merge(workspace)?;
1161
1162 // Move the merged incoming workspace into the caller's workspace
1163 // so the next try_push operates against the fresh branch state.
1164 // Using assignment here is equivalent to `swap` but avoids
1165 // retaining the previous `workspace` contents in the temp var.
1166 *workspace = conflict_ws;
1167 }
1168
1169 Ok(())
1170 }
1171
1172 /// Single-attempt push: upload local blobs and try to update the branch
1173 /// head once. Returns `Ok(None)` on success, or `Ok(Some(conflict_ws))`
1174 /// when the branch was updated concurrently and the caller should merge.
1175 pub fn try_push(
1176 &mut self,
1177 workspace: &mut Workspace<Storage>,
1178 ) -> Result<Option<Workspace<Storage>>, PushError<Storage>> {
1179 // 1. Sync `workspace.staged` to repository's BlobStore.
1180 let workspace_reader = workspace.staged.reader().unwrap();
1181 for handle in workspace_reader.blobs() {
1182 let handle = handle.expect("infallible blob enumeration");
1183 let blob: Blob<UnknownBlob> =
1184 workspace_reader.get(handle).expect("infallible blob read");
1185 self.storage
1186 .put::<UnknownBlob, _>(blob)
1187 .map_err(PushError::StoragePut)?;
1188 }
1189
1190 // 1.5 If the workspace's head did not change since the workspace was
1191 // created, there's no commit to reference and therefore no branch
1192 // metadata update is required. This avoids touching the branch store
1193 // in the common case where only blobs were staged or nothing changed.
1194 if workspace.base_head == workspace.head {
1195 return Ok(None);
1196 }
1197
1198 // 2. Create a new branch meta blob referencing the new workspace head.
1199 let repo_reader = self.storage.reader().map_err(PushError::StorageReader)?;
1200 let base_branch_meta: TribleSet = repo_reader
1201 .get(workspace.base_branch_meta)
1202 .map_err(PushError::StorageGet)?;
1203
1204 let Ok((branch_name,)) = find!(
1205 (name: Inline<Handle<LongString>>),
1206 pattern!(base_branch_meta, [{ crate::metadata::name: ?name }])
1207 )
1208 .exactly_one() else {
1209 return Err(PushError::BadBranchMetadata());
1210 };
1211
1212 let head_handle = workspace.head.ok_or(PushError::BadBranchMetadata())?;
1213 let head_: TribleSet = repo_reader
1214 .get(head_handle)
1215 .map_err(PushError::StorageGet)?;
1216
1217 let branch_meta = branch_metadata(
1218 &workspace.signing_key,
1219 workspace.base_branch_id,
1220 branch_name,
1221 Some(head_.to_blob()),
1222 // A fresh commit invalidates any prior rollup (it was computed
1223 // against the old HEAD). Readers fall back to checkout until
1224 // `compute_rollup` runs against the new HEAD.
1225 None,
1226 );
1227
1228 let branch_meta_handle = self
1229 .storage
1230 .put(branch_meta)
1231 .map_err(PushError::StoragePut)?;
1232
1233 // 3. Use CAS (comparing against workspace.base_branch_meta) to update the branch pointer.
1234 let result = self
1235 .storage
1236 .update(
1237 workspace.base_branch_id,
1238 Some(workspace.base_branch_meta),
1239 Some(branch_meta_handle),
1240 )
1241 .map_err(PushError::BranchUpdate)?;
1242
1243 match result {
1244 PushResult::Success() => {
1245 // Update workspace base pointers so subsequent pushes can detect
1246 // that the workspace is already synchronized and avoid re-upload.
1247 workspace.base_branch_meta = branch_meta_handle;
1248 workspace.base_head = workspace.head;
1249 // Refresh the workspace base blob reader to ensure newly
1250 // uploaded blobs are visible to subsequent checkout operations.
1251 workspace.base_blobs = self.storage.reader().map_err(PushError::StorageReader)?;
1252 // Clear staged local blobs now that they have been uploaded and
1253 // the branch metadata updated. This frees memory and prevents
1254 // repeated uploads of the same staged blobs on subsequent pushes.
1255 workspace.staged = MemoryBlobStore::new();
1256 Ok(None)
1257 }
1258 PushResult::Conflict(conflicting_meta) => {
1259 let conflicting_meta = conflicting_meta.ok_or(PushError::BadBranchMetadata())?;
1260
1261 let repo_reader = self.storage.reader().map_err(PushError::StorageReader)?;
1262 let branch_meta: TribleSet = repo_reader
1263 .get(conflicting_meta)
1264 .map_err(PushError::StorageGet)?;
1265
1266 let head_ = match find!((head_: Inline<_>),
1267 pattern!(&branch_meta, [{ head: ?head_ }])
1268 )
1269 .at_most_one()
1270 {
1271 Ok(Some((h,))) => Some(h),
1272 Ok(None) => None,
1273 Err(_) => return Err(PushError::BadBranchMetadata()),
1274 };
1275
1276 let conflict_ws = Workspace {
1277 base_blobs: self.storage.reader().map_err(PushError::StorageReader)?,
1278 staged: MemoryBlobStore::new(),
1279 head: head_,
1280 base_head: head_,
1281 base_branch_id: workspace.base_branch_id,
1282 base_branch_meta: conflicting_meta,
1283 signing_key: workspace.signing_key.clone(),
1284 commit_metadata: workspace.commit_metadata,
1285 };
1286
1287 Ok(Some(conflict_ws))
1288 }
1289 }
1290 }
1291
1292 /// Builds a [`SuccinctArchive`](crate::blob::encodings::succinctarchive::SuccinctArchive) rollup of the branch's current HEAD,
1293 /// stores it as a blob in the underlying storage, and attaches the
1294 /// resulting handle to the branch metadata via CAS.
1295 ///
1296 /// Returns the new rollup handle on success.
1297 ///
1298 /// Returns [`RollupError::HeadAdvanced`] if the branch HEAD moved
1299 /// between `pull` and the CAS-update. The caller may retry — the
1300 /// archive blob is content-addressed, so subsequent calls dedupe
1301 /// against already-uploaded blobs.
1302 ///
1303 /// Returns [`RollupError::EmptyBranch`] if the branch has no HEAD
1304 /// commit yet (nothing to roll up).
1305 ///
1306 /// This is the sole public write-path for rollups. The companion
1307 /// read-path is [`Workspace::rollup`].
1308 pub fn compute_rollup(
1309 &mut self,
1310 branch_id: Id,
1311 ) -> Result<
1312 Inline<Handle<crate::blob::encodings::succinctarchive::SuccinctArchiveBlob>>,
1313 RollupError<Storage>,
1314 > {
1315 use crate::blob::encodings::succinctarchive::{OrderedUniverse, SuccinctArchive};
1316 use crate::blob::IntoBlob;
1317
1318 let mut ws = self.pull(branch_id).map_err(RollupError::Pull)?;
1319 let head_handle = ws.head().ok_or(RollupError::EmptyBranch)?;
1320
1321 // Materialise the branch state from its commit chain and build the
1322 // succinct index over it.
1323 let space = ws.checkout(..).map_err(RollupError::Checkout)?;
1324 let archive: SuccinctArchive<OrderedUniverse> = (&*space).into();
1325 drop(space);
1326
1327 // Upload the archive blob directly to storage — no workspace-local
1328 // staging needed; the CAS below references it by handle.
1329 let archive_blob = (&archive).to_blob();
1330 let handle: Inline<
1331 Handle<crate::blob::encodings::succinctarchive::SuccinctArchiveBlob>,
1332 > = self
1333 .storage
1334 .put(archive_blob)
1335 .map_err(|e| RollupError::Push(PushError::StoragePut(e)))?;
1336
1337 // Construct a fresh branch meta that carries the same head as
1338 // `base_branch_meta` plus the new rollup attribute.
1339 let reader = self
1340 .storage
1341 .reader()
1342 .map_err(|e| RollupError::Push(PushError::StorageReader(e)))?;
1343 let base_meta: TribleSet = reader
1344 .get(ws.base_branch_meta)
1345 .map_err(|e| RollupError::Push(PushError::StorageGet(e)))?;
1346 let (branch_name,) = find!(
1347 (name: Inline<Handle<LongString>>),
1348 pattern!(&base_meta, [{ crate::metadata::name: ?name }])
1349 )
1350 .exactly_one()
1351 .map_err(|_| RollupError::Push(PushError::BadBranchMetadata()))?;
1352 let head_blob: TribleSet = reader
1353 .get(head_handle)
1354 .map_err(|e| RollupError::Push(PushError::StorageGet(e)))?;
1355
1356 let new_meta = branch::branch_metadata(
1357 &ws.signing_key,
1358 branch_id,
1359 branch_name,
1360 Some(head_blob.to_blob()),
1361 Some(handle),
1362 );
1363 let new_meta_handle = self
1364 .storage
1365 .put(new_meta)
1366 .map_err(|e| RollupError::Push(PushError::StoragePut(e)))?;
1367
1368 // CAS: swap `base_branch_meta` for the new meta. On conflict, the
1369 // head advanced between our pull and this CAS — the rollup we built
1370 // is stale against the new head, so report upstream.
1371 let update_result = self
1372 .storage
1373 .update(branch_id, Some(ws.base_branch_meta), Some(new_meta_handle))
1374 .map_err(|e| RollupError::Push(PushError::BranchUpdate(e)))?;
1375 match update_result {
1376 PushResult::Success() => Ok(handle),
1377 PushResult::Conflict(_) => Err(RollupError::HeadAdvanced),
1378 }
1379 }
1380}
1381
1382/// A handle to a commit blob in the repository.
1383pub type CommitHandle = Inline<Handle<SimpleArchive>>;
1384type MetadataHandle = Inline<Handle<SimpleArchive>>;
1385/// A set of commit handles, used by [`CommitSelector`] and [`Checkout`].
1386pub type CommitSet = PATCH<INLINE_LEN, IdentitySchema, ()>;
1387type BranchMetaHandle = Inline<Handle<SimpleArchive>>;
1388
1389/// The result of a [`Workspace::checkout`] operation: a [`TribleSet`] paired
1390/// with the set of commits that produced it. Pass the commit set as the start
1391/// of a range selector to obtain incremental deltas on the next checkout.
1392///
1393/// [`Checkout`] dereferences to [`TribleSet`], so it can be used directly with
1394/// `find!`, `pattern!`, and `pattern_changes!`.
1395///
1396/// # Example: incremental updates
1397///
1398/// ```rust,ignore
1399/// let mut changed = repo.pull(branch_id)?.checkout(..)?;
1400/// let mut full = changed.facts().clone();
1401///
1402/// loop {
1403/// // full already includes changed
1404/// for result in pattern_changes!(&full, &changed, [{ ... }]) {
1405/// // process new results
1406/// }
1407///
1408/// // Advance — exclude exactly the commits we already processed.
1409/// changed = repo.pull(branch_id)?.checkout(changed.commits()..)?;
1410/// full += &changed;
1411/// }
1412/// ```
1413#[derive(Debug, Clone)]
1414pub struct Checkout {
1415 facts: TribleSet,
1416 commits: CommitSet,
1417}
1418
1419impl PartialEq<TribleSet> for Checkout {
1420 fn eq(&self, other: &TribleSet) -> bool {
1421 self.facts == *other
1422 }
1423}
1424
1425impl PartialEq<Checkout> for TribleSet {
1426 fn eq(&self, other: &Checkout) -> bool {
1427 *self == other.facts
1428 }
1429}
1430
1431impl Checkout {
1432 /// The checked-out tribles.
1433 pub fn facts(&self) -> &TribleSet {
1434 &self.facts
1435 }
1436
1437 /// The set of commits that produced this checkout. Use as the start of a
1438 /// range selector (`checkout.commits()..`) to exclude these commits
1439 /// on the next checkout and obtain only new data.
1440 pub fn commits(&self) -> CommitSet {
1441 self.commits.clone()
1442 }
1443
1444 /// Consume the checkout and return the inner TribleSet.
1445 pub fn into_facts(self) -> TribleSet {
1446 self.facts
1447 }
1448}
1449
1450impl std::ops::Deref for Checkout {
1451 type Target = TribleSet;
1452 fn deref(&self) -> &TribleSet {
1453 &self.facts
1454 }
1455}
1456
1457impl std::ops::AddAssign<&Checkout> for Checkout {
1458 fn add_assign(&mut self, rhs: &Checkout) {
1459 self.facts += rhs.facts.clone();
1460 self.commits.union(rhs.commits.clone());
1461 }
1462}
1463
1464impl std::ops::Add for Checkout {
1465 type Output = Self;
1466 fn add(mut self, rhs: Self) -> Self {
1467 self.facts += rhs.facts;
1468 self.commits.union(rhs.commits);
1469 self
1470 }
1471}
1472
1473impl std::ops::Add<&Checkout> for Checkout {
1474 type Output = Self;
1475 fn add(mut self, rhs: &Checkout) -> Self {
1476 self += rhs;
1477 self
1478 }
1479}
1480
1481/// The Workspace represents the mutable working area or "staging" state.
1482/// It was formerly known as `Head`. It is sent to worker threads,
1483/// modified (via commits, merges, etc.), and then merged back into the Repository.
1484pub struct Workspace<Blobs: BlobStore> {
1485 /// Staged blobs — added to this workspace but not yet pushed to
1486 /// the underlying repo. Analogous to git's staging area (the
1487 /// index): blobs accumulate here via `put` and friends, then
1488 /// `repo.push(&mut ws)` ships everything as one batch to the
1489 /// durable backend.
1490 pub staged: MemoryBlobStore,
1491 /// The blob storage base for the workspace.
1492 base_blobs: Blobs::Reader,
1493 /// The branch id this workspace is tracking; None for a detached workspace.
1494 base_branch_id: Id,
1495 /// The meta-handle corresponding to the base branch state used for CAS.
1496 base_branch_meta: BranchMetaHandle,
1497 /// Handle to the current commit in the working branch. `None` for an empty branch.
1498 head: Option<CommitHandle>,
1499 /// The branch head snapshot when this workspace was created (pull time).
1500 ///
1501 /// This allows `try_push` to cheaply detect whether the commit head has
1502 /// advanced since the workspace was created without querying the remote
1503 /// branch store.
1504 base_head: Option<CommitHandle>,
1505 /// Signing key used for commit/branch signing.
1506 signing_key: SigningKey,
1507 /// Metadata handle for commits created in this workspace.
1508 commit_metadata: MetadataHandle,
1509}
1510
1511impl<Blobs> fmt::Debug for Workspace<Blobs>
1512where
1513 Blobs: BlobStore,
1514 Blobs::Reader: fmt::Debug,
1515{
1516 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1517 f.debug_struct("Workspace")
1518 .field("staged", &self.staged)
1519 .field("base_blobs", &self.base_blobs)
1520 .field("base_branch_id", &self.base_branch_id)
1521 .field("base_branch_meta", &self.base_branch_meta)
1522 .field("base_head", &self.base_head)
1523 .field("head", &self.head)
1524 .field("commit_metadata", &self.commit_metadata)
1525 .finish()
1526 }
1527}
1528
1529/// Helper trait for [`Workspace::checkout`] specifying commit handles or ranges.
1530pub trait CommitSelector<Blobs: BlobStore> {
1531 fn select(
1532 self,
1533 ws: &mut Workspace<Blobs>,
1534 ) -> Result<
1535 CommitSet,
1536 WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet>::GetError<UnarchiveError>>,
1537 >;
1538}
1539
1540/// Selector that returns every commit reachable from a starting selector.
1541pub struct Ancestors<S>(pub S);
1542
1543/// Convenience function to create an [`Ancestors`] selector.
1544pub fn ancestors<S>(selector: S) -> Ancestors<S> {
1545 Ancestors(selector)
1546}
1547
1548/// Selector that walks every commit in the input set back N parent steps,
1549/// following all parent links (including merge parents). Returns the set
1550/// of all commits found at exactly depth N from the starting set.
1551///
1552/// This is a wavefront expansion: at each step, every commit in the current
1553/// frontier is replaced by all of its parents. After N steps the frontier
1554/// is the result.
1555pub struct NthAncestors<S>(pub S, pub usize);
1556
1557/// Walk `selector` back `n` parent steps through all parent links.
1558pub fn nth_ancestors<S>(selector: S, n: usize) -> NthAncestors<S> {
1559 NthAncestors(selector, n)
1560}
1561
1562/// Selector that returns the direct parents of commits from a starting selector.
1563pub struct Parents<S>(pub S);
1564
1565/// Convenience function to create a [`Parents`] selector.
1566pub fn parents<S>(selector: S) -> Parents<S> {
1567 Parents(selector)
1568}
1569
1570/// Selector that returns commits reachable from either of two selectors but
1571/// not both.
1572pub struct SymmetricDiff<A, B>(pub A, pub B);
1573
1574/// Convenience function to create a [`SymmetricDiff`] selector.
1575pub fn symmetric_diff<A, B>(a: A, b: B) -> SymmetricDiff<A, B> {
1576 SymmetricDiff(a, b)
1577}
1578
1579/// Selector that returns the union of commits returned by two selectors.
1580pub struct Union<A, B> {
1581 left: A,
1582 right: B,
1583}
1584
1585/// Convenience function to create a [`Union`] selector.
1586pub fn union<A, B>(left: A, right: B) -> Union<A, B> {
1587 Union { left, right }
1588}
1589
1590/// Selector that returns the intersection of commits returned by two selectors.
1591pub struct Intersect<A, B> {
1592 left: A,
1593 right: B,
1594}
1595
1596/// Convenience function to create an [`Intersect`] selector.
1597pub fn intersect<A, B>(left: A, right: B) -> Intersect<A, B> {
1598 Intersect { left, right }
1599}
1600
1601/// Selector that returns commits from the left selector that are not also
1602/// returned by the right selector.
1603pub struct Difference<A, B> {
1604 left: A,
1605 right: B,
1606}
1607
1608/// Convenience function to create a [`Difference`] selector.
1609pub fn difference<A, B>(left: A, right: B) -> Difference<A, B> {
1610 Difference { left, right }
1611}
1612
1613/// Selector that returns commits with timestamps in the given inclusive range.
1614pub struct TimeRange(pub Epoch, pub Epoch);
1615
1616/// Convenience function to create a [`TimeRange`] selector.
1617pub fn time_range(start: Epoch, end: Epoch) -> TimeRange {
1618 TimeRange(start, end)
1619}
1620
1621/// Selector that filters commits returned by another selector.
1622pub struct Filter<S, F> {
1623 selector: S,
1624 filter: F,
1625}
1626
1627/// Convenience function to create a [`Filter`] selector.
1628pub fn filter<S, F>(selector: S, filter: F) -> Filter<S, F> {
1629 Filter { selector, filter }
1630}
1631
1632impl<Blobs> CommitSelector<Blobs> for CommitHandle
1633where
1634 Blobs: BlobStore,
1635{
1636 fn select(
1637 self,
1638 _ws: &mut Workspace<Blobs>,
1639 ) -> Result<
1640 CommitSet,
1641 WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet>::GetError<UnarchiveError>>,
1642 > {
1643 let mut patch = CommitSet::new();
1644 patch.insert(&Entry::new(&self.raw));
1645 Ok(patch)
1646 }
1647}
1648
1649impl<Blobs> CommitSelector<Blobs> for CommitSet
1650where
1651 Blobs: BlobStore,
1652{
1653 fn select(
1654 self,
1655 _ws: &mut Workspace<Blobs>,
1656 ) -> Result<
1657 CommitSet,
1658 WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet>::GetError<UnarchiveError>>,
1659 > {
1660 Ok(self)
1661 }
1662}
1663
1664impl<Blobs> CommitSelector<Blobs> for Vec<CommitHandle>
1665where
1666 Blobs: BlobStore,
1667{
1668 fn select(
1669 self,
1670 _ws: &mut Workspace<Blobs>,
1671 ) -> Result<
1672 CommitSet,
1673 WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet>::GetError<UnarchiveError>>,
1674 > {
1675 let mut patch = CommitSet::new();
1676 for handle in self {
1677 patch.insert(&Entry::new(&handle.raw));
1678 }
1679 Ok(patch)
1680 }
1681}
1682
1683impl<Blobs> CommitSelector<Blobs> for &[CommitHandle]
1684where
1685 Blobs: BlobStore,
1686{
1687 fn select(
1688 self,
1689 _ws: &mut Workspace<Blobs>,
1690 ) -> Result<
1691 CommitSet,
1692 WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet>::GetError<UnarchiveError>>,
1693 > {
1694 let mut patch = CommitSet::new();
1695 for handle in self {
1696 patch.insert(&Entry::new(&handle.raw));
1697 }
1698 Ok(patch)
1699 }
1700}
1701
1702impl<Blobs> CommitSelector<Blobs> for Option<CommitHandle>
1703where
1704 Blobs: BlobStore,
1705{
1706 fn select(
1707 self,
1708 _ws: &mut Workspace<Blobs>,
1709 ) -> Result<
1710 CommitSet,
1711 WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet>::GetError<UnarchiveError>>,
1712 > {
1713 let mut patch = CommitSet::new();
1714 if let Some(handle) = self {
1715 patch.insert(&Entry::new(&handle.raw));
1716 }
1717 Ok(patch)
1718 }
1719}
1720
1721impl<S, Blobs> CommitSelector<Blobs> for Ancestors<S>
1722where
1723 S: CommitSelector<Blobs>,
1724 Blobs: BlobStore,
1725{
1726 fn select(
1727 self,
1728 ws: &mut Workspace<Blobs>,
1729 ) -> Result<
1730 CommitSet,
1731 WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet>::GetError<UnarchiveError>>,
1732 > {
1733 let seeds = self.0.select(ws)?;
1734 collect_reachable_from_patch(ws, seeds)
1735 }
1736}
1737
1738impl<Blobs, S> CommitSelector<Blobs> for NthAncestors<S>
1739where
1740 Blobs: BlobStore,
1741 S: CommitSelector<Blobs>,
1742{
1743 fn select(
1744 self,
1745 ws: &mut Workspace<Blobs>,
1746 ) -> Result<
1747 CommitSet,
1748 WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet>::GetError<UnarchiveError>>,
1749 > {
1750 let mut frontier = self.0.select(ws)?;
1751 let mut remaining = self.1;
1752
1753 while remaining > 0 && !frontier.is_empty() {
1754 // Collect current frontier keys before mutating.
1755 let keys: Vec<[u8; INLINE_LEN]> = frontier.iter().copied().collect();
1756 let mut next_frontier = CommitSet::new();
1757 for raw in keys {
1758 let handle = CommitHandle::new(raw);
1759 let meta: TribleSet = ws.get(handle).map_err(WorkspaceCheckoutError::Storage)?;
1760 for (p,) in find!((p: Inline<_>), pattern!(&meta, [{ parent: ?p }])) {
1761 next_frontier.insert(&Entry::new(&p.raw));
1762 }
1763 }
1764 frontier = next_frontier;
1765 remaining -= 1;
1766 }
1767
1768 Ok(frontier)
1769 }
1770}
1771
1772impl<S, Blobs> CommitSelector<Blobs> for Parents<S>
1773where
1774 S: CommitSelector<Blobs>,
1775 Blobs: BlobStore,
1776{
1777 fn select(
1778 self,
1779 ws: &mut Workspace<Blobs>,
1780 ) -> Result<
1781 CommitSet,
1782 WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet>::GetError<UnarchiveError>>,
1783 > {
1784 let seeds = self.0.select(ws)?;
1785 let mut result = CommitSet::new();
1786 for raw in seeds.iter() {
1787 let handle = Inline::new(*raw);
1788 let meta: TribleSet = ws.get(handle).map_err(WorkspaceCheckoutError::Storage)?;
1789 for (p,) in find!((p: Inline<_>), pattern!(&meta, [{ parent: ?p }])) {
1790 result.insert(&Entry::new(&p.raw));
1791 }
1792 }
1793 Ok(result)
1794 }
1795}
1796
1797impl<A, B, Blobs> CommitSelector<Blobs> for SymmetricDiff<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 seeds_a = self.0.select(ws)?;
1811 let seeds_b = self.1.select(ws)?;
1812 let a = collect_reachable_from_patch(ws, seeds_a)?;
1813 let b = collect_reachable_from_patch(ws, seeds_b)?;
1814 let inter = a.intersect(&b);
1815 let mut union = a;
1816 union.union(b);
1817 Ok(union.difference(&inter))
1818 }
1819}
1820
1821impl<A, B, Blobs> CommitSelector<Blobs> for Union<A, B>
1822where
1823 A: CommitSelector<Blobs>,
1824 B: CommitSelector<Blobs>,
1825 Blobs: BlobStore,
1826{
1827 fn select(
1828 self,
1829 ws: &mut Workspace<Blobs>,
1830 ) -> Result<
1831 CommitSet,
1832 WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet>::GetError<UnarchiveError>>,
1833 > {
1834 let mut left = self.left.select(ws)?;
1835 let right = self.right.select(ws)?;
1836 left.union(right);
1837 Ok(left)
1838 }
1839}
1840
1841impl<A, B, Blobs> CommitSelector<Blobs> for Intersect<A, B>
1842where
1843 A: CommitSelector<Blobs>,
1844 B: CommitSelector<Blobs>,
1845 Blobs: BlobStore,
1846{
1847 fn select(
1848 self,
1849 ws: &mut Workspace<Blobs>,
1850 ) -> Result<
1851 CommitSet,
1852 WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet>::GetError<UnarchiveError>>,
1853 > {
1854 let left = self.left.select(ws)?;
1855 let right = self.right.select(ws)?;
1856 Ok(left.intersect(&right))
1857 }
1858}
1859
1860impl<A, B, Blobs> CommitSelector<Blobs> for Difference<A, B>
1861where
1862 A: CommitSelector<Blobs>,
1863 B: CommitSelector<Blobs>,
1864 Blobs: BlobStore,
1865{
1866 fn select(
1867 self,
1868 ws: &mut Workspace<Blobs>,
1869 ) -> Result<
1870 CommitSet,
1871 WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet>::GetError<UnarchiveError>>,
1872 > {
1873 let left = self.left.select(ws)?;
1874 let right = self.right.select(ws)?;
1875 Ok(left.difference(&right))
1876 }
1877}
1878
1879impl<S, F, Blobs> CommitSelector<Blobs> for Filter<S, F>
1880where
1881 Blobs: BlobStore,
1882 S: CommitSelector<Blobs>,
1883 F: for<'x, 'y> Fn(&'x TribleSet, &'y TribleSet) -> bool,
1884{
1885 fn select(
1886 self,
1887 ws: &mut Workspace<Blobs>,
1888 ) -> Result<
1889 CommitSet,
1890 WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet>::GetError<UnarchiveError>>,
1891 > {
1892 let patch = self.selector.select(ws)?;
1893 let mut result = CommitSet::new();
1894 let filter = self.filter;
1895 for raw in patch.iter() {
1896 let handle = Inline::new(*raw);
1897 let meta: TribleSet = ws.get(handle).map_err(WorkspaceCheckoutError::Storage)?;
1898
1899 let Ok((content_handle,)) = find!(
1900 (c: Inline<_>),
1901 pattern!(&meta, [{ content: ?c }])
1902 )
1903 .exactly_one() else {
1904 return Err(WorkspaceCheckoutError::BadCommitMetadata());
1905 };
1906
1907 let payload: TribleSet = ws
1908 .get(content_handle)
1909 .map_err(WorkspaceCheckoutError::Storage)?;
1910
1911 if filter(&meta, &payload) {
1912 result.insert(&Entry::new(raw));
1913 }
1914 }
1915 Ok(result)
1916 }
1917}
1918
1919/// Selector that yields commits touching a specific entity.
1920pub struct HistoryOf(pub Id);
1921
1922/// Convenience function to create a [`HistoryOf`] selector.
1923pub fn history_of(entity: Id) -> HistoryOf {
1924 HistoryOf(entity)
1925}
1926
1927impl<Blobs> CommitSelector<Blobs> for HistoryOf
1928where
1929 Blobs: BlobStore,
1930{
1931 fn select(
1932 self,
1933 ws: &mut Workspace<Blobs>,
1934 ) -> Result<
1935 CommitSet,
1936 WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet>::GetError<UnarchiveError>>,
1937 > {
1938 let Some(head_) = ws.head else {
1939 return Ok(CommitSet::new());
1940 };
1941 let entity = self.0;
1942 filter(
1943 ancestors(head_),
1944 move |_: &TribleSet, payload: &TribleSet| payload.iter().any(|t| t.e() == &entity),
1945 )
1946 .select(ws)
1947 }
1948}
1949
1950// Generic range selectors: allow any selector type to be used as a range
1951// endpoint. We still walk the history reachable from the end selector but now
1952// stop descending a branch as soon as we encounter a commit produced by the
1953// start selector. This keeps the mechanics explicit—`start..end` literally
1954// walks from `end` until it hits `start`—while continuing to support selectors
1955// such as `Ancestors(...)` at either boundary.
1956
1957fn collect_reachable_from_patch<Blobs: BlobStore>(
1958 ws: &mut Workspace<Blobs>,
1959 patch: CommitSet,
1960) -> Result<
1961 CommitSet,
1962 WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet>::GetError<UnarchiveError>>,
1963> {
1964 let mut result = CommitSet::new();
1965 for raw in patch.iter() {
1966 let handle = Inline::new(*raw);
1967 let reach = collect_reachable(ws, handle)?;
1968 result.union(reach);
1969 }
1970 Ok(result)
1971}
1972
1973fn collect_reachable_from_patch_until<Blobs: BlobStore>(
1974 ws: &mut Workspace<Blobs>,
1975 seeds: CommitSet,
1976 stop: &CommitSet,
1977) -> Result<
1978 CommitSet,
1979 WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet>::GetError<UnarchiveError>>,
1980> {
1981 let mut visited = HashSet::new();
1982 let mut stack: Vec<CommitHandle> = seeds.iter().map(|raw| Inline::new(*raw)).collect();
1983 let mut result = CommitSet::new();
1984
1985 while let Some(commit) = stack.pop() {
1986 if !visited.insert(commit) {
1987 continue;
1988 }
1989
1990 if stop.get(&commit.raw).is_some() {
1991 continue;
1992 }
1993
1994 result.insert(&Entry::new(&commit.raw));
1995
1996 let meta: TribleSet = ws
1997 .staged
1998 .reader()
1999 .unwrap()
2000 .get(commit)
2001 .or_else(|_| ws.base_blobs.get(commit))
2002 .map_err(WorkspaceCheckoutError::Storage)?;
2003
2004 for (p,) in find!((p: Inline<_>,), pattern!(&meta, [{ parent: ?p }])) {
2005 stack.push(p);
2006 }
2007 }
2008
2009 Ok(result)
2010}
2011
2012impl<T, Blobs> CommitSelector<Blobs> for std::ops::Range<T>
2013where
2014 T: CommitSelector<Blobs>,
2015 Blobs: BlobStore,
2016{
2017 fn select(
2018 self,
2019 ws: &mut Workspace<Blobs>,
2020 ) -> Result<
2021 CommitSet,
2022 WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet>::GetError<UnarchiveError>>,
2023 > {
2024 let end_patch = self.end.select(ws)?;
2025 let start_patch = self.start.select(ws)?;
2026
2027 collect_reachable_from_patch_until(ws, end_patch, &start_patch)
2028 }
2029}
2030
2031impl<T, Blobs> CommitSelector<Blobs> for std::ops::RangeFrom<T>
2032where
2033 T: CommitSelector<Blobs>,
2034 Blobs: BlobStore,
2035{
2036 fn select(
2037 self,
2038 ws: &mut Workspace<Blobs>,
2039 ) -> Result<
2040 CommitSet,
2041 WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet>::GetError<UnarchiveError>>,
2042 > {
2043 let Some(head_) = ws.head else {
2044 return Ok(CommitSet::new());
2045 };
2046 let exclude_patch = self.start.select(ws)?;
2047
2048 let mut head_patch = CommitSet::new();
2049 head_patch.insert(&Entry::new(&head_.raw));
2050
2051 collect_reachable_from_patch_until(ws, head_patch, &exclude_patch)
2052 }
2053}
2054
2055impl<T, Blobs> CommitSelector<Blobs> for std::ops::RangeTo<T>
2056where
2057 T: CommitSelector<Blobs>,
2058 Blobs: BlobStore,
2059{
2060 fn select(
2061 self,
2062 ws: &mut Workspace<Blobs>,
2063 ) -> Result<
2064 CommitSet,
2065 WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet>::GetError<UnarchiveError>>,
2066 > {
2067 let end_patch = self.end.select(ws)?;
2068 collect_reachable_from_patch(ws, end_patch)
2069 }
2070}
2071
2072impl<Blobs> CommitSelector<Blobs> for std::ops::RangeFull
2073where
2074 Blobs: BlobStore,
2075{
2076 fn select(
2077 self,
2078 ws: &mut Workspace<Blobs>,
2079 ) -> Result<
2080 CommitSet,
2081 WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet>::GetError<UnarchiveError>>,
2082 > {
2083 let Some(head_) = ws.head else {
2084 return Ok(CommitSet::new());
2085 };
2086 collect_reachable(ws, head_)
2087 }
2088}
2089
2090impl<Blobs> CommitSelector<Blobs> for TimeRange
2091where
2092 Blobs: BlobStore,
2093{
2094 fn select(
2095 self,
2096 ws: &mut Workspace<Blobs>,
2097 ) -> Result<
2098 CommitSet,
2099 WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet>::GetError<UnarchiveError>>,
2100 > {
2101 let Some(head_) = ws.head else {
2102 return Ok(CommitSet::new());
2103 };
2104 let start = self.0;
2105 let end = self.1;
2106 filter(
2107 ancestors(head_),
2108 move |meta: &TribleSet, _payload: &TribleSet| {
2109 if let Ok(Some(((ts_start, ts_end),))) =
2110 find!((t: (Epoch, Epoch)), pattern!(meta, [{ crate::metadata::created_at: ?t }])).at_most_one()
2111 {
2112 ts_start <= end && ts_end >= start
2113 } else {
2114 false
2115 }
2116 },
2117 )
2118 .select(ws)
2119 }
2120}
2121
2122/// Minimum number of commits at which `checkout_commits*` switches
2123/// from the serial loop to a `rayon::par_iter().try_reduce()` over
2124/// the commits. Each commit involves one (or two) blob fetches plus
2125/// an unarchive — independent work per commit — so the crossover is
2126/// small. Below this the rayon overhead dominates.
2127#[cfg(feature = "parallel")]
2128const PARALLEL_CHECKOUT_THRESHOLD: usize = 8;
2129
2130impl<Blobs: BlobStore> Workspace<Blobs> {
2131 /// Returns the branch id associated with this workspace.
2132 pub fn branch_id(&self) -> Id {
2133 self.base_branch_id
2134 }
2135
2136 /// Returns the current commit handle if one exists.
2137 pub fn head(&self) -> Option<CommitHandle> {
2138 self.head
2139 }
2140
2141 /// Returns the workspace metadata handle.
2142 pub fn metadata(&self) -> MetadataHandle {
2143 self.commit_metadata
2144 }
2145
2146 /// Reads the rollup handle, if any, from the workspace's base branch
2147 /// metadata. Returns `None` if the branch has no rollup yet or if the
2148 /// metadata is missing the attribute. Readers can use this to fetch
2149 /// the archive blob directly and skip `checkout(..)` for warm queries:
2150 ///
2151 /// ```rust,ignore
2152 /// let mut ws = repo.pull(branch)?;
2153 /// match ws.rollup()? {
2154 /// Some(h) => {
2155 /// let archive: SuccinctArchive<_> = ws.get(h)?;
2156 /// // query archive
2157 /// }
2158 /// None => {
2159 /// let space = ws.checkout(..)?;
2160 /// // query space (commit-chain materialisation)
2161 /// }
2162 /// }
2163 /// ```
2164 ///
2165 /// Writers don't go through this — attach a rollup via
2166 /// [`Repository::compute_rollup`] instead.
2167 pub fn rollup(
2168 &mut self,
2169 ) -> Result<
2170 Option<Inline<Handle<SuccinctArchiveBlob>>>,
2171 <Blobs::Reader as BlobStoreGet>::GetError<UnarchiveError>,
2172 > {
2173 let base_meta: TribleSet = self.base_blobs.get(self.base_branch_meta)?;
2174 Ok(
2175 find!(
2176 (r: Inline<Handle<SuccinctArchiveBlob>>),
2177 pattern!(&base_meta, [{ rollup: ?r }])
2178 )
2179 .next()
2180 .map(|(r,)| r),
2181 )
2182 }
2183
2184 /// Adds a blob to the workspace's local blob store.
2185 /// Mirrors [`BlobStorePut::put`](crate::repo::BlobStorePut) for ease of use.
2186 pub fn put<S, T>(&mut self, item: T) -> Inline<Handle<S>>
2187 where
2188 S: BlobEncoding + 'static,
2189 T: IntoBlob<S>,
2190 Handle<S>: InlineEncoding,
2191 {
2192 self.staged.put(item).expect("infallible blob put")
2193 }
2194
2195
2196 /// Retrieves a blob from the workspace.
2197 ///
2198 /// The method first checks the workspace's local blob store and falls back
2199 /// to the base blob store if the blob is not found locally.
2200 pub fn get<T, S>(
2201 &mut self,
2202 handle: Inline<Handle<S>>,
2203 ) -> Result<T, <Blobs::Reader as BlobStoreGet>::GetError<<T as TryFromBlob<S>>::Error>>
2204 where
2205 S: BlobEncoding + 'static,
2206 T: TryFromBlob<S>,
2207 Handle<S>: InlineEncoding,
2208 {
2209 self.staged
2210 .reader()
2211 .unwrap()
2212 .get(handle)
2213 .or_else(|_| self.base_blobs.get(handle))
2214 }
2215
2216 /// Performs a commit in the workspace.
2217 ///
2218 /// Accepts anything that converts into a [`Fragment`] — either a
2219 /// raw [`TribleSet`] (auto-promoted to a Fragment with empty blob
2220 /// store), or a Fragment built up via `entity!{}` /
2221 /// `MetaDescribe::describe()` whose embedded blobs get absorbed
2222 /// into `self.staged` alongside the commit-content blob.
2223 /// This method creates a new commit blob (stored in the local
2224 /// blobset) and updates the current commit handle.
2225 pub fn commit(&mut self, content_: impl Into<Fragment>, message_: &str) {
2226 self.commit_internal(content_.into(), Some(self.commit_metadata), Some(message_));
2227 }
2228
2229 /// Like [`commit`](Self::commit) but attaches a one-off metadata handle
2230 /// instead of the repository default.
2231 pub fn commit_with_metadata(
2232 &mut self,
2233 content_: impl Into<Fragment>,
2234 metadata_: MetadataHandle,
2235 message_: &str,
2236 ) {
2237 self.commit_internal(content_.into(), Some(metadata_), Some(message_));
2238 }
2239
2240 fn commit_internal(
2241 &mut self,
2242 content_: Fragment,
2243 metadata_handle: Option<MetadataHandle>,
2244 message_: Option<&str>,
2245 ) {
2246 let (content_facts, content_blobs) = content_.into_facts_and_blobs();
2247 // 0. Absorb any blobs the Fragment carried with it into the
2248 // staging area before producing the commit blob, so handles
2249 // inside `content_facts` resolve against `self.staged`.
2250 self.staged.union(content_blobs);
2251 // 1. Create a commit blob from the current head, content, metadata and the commit message.
2252 let content_blob: Blob<SimpleArchive> = content_facts.to_blob();
2253 // If a message is provided, store it as a LongString blob and pass the handle.
2254 let message_handle = message_.map(|m| self.put(m.to_string()));
2255 let parents = self.head.iter().copied();
2256
2257 let commit_set = crate::repo::commit::commit_metadata(
2258 &self.signing_key,
2259 parents,
2260 message_handle,
2261 Some(content_blob.clone()),
2262 metadata_handle,
2263 );
2264 // 2. Store the content and commit blobs in `self.staged`.
2265 let _ = self
2266 .staged
2267 .put::<SimpleArchive, _>(content_blob)
2268 .expect("failed to put content blob");
2269 let commit_handle = self
2270 .staged
2271 .put(commit_set)
2272 .expect("failed to put commit blob");
2273 // 3. Update `self.head` to point to the new commit.
2274 self.head = Some(commit_handle);
2275 }
2276
2277 /// Merge another workspace into this one.
2278 ///
2279 /// Always copies the *staged* blobs from `other.staged` into
2280 /// `self.staged` (so standalone blobs that aren't referenced by any
2281 /// commit chain still come along — useful when the other workspace was
2282 /// being used to stage content).
2283 ///
2284 /// Then integrates `other.head` via [`merge_commit`](Self::merge_commit),
2285 /// which picks no-op / fast-forward / merge commit as appropriate.
2286 ///
2287 /// Returns the workspace's new head, or `None` if both workspaces were
2288 /// empty (nothing to merge into anything).
2289 ///
2290 /// Notes:
2291 /// - The merge does *not* automatically import the entire base history
2292 /// reachable from `other`'s head. If the incoming parent commits
2293 /// reference blobs that do not exist in this repository's storage,
2294 /// reading those commits later will fail until the missing blobs are
2295 /// explicitly imported (for example via `repo::transfer(reachable(...))`).
2296 /// - This design keeps merge permissive and leaves cross-repository blob
2297 /// import as an explicit user action.
2298 pub fn merge(
2299 &mut self,
2300 other: &mut Workspace<Blobs>,
2301 ) -> Result<Option<CommitHandle>, MergeError> {
2302 // 1. Always transfer staged blobs from `other`. They may include
2303 // standalone blobs (no commit referring to them yet) that the
2304 // caller wanted to stash in the workspace independent of any
2305 // branch state.
2306 let other_local = other.staged.reader().unwrap();
2307 for r in other_local.blobs() {
2308 let handle = r.expect("infallible blob enumeration");
2309 let blob: Blob<UnknownBlob> = other_local.get(handle).expect("infallible blob read");
2310 self.staged
2311 .put::<UnknownBlob, _>(blob)
2312 .expect("infallible blob put");
2313 }
2314
2315 // 2. Integrate `other`'s head via the smart merge_commit. If `other`
2316 // has no head, there's nothing further to integrate — just return
2317 // our current head (which may or may not exist).
2318 match other.head {
2319 Some(other_head) => Ok(Some(self.merge_commit(other_head)?)),
2320 None => Ok(self.head),
2321 }
2322 }
2323
2324 /// Integrate another commit into this workspace's history.
2325 ///
2326 /// Picks the cheapest correct strategy:
2327 ///
2328 /// - **No-op** if the workspace has no head and `other` *is* the head, or
2329 /// if `other` is already in the current head's ancestry.
2330 /// - **Fast-forward** if the workspace has no head, or if the current head
2331 /// is in `other`'s ancestry — `self.head` is set to `other` directly.
2332 /// - **Merge commit** otherwise — a new commit with `[current_head, other]`
2333 /// as parents is created and `self.head` advances to it.
2334 ///
2335 /// Returns the workspace's new head in all cases.
2336 ///
2337 /// The ancestor checks are best-effort: if the relevant commit blobs are
2338 /// missing from the workspace's view, the function falls through to the
2339 /// always-correct merge-commit path. Callers that mirror remote chains
2340 /// should ensure reachable blobs were imported (e.g. via `reachable` +
2341 /// `transfer`) for the optimization to kick in.
2342 pub fn merge_commit(
2343 &mut self,
2344 other: Inline<Handle<SimpleArchive>>,
2345 ) -> Result<CommitHandle, MergeError> {
2346 // Trivial cases first.
2347 let local_head = match self.head {
2348 None => {
2349 // No local head — fast-forward to `other`.
2350 self.head = Some(other);
2351 return Ok(other);
2352 }
2353 Some(h) if h == other => {
2354 // Identical — no-op.
2355 return Ok(h);
2356 }
2357 Some(h) => h,
2358 };
2359
2360 // Walk both ancestry chains. If either walk fails because a commit
2361 // blob is missing locally, refuse to merge — falling through to a
2362 // divergent-merge here would write a new commit referencing an
2363 // unknown parent, which `pile diagnose check` would later report as
2364 // a chain break and which `fetch_reachable`'s Phase-1
2365 // `have_local` short-circuit would never re-fetch. Better to fail
2366 // loudly so the caller can re-sync the missing closure and retry.
2367 let remote_in_local = ancestors(local_head)
2368 .select(self)
2369 .map_err(|e| MergeError::AncestryWalkFailed(format!("walking local ancestry: {e:?}")))?
2370 .get(&other.raw)
2371 .is_some();
2372 if remote_in_local {
2373 // `other` is already in our history → no-op.
2374 return Ok(local_head);
2375 }
2376
2377 let local_in_remote = ancestors(other)
2378 .select(self)
2379 .map_err(|e| MergeError::AncestryWalkFailed(format!("walking remote ancestry: {e:?}")))?
2380 .get(&local_head.raw)
2381 .is_some();
2382 if local_in_remote {
2383 // We're behind `other` → fast-forward.
2384 self.head = Some(other);
2385 return Ok(other);
2386 }
2387
2388 // Truly divergent — create a merge commit.
2389 let parents = self.head.iter().copied().chain(Some(other));
2390 let merge_commit = commit_metadata(&self.signing_key, parents, None, None, None);
2391 let commit_handle = self
2392 .staged
2393 .put(merge_commit)
2394 .expect("failed to put merge commit blob");
2395 self.head = Some(commit_handle);
2396 Ok(commit_handle)
2397 }
2398
2399 /// Move the workspace's head to `commit` without creating a new commit.
2400 ///
2401 /// This is the "fast-forward" case: when the new commit is a descendant
2402 /// of (or equal to) the current head, you can advance directly without
2403 /// a merge commit. The caller is responsible for verifying the
2404 /// descendancy relationship — typically via [`ancestors`] over `commit`.
2405 ///
2406 /// Use this in pull/sync flows to avoid spurious merge commits when one
2407 /// peer is simply behind the other.
2408 pub fn set_head(&mut self, commit: CommitHandle) {
2409 self.head = Some(commit);
2410 }
2411
2412 /// Returns the combined [`TribleSet`] for the specified commits.
2413 ///
2414 /// Each commit handle must reference a commit blob stored either in the
2415 /// workspace's local blob store or the repository's base store. The
2416 /// associated content blobs are loaded and unioned together. An error is
2417 /// returned if any commit or content blob is missing or malformed.
2418 fn checkout_commits<I>(
2419 &mut self,
2420 commits: I,
2421 ) -> Result<
2422 TribleSet,
2423 WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet>::GetError<UnarchiveError>>,
2424 >
2425 where
2426 I: IntoIterator<Item = CommitHandle>,
2427 {
2428 let local = self.staged.reader().unwrap();
2429 let commits: Vec<CommitHandle> = commits.into_iter().collect();
2430
2431 #[cfg(feature = "parallel")]
2432 {
2433 if commits.len() >= PARALLEL_CHECKOUT_THRESHOLD {
2434 use rayon::prelude::*;
2435 let base = self.base_blobs.clone();
2436 return commits
2437 .into_par_iter()
2438 .map_with(
2439 (local, base),
2440 |(local, base), commit| -> Result<TribleSet, _> {
2441 let meta: TribleSet = local
2442 .get(commit)
2443 .or_else(|_| base.get(commit))
2444 .map_err(WorkspaceCheckoutError::Storage)?;
2445 let content_opt = match find!(
2446 (c: Inline<_>),
2447 pattern!(&meta, [{ content: ?c }])
2448 )
2449 .at_most_one()
2450 {
2451 Ok(Some((c,))) => Some(c),
2452 Ok(None) => None,
2453 Err(_) => {
2454 return Err(WorkspaceCheckoutError::BadCommitMetadata())
2455 }
2456 };
2457 if let Some(c) = content_opt {
2458 let set: TribleSet = local
2459 .get(c)
2460 .or_else(|_| base.get(c))
2461 .map_err(WorkspaceCheckoutError::Storage)?;
2462 Ok(set)
2463 } else {
2464 Ok(TribleSet::new())
2465 }
2466 },
2467 )
2468 .try_reduce(TribleSet::new, |a, b| Ok(a + b));
2469 }
2470 }
2471
2472 let mut result = TribleSet::new();
2473 for commit in commits {
2474 let meta: TribleSet = local
2475 .get(commit)
2476 .or_else(|_| self.base_blobs.get(commit))
2477 .map_err(WorkspaceCheckoutError::Storage)?;
2478
2479 // Some commits (for example merge commits) intentionally do not
2480 // carry a content blob. Treat those as no-ops during checkout so
2481 // callers can request ancestor ranges without failing when a
2482 // merge commit is encountered.
2483 let content_opt =
2484 match find!((c: Inline<_>), pattern!(&meta, [{ content: ?c }])).at_most_one() {
2485 Ok(Some((c,))) => Some(c),
2486 Ok(None) => None,
2487 Err(_) => return Err(WorkspaceCheckoutError::BadCommitMetadata()),
2488 };
2489
2490 if let Some(c) = content_opt {
2491 let set: TribleSet = local
2492 .get(c)
2493 .or_else(|_| self.base_blobs.get(c))
2494 .map_err(WorkspaceCheckoutError::Storage)?;
2495 result += set;
2496 } else {
2497 // No content for this commit (e.g. merge-only commit); skip it.
2498 continue;
2499 }
2500 }
2501 Ok(result)
2502 }
2503
2504 fn checkout_commits_metadata<I>(
2505 &mut self,
2506 commits: I,
2507 ) -> Result<
2508 TribleSet,
2509 WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet>::GetError<UnarchiveError>>,
2510 >
2511 where
2512 I: IntoIterator<Item = CommitHandle>,
2513 {
2514 let local = self.staged.reader().unwrap();
2515 let commits: Vec<CommitHandle> = commits.into_iter().collect();
2516
2517 #[cfg(feature = "parallel")]
2518 {
2519 if commits.len() >= PARALLEL_CHECKOUT_THRESHOLD {
2520 use rayon::prelude::*;
2521 let base = self.base_blobs.clone();
2522 return commits
2523 .into_par_iter()
2524 .map_with(
2525 (local, base),
2526 |(local, base), commit| -> Result<TribleSet, _> {
2527 let meta: TribleSet = local
2528 .get(commit)
2529 .or_else(|_| base.get(commit))
2530 .map_err(WorkspaceCheckoutError::Storage)?;
2531 let metadata_opt = match find!(
2532 (c: Inline<_>),
2533 pattern!(&meta, [{ metadata: ?c }])
2534 )
2535 .at_most_one()
2536 {
2537 Ok(Some((c,))) => Some(c),
2538 Ok(None) => None,
2539 Err(_) => {
2540 return Err(WorkspaceCheckoutError::BadCommitMetadata())
2541 }
2542 };
2543 if let Some(c) = metadata_opt {
2544 let set: TribleSet = local
2545 .get(c)
2546 .or_else(|_| base.get(c))
2547 .map_err(WorkspaceCheckoutError::Storage)?;
2548 Ok(set)
2549 } else {
2550 Ok(TribleSet::new())
2551 }
2552 },
2553 )
2554 .try_reduce(TribleSet::new, |a, b| Ok(a + b));
2555 }
2556 }
2557
2558 let mut result = TribleSet::new();
2559 for commit in commits {
2560 let meta: TribleSet = local
2561 .get(commit)
2562 .or_else(|_| self.base_blobs.get(commit))
2563 .map_err(WorkspaceCheckoutError::Storage)?;
2564
2565 let metadata_opt =
2566 match find!((c: Inline<_>), pattern!(&meta, [{ metadata: ?c }])).at_most_one() {
2567 Ok(Some((c,))) => Some(c),
2568 Ok(None) => None,
2569 Err(_) => return Err(WorkspaceCheckoutError::BadCommitMetadata()),
2570 };
2571
2572 if let Some(c) = metadata_opt {
2573 let set: TribleSet = local
2574 .get(c)
2575 .or_else(|_| self.base_blobs.get(c))
2576 .map_err(WorkspaceCheckoutError::Storage)?;
2577 result += set;
2578 }
2579 }
2580 Ok(result)
2581 }
2582
2583 fn checkout_commits_with_metadata<I>(
2584 &mut self,
2585 commits: I,
2586 ) -> Result<
2587 (TribleSet, TribleSet),
2588 WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet>::GetError<UnarchiveError>>,
2589 >
2590 where
2591 I: IntoIterator<Item = CommitHandle>,
2592 {
2593 let local = self.staged.reader().unwrap();
2594 let commits: Vec<CommitHandle> = commits.into_iter().collect();
2595
2596 #[cfg(feature = "parallel")]
2597 {
2598 if commits.len() >= PARALLEL_CHECKOUT_THRESHOLD {
2599 use rayon::prelude::*;
2600 let base = self.base_blobs.clone();
2601 return commits
2602 .into_par_iter()
2603 .map_with(
2604 (local, base),
2605 |(local, base), commit| -> Result<(TribleSet, TribleSet), _> {
2606 let meta: TribleSet = local
2607 .get(commit)
2608 .or_else(|_| base.get(commit))
2609 .map_err(WorkspaceCheckoutError::Storage)?;
2610 let content_opt = match find!(
2611 (c: Inline<_>),
2612 pattern!(&meta, [{ content: ?c }])
2613 )
2614 .at_most_one()
2615 {
2616 Ok(Some((c,))) => Some(c),
2617 Ok(None) => None,
2618 Err(_) => {
2619 return Err(WorkspaceCheckoutError::BadCommitMetadata())
2620 }
2621 };
2622 let data_set = if let Some(c) = content_opt {
2623 local
2624 .get(c)
2625 .or_else(|_| base.get(c))
2626 .map_err(WorkspaceCheckoutError::Storage)?
2627 } else {
2628 TribleSet::new()
2629 };
2630 let metadata_opt = match find!(
2631 (c: Inline<_>),
2632 pattern!(&meta, [{ metadata: ?c }])
2633 )
2634 .at_most_one()
2635 {
2636 Ok(Some((c,))) => Some(c),
2637 Ok(None) => None,
2638 Err(_) => {
2639 return Err(WorkspaceCheckoutError::BadCommitMetadata())
2640 }
2641 };
2642 let metadata_set = if let Some(c) = metadata_opt {
2643 local
2644 .get(c)
2645 .or_else(|_| base.get(c))
2646 .map_err(WorkspaceCheckoutError::Storage)?
2647 } else {
2648 TribleSet::new()
2649 };
2650 Ok((data_set, metadata_set))
2651 },
2652 )
2653 .try_reduce(
2654 || (TribleSet::new(), TribleSet::new()),
2655 |(a_data, a_meta), (b_data, b_meta)| {
2656 Ok((a_data + b_data, a_meta + b_meta))
2657 },
2658 );
2659 }
2660 }
2661
2662 let mut data = TribleSet::new();
2663 let mut metadata_set = TribleSet::new();
2664 for commit in commits {
2665 let meta: TribleSet = local
2666 .get(commit)
2667 .or_else(|_| self.base_blobs.get(commit))
2668 .map_err(WorkspaceCheckoutError::Storage)?;
2669
2670 let content_opt =
2671 match find!((c: Inline<_>), pattern!(&meta, [{ content: ?c }])).at_most_one() {
2672 Ok(Some((c,))) => Some(c),
2673 Ok(None) => None,
2674 Err(_) => return Err(WorkspaceCheckoutError::BadCommitMetadata()),
2675 };
2676
2677 if let Some(c) = content_opt {
2678 let set: TribleSet = local
2679 .get(c)
2680 .or_else(|_| self.base_blobs.get(c))
2681 .map_err(WorkspaceCheckoutError::Storage)?;
2682 data += set;
2683 }
2684
2685 let metadata_opt =
2686 match find!((c: Inline<_>), pattern!(&meta, [{ metadata: ?c }])).at_most_one() {
2687 Ok(Some((c,))) => Some(c),
2688 Ok(None) => None,
2689 Err(_) => return Err(WorkspaceCheckoutError::BadCommitMetadata()),
2690 };
2691
2692 if let Some(c) = metadata_opt {
2693 let set: TribleSet = local
2694 .get(c)
2695 .or_else(|_| self.base_blobs.get(c))
2696 .map_err(WorkspaceCheckoutError::Storage)?;
2697 metadata_set += set;
2698 }
2699 }
2700 Ok((data, metadata_set))
2701 }
2702
2703 /// Returns the combined [`TribleSet`] for the specified commits or commit
2704 /// ranges. `spec` can be a single [`CommitHandle`], an iterator of handles
2705 /// or any of the standard range types over [`CommitHandle`].
2706 pub fn checkout<R>(
2707 &mut self,
2708 spec: R,
2709 ) -> Result<
2710 Checkout,
2711 WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet>::GetError<UnarchiveError>>,
2712 >
2713 where
2714 R: CommitSelector<Blobs>,
2715 {
2716 let commits = spec.select(self)?;
2717 let facts = self.checkout_commits(commits.iter().map(|raw| Inline::new(*raw)))?;
2718 Ok(Checkout { facts, commits })
2719 }
2720
2721 /// Returns the combined metadata [`TribleSet`] for the specified commits.
2722 /// Commits without metadata handles contribute an empty set.
2723 pub fn checkout_metadata<R>(
2724 &mut self,
2725 spec: R,
2726 ) -> Result<
2727 TribleSet,
2728 WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet>::GetError<UnarchiveError>>,
2729 >
2730 where
2731 R: CommitSelector<Blobs>,
2732 {
2733 let patch = spec.select(self)?;
2734 let commits = patch.iter().map(|raw| Inline::new(*raw));
2735 self.checkout_commits_metadata(commits)
2736 }
2737
2738 /// Returns the combined data and metadata [`TribleSet`] for the specified commits.
2739 /// Metadata is loaded from each commit's `metadata` handle, when present.
2740 pub fn checkout_with_metadata<R>(
2741 &mut self,
2742 spec: R,
2743 ) -> Result<
2744 (TribleSet, TribleSet),
2745 WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet>::GetError<UnarchiveError>>,
2746 >
2747 where
2748 R: CommitSelector<Blobs>,
2749 {
2750 let patch = spec.select(self)?;
2751 let commits = patch.iter().map(|raw| Inline::new(*raw));
2752 self.checkout_commits_with_metadata(commits)
2753 }
2754}
2755
2756#[derive(Debug)]
2757pub enum WorkspaceCheckoutError<GetErr: Error> {
2758 /// Error retrieving blobs from storage.
2759 Storage(GetErr),
2760 /// Commit metadata is malformed or ambiguous.
2761 BadCommitMetadata(),
2762}
2763
2764impl<E: Error + fmt::Debug> fmt::Display for WorkspaceCheckoutError<E> {
2765 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2766 match self {
2767 WorkspaceCheckoutError::Storage(e) => write!(f, "storage error: {e}"),
2768 WorkspaceCheckoutError::BadCommitMetadata() => {
2769 write!(f, "commit metadata malformed")
2770 }
2771 }
2772 }
2773}
2774
2775impl<E: Error + fmt::Debug> Error for WorkspaceCheckoutError<E> {}
2776
2777fn collect_reachable<Blobs: BlobStore>(
2778 ws: &mut Workspace<Blobs>,
2779 from: CommitHandle,
2780) -> Result<
2781 CommitSet,
2782 WorkspaceCheckoutError<<Blobs::Reader as BlobStoreGet>::GetError<UnarchiveError>>,
2783> {
2784 let mut visited = HashSet::new();
2785 let mut stack = vec![from];
2786 let mut result = CommitSet::new();
2787
2788 while let Some(commit) = stack.pop() {
2789 if !visited.insert(commit) {
2790 continue;
2791 }
2792 result.insert(&Entry::new(&commit.raw));
2793
2794 let meta: TribleSet = ws
2795 .staged
2796 .reader()
2797 .unwrap()
2798 .get(commit)
2799 .or_else(|_| ws.base_blobs.get(commit))
2800 .map_err(WorkspaceCheckoutError::Storage)?;
2801
2802 for (p,) in find!((p: Inline<_>,), pattern!(&meta, [{ parent: ?p }])) {
2803 stack.push(p);
2804 }
2805 }
2806
2807 Ok(result)
2808}