Skip to main content

Repository

Struct Repository 

Source
pub struct Repository<R = RefManager, O = OpLog, S = AnyStore>{ /* private fields */ }
Expand description

A Heddle repository.

Generic over its reference, operation-log, and object-store backends. The CLI uses the defaults — Repository<RefManager, OpLog, AnyStore> (the on-disk local backends) — so the bare name Repository resolves to the local flavor everywhere. The hosted server instantiates Repository<PgRefBackend, PgOpLogBackend, …> via Repository::from_parts.

The object store is the AnyStore enum by default: Repository::open selects FsStore vs S3Store at runtime from config (build_store), but the choice is a concrete enum variant rather than a Box<dyn>, so every object access is static-dispatched through the enum to the inner store — no vtable (heddle#283). S goes last so existing Repository<R, O> references keep resolving with S = AnyStore.

Implementations§

Source§

impl Repository

Source§

impl Repository

Source§

impl Repository

Source

pub fn get_context_blob( &self, context_root: &ContentHash, target: &ContextTarget, ) -> Result<Option<ContextBlob>, HeddleError>

Get the context blob for a target from the given state’s context tree.

Source

pub fn set_context_blob( &self, context_root: Option<&ContentHash>, target: &ContextTarget, blob: &ContextBlob, ) -> Result<ContentHash, HeddleError>

Store a context blob at a target, returning the new context tree root hash.

If context_root is None, creates a new context tree from scratch.

Source

pub fn remove_context_at_target( &self, context_root: &ContentHash, target: &ContextTarget, scope: Option<&AnnotationScope>, ) -> Result<Option<ContentHash>, HeddleError>

Remove context at a target (optionally filtered by scope).

Returns the new context tree root, or None if the tree is now empty.

Source

pub fn remove_context_target( &self, context_root: &ContentHash, target: &ContextTarget, ) -> Result<Option<ContentHash>, HeddleError>

Source

pub fn list_context_entries( &self, context_root: &ContentHash, prefix: Option<&Path>, ) -> Result<Vec<ContextEntry>, HeddleError>

List all context entries in the tree, optionally filtered by file prefix.

Source

pub fn find_annotation( &self, context_root: &ContentHash, annotation_id: &str, ) -> Result<Option<(ContextTarget, ContextBlob, usize)>, HeddleError>

Source

pub fn inherit_parent_context(parent: &State) -> Option<ContentHash>

Carry a single parent state’s context tree forward onto a new snapshot. Because context trees are content-addressed, this is a pointer copy: the new state’s context field gets the same ContentHash as the parent. Annotations attached upstream remain active at the new state, and the existing on-demand staleness check (which compares the stored source_hash against the current bytes at the anchor) naturally reports drift caused by the new tree.

Returns None when the parent has no context tree.

Source

pub fn union_parent_contexts( &self, parents: &[&State], ) -> Result<Option<ContentHash>, HeddleError>

Build a unioned context tree across multiple parent states for a merge snapshot. Annotations from every parent appear in the result; when the same annotation_id is present on more than one parent the revision with the latest created_at wins (with a stable tiebreak on revision_id so the merge stays deterministic).

Targets that only exist on one side propagate unchanged (single-blob pointer copy via the existing tree). Targets present on both sides are merged blob-by-blob: annotations are deduped by id, the per-id revisions are picked by latest-created_at, and the resulting blob is rewritten via set_context_blob.

Returns None when none of the parents has any context.

Source§

impl Repository

Source

pub fn diff_trees( &self, from: &ContentHash, to: &ContentHash, ) -> Result<FileChangeSet, HeddleError>

Get the difference between two trees.

Source

pub fn diff_trees_visit<V, B>( &self, from: &ContentHash, to: &ContentHash, visitor: V, ) -> Result<ControlFlow<B>, HeddleError>
where V: FnMut(FileChange) -> ControlFlow<B>,

Diff two trees with internal iteration, invoking visitor for each change in traversal order.

Unlike Repository::diff_trees, this never materializes the full change list: the visitor returns a ControlFlow so early-exit consumers can stop on the first relevant change without allocating a FileChangeSet. Returns the carried B on early exit, or ControlFlow::Continue(()) when the full diff is walked.

Source§

impl Repository

Source

pub fn goto(&self, target: &ChangeId) -> Result<(), HeddleError>

Move worktree to a different state.

Source

pub fn goto_discard_local(&self, target: &ChangeId) -> Result<(), HeddleError>

Move worktree to a different state, discarding unsnapped local edits.

Source

pub fn goto_from_materialized_state( &self, target: &ChangeId, materialized: Option<&ChangeId>, ) -> Result<(), HeddleError>

Move worktree to target using the state that the worktree currently represents as the dirty-check and incremental-apply baseline.

This is for callers that must publish or import refs before they can materialize the checkout. In those flows HEAD may already resolve to target, even though the files on disk still represent materialized.

Source

pub fn fast_forward_attached( &self, target: &ChangeId, ) -> Result<(), HeddleError>

Fast-forward the current checkout to target.

If HEAD was attached to a thread, advance that thread’s ref so the thread now points at target and re-attach HEAD. If HEAD was detached, advance to target while remaining detached.

Use this anywhere you’d previously call Repository::goto from a context where HEAD was potentially attached (merge/rebase fast-forward, pull/fetch, etc). The low-level goto unconditionally writes Head::Detached, which silently strands the attached thread at its pre-op state — this helper preserves attached-HEAD semantics so the thread’s ref and metadata advance with the worktree.

Source

pub fn fast_forward_attached_discard_local( &self, target: &ChangeId, ) -> Result<(), HeddleError>

Source

pub fn fast_forward_attached_from_materialized_state( &self, target: &ChangeId, materialized: Option<&ChangeId>, ) -> Result<(), HeddleError>

Source

pub fn fast_forward_attached_without_record( &self, target: &ChangeId, ) -> Result<(), HeddleError>

Variant of Self::fast_forward_attached that performs the fast-forward without recording an OpRecord::Goto. The merge command uses this so it can record an OpRecord::FastForward instead — the FF-specific variant carries both pre_target_id (for undo) and post_target_id (for deterministic redo). The generic Goto inverse only rewinds HEAD, which stranded the merged-into thread ref (heddle#99 r1); a name-resolved redo was also non-deterministic if the source thread moved (heddle#99 r2).

Source

pub fn fast_forward_attached_without_record_discard_local( &self, target: &ChangeId, ) -> Result<(), HeddleError>

Source

pub fn fast_forward_attached_from_materialized_state_without_record( &self, target: &ChangeId, materialized: Option<&ChangeId>, ) -> Result<(), HeddleError>

Source

pub fn goto_verified_clean(&self, target: &ChangeId) -> Result<(), HeddleError>

Source

pub fn goto_verified_clean_without_record( &self, target: &ChangeId, ) -> Result<(), HeddleError>

Source

pub fn goto_without_record(&self, target: &ChangeId) -> Result<(), HeddleError>

Source

pub fn goto_without_record_discard_local( &self, target: &ChangeId, ) -> Result<(), HeddleError>

Source§

impl Repository

Source

pub fn query_history( &self, query: &HistoryQuery, ) -> Result<Vec<State>, HeddleError>

Walk first-parent history and return states matching the query.

Source§

impl Repository

Source§

impl Repository

Source

pub fn warm_canonical_store_for_state( &self, state_id: &ChangeId, ) -> Result<WarmCanonicalStoreStats, HeddleError>

Promote every reachable blob from state_id’s tree(s) into the canonical loose-uncompressed store, so a subsequent materialize_tree (or N parallel materializations) can reflink from the canonical store without paying the decompress-on-first-clone tax.

Returns counts of work done. Errors per blob are accumulated rather than bubbled up so a single corrupt or missing object doesn’t poison the whole warm pass — the lazy path inside materialize_blob will surface that loudly when it actually matters.

Source

pub fn warm_canonical_store_for_states( &self, state_ids: &[ChangeId], ) -> Result<WarmCanonicalStoreStats, HeddleError>

Multi-state variant. Walks each state’s tree once, dedupes the union of reachable blob hashes across all of them, and promotes them. Useful when materializing several sibling states from the same parent in quick succession (the heddle delegate-style flow).

Source

pub fn materialize_computed_tree( &self, tree: &Tree, dir: &Path, ) -> Result<(), HeddleError>

Materialize a locally-computed tree to dir — a merge or cherry-pick result that is not a single named committed state and so carries no audience to gate against. The operator already holds every byte the computation combined; this is a working-tree write, not a serve to an audience. For serving a named state to a checkout, use Repository::checkout_state_gated instead.

Source§

impl Repository

Source§

impl Repository

Source

pub fn resolve_state(&self, spec: &str) -> Result<Option<ChangeId>, HeddleError>

Resolve a state specifier (HEAD, thread, marker, full/short ID, HEAD~N).

Source

pub fn resolve_agent(&self) -> Option<Agent>

Source§

impl Repository

Source

pub fn put_authored_state(&self, state: &mut State) -> Result<(), HeddleError>

The authored-state write chokepoint (heddle#482): auto-sign (best-effort) then persist a freshly authored State. EVERY writer that records a new authored state routes through here — capture/snapshot, merge, mount capture, thread materialize, fork, collapse, context annotation, and both rebase replay paths, across this crate AND the mount/cli crates — so no authored state can reach the store unsigned. Signing is the LAST mutation before the write, so the signature covers the final field set; any later change would invalidate it.

Non-authored writes deliberately do NOT use this path — they keep their existing signature (or stay legitimately unsigned) rather than minting a fresh one: replaying/transferring an already-signed state (put_state_serialized, sync/packfile ops), the synthetic init root (seed_default_thread), git-import of foreign history, and server-side review/signal mutations. Re-signing those would either clobber an existing signature or falsely attribute foreign content to this device.

Source

pub fn resign_if_owned( &self, state: &mut State, prior_hashes: &[ContentHash], ) -> ResignOutcome

Re-sign state over its CURRENT compute_hash() IFF its existing signature was both produced by a key this repo controls AND already valid over one of prior_hashes (the hash candidates the signature could have been made over, BEFORE the caller’s rewrite). So an authorized rewrite (e.g. the #570 fidelity backfill, which re-derives hash-bearing fields) keeps a valid signature instead of shipping one that no longer verifies.

Multiple candidates are accepted because the #565 format bump changed how compute_hash folds the git-fidelity fields: a state signed BEFORE the bump was signed over its pre-fidelity hash (State::compute_hash_pre_fidelity), while one signed after was signed over the current hash. The backfill passes BOTH so a valid legacy signature isn’t misread as unreproducible just because the new hash doesn’t match (heddle#570).

Ownership is decided by Self::owning_signer_for, which tries both the active signer and the per-repo local key — a repo that signed states with its local key before linking a device key is still recognised as the owner (heddle#570).

Returns ResignOutcome::Unreproducible WITHOUT modifying state when:

  • the signature belongs to a foreign key (or no owned signer resolves) — re-signing foreign content would falsely attribute it to this device; or
  • the existing signature does NOT verify against ANY of prior_hashes — re-signing over it would launder a never-valid signature into a fresh, valid-looking one (heddle#570).

In both cases the caller MUST NOT persist a rewritten version of the state.

Source

pub fn sign_state( &self, state_id: &ChangeId, signer: &dyn Signer, ) -> Result<(), HeddleError>

Sign a state with the given signer.

This loads the state, signs it, and stores the updated state.

§Arguments
  • state_id - The change ID of the state to sign
  • signer - The signer to use
§Errors

Returns an error if the state is not found or signing fails.

Source

pub fn sign_state_with_key( &self, state_id: &ChangeId, key_path: &Path, algorithm: Option<&str>, ) -> Result<(), HeddleError>

Sign a state using a key file.

§Arguments
  • state_id - The change ID of the state to sign
  • key_path - Path to the private key file
  • algorithm - Optional algorithm hint (auto-detected if not specified)
Source

pub fn verify_state_signature( &self, state_id: &ChangeId, ) -> Result<SignatureStatus, HeddleError>

Verify a state’s signature.

Returns the signature status:

  • SignatureStatus::Valid if the signature is valid
  • SignatureStatus::Invalid if the signature is invalid
  • SignatureStatus::Unsigned if the state has no signature
§Arguments
  • state_id - The change ID of the state to verify
Source

pub fn get_state_signature( &self, state_id: &ChangeId, ) -> Result<Option<StateSignature>, HeddleError>

Get the signature of a state.

Returns None if the state is not found or has no signature.

Source§

impl Repository

Source

pub fn snapshot( &self, intent: Option<String>, confidence: Option<f32>, ) -> Result<State, HeddleError>

Create a snapshot of the current worktree.

Source

pub fn snapshot_with_attribution( &self, intent: Option<String>, confidence: Option<f32>, attribution: Attribution, ) -> Result<State, HeddleError>

Create a snapshot with explicit attribution.

Source

pub fn snapshot_with_attribution_profiled( &self, intent: Option<String>, confidence: Option<f32>, attribution: Attribution, ) -> Result<SnapshotExecution, HeddleError>

Create a snapshot with profiling details for the hot path.

Snapshot chokepoint (heddle#317, PR #529): EVERY worktree-snapshot creator — capture, cherry-pick, revert, retro — funnels through here, so the configured default visibility tier is bound to the freshly created state at this single site. The binding is folded into the snapshot’s own oplog batch inside [SnapshotMutation::apply] (via stage_default_visibility_binding), so one heddle undo reverts the snapshot and its auto-applied default together — never a separate trailing batch (PR #529 P1). The in-progress merge branch commits through commit_snapshot_atomic and folds the binding there for the same reason.

Source

pub fn snapshot_tree_with_attribution_profiled( &self, tree: Tree, intent: Option<String>, confidence: Option<f32>, attribution: Attribution, ) -> Result<SnapshotExecution, HeddleError>

Create a snapshot from a caller-supplied tree instead of walking the worktree. Used by Git-overlay staged-index commits, where the desired snapshot is the Git index boundary and the worktree may intentionally still contain unstaged files.

Routes through the same default-visibility chokepoint as snapshot_with_attribution_profiled: a Git-overlay capture must inherit the configured default tier too. The binding is folded into the snapshot’s batch inside [SnapshotMutation::apply] (PR #529 P1).

Source

pub fn snapshot_merge_with_attribution( &self, merge_parent: &ChangeId, intent: Option<String>, confidence: Option<f32>, attribution: Attribution, merge_base: Option<ChangeId>, fold_default_visibility: bool, ) -> Result<State, HeddleError>

Create a merge state with two parents.

fold_default_visibility binds the configured capture-time default visibility tier to the new merge state and folds the resulting OpRecord::StateVisibilitySet into the merge’s own commit batch (so one heddle undo reverts both — heddle#317 / PR #529 P1). It is true ONLY for the in-progress-merge capture branch of snapshot_with_attribution_profiled, which calls this while already holding the snapshot write lock — so the sidecar write runs lock-held. The direct merge callers (the merge verb, thread refresh, signing tests) pass false, preserving their prior no-auto-binding behavior.

Source§

impl Repository

Source

pub fn materialize_thread( &self, thread: &str, dest: &Path, audience: &AudienceTier, ) -> Result<ThreadManifest, HeddleError>

Materialize the captured tree of thread to dest and write a ThreadManifest sidecar to <heddle_dir>/threads/<thread>/manifest.toml.

Order of operations:

  1. Resolve threadChangeIdStateTree.
  2. Call Repository::materialize_tree(&tree, dest) — the existing clonefile-first materializer does the heavy lifting (loose-uncompressed promotion, parallel writes).
  3. Walk the materialized tree and capture per-file (hash, inode, mtime_ns, ctime_ns, mode) into the manifest.
  4. Atomically write the manifest.

The walk step in (3) is a single stat per file — sub-ms for the 643-file heddle workspace. Doing the walk after materialize rather than capturing stats during materialize keeps the existing materializer untouched.

Source

pub fn checkout_state_gated( &self, change_id: &ChangeId, state: &State, dest: &Path, audience: &AudienceTier, ) -> Result<CheckoutMaterialization, HeddleError>

THE visibility-gated checkout chokepoint. Resolve change_id’s effective tier against audience and either materialize its real tree to dest (visible) or write the operator-local courtesy stub and withhold the tracked bytes (under-tier).

Every path that serves a named committed state’s content to a local checkout MUST funnel through here — materialize_thread and the CLI’s write_isolated_checkout (heddle start --path) both do — so the visibility gate cannot be bypassed by a caller reaching for the raw, blob-keyed materialize_tree. The decision is made HERE, where the ChangeId and the audience are both in scope; materialize_tree carries neither and so cannot make it. materialize_tree stays the primitive for computed trees (merge/cherry-pick results), which are not a single named state and carry no audience.

The courtesy stub is a working-tree convenience on bytes the operator already holds — NOT a security boundary and NOT a public-mirror surface (the public mirror emits absence, spike §5.3).

Source

pub fn clear_materialized_root_records( &self, worktree_root: &Path, ) -> Result<(), HeddleError>

Remove the per-worktree-root sidecars checkout_state_gated writes — the clobber-proof materialized-leaves record and (if present) the withheld marker — for the checkout at worktree_root. Both live under the SHARED heddle dir keyed by the canonical worktree root, so the atomic start rollback’s checkout-directory rewind never reaches them; a failed-then- rolled-back start would otherwise orphan them. Canonicalizes worktree_root the same way the chokepoint did, so the key matches; the dir must still exist at call time (the rollback clears these BEFORE rewinding the dir). Idempotent: missing sidecars are a no-op (heddle#316 r11 P2).

Source

pub fn record_thread_manifest( &self, thread: &str, state_id: &ChangeId, dest: &Path, ) -> Result<ThreadManifest, HeddleError>

Write the ThreadManifest sidecar for a worktree that’s already been materialised to dest against state_id. Used by the CLI’s start path, which calls materialize_tree directly via write_isolated_checkout and then needs the matching manifest written so the rest of the clonefile-thread machinery (heddle status advisory, Repository::snapshot auto-detection, capture_thread_from_disk fast no-op) sees a fully-formed sidecar.

state_id is the captured state the worktree was materialised against; its tree is resolved and walked to populate the manifest’s per-file stat-cache entries (one lstat per file). Atomic write: a torn manifest can’t half-land. Idempotent at the manifest-key level: rewriting a manifest for the same thread is supported (and is what capture_thread_from_disk does post-capture).

Source

pub fn record_withheld_thread_manifest( &self, thread: &str, state_id: &ChangeId, dest: &Path, ) -> Result<ThreadManifest, HeddleError>

Record a WITHHELD-consistent manifest sidecar for a worktree whose checkout was withheld — the base state’s visibility tier was not visible to the materializing audience, so Repository::checkout_state_gated wrote ONLY the operator-local courtesy stub and the tracked bytes were never materialized.

Mirrors the withheld arm of Repository::materialize_thread: tree_hash still names the real (unserved) state’s tree so the sidecar identifies which state the stub stands in for, but files is empty (no tracked leaf is on disk) and withheld = true. Crucially this does NOT walk/stat the real tree against dest the way Repository::record_thread_manifest does — those files were intentionally not materialized, so stat-ing them would record phantom stat-cache entries (or fail) against a checkout that holds only the stub. The CLI’s atomic start path calls this instead of record_thread_manifest when the checkout came back withheld, so a start on a Private base produces a withheld checkout + a consistent manifest rather than erroring (heddle#316 / PR #528 r9 Finding 3).

Source

pub fn thread_create_op_record(&self, name: &str, state: ChangeId) -> OpRecord

The staged domain commit record for a brand-new materialized-thread start. The repo owns the op-record shape so callers don’t reconstruct OpRecord::ThreadCreate’s fields. manager_snapshot is None: the thread record is written by the start’s converge step (so there is nothing to snapshot at record-construction time — heddle#23 r2). The caller stages this as the executor’s single commit record (it is NOT appended eagerly); the commit marker dedups on the stable transaction_id.

Source

pub fn cas_guarded_thread_ref_rollback( &self, name: &ThreadName, set_value: ChangeId, restore_to: Option<ChangeId>, ) -> Result<(), HeddleError>

CAS-guarded rollback of a materialized-thread-start ref forward (heddle#356 cid 3333881583).

The forward set the thread ref to set_value (the start’s base state). Undo it ONLY if the ref STILL points there: restore restore_to when a prior value existed (a re-start that reused the ref), or delete a ref this start created (restore_to == None). If a concurrent process advanced/changed the ref after our forward (a concurrent start or crash-recovery), leave their write in place — an unconditional reset/delete would clobber it.

Source

pub fn restore_thread_manifest( &self, thread: &str, prior: Option<Vec<u8>>, ) -> Result<(), HeddleError>

Restore the thread manifest sidecar to its captured pre-start snapshot: rewrite the prior manifest.toml bytes if one existed, or remove the directory this start created. Restoring (not blind-deleting) preserves an OLD manifest left by a prior materialization of a reused thread ref (heddle#356 cid 3333881561).

Source

pub fn capture_thread_from_disk( &self, thread: &str, root: &Path, ) -> Result<ThreadCaptureOutcome, HeddleError>

Scan the materialized worktree at root, build a fresh tree from the on-disk bytes, and (if anything changed) advance thread’s head to a new state pointing at that tree. The manifest is rewritten to reflect the new state and the post-capture stat fields.

Returns ThreadCaptureOutcome::NoOp when the new tree’s hash equals the manifest’s recorded tree_hash — the agent touched nothing material. Otherwise ThreadCaptureOutcome::Captured with the new state id.

The reason this method exists alongside Repository::snapshot is two-fold:

  1. snapshot always advances HEAD’s currently-attached thread. Capture-from-disk targets a specific thread by name, which is what auto-capture-on-switch needs.
  2. snapshot walks self.root. Capture-from-disk walks whatever directory the materializer put the thread at — managed checkouts under <repo>/.heddle/threads/<thread>/, which are NOT self.root.

Walks Repository::build_tree for the slow path so the resulting trees are byte-identical to what heddle capture produces against the same content. A stat-cache fast path (see [stat_cache_no_op]) short-circuits the common case of “switch threads, nothing changed” so the dominant auto-capture-on-switch latency is a stat walk, not a blob rehash.

Source§

impl Repository

Source

pub fn build_tree(&self, dir: &Path) -> Result<Tree, HeddleError>

Build a tree from a directory.

Source

pub fn build_tree_with_stat_cache( &self, dir: &Path, manifest: &ThreadManifest, ) -> Result<Tree, HeddleError>

Build a tree from a directory, reusing per-file hashes from a thread manifest when the on-disk (inode, mtime, ctime, mode) still matches the recorded snapshot.

Same output as Self::build_tree — a complete Tree object — but files whose stat fields match the cache skip the read + hash + put_blob cycle entirely. Net effect on capture_thread_from_disk for a single-file edit on a 643-file fixture: blob work drops from ~30 MB of reads to ~one file’s worth. Wall-clock follows.

Safe-by-default: any uncertainty (entry missing from cache, stat mismatch) falls back to the full read path for that specific file. Other files in the same tree still benefit.

Source

pub fn build_tree_profiled( &self, dir: &Path, ) -> Result<(Tree, TreeBuildProfile), HeddleError>

Source

pub fn build_tree_profiled_with_stat_cache( &self, dir: &Path, manifest: &ThreadManifest, ) -> Result<(Tree, TreeBuildProfile), HeddleError>

Profiled tree-build that reuses a manifest’s stat-cache. Same contract as Self::build_tree_profiled — returns the full (Tree, TreeBuildProfile) for downstream timing — but skips the read + hash + put_blob cycle for files whose stat fields match the cache. The fall-through path for changed/new files is identical, so the resulting tree is byte-identical to what the un-cached build would produce.

Source

pub fn compare_worktree_cached( &self, tree: &Tree, ) -> Result<WorktreeStatus, HeddleError>

Compare the worktree against a tree using the persisted binary index.

Source

pub fn compare_worktree_cached_detailed( &self, tree: &Tree, ) -> Result<WorktreeStatusDetailed, HeddleError>

Source

pub fn compare_worktree_cached_with_options( &self, tree: &Tree, options: &WorktreeStatusOptions, ) -> Result<WorktreeStatus, HeddleError>

Compare the worktree against a tree using the persisted binary index.

Source

pub fn compare_worktree_cached_detailed_with_options( &self, tree: &Tree, options: &WorktreeStatusOptions, ) -> Result<WorktreeStatusDetailed, HeddleError>

Source

pub fn compare_worktree_cached_profiled_with_options( &self, tree: &Tree, options: &WorktreeStatusOptions, ) -> Result<(WorktreeStatus, WorktreeCompareProfile), HeddleError>

Source

pub fn compare_worktree_cached_detailed_profiled_with_options( &self, tree: &Tree, options: &WorktreeStatusOptions, ) -> Result<(WorktreeStatusDetailed, WorktreeCompareProfile), HeddleError>

Source

pub fn worktree_is_clean_cached(&self, tree: &Tree) -> Result<bool, HeddleError>

Return whether the worktree matches the provided tree.

Source

pub fn worktree_is_clean_cached_with_options( &self, tree: &Tree, options: &WorktreeStatusOptions, ) -> Result<bool, HeddleError>

Return whether the worktree matches the provided tree.

Source

pub fn inspect_change_monitor_with_options( &self, options: &WorktreeStatusOptions, ) -> Result<ChangeMonitorReport, HeddleError>

Source§

impl Repository

Source

pub fn remove_tracked_descendants(&self, path: &Path) -> Result<(), HeddleError>

Remove only the heddle-tracked descendants beneath path, preserving any untracked or explicitly ignored siblings.

This exists so commands that mutate the worktree at the top-level tree-entry granularity (merge, cherry-pick, revert) can drop a tracked directory without recursively destroying the user’s local build artifacts, dependencies, or co-located Git state. The shape matches remove_existing_path in this module: tracked content is removed, then the directory itself is removed if empty; if ignored content keeps it occupied, the dir is left in place. That keeps disk in lock-step with the new tree (no stale tracked file under the dir) without nuking work the user expects to survive.

Ignore-pattern based variant. Uses the current .heddleignore to decide which children to preserve. This is unsafe for the merge/cherry-pick/revert flow when a tracked path is also matched by a current ignore rule: the file would silently survive on disk after HEAD advances. Prefer Self::remove_tracked_descendants_with_source in those flows so removal is driven by the source-tree’s actual tracked set.

path must be inside the repository root. If it doesn’t exist, this is a no-op. If it’s a regular file or symlink, it is removed.

Source

pub fn remove_tracked_descendants_with_source( &self, path: &Path, source_subtree: &Tree, ) -> Result<(), HeddleError>

Tree-driven variant of Self::remove_tracked_descendants.

Removal is driven by source_subtree — the subtree at path in the state we’re transitioning AWAY from. Every blob/symlink it lists is removed; nested directory entries are recursed into using the matching child subtree. This is intentionally independent of the current ignore rules: a .heddleignore (or config-level) rule that newly matches a previously-tracked path must NOT silently preserve that path on disk. Doing so would let HEAD advance past a tree where the path is gone while the worktree still holds the stale content, hidden from heddle status by the same ignore rule. Tracked-tree membership is the only source of truth here.

path must be inside the repository root. If it doesn’t exist, this is a no-op. If it’s a regular file or symlink, it is removed.

Source

pub fn resolve_subtree( &self, root_tree: &Tree, rel_path: &Path, ) -> Result<Option<Tree>, HeddleError>

Look up the subtree at rel_path within root_tree. Returns None if the path isn’t reachable as a Tree-typed entry (missing entry, blob entry, or unresolved hash). Used by Self::remove_tracked_descendants_with_source callers to derive the source subtree from a top-level tree entry.

Source§

impl<R, O, S> Repository<R, O, S>

Source

pub fn from_parts( root: PathBuf, heddle_dir: PathBuf, store: S, refs: R, oplog: O, config: RepoConfig, shallow: ShallowInfo, ) -> Repository<R, O, S>

Expert-only constructor for callers that already own the repository’s component backends and invariant state.

Callers must ensure all backends point at the same repository root, the heddle_dir exists and is canonical for that root, and shallow matches the on-disk shallow metadata. Prefer Repository::init, Repository::open, or Repository::open_with_store unless a cross-crate integration genuinely needs to assemble the pieces manually.

Source

pub fn store(&self) -> &S

The object store backing this repository.

Source

pub fn refs(&self) -> &R

The reference backend (threads, markers, HEAD).

Source

pub fn oplog(&self) -> &O

The operation-log backend.

Source§

impl<S> Repository<RefManager, OpLog, S>
where S: ObjectStore,

Source

pub fn open_with_store( heddle_dir: impl AsRef<Path>, store: S, ) -> Result<Repository<RefManager, OpLog, S>, HeddleError>

Open an existing Heddle repository using a custom object store backend.

Expert/test injection point: takes the store by value (any ObjectStore) and skips the local-only open hooks (declarative migrations, lazy-clone hydrator reconstruction) that Repository::open runs for the default AnyStore flavor.

Source§

impl Repository

Source

pub fn init(path: impl AsRef<Path>) -> Result<Repository, HeddleError>

Initialize a new bare repository at the given path.

Creates the on-disk .heddle structure and an attached main HEAD, but does not seed any threads or states. Callers that want a ready-to-use repository (with a main thread pointing at an empty-tree snapshot) should use Repository::init_default. Callers that intend to populate the repository from an external source (e.g. git import) should use init directly so the imported refs become the sole source of truth.

Source

pub fn init_default(path: impl AsRef<Path>) -> Result<Repository, HeddleError>

Initialize a new repository with a seeded main thread.

Convenience wrapper: equivalent to Repository::init followed by Repository::seed_default_thread. This is the normal entry point for fresh, user-created repositories where main should exist immediately.

Source

pub fn bootstrap_git_overlay( path: impl AsRef<Path>, ) -> Result<Repository, HeddleError>

Initialize Heddle sidecar storage in an existing Git repository.

Unlike Repository::init_default, this keeps the repo unseeded and mirrors the current Git branch attachment into Heddle’s HEAD so commands like heddle status can immediately reflect the user’s current branch and dirty worktree.

Source

pub fn ensure_git_overlay_local_excludes( path: impl AsRef<Path>, ) -> Result<(), HeddleError>

Install local, untracked Git exclude rules Heddle needs for Git-overlay repos. Only Heddle’s sidecar is excluded automatically; project artifacts must be covered by .gitignore or .heddleignore.

Source

pub fn open(path: impl AsRef<Path>) -> Result<Repository, HeddleError>

Open an existing repository.

Searches for .heddle/ in the given path and its ancestors. .heddle/ is always a directory; its contents distinguish a main repo from a worktree pointer:

  • Main repo: .heddle/objects/, .heddle/refs/, .heddle/HEAD, .heddle/state/, etc.
  • Worktree: .heddle/objectstore (text pointer to the shared .heddle/), .heddle/HEAD (per-checkout), .heddle/state/ (per-checkout cached state).
Source

pub fn root(&self) -> &Path

Source

pub fn heddle_dir(&self) -> &Path

Source

pub fn managed_checkout_source_root(&self) -> &Path

Root whose directory name should be used for managed thread checkout leaves.

For the main checkout this is repo.root(). For an isolated checkout, repo.root() is the checkout’s own directory (possibly custom-named), while heddle_dir points back at the shared source repository’s .heddle; use that shared parent so child threads keep the original repo name.

Source

pub fn managed_checkout_path(&self, thread: &str) -> PathBuf

Default managed checkout path for thread.

Source

pub fn capability(&self) -> RepositoryCapability

Source

pub fn git_overlay_sley_repository( &self, ) -> Result<Option<Repository>, HeddleError>

Source

pub fn capability_label(&self) -> &'static str

Source

pub fn storage_model_label(&self) -> &'static str

Source

pub fn hosted_enabled(&self) -> bool

Source

pub fn current_lane(&self) -> Result<Option<String>, HeddleError>

Source

pub fn operation_status( &self, ) -> Result<Option<RepositoryOperationStatus>, HeddleError>

Source

pub fn git_remote_tracking_status( &self, ) -> Result<Option<GitRemoteTrackingStatus>, HeddleError>

Source

pub fn git_overlay_import_hint( &self, ) -> Result<Option<GitOverlayImportHint>, HeddleError>

Source

pub fn git_overlay_branch_tips( &self, ) -> Result<Vec<GitOverlayBranchTip>, HeddleError>

Source

pub fn git_overlay_tag_tips(&self) -> Result<Vec<GitOverlayTagTip>, HeddleError>

Source

pub fn git_overlay_branch_tip( &self, name: &str, ) -> Result<Option<GitOverlayBranchTip>, HeddleError>

Source

pub fn git_overlay_tag_tip( &self, name: &str, ) -> Result<Option<GitOverlayTagTip>, HeddleError>

Source

pub fn git_overlay_mapped_change_for_branch( &self, name: &str, ) -> Result<Option<ChangeId>, HeddleError>

Source

pub fn git_overlay_mapped_change_for_remote_tracking_ref( &self, name: &str, ) -> Result<Option<ChangeId>, HeddleError>

Source

pub fn git_overlay_mapped_change_for_tag( &self, name: &str, ) -> Result<Option<ChangeId>, HeddleError>

Source

pub fn git_overlay_worktree_status( &self, ) -> Result<Option<WorktreeStatus>, HeddleError>

Source

pub fn git_overlay_ingest_commit_mapping( &self, ) -> Result<HashMap<String, String>, HeddleError>

Source

pub fn git_overlay_mapped_git_commit_for_change( &self, change_id: &ChangeId, ) -> Result<Option<String>, HeddleError>

Source

pub fn git_overlay_mapped_change_for_git_commit( &self, git_commit: &str, ) -> Result<Option<ChangeId>, HeddleError>

Source

pub fn git_overlay_out_of_band_commits( &self, tip_git_commit: &str, ) -> Result<Option<GitOverlayOutOfBandCommits>, HeddleError>

Count the Git commits reachable from tip_git_commit that are not represented in Heddle state (no served bridge mapping, ingest identity mapping, or checkpoint mapping). The walk prunes at the first mapped commit on each lineage, so the cost is proportional to the out-of-band suffix, capped at GIT_OVERLAY_OUT_OF_BAND_SCAN_LIMIT.

Returns Ok(None) when the repository is not a Git overlay or the tip cannot be resolved; callers should degrade to a countless report.

Source

pub fn git_overlay_current_branch(&self) -> Result<Option<String>, HeddleError>

Source

pub fn git_overlay_head_is_detached(&self) -> Result<bool, HeddleError>

Source

pub fn git_overlay_detached_head_commit( &self, ) -> Result<Option<String>, HeddleError>

Source

pub fn list_git_checkpoints( &self, ) -> Result<Vec<GitCheckpointRecord>, HeddleError>

Source

pub fn latest_git_checkpoint_for_change( &self, change_id: &ChangeId, ) -> Result<Option<GitCheckpointRecord>, HeddleError>

Source

pub fn record_git_checkpoint( &self, change_id: &ChangeId, git_commit: impl Into<String>, summary: impl Into<String>, ) -> Result<GitCheckpointRecord, HeddleError>

Source

pub fn init_worktree( path: impl AsRef<Path>, shared_galeed_dir: impl AsRef<Path>, ) -> Result<(), HeddleError>

Source

pub fn op_scope(&self) -> String

Source

pub fn commit_and_publish( &self, records: Vec<OpRecord>, ref_updates: &[RefUpdate], ) -> Result<(), HeddleError>

The write chokepoint (heddle#330 §2.2): commit the ref-carrying OpRecord batch (phase 4) before publishing the atomic ref_updates batch (phase 5), record-before-publish. Encodes the records opaquely and routes through RefBackend::commit_and_publish so the backend’s seam enforces the ordering — the file backend appends-then-publishes, a Postgres backend would co-commit in one SQL transaction. Replaces the publish-then-record order that left a reader-visible ref with no undo record (the fork/collapse bug).

Source

pub fn commit_snapshot_atomic( &self, new_state: &ChangeId, prev_head: Option<ChangeId>, thread: Option<&ThreadName>, ) -> Result<(), HeddleError>

Atomically commit a snapshot’s OpRecord::Snapshot and its paired ref publish through the write chokepoint, record-first (heddle#354 r8).

The pre-r8 snapshot path published the ref FIRST (refs.set_thread / refs.write_head) and recorded SECOND. Because the reconciler folds a Snapshot record authoritatively (newest committed record wins), a late snapshot record carrying a stale thread value could clobber a newer concurrent write that had already recorded. Routing every snapshot ref write through this single chokepoint makes the record the unit of ordering: the newest committed record IS the newest write, so the authoritative fold can no longer resurrect a stale snapshot.

thread = Some(name) advances that thread (HEAD stays attached); thread = None republishes a detached HEAD. The detached case is now record-first too, so a phase-4-committed / phase-5-unpublished crash is recovered by the reconciler reconstructing Head::Detached{new_state} (see atomic::reconciler’s detached-Snapshot arm).

Source

pub fn commit_snapshot_atomic_with_records( &self, new_state: &ChangeId, prev_head: Option<ChangeId>, thread: Option<&ThreadName>, extra: Vec<OpRecord>, ) -> Result<(), HeddleError>

commit_snapshot_atomic plus extra records folded into the SAME batch as the OpRecord::Snapshot.

Used by the snapshot creators that commit through this chokepoint rather than the SnapshotMutation transaction (the in-progress merge branch and the mount capture path) to fold the automatic capture-time default-visibility binding’s OpRecord::StateVisibilitySet into the snapshot’s batch, so one heddle undo reverts the snapshot and its auto-applied default tier together (heddle#317 / PR #529 P1).

Source

pub fn commit_snapshot_atomic_with_capture_visibility( &self, new_state: &ChangeId, prev_head: Option<ChangeId>, thread: Option<&ThreadName>, lock_held: bool, ) -> Result<(), HeddleError>

Commit a snapshot batch that folds the automatic capture-time default-visibility binding, rewinding the staged sidecar if the commit fails (heddle#317 invariant 2).

This is THE single fold-and-rewind chokepoint for snapshot creators that commit outside the SnapshotMutation executor — the mount capture path and the in-progress-merge branch. Those paths cannot lean on the executor’s rewind, so the rollback guarantee lives here, by construction: the binding’s sidecar is written by stage_default_visibility_binding before the batch commits, and if the commit errors the sidecar is rewound to its pre-binding image so no orphaned non-public sidecar is left for a state whose snapshot batch never committed.

lock_held is forwarded to stage_default_visibility_binding: the merge branch already holds the snapshot write lock (true); the mount path holds none (false). A public default stages nothing (absence ≡ public) and the commit runs with no folded record.

Source

pub fn repo_config(&self) -> &RepoConfig

Source

pub fn config(&self) -> &RepoConfig

Source

pub fn get_tree_for_state( &self, state_id: &ChangeId, ) -> Result<Option<Tree>, HeddleError>

Source

pub fn ignore_patterns(&self) -> Result<Vec<String>, HeddleError>

Source

pub fn nested_thread_worktree_exclusions( &self, walk_root: &Path, ) -> Result<Vec<PathBuf>, HeddleError>

Canonical absolute paths of other threads’ worktrees that are strict descendants of walk_root. The walker uses these to avoid scanning a sibling thread’s files into the current thread’s tree (a common shape when an agent worktree is materialized inside the parent repo, e.g. --path-prefix ./agents). Computed once per scan, not once per file.

Returns paths that

  • are strict descendants of canonical walk_root, and
  • are NOT equal to walk_root itself (each thread can scan its own worktree without excluding itself).

Threads with no recorded worktree, or worktrees that no longer exist on disk, are skipped without error.

Source

pub fn head(&self) -> Result<Option<ChangeId>, HeddleError>

Source

pub fn head_ref(&self) -> Result<Head, HeddleError>

Source

pub fn active_worktree_path(&self) -> Result<PathBuf, HeddleError>

Resolve the on-disk worktree path for the active thread.

This is the canonical “where does the current thread live on disk” lookup. It reads HEAD, looks up the attached thread’s metadata (via crate::ThreadManager), and returns the recorded execution_path (or materialized_path if unset). When no thread has a recorded path — main, threads created without a separate worktree, or HEAD::Detached — this falls back to Self::root.

Worktree-mutating commands (merge, rebase, goto, ship) should resolve their target via this helper so that heddle thread switch X && heddle merge Y lands the merge into thread X’s dedicated worktree, not into whichever directory the operator happened to invoke heddle from. Snapshot/capture intentionally stay CWD-based: the agent inside their worktree captures that worktree.

Source

pub fn current_state(&self) -> Result<Option<State>, HeddleError>

Source

pub fn get_principal(&self) -> Result<Principal, HeddleError>

Source

pub fn get_attribution(&self) -> Result<Attribution, HeddleError>

Source

pub fn is_shallow(&self, id: &ChangeId) -> bool

Source

pub fn set_shallow( &self, state_id: &ChangeId, _parents: &[ChangeId], ) -> Result<(), HeddleError>

Source

pub fn record_missing_blob(&self, hash: ContentHash) -> Result<(), HeddleError>

Source

pub fn seed_default_thread(&self) -> Result<(), HeddleError>

Seed a main thread pointing at an empty-tree root state.

The seeded state is written to the object store and pointed at by the main thread ref, but is deliberately NOT recorded in the oplog: init is a point-of-creation event, not user work, and should not be undoable. No-op if main already exists.

The seed state uses a stable Heddle <init@heddle> attribution instead of the user’s principal because the user’s principal may not yet be configured at init time (e.g. the user writes .heddle/config.toml after heddle init). Falling back to Unknown <unknown@example.com> would surface in heddle log as a state owned by no one. The genesis state is also filtered out of user-facing log output (see repository_history::is_synthetic_root).

Source

pub fn clear_missing_blob(&self, hash: &ContentHash) -> Result<(), HeddleError>

Source

pub fn missing_blobs(&self) -> Result<Vec<ContentHash>, HeddleError>

Source

pub fn clear_all_missing_blobs(&self) -> Result<bool, HeddleError>

Source

pub fn is_missing_blob(&self, hash: &ContentHash) -> Result<bool, HeddleError>

Source

pub fn require_tree(&self, hash: &ContentHash) -> Result<Tree, HeddleError>

Load a tree by hash from the object store, surfacing a clear error when the hash resolves to nothing.

Use this whenever a hash recorded in a State.tree field or as a subtree TreeEntry MUST resolve to an object: presentation paths (heddle status, heddle ready, heddle stash show), mutation paths (heddle revert, heddle cherry-pick, heddle goto, heddle resolve), and inspection paths (semantic diff, harness baseline) all qualify.

Replaces the legacy get_tree(...)?.unwrap_or_default() pattern. That pattern silently substituted Tree::default() for a missing object, so presentation paths rendered “no content” and mutation paths committed subtree-erasure merges (see heddle#90 for the merge-path lock and heddle#93 for the non-merge sweep that motivated this method).

Returns HeddleError::MissingObject with object_type = "tree" so callers and the top-level error printer can recognize the bug class. The Display impl on MissingObject includes the heddle fsck --full recovery hint, so call sites don’t need to wrap with anyhow context to give the operator a next step.

Pair with Repository::require_blob for the blob side of the same contract.

Source

pub fn require_blob(&self, hash: &ContentHash) -> Result<Blob, HeddleError>

Source

pub fn set_blob_hydrator(&self, hydrator: Arc<dyn BlobHydrator>)

Register a BlobHydrator to fetch blobs on demand from the upstream when require_blob hits a missing-blob marker. Used by the clone command after a --lazy / --filter blob:none clone. Replaces any previously registered hydrator.

The trait-object handle itself is process-local, but persistence across Repository::open calls is handled by the crate::lazy_hydrator module: clone writes .heddle/lazy-hydrator.toml recording the hydrator kind + config, and Repository::open consults crate::lazy_hydrator::try_reconstruct to look up the registered factory and re-install the hydrator automatically.

Source

pub fn blob_hydrator(&self) -> Option<Arc<dyn BlobHydrator>>

The currently registered hydrator, if any.

Source

pub fn shallow(&self) -> RwLockReadGuard<'_, ShallowInfo>

Source§

impl Repository

Source

pub fn put_redaction(&self, redaction: Redaction) -> Result<ContentHash, Error>

Append a redaction. Returns the redaction’s content-addressed id.

Idempotent: if a redaction with the same canonical bytes already exists on the blob, no second entry is written and the existing id is returned.

Source

pub fn get_redactions_for_blob( &self, blob: &ContentHash, ) -> Result<RedactionsBlob, Error>

Load all redactions targeting blob. Returns an empty RedactionsBlob (not an error) when none exist — callers can treat the result uniformly.

Source

pub fn list_all_redactions( &self, ) -> Result<Vec<(ContentHash, RedactionsBlob)>, Error>

Walk every redactions file in the repo. Used by heddle redact list and the GC’s “never collect a redaction” guard. Returns (blob_hash, blob) pairs so callers can correlate.

Source

pub fn get_redaction( &self, redaction_id: &ContentHash, ) -> Result<Option<(ContentHash, Redaction)>, Error>

Look up a single redaction by its id. Returns Some((blob_hash, redaction)) if found; None if no redaction by that id exists.

Today this walks every redactions file — operators rarely have more than a handful of redactions in a repo, and the operation is interactive (heddle redact show). If listings become frequent enough to matter, a flat <heddle_dir>/redactions/index.bin can be added without changing the public signature.

Source

pub fn remove_redaction( &self, blob: &ContentHash, state: &ChangeId, path: &str, redaction_id: &ContentHash, ) -> Result<RemoveRedactionOutcome, Error>

Reverse a redaction declaration by removing the matching record from the per-blob sidecar. The undo-path inverse of Repository::put_redaction — see OpRecord::Redact’s undo contract.

Matches by the redaction’s content-addressed redaction_id first so a refinement pass that produced multiple records for the same (blob, state, path) (e.g. a re-run of redact apply with an updated --reason or --sign-with) undoes the exact record the OpRecord references rather than the first one in sidecar order.

Falls back to (blob, state, path) lookup when the id doesn’t match any on-disk record. That case is the purge-id-shift scenario: redaction_content_hash covers every field including purged_at, so a Purge between redact apply and this call rewrites every on-disk id away from the pre-purge id carried in OpRecord::Redact.redaction_id. The fallback locates the now-purged record by its stable user-visible identity so the refusal below surfaces with the right “audit trail” message instead of a generic not-found.

Refuses if the matched redaction is purged: the record is then load-bearing audit trail for “these bytes were physically destroyed”, and removing it would lie. Purge is irreversible.

Returns the outcome — whether the surrounding sidecar file is now empty (single-redaction case) or rewritten in place.

Source

pub fn capture_redaction_sidecar( &self, blob: &ContentHash, ) -> Result<Option<Vec<u8>>, Error>

Capture the raw bytes of the per-blob redaction sidecar (or None when the sidecar file does not exist), for a capture-restore rollback step. remove_redaction is a non-atomic forward — it rewrites or deletes the sidecar — so the undo path snapshots the whole sidecar before the removal and restores it via restore_redaction_sidecar if the surrounding transaction later fails, so a rolled-back undo never re-exposes a still-redacted blob.

Source

pub fn restore_redaction_sidecar( &self, blob: &ContentHash, snapshot: Option<Vec<u8>>, ) -> Result<(), Error>

Restore the per-blob redaction sidecar to a snapshot captured by capture_redaction_sidecar: rewrite the captured bytes, or delete the sidecar if it was absent at capture time. Absolute (write-or-delete), so re-running it on the rollback path is idempotent.

Source

pub fn redaction_is_purged( &self, blob: &ContentHash, state: &ChangeId, path: &str, ) -> Result<bool, Error>

Look up the redaction matching (blob, state, path) and report whether its underlying bytes have been purged. Used by the undo pre-flight to refuse before any mutation when removing the redaction record would lie about destroyed bytes.

Returns false when no matching record exists — the caller’s own missing-record path surfaces that condition with the right error.

Source

pub fn purge_blob( &self, blob: &ContentHash, _purger: &Principal, ) -> Result<PurgeOutcome, Error>

Mark every redaction on blob as purged and physically remove the blob bytes from the local loose object store. The Redaction records stay in place; only the bytes are gone.

Refuses if no redaction exists on the blob — operators must redact before purge. This is the contract from the build brief: “Refuses unless a Redaction already exists on the blob.”

_purger is recorded by the caller in the oplog Purge entry; it’s accepted here so the helper can be extended (e.g. to embed the purger in a purge record) without changing the signature.

Source

pub fn accept_wire_redactions( &self, blob: ContentHash, bytes: &[u8], ) -> Result<WireAcceptOutcome, Error>

Accept a wire-transferred RedactionsBlob for a specific blob hash. Verifies every signature, refuses unsigned records, then merges new records into the local sidecar (idempotent on content-addressed duplicates). If any incoming record carries purged_at: Some(_), the local blob bytes are dropped via purge_blob.

bytes is the rmp-encoded RedactionsBlob payload that arrived over the wire; it must decode and every contained Redaction must match blob in its redacted_blob field.

Errors:

  • [WireRejection::Unsigned] if any incoming redaction has no signature. The whole blob is refused (atomic — partial accept would let an unsigned sibling smuggle in via a signed peer).
  • [WireRejection::Tampered] if any signature fails to verify against the canonical payload.
  • [WireRejection::UntrustedKey] if the signer’s public key is not on this receiver’s [redact] trusted_keys list. The list is fail-closed: an empty list rejects every incoming signed redaction, forcing operators to make an explicit trust decision before secrets-scrubbing primitives are accepted.
  • Other errors propagate as anyhow::Error (decode failure, io, crypto subsystem failure).
Source

pub fn redaction_stub_for_blob( &self, blob: &ContentHash, ) -> Result<Option<String>, Error>

If blob has any active redaction, return the stub text the materialize path should write to disk in place of the blob content. Returns None when no redaction exists — callers should then proceed with normal materialization.

Picks the latest redaction (by redacted_at) to source the stub. Multiple redactions on the same blob converge to the most-recent message; the older ones remain in the audit trail.

Source

pub fn reachable_states(&self) -> Result<Vec<ChangeId>, Error>

Enumerate every state reachable from any thread tip or marker by walking parent pointers transitively. Used by --all-states redaction propagation so a leaked secret can be scrubbed from every state in which its blob hash appears.

The walk is breadth-first and dedups by ChangeId, so a state reached from multiple tips appears once. Missing states (broken parent links) are skipped silently — redaction propagation is best-effort across the reachable graph, not a graph-integrity check.

Source

pub fn paths_to_blob_in_state( &self, state: &ChangeId, target: &ContentHash, ) -> Result<Vec<String>, Error>

Find every path under state whose terminal blob hashes to target. Used by --all-states propagation: a leaked secret may live at different paths across history (renames, copies), so we propagate by blob hash, not by path.

Returns paths as forward-slash strings, lexicographically stable thanks to Tree entry ordering.

Source§

impl Repository

Source

pub fn put_state_visibility( &self, record: StateVisibility, ) -> Result<PutVisibilityOutcome, Error>

Record a visibility declaration for its state. Returns the record’s content-addressed id.

The whole read → dedupe → write sequence runs under the repository write lock, so two concurrent puts on the same state can’t both load the same blob and have the second write_file_atomic clobber the first (a lost update would silently drop an embargo or a promotion). This mirrors the SignState handler, which serializes its per-state read-modify-write the same way — reusing the existing repo lock, not a new one.

Idempotent: if a record with the same canonical bytes already exists on the state, no second entry is written and the existing id is returned.

Absence ≡ public, enforced here. When the effective (latest) tier resolves to VisibilityTier::Public, the state’s sidecar is removed so it returns to public-by-absence and has_visibility_for_state reports false. This holds whether the Public put is fresh or supersedes a prior private record — no caller can leave a Public record that makes a public state read as non-public.

Source

pub fn put_state_visibility_if_absent( &self, record: StateVisibility, ) -> Result<Option<PutVisibilityOutcome>, Error>

Like put_state_visibility but a no-op when the state already carries any visibility record. The existence test and the write are ONE locked critical section, so a concurrent visibility set landing between “is it absent?” and “write the default” cannot be clobbered — the guard sees the racer’s record and skips (heddle#317 r5). Returns Ok(None) when a record already existed, else the put outcome. Convenience wrapper that takes the repo write lock around put_state_visibility_locked_if_absent; the capture-time default binding instead holds the lock itself (to stamp declared_at under it) and calls the locked body directly.

Source

pub fn commit_state_visibility( &self, record: StateVisibility, kind: VisibilityCommitKind, ) -> Result<Option<VisibilityCommitOutcome>, Error>

Commit a visibility mutation as ONE serialized unit: write the per-state sidecar AND append its OpRecord audit entry while holding a single repo write lock (heddle#317 / PR #529 P1 r6).

Pre-r6 the CLI wrote the sidecar under the repo lock, RELEASED it, then appended the oplog entry under the separate oplog.lock. Two overlapping visibility set/promote commands could therefore append their oplog records in the OPPOSITE order to their sidecar writes: if B’s sidecar landed after A’s, but B’s oplog append raced ahead of A’s, undo would treat A as the latest op and restore A’s (None) before-image — deleting B’s record and silently dropping the state to public-by-absence. Holding ONE lock across both steps totally orders concurrent mutations, so the oplog-append order always matches the sidecar-write order.

Lock ordering (heddle#317 r6). This acquires the repo write lock FIRST, then the oplog append takes oplog.lock — the same nesting order the snapshot chokepoint uses (snapshot_with_attribution_profiled_locked holds the repo write lock across apply’s sidecar write and the atomic batch commit). No path holds oplog.lock across a repo-lock acquisition (the oplog crate sits below repo and never reaches for the repo lock), so the nesting cannot deadlock.

Set always commits. Promote resolves the superseded record under the lock and returns Ok(None) on a public-by-absence state (nothing to promote); the caller maps that to a user-facing error.

Source

pub fn get_state_visibility_bytes_for_state( &self, state: &ChangeId, ) -> Result<Option<Vec<u8>>, Error>

Return the raw rmp-encoded StateVisibilityBlob bytes for the given state, or Ok(None) if no sidecar exists. The bytes are the wire-transfer payload, not a re-serialized view.

Source

pub fn accept_wire_state_visibility( &self, state: ChangeId, bytes: &[u8], ) -> Result<(), Error>

Accept a wire-transferred StateVisibilityBlob for a specific state. The payload must decode, every contained record must target state, and each record is persisted through put_state_visibility so validation and public-by-absence normalization run at the same boundary as local writes.

Source

pub fn get_state_visibility_for_state( &self, state: &ChangeId, ) -> Result<StateVisibilityBlob, Error>

Load all visibility records targeting state. Returns an empty StateVisibilityBlob (not an error) when none exist — callers can treat the result uniformly.

Source

pub fn has_visibility_for_state(&self, state: &ChangeId) -> Result<bool, Error>

Whether state resolves to a non-public effective tier. false means public-by-absence — either no record exists, or the latest declaration resolves to VisibilityTier::Public. By construction (see put_state_visibility) a public resolution is never persisted, so this is equivalent to “a record exists”; computing it from the effective tier keeps the keystone invariant — true iff the effective tier is non-public — explicit and robust against any blob a future path might introduce. This is the query the serve-side gate keys off.

Source

pub fn effective_state_visibility( &self, state: &ChangeId, ) -> Result<Option<StateVisibility>, Error>

The effective visibility declaration for state: the latest non-superseded record, or None when the state is public-by-absence. Pure over the persisted records — never wall-clock (an embargo_until is materialized into a superseding record before it can change the effective tier, see the spike §5.4).

Source

pub fn effective_visibility_tier( &self, state: &ChangeId, ) -> Result<VisibilityTier, Error>

The effective tier of state: the latest declaration’s tier, or VisibilityTier::Public when the state is public-by-absence. This is the single resolution the checkout courtesy stub and the bridge frontier both key off, so they agree on who-sees-what.

Source

pub fn list_all_state_visibility( &self, ) -> Result<Vec<(ChangeId, StateVisibilityBlob)>, Error>

Walk every visibility sidecar file in the repo. Returns (state_id, blob) pairs so callers can correlate. Used by listing surfaces and the GC’s “never collect a visibility record” guard.

Source

pub fn state_visibility_record_id( &self, record: &StateVisibility, ) -> Result<ContentHash, Error>

Canonical content id of a StateVisibility record — the same id Self::put_state_visibility assigns. Exposed so callers (e.g. the visibility promote verb) can name an existing record to supersede without re-deriving the hashing scheme and drifting out of sync.

Source

pub fn resolve_capture_default_visibility(&self) -> VisibilityTier

Resolve the default visibility tier a freshly captured state inherits.

Runs the resolve_default_visibility chain with the repo-wide default from [review.discussion] default_visibility. That field defaults to VisibilityTier::Public, so an unconfigured repo resolves public and the common case stays record-free. Namespace policies are not yet loaded from config, so the namespace tier of the chain is currently unused here.

Source

pub fn stage_default_visibility_binding( &self, state: &ChangeId, lock_held: bool, ) -> Result<Option<DefaultVisibilityBinding>, Error>

Invariant A — immutable-at-creation (spike #266 §5.4).

Bind the inherited default tier to a brand-new state at creation time: resolve the chain once, and persist any resolution more restrictive than public as the state’s initial StateVisibility record (plus an OpRecord::StateVisibilitySet audit entry). A public resolution stays record-free (absence ≡ public). Resolving here — not at first serve — means a [namespace]/repo default that later drifts more-open cannot retroactively expose an already-created state.

This is the single decision site for default visibility. Every snapshot creator funnels through the snapshot chokepoint (snapshot_with_attribution_profiled / snapshot_tree_with_attribution_profiled, plus the mount capture path), so capture, cherry-pick, revert, and daemon/mount captures all inherit the configured default by construction rather than each call site re-binding (and one of them leaking when it forgets to).

Idempotent: a state that already carries a visibility record is left untouched, so a re-capture that mints the same ChangeId never double-binds, and a caller that explicitly set a tier before this runs is respected. Returns the binding to fold when one was written.

Folds into the snapshot’s own batch (PR #529 P1). The returned OpRecord::StateVisibilitySet is appended to the same oplog batch as the snapshot that triggered the binding — never a separate trailing batch — so a single heddle undo reverts the snapshot AND its auto-applied default tier together. The old separate-batch binding made the first undo after a capture restore only the sidecar, leaving the snapshot in place (undo took two presses). Explicit user actions (heddle visibility set/promote) stay their own undoable batch — only this automatic, snapshot-time default binding folds in.

lock_held declares whether the caller already holds the repo write lock. The snapshot chokepoint’s SnapshotMutation::apply runs under the snapshot write lock and passes true (the sidecar write must not re-enter the non-reentrant flock repo lock); the mount capture path holds no lock and passes false (the sidecar write takes the lock itself).

Source

pub fn restore_state_visibility_sidecar( &self, state: &ChangeId, snapshot: Option<Vec<u8>>, ) -> Result<(), Error>

Restore the per-state visibility sidecar to an absolute snapshot: rewrite snapshot’s bytes, or remove the sidecar when snapshot is None (public-by-absence). Absolute (write-or-delete), so re-running it on a rollback path is idempotent. This is the undo/redo restore point — undo passes the op’s prior_sidecar, redo its new_sidecar. Mirrors restore_redaction_sidecar.

Source

pub fn restore_state_visibility_sidecar_if_unchanged( &self, state: &ChangeId, expected_current: &Option<Vec<u8>>, target: Option<Vec<u8>>, ) -> Result<VisibilitySidecarRestore, Error>

Restore the per-state visibility sidecar to target only if the current on-disk sidecar still equals expected_current, with the read → compare → write run as ONE critical section under the SAME repo write lock commit_state_visibility holds (heddle#317 / PR #529 P1 r7).

This is the undo/redo restore writer. The plain restore_state_visibility_sidecar rewrites unconditionally and is safe only where the caller already serializes against concurrent commits (the explicit set/promote rollback holds the lock; the snapshot-binding rewind targets a brand-new, not-yet-shared state). The undo/redo restore mutates an EXISTING state’s sidecar OUTSIDE that lock, so a concurrent visibility set/promote could be physically clobbered — either by the forward restore or by the transaction’s rollback running after the conflict was already detected. Folding the conflict re-check and the write into one locked section closes that TOCTOU: if the current sidecar no longer matches expected_current (a concurrent commit superseded it), nothing is written and VisibilitySidecarRestore::Superseded is returned so the caller aborts/skips instead of overwriting the newer record. The oplog isolation key (invariant 3) still rejects the transaction at commit; this adds the physical serialization that detection alone cannot provide.

Source§

impl Repository

Source§

impl Repository

Source

pub fn compute_thread_stacks(&self) -> Result<Vec<ThreadStack>, HeddleError>

Load every thread record from disk and compute every stack in the repo. Convenience wrapper around compute_stacks that handles the ThreadManager read.

Source

pub fn thread_stack_for( &self, thread_name: &str, ) -> Result<Option<ThreadStack>, HeddleError>

Find the stack containing thread_name, walking up to the root before computing the descendant tree. Returns None when the thread isn’t in the corpus.

Source

pub fn plan_thread_stack_rebase( &self, root_thread: &str, onto: &str, ) -> Result<Result<StackRebasePlan, PlanRebaseError>, HeddleError>

Plan a rebase for the stack rooted at root_thread. The planner reads each thread’s live tip via refs.get_thread, so it stays correct even when the on-disk current_state in ThreadRecord hasn’t been refreshed yet.

Returns Ok(Err(PlanRebaseError)) for shape-level errors (unknown root, non-root target) so callers can distinguish a planner refusal from an I/O failure.

Source§

impl Repository

Source

pub fn fork_timeline_from_selector( &self, store: &TimelineStore, thread: &str, selector: &TimelineSeekSelector, branch_constraint: Option<&TimelineSeekBranchConstraint>, branch_id: Option<TimelineBranchId>, reason: TimelineBranchReason, created_at_ms: i64, ) -> Result<TimelineForkOutcome, HeddleError>

Source

pub fn reset_timeline_cursor( &self, store: &TimelineStore, thread: &str, selector: &TimelineSeekSelector, mode: TimelineMaterializeMode, branch_constraint: Option<&TimelineSeekBranchConstraint>, materialize_checkout: bool, moved_at_ms: i64, ) -> Result<TimelineResetOutcome, HeddleError>

Source

pub fn recover_timeline_materialization_action( &self, store: &TimelineStore, thread: &str, ) -> Result<TimelineRecoverOutcome, HeddleError>

Source§

impl Repository

Source

pub fn preview_timeline_seek( &self, store: &TimelineStore, thread: &str, selector: &TimelineSeekSelector, mode: TimelineMaterializeMode, ) -> Result<TimelineSeekPreview, HeddleError>

Preview a timeline seek target without mutating the checkout or timeline.

Source

pub fn preview_timeline_seek_constrained( &self, store: &TimelineStore, thread: &str, selector: &TimelineSeekSelector, mode: TimelineMaterializeMode, branch_constraint: Option<&TimelineSeekBranchConstraint>, ) -> Result<TimelineSeekPreview, HeddleError>

Preview a timeline seek target with an optional branch invariant.

Source

pub fn materialize_timeline_cursor( &self, store: &TimelineStore, thread: &str, selector: &TimelineSeekSelector, mode: TimelineMaterializeMode, moved_at_ms: i64, ) -> Result<TimelineMaterializeOutcome, HeddleError>

Materialize a logical timeline cursor target into the physical checkout.

This writes the timeline cursor_moved operation only after the checkout is known to be at the target state. A per-thread recovery sidecar is staged before the checkout move and cleared after the cursor move, so a later call can finish the logical cursor update if a process crashes between those writes.

Source

pub fn materialize_timeline_cursor_constrained( &self, store: &TimelineStore, thread: &str, selector: &TimelineSeekSelector, mode: TimelineMaterializeMode, branch_constraint: Option<&TimelineSeekBranchConstraint>, moved_at_ms: i64, ) -> Result<TimelineMaterializeOutcome, HeddleError>

Materialize a logical timeline cursor target with an optional branch invariant checked after pending recovery has been applied.

Source

pub fn materialize_timeline_cursor_constrained_with_reason( &self, store: &TimelineStore, thread: &str, selector: &TimelineSeekSelector, mode: TimelineMaterializeMode, branch_constraint: Option<&TimelineSeekBranchConstraint>, reason: TimelineCursorMoveReason, moved_at_ms: i64, ) -> Result<TimelineMaterializeOutcome, HeddleError>

Source

pub fn recover_pending_timeline_materialization( &self, store: &TimelineStore, thread: &str, ) -> Result<TimelineMaterializationRecoveryOutcome, HeddleError>

Source§

impl Repository

Trait Implementations§

Source§

impl<R, O, S> RepositoryLockExt for Repository<R, O, S>

Auto Trait Implementations§

§

impl<R = RefManager, O = OpLog, S = AnyStore> !Freeze for Repository<R, O, S>

§

impl<R, O, S> RefUnwindSafe for Repository<R, O, S>

§

impl<R, O, S> Send for Repository<R, O, S>

§

impl<R, O, S> Sync for Repository<R, O, S>

§

impl<R, O, S> Unpin for Repository<R, O, S>
where S: Unpin, R: Unpin, O: Unpin,

§

impl<R, O, S> UnsafeUnpin for Repository<R, O, S>

§

impl<R, O, S> UnwindSafe for Repository<R, O, S>
where S: UnwindSafe, R: UnwindSafe, O: UnwindSafe,

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoRequest<T> for T

Source§

fn into_request(self) -> Request<T>

Wrap the input message T in a tonic::Request
Source§

impl<L> LayerExt<L> for L

Source§

fn named_layer<S>(&self, service: S) -> Layered<<L as Layer<S>>::Service, S>
where L: Layer<S>,

Applies the layer to a service and wraps it in Layered.
Source§

impl<T> Pointable for T

Source§

const ALIGN: usize

The alignment of pointer.
Source§

type Init = T

The type for initializers.
Source§

unsafe fn init(init: <T as Pointable>::Init) -> usize

Initializes a with the given initializer. Read more
Source§

unsafe fn deref<'a>(ptr: usize) -> &'a T

Dereferences the given pointer. Read more
Source§

unsafe fn deref_mut<'a>(ptr: usize) -> &'a mut T

Mutably dereferences the given pointer. Read more
Source§

unsafe fn drop(ptr: usize)

Drops the object pointed to by the given pointer. Read more
Source§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more