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
impl Repository
pub fn merge_state_manager(&self) -> MergeStateManager
Source§impl Repository
impl Repository
pub fn suggest_context_targets( &self, state: &State, limit: usize, ) -> Result<Vec<ContextSuggestion>, Error>
Source§impl Repository
impl Repository
Sourcepub fn get_context_blob(
&self,
context_root: &ContentHash,
target: &ContextTarget,
) -> Result<Option<ContextBlob>, HeddleError>
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.
Sourcepub fn set_context_blob(
&self,
context_root: Option<&ContentHash>,
target: &ContextTarget,
blob: &ContextBlob,
) -> Result<ContentHash, HeddleError>
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.
Sourcepub fn remove_context_at_target(
&self,
context_root: &ContentHash,
target: &ContextTarget,
scope: Option<&AnnotationScope>,
) -> Result<Option<ContentHash>, HeddleError>
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.
pub fn remove_context_target( &self, context_root: &ContentHash, target: &ContextTarget, ) -> Result<Option<ContentHash>, HeddleError>
Sourcepub fn list_context_entries(
&self,
context_root: &ContentHash,
prefix: Option<&Path>,
) -> Result<Vec<ContextEntry>, HeddleError>
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.
pub fn find_annotation( &self, context_root: &ContentHash, annotation_id: &str, ) -> Result<Option<(ContextTarget, ContextBlob, usize)>, HeddleError>
Sourcepub fn inherit_parent_context(parent: &State) -> Option<ContentHash>
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.
Sourcepub fn union_parent_contexts(
&self,
parents: &[&State],
) -> Result<Option<ContentHash>, HeddleError>
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
impl Repository
Sourcepub fn diff_trees(
&self,
from: &ContentHash,
to: &ContentHash,
) -> Result<FileChangeSet, HeddleError>
pub fn diff_trees( &self, from: &ContentHash, to: &ContentHash, ) -> Result<FileChangeSet, HeddleError>
Get the difference between two trees.
Sourcepub fn diff_trees_visit<V, B>(
&self,
from: &ContentHash,
to: &ContentHash,
visitor: V,
) -> Result<ControlFlow<B>, HeddleError>
pub fn diff_trees_visit<V, B>( &self, from: &ContentHash, to: &ContentHash, visitor: V, ) -> Result<ControlFlow<B>, HeddleError>
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
impl Repository
Sourcepub fn goto(&self, target: &ChangeId) -> Result<(), HeddleError>
pub fn goto(&self, target: &ChangeId) -> Result<(), HeddleError>
Move worktree to a different state.
Sourcepub fn goto_discard_local(&self, target: &ChangeId) -> Result<(), HeddleError>
pub fn goto_discard_local(&self, target: &ChangeId) -> Result<(), HeddleError>
Move worktree to a different state, discarding unsnapped local edits.
Sourcepub fn goto_from_materialized_state(
&self,
target: &ChangeId,
materialized: Option<&ChangeId>,
) -> Result<(), HeddleError>
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.
Sourcepub fn fast_forward_attached(
&self,
target: &ChangeId,
) -> Result<(), HeddleError>
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.
pub fn fast_forward_attached_discard_local( &self, target: &ChangeId, ) -> Result<(), HeddleError>
pub fn fast_forward_attached_from_materialized_state( &self, target: &ChangeId, materialized: Option<&ChangeId>, ) -> Result<(), HeddleError>
Sourcepub fn fast_forward_attached_without_record(
&self,
target: &ChangeId,
) -> Result<(), HeddleError>
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).
pub fn fast_forward_attached_without_record_discard_local( &self, target: &ChangeId, ) -> Result<(), HeddleError>
pub fn fast_forward_attached_from_materialized_state_without_record( &self, target: &ChangeId, materialized: Option<&ChangeId>, ) -> Result<(), HeddleError>
pub fn goto_verified_clean(&self, target: &ChangeId) -> Result<(), HeddleError>
pub fn goto_verified_clean_without_record( &self, target: &ChangeId, ) -> Result<(), HeddleError>
pub fn goto_without_record(&self, target: &ChangeId) -> Result<(), HeddleError>
pub fn goto_without_record_discard_local( &self, target: &ChangeId, ) -> Result<(), HeddleError>
Source§impl Repository
impl Repository
Sourcepub fn query_history(
&self,
query: &HistoryQuery,
) -> Result<Vec<State>, HeddleError>
pub fn query_history( &self, query: &HistoryQuery, ) -> Result<Vec<State>, HeddleError>
Walk first-parent history and return states matching the query.
Source§impl Repository
impl Repository
pub fn inspect_performance( &self, ) -> Result<RepositoryPerformanceInspectionReport, HeddleError>
pub fn inspect_performance_with_options( &self, options: &WorktreeStatusOptions, ) -> Result<RepositoryPerformanceInspectionReport, HeddleError>
pub fn run_maintenance( &self, ) -> Result<RepositoryMaintenanceRunReport, HeddleError>
pub fn run_maintenance_with_options( &self, options: &WorktreeStatusOptions, ) -> Result<RepositoryMaintenanceRunReport, HeddleError>
Source§impl Repository
impl Repository
Sourcepub fn warm_canonical_store_for_state(
&self,
state_id: &ChangeId,
) -> Result<WarmCanonicalStoreStats, HeddleError>
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.
Sourcepub fn warm_canonical_store_for_states(
&self,
state_ids: &[ChangeId],
) -> Result<WarmCanonicalStoreStats, HeddleError>
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).
Sourcepub fn materialize_computed_tree(
&self,
tree: &Tree,
dir: &Path,
) -> Result<(), HeddleError>
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
impl Repository
pub fn state_origin(&self, state: &State) -> Origin
pub fn get_state_provenance_root( &self, state: &State, ) -> Result<Option<ContentHash>, HeddleError>
pub fn get_file_provenance_for_state( &self, state: &State, path: &Path, ) -> Result<Option<FileProvenance>, HeddleError>
Source§impl Repository
impl Repository
Sourcepub fn resolve_state(&self, spec: &str) -> Result<Option<ChangeId>, HeddleError>
pub fn resolve_state(&self, spec: &str) -> Result<Option<ChangeId>, HeddleError>
Resolve a state specifier (HEAD, thread, marker, full/short ID, HEAD~N).
pub fn resolve_agent(&self) -> Option<Agent>
Source§impl Repository
impl Repository
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.
Sourcepub fn resign_if_owned(
&self,
state: &mut State,
prior_hashes: &[ContentHash],
) -> ResignOutcome
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.
Sourcepub fn sign_state(
&self,
state_id: &ChangeId,
signer: &dyn Signer,
) -> Result<(), HeddleError>
pub fn sign_state( &self, state_id: &ChangeId, signer: &dyn Signer, ) -> Result<(), HeddleError>
Sourcepub fn sign_state_with_key(
&self,
state_id: &ChangeId,
key_path: &Path,
algorithm: Option<&str>,
) -> Result<(), HeddleError>
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 signkey_path- Path to the private key filealgorithm- Optional algorithm hint (auto-detected if not specified)
Sourcepub fn verify_state_signature(
&self,
state_id: &ChangeId,
) -> Result<SignatureStatus, HeddleError>
pub fn verify_state_signature( &self, state_id: &ChangeId, ) -> Result<SignatureStatus, HeddleError>
Verify a state’s signature.
Returns the signature status:
SignatureStatus::Validif the signature is validSignatureStatus::Invalidif the signature is invalidSignatureStatus::Unsignedif the state has no signature
§Arguments
state_id- The change ID of the state to verify
Sourcepub fn get_state_signature(
&self,
state_id: &ChangeId,
) -> Result<Option<StateSignature>, HeddleError>
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
impl Repository
Sourcepub fn snapshot(
&self,
intent: Option<String>,
confidence: Option<f32>,
) -> Result<State, HeddleError>
pub fn snapshot( &self, intent: Option<String>, confidence: Option<f32>, ) -> Result<State, HeddleError>
Create a snapshot of the current worktree.
Sourcepub fn snapshot_with_attribution(
&self,
intent: Option<String>,
confidence: Option<f32>,
attribution: Attribution,
) -> Result<State, HeddleError>
pub fn snapshot_with_attribution( &self, intent: Option<String>, confidence: Option<f32>, attribution: Attribution, ) -> Result<State, HeddleError>
Create a snapshot with explicit attribution.
Sourcepub fn snapshot_with_attribution_profiled(
&self,
intent: Option<String>,
confidence: Option<f32>,
attribution: Attribution,
) -> Result<SnapshotExecution, HeddleError>
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.
Sourcepub fn snapshot_tree_with_attribution_profiled(
&self,
tree: Tree,
intent: Option<String>,
confidence: Option<f32>,
attribution: Attribution,
) -> Result<SnapshotExecution, HeddleError>
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).
Sourcepub 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>
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
impl Repository
Sourcepub fn materialize_thread(
&self,
thread: &str,
dest: &Path,
audience: &AudienceTier,
) -> Result<ThreadManifest, HeddleError>
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:
- Resolve
thread→ChangeId→State→Tree. - Call
Repository::materialize_tree(&tree, dest)— the existing clonefile-first materializer does the heavy lifting (loose-uncompressed promotion, parallel writes). - Walk the materialized tree and capture per-file
(hash, inode, mtime_ns, ctime_ns, mode)into the manifest. - 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.
Sourcepub fn checkout_state_gated(
&self,
change_id: &ChangeId,
state: &State,
dest: &Path,
audience: &AudienceTier,
) -> Result<CheckoutMaterialization, HeddleError>
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).
Sourcepub fn clear_materialized_root_records(
&self,
worktree_root: &Path,
) -> Result<(), HeddleError>
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).
Sourcepub fn record_thread_manifest(
&self,
thread: &str,
state_id: &ChangeId,
dest: &Path,
) -> Result<ThreadManifest, HeddleError>
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).
Sourcepub fn record_withheld_thread_manifest(
&self,
thread: &str,
state_id: &ChangeId,
dest: &Path,
) -> Result<ThreadManifest, HeddleError>
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).
Sourcepub fn thread_create_op_record(&self, name: &str, state: ChangeId) -> OpRecord
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.
Sourcepub fn cas_guarded_thread_ref_rollback(
&self,
name: &ThreadName,
set_value: ChangeId,
restore_to: Option<ChangeId>,
) -> Result<(), HeddleError>
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.
Sourcepub fn restore_thread_manifest(
&self,
thread: &str,
prior: Option<Vec<u8>>,
) -> Result<(), HeddleError>
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).
Sourcepub fn capture_thread_from_disk(
&self,
thread: &str,
root: &Path,
) -> Result<ThreadCaptureOutcome, HeddleError>
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:
snapshotalways advancesHEAD’s currently-attached thread. Capture-from-disk targets a specific thread by name, which is what auto-capture-on-switch needs.snapshotwalksself.root. Capture-from-disk walks whatever directory the materializer put the thread at — managed checkouts under<repo>/.heddle/threads/<thread>/, which are NOTself.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
impl Repository
Sourcepub fn build_tree(&self, dir: &Path) -> Result<Tree, HeddleError>
pub fn build_tree(&self, dir: &Path) -> Result<Tree, HeddleError>
Build a tree from a directory.
Sourcepub fn build_tree_with_stat_cache(
&self,
dir: &Path,
manifest: &ThreadManifest,
) -> Result<Tree, HeddleError>
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.
pub fn build_tree_profiled( &self, dir: &Path, ) -> Result<(Tree, TreeBuildProfile), HeddleError>
Sourcepub fn build_tree_profiled_with_stat_cache(
&self,
dir: &Path,
manifest: &ThreadManifest,
) -> Result<(Tree, TreeBuildProfile), HeddleError>
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.
Sourcepub fn compare_worktree_cached(
&self,
tree: &Tree,
) -> Result<WorktreeStatus, HeddleError>
pub fn compare_worktree_cached( &self, tree: &Tree, ) -> Result<WorktreeStatus, HeddleError>
Compare the worktree against a tree using the persisted binary index.
pub fn compare_worktree_cached_detailed( &self, tree: &Tree, ) -> Result<WorktreeStatusDetailed, HeddleError>
Sourcepub fn compare_worktree_cached_with_options(
&self,
tree: &Tree,
options: &WorktreeStatusOptions,
) -> Result<WorktreeStatus, HeddleError>
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.
pub fn compare_worktree_cached_detailed_with_options( &self, tree: &Tree, options: &WorktreeStatusOptions, ) -> Result<WorktreeStatusDetailed, HeddleError>
pub fn compare_worktree_cached_profiled_with_options( &self, tree: &Tree, options: &WorktreeStatusOptions, ) -> Result<(WorktreeStatus, WorktreeCompareProfile), HeddleError>
pub fn compare_worktree_cached_detailed_profiled_with_options( &self, tree: &Tree, options: &WorktreeStatusOptions, ) -> Result<(WorktreeStatusDetailed, WorktreeCompareProfile), HeddleError>
Sourcepub fn worktree_is_clean_cached(&self, tree: &Tree) -> Result<bool, HeddleError>
pub fn worktree_is_clean_cached(&self, tree: &Tree) -> Result<bool, HeddleError>
Return whether the worktree matches the provided tree.
Sourcepub fn worktree_is_clean_cached_with_options(
&self,
tree: &Tree,
options: &WorktreeStatusOptions,
) -> Result<bool, HeddleError>
pub fn worktree_is_clean_cached_with_options( &self, tree: &Tree, options: &WorktreeStatusOptions, ) -> Result<bool, HeddleError>
Return whether the worktree matches the provided tree.
pub fn inspect_change_monitor_with_options( &self, options: &WorktreeStatusOptions, ) -> Result<ChangeMonitorReport, HeddleError>
Source§impl Repository
impl Repository
Sourcepub fn remove_tracked_descendants(&self, path: &Path) -> Result<(), HeddleError>
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.
Sourcepub fn remove_tracked_descendants_with_source(
&self,
path: &Path,
source_subtree: &Tree,
) -> Result<(), HeddleError>
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.
Sourcepub fn resolve_subtree(
&self,
root_tree: &Tree,
rel_path: &Path,
) -> Result<Option<Tree>, HeddleError>
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>
impl<R, O, S> Repository<R, O, S>
Sourcepub fn from_parts(
root: PathBuf,
heddle_dir: PathBuf,
store: S,
refs: R,
oplog: O,
config: RepoConfig,
shallow: ShallowInfo,
) -> Repository<R, O, S>
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§impl<S> Repository<RefManager, OpLog, S>where
S: ObjectStore,
impl<S> Repository<RefManager, OpLog, S>where
S: ObjectStore,
Sourcepub fn open_with_store(
heddle_dir: impl AsRef<Path>,
store: S,
) -> Result<Repository<RefManager, OpLog, S>, HeddleError>
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
impl Repository
Sourcepub fn init(path: impl AsRef<Path>) -> Result<Repository, HeddleError>
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.
Sourcepub fn init_default(path: impl AsRef<Path>) -> Result<Repository, HeddleError>
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.
Sourcepub fn bootstrap_git_overlay(
path: impl AsRef<Path>,
) -> Result<Repository, HeddleError>
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.
Sourcepub fn ensure_git_overlay_local_excludes(
path: impl AsRef<Path>,
) -> Result<(), HeddleError>
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.
Sourcepub fn open(path: impl AsRef<Path>) -> Result<Repository, HeddleError>
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).
pub fn root(&self) -> &Path
pub fn heddle_dir(&self) -> &Path
Sourcepub fn managed_checkout_source_root(&self) -> &Path
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.
Sourcepub fn managed_checkout_path(&self, thread: &str) -> PathBuf
pub fn managed_checkout_path(&self, thread: &str) -> PathBuf
Default managed checkout path for thread.
pub fn capability(&self) -> RepositoryCapability
pub fn git_overlay_sley_repository( &self, ) -> Result<Option<Repository>, HeddleError>
pub fn capability_label(&self) -> &'static str
pub fn storage_model_label(&self) -> &'static str
pub fn hosted_enabled(&self) -> bool
pub fn current_lane(&self) -> Result<Option<String>, HeddleError>
pub fn operation_status( &self, ) -> Result<Option<RepositoryOperationStatus>, HeddleError>
pub fn git_remote_tracking_status( &self, ) -> Result<Option<GitRemoteTrackingStatus>, HeddleError>
pub fn git_overlay_import_hint( &self, ) -> Result<Option<GitOverlayImportHint>, HeddleError>
pub fn git_overlay_branch_tips( &self, ) -> Result<Vec<GitOverlayBranchTip>, HeddleError>
pub fn git_overlay_tag_tips(&self) -> Result<Vec<GitOverlayTagTip>, HeddleError>
pub fn git_overlay_branch_tip( &self, name: &str, ) -> Result<Option<GitOverlayBranchTip>, HeddleError>
pub fn git_overlay_tag_tip( &self, name: &str, ) -> Result<Option<GitOverlayTagTip>, HeddleError>
pub fn git_overlay_mapped_change_for_branch( &self, name: &str, ) -> Result<Option<ChangeId>, HeddleError>
pub fn git_overlay_mapped_change_for_remote_tracking_ref( &self, name: &str, ) -> Result<Option<ChangeId>, HeddleError>
pub fn git_overlay_mapped_change_for_tag( &self, name: &str, ) -> Result<Option<ChangeId>, HeddleError>
pub fn git_overlay_worktree_status( &self, ) -> Result<Option<WorktreeStatus>, HeddleError>
pub fn git_overlay_ingest_commit_mapping( &self, ) -> Result<HashMap<String, String>, HeddleError>
pub fn git_overlay_mapped_git_commit_for_change( &self, change_id: &ChangeId, ) -> Result<Option<String>, HeddleError>
pub fn git_overlay_mapped_change_for_git_commit( &self, git_commit: &str, ) -> Result<Option<ChangeId>, HeddleError>
Sourcepub fn git_overlay_out_of_band_commits(
&self,
tip_git_commit: &str,
) -> Result<Option<GitOverlayOutOfBandCommits>, HeddleError>
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.
pub fn git_overlay_current_branch(&self) -> Result<Option<String>, HeddleError>
pub fn git_overlay_head_is_detached(&self) -> Result<bool, HeddleError>
pub fn git_overlay_detached_head_commit( &self, ) -> Result<Option<String>, HeddleError>
pub fn list_git_checkpoints( &self, ) -> Result<Vec<GitCheckpointRecord>, HeddleError>
pub fn latest_git_checkpoint_for_change( &self, change_id: &ChangeId, ) -> Result<Option<GitCheckpointRecord>, HeddleError>
pub fn record_git_checkpoint( &self, change_id: &ChangeId, git_commit: impl Into<String>, summary: impl Into<String>, ) -> Result<GitCheckpointRecord, HeddleError>
pub fn init_worktree( path: impl AsRef<Path>, shared_galeed_dir: impl AsRef<Path>, ) -> Result<(), HeddleError>
pub fn op_scope(&self) -> String
Sourcepub fn commit_and_publish(
&self,
records: Vec<OpRecord>,
ref_updates: &[RefUpdate],
) -> Result<(), HeddleError>
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).
Sourcepub fn commit_snapshot_atomic(
&self,
new_state: &ChangeId,
prev_head: Option<ChangeId>,
thread: Option<&ThreadName>,
) -> Result<(), HeddleError>
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).
Sourcepub fn commit_snapshot_atomic_with_records(
&self,
new_state: &ChangeId,
prev_head: Option<ChangeId>,
thread: Option<&ThreadName>,
extra: Vec<OpRecord>,
) -> Result<(), HeddleError>
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).
Sourcepub fn commit_snapshot_atomic_with_capture_visibility(
&self,
new_state: &ChangeId,
prev_head: Option<ChangeId>,
thread: Option<&ThreadName>,
lock_held: bool,
) -> Result<(), HeddleError>
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.
pub fn repo_config(&self) -> &RepoConfig
pub fn config(&self) -> &RepoConfig
pub fn get_tree_for_state( &self, state_id: &ChangeId, ) -> Result<Option<Tree>, HeddleError>
pub fn ignore_patterns(&self) -> Result<Vec<String>, HeddleError>
Sourcepub fn nested_thread_worktree_exclusions(
&self,
walk_root: &Path,
) -> Result<Vec<PathBuf>, HeddleError>
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_rootitself (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.
pub fn head(&self) -> Result<Option<ChangeId>, HeddleError>
pub fn head_ref(&self) -> Result<Head, HeddleError>
Sourcepub fn active_worktree_path(&self) -> Result<PathBuf, HeddleError>
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.
pub fn current_state(&self) -> Result<Option<State>, HeddleError>
pub fn get_principal(&self) -> Result<Principal, HeddleError>
pub fn get_attribution(&self) -> Result<Attribution, HeddleError>
pub fn is_shallow(&self, id: &ChangeId) -> bool
pub fn set_shallow( &self, state_id: &ChangeId, _parents: &[ChangeId], ) -> Result<(), HeddleError>
pub fn record_missing_blob(&self, hash: ContentHash) -> Result<(), HeddleError>
Sourcepub fn seed_default_thread(&self) -> Result<(), HeddleError>
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).
pub fn clear_missing_blob(&self, hash: &ContentHash) -> Result<(), HeddleError>
pub fn missing_blobs(&self) -> Result<Vec<ContentHash>, HeddleError>
pub fn clear_all_missing_blobs(&self) -> Result<bool, HeddleError>
pub fn is_missing_blob(&self, hash: &ContentHash) -> Result<bool, HeddleError>
Sourcepub fn require_tree(&self, hash: &ContentHash) -> Result<Tree, HeddleError>
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.
pub fn require_blob(&self, hash: &ContentHash) -> Result<Blob, HeddleError>
Sourcepub fn set_blob_hydrator(&self, hydrator: Arc<dyn BlobHydrator>)
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.
Sourcepub fn blob_hydrator(&self) -> Option<Arc<dyn BlobHydrator>>
pub fn blob_hydrator(&self) -> Option<Arc<dyn BlobHydrator>>
The currently registered hydrator, if any.
pub fn shallow(&self) -> RwLockReadGuard<'_, ShallowInfo>
Source§impl Repository
impl Repository
Sourcepub fn put_redaction(&self, redaction: Redaction) -> Result<ContentHash, Error>
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.
Sourcepub fn get_redactions_for_blob(
&self,
blob: &ContentHash,
) -> Result<RedactionsBlob, Error>
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.
Sourcepub fn list_all_redactions(
&self,
) -> Result<Vec<(ContentHash, RedactionsBlob)>, Error>
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.
Sourcepub fn get_redaction(
&self,
redaction_id: &ContentHash,
) -> Result<Option<(ContentHash, Redaction)>, Error>
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.
Sourcepub fn remove_redaction(
&self,
blob: &ContentHash,
state: &ChangeId,
path: &str,
redaction_id: &ContentHash,
) -> Result<RemoveRedactionOutcome, Error>
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.
Sourcepub fn capture_redaction_sidecar(
&self,
blob: &ContentHash,
) -> Result<Option<Vec<u8>>, Error>
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.
Sourcepub fn restore_redaction_sidecar(
&self,
blob: &ContentHash,
snapshot: Option<Vec<u8>>,
) -> Result<(), Error>
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.
Sourcepub fn redaction_is_purged(
&self,
blob: &ContentHash,
state: &ChangeId,
path: &str,
) -> Result<bool, Error>
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.
Sourcepub fn purge_blob(
&self,
blob: &ContentHash,
_purger: &Principal,
) -> Result<PurgeOutcome, Error>
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.
Sourcepub fn accept_wire_redactions(
&self,
blob: ContentHash,
bytes: &[u8],
) -> Result<WireAcceptOutcome, Error>
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_keyslist. 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).
Sourcepub fn redaction_stub_for_blob(
&self,
blob: &ContentHash,
) -> Result<Option<String>, Error>
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.
Sourcepub fn reachable_states(&self) -> Result<Vec<ChangeId>, Error>
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.
Sourcepub fn paths_to_blob_in_state(
&self,
state: &ChangeId,
target: &ContentHash,
) -> Result<Vec<String>, Error>
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
impl Repository
Sourcepub fn put_state_visibility(
&self,
record: StateVisibility,
) -> Result<PutVisibilityOutcome, Error>
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.
Sourcepub fn put_state_visibility_if_absent(
&self,
record: StateVisibility,
) -> Result<Option<PutVisibilityOutcome>, Error>
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.
Sourcepub fn commit_state_visibility(
&self,
record: StateVisibility,
kind: VisibilityCommitKind,
) -> Result<Option<VisibilityCommitOutcome>, Error>
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.
Sourcepub fn get_state_visibility_bytes_for_state(
&self,
state: &ChangeId,
) -> Result<Option<Vec<u8>>, Error>
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.
Sourcepub fn accept_wire_state_visibility(
&self,
state: ChangeId,
bytes: &[u8],
) -> Result<(), Error>
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.
Sourcepub fn get_state_visibility_for_state(
&self,
state: &ChangeId,
) -> Result<StateVisibilityBlob, Error>
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.
Sourcepub fn has_visibility_for_state(&self, state: &ChangeId) -> Result<bool, Error>
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.
Sourcepub fn effective_state_visibility(
&self,
state: &ChangeId,
) -> Result<Option<StateVisibility>, Error>
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).
Sourcepub fn effective_visibility_tier(
&self,
state: &ChangeId,
) -> Result<VisibilityTier, Error>
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.
Sourcepub fn list_all_state_visibility(
&self,
) -> Result<Vec<(ChangeId, StateVisibilityBlob)>, Error>
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.
Sourcepub fn state_visibility_record_id(
&self,
record: &StateVisibility,
) -> Result<ContentHash, Error>
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.
Sourcepub fn resolve_capture_default_visibility(&self) -> VisibilityTier
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.
Sourcepub fn stage_default_visibility_binding(
&self,
state: &ChangeId,
lock_held: bool,
) -> Result<Option<DefaultVisibilityBinding>, Error>
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).
Sourcepub fn restore_state_visibility_sidecar(
&self,
state: &ChangeId,
snapshot: Option<Vec<u8>>,
) -> Result<(), Error>
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.
Sourcepub fn restore_state_visibility_sidecar_if_unchanged(
&self,
state: &ChangeId,
expected_current: &Option<Vec<u8>>,
target: Option<Vec<u8>>,
) -> Result<VisibilitySidecarRestore, Error>
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
impl Repository
pub fn stash_manager(&self) -> StashManager
Source§impl Repository
impl Repository
Sourcepub fn compute_thread_stacks(&self) -> Result<Vec<ThreadStack>, HeddleError>
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.
Sourcepub fn thread_stack_for(
&self,
thread_name: &str,
) -> Result<Option<ThreadStack>, HeddleError>
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.
Sourcepub fn plan_thread_stack_rebase(
&self,
root_thread: &str,
onto: &str,
) -> Result<Result<StackRebasePlan, PlanRebaseError>, HeddleError>
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
impl Repository
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>
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>
pub fn recover_timeline_materialization_action( &self, store: &TimelineStore, thread: &str, ) -> Result<TimelineRecoverOutcome, HeddleError>
Source§impl Repository
impl Repository
Sourcepub fn preview_timeline_seek(
&self,
store: &TimelineStore,
thread: &str,
selector: &TimelineSeekSelector,
mode: TimelineMaterializeMode,
) -> Result<TimelineSeekPreview, HeddleError>
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.
Sourcepub fn preview_timeline_seek_constrained(
&self,
store: &TimelineStore,
thread: &str,
selector: &TimelineSeekSelector,
mode: TimelineMaterializeMode,
branch_constraint: Option<&TimelineSeekBranchConstraint>,
) -> Result<TimelineSeekPreview, HeddleError>
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.
Sourcepub fn materialize_timeline_cursor(
&self,
store: &TimelineStore,
thread: &str,
selector: &TimelineSeekSelector,
mode: TimelineMaterializeMode,
moved_at_ms: i64,
) -> Result<TimelineMaterializeOutcome, HeddleError>
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.
Sourcepub 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>
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.
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>
pub fn recover_pending_timeline_materialization( &self, store: &TimelineStore, thread: &str, ) -> Result<TimelineMaterializationRecoveryOutcome, HeddleError>
Source§impl Repository
impl Repository
Trait Implementations§
Source§impl<R, O, S> RepositoryLockExt for Repository<R, O, S>
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>
impl<R, O, S> UnsafeUnpin for Repository<R, O, S>
impl<R, O, S> UnwindSafe for Repository<R, O, S>
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self>
fn instrument(self, span: Span) -> Instrumented<Self>
Source§fn in_current_span(self) -> Instrumented<Self>
fn in_current_span(self) -> Instrumented<Self>
Source§impl<T> IntoRequest<T> for T
impl<T> IntoRequest<T> for T
Source§fn into_request(self) -> Request<T>
fn into_request(self) -> Request<T>
T in a tonic::Request