pub struct OpLog { /* private fields */ }Implementations§
Source§impl OpLog
impl OpLog
pub fn open(root: &Path) -> Result<Self>
Sourcepub fn put(&self, rec: &OperationRecord) -> Result<()>
pub fn put(&self, rec: &OperationRecord) -> Result<()>
Persist a record. Idempotent on existing op_ids (the bytes must match by content addressing).
Crash safety: the tempfile’s data is fsync’d before rename,
so a successful return implies a durable file at the final
path. The containing directory is not fsync’d; on a crash
between rename and the directory’s metadata flush, the file
can be lost. For a content-addressed log this is acceptable
— a lost record can be re-derived from the same source — but
callers that also persist references to the op_id (e.g.
branch heads) should fsync those refs after put returns.
pub fn get(&self, op_id: &OpId) -> Result<Option<OperationRecord>>
Sourcepub fn repack(&self, threshold: usize) -> Result<usize>
pub fn repack(&self, threshold: usize) -> Result<usize>
Consolidate loose op records into a deterministic, content- addressed packfile (#261 slice 1). Returns the number of ops moved into the new pack.
threshold is the minimum number of loose ops required to
trigger a repack — under that, returns 0 and leaves the
log alone. The idea: small stores stay loose; only repack
when the file count starts to matter.
Determinism: the pack name is the SHA-256 of the sorted op_ids (newline-joined), so two independent runs against the same set of loose ops produce a byte-identical pack. Re-running on an empty loose directory is a no-op.
Crash safety: the .pack.tmp and .idx.tmp files are
fsync’d before rename; loose files are deleted only after
both renames succeed. A crash mid-repack leaves both loose
and partial-pack files; a subsequent get finds the loose
version, and a subsequent repack cleans up.
Sourcepub fn evict(&self, victims: &BTreeSet<OpId>) -> Result<usize>
pub fn evict(&self, victims: &BTreeSet<OpId>) -> Result<usize>
Remove every op_id in victims from the log, across both
loose files and packfiles (#261 slice 2). Used by
lex op gc after a retention plan identifies which ops to
drop. Idempotent — calling twice with the same set is a
no-op on the second pass.
Pack handling: any pack containing one or more victims is rewritten to a new content-addressed pack with only the surviving ops; the old pack and its index file are deleted. A pack whose every op is a victim is deleted outright.
Returns the count of ops actually removed (loose files deleted + packed ops dropped). Pre-existing absences don’t contribute.
Sourcepub fn delete(&self, op_id: &OpId) -> Result<()>
pub fn delete(&self, op_id: &OpId) -> Result<()>
Remove a record from the log. Used by crate::migrate to
delete the old <op_id>.json files after a format migration
has written their replacements. Idempotent on missing files.
Not part of the day-to-day op-log API — the log is
append-only by design (#129). The only legitimate caller is
the migration tool, which is supervising a destructive,
--confirm-gated batch.
Sourcepub fn walk_back(
&self,
head: &OpId,
limit: Option<usize>,
) -> Result<Vec<OperationRecord>>
pub fn walk_back( &self, head: &OpId, limit: Option<usize>, ) -> Result<Vec<OperationRecord>>
Walk parents transitively. Newest-first, BFS, dedup’d by op_id.
Stops at parentless ops or after limit records.
Sourcepub fn walk_forward(
&self,
head: &OpId,
limit: Option<usize>,
) -> Result<Vec<OperationRecord>>
pub fn walk_forward( &self, head: &OpId, limit: Option<usize>, ) -> Result<Vec<OperationRecord>>
Same set as walk_back but oldest-first, and topological: every op comes after all of its parents. Used by branch_head (and every other consumer) for left-to-right transition replay, so this is the one definition of “the order a head is replayed in”.
walk_back is a breadth-first walk, and its reverse is not a
topological order once a merge joins lines of different lengths: an op
reachable by a short path is emitted before its own descendant on the
long one, so it lands after that descendant here. Replaying it then
overwrote a later change with an earlier one (#1062). The order is now
repaired by Self::linearize, which keeps the BFS order wherever it
was already topological — every linear history, and any merge of
equal-length lines — so those replay exactly as they always did.
Sourcepub fn linearize(records: Vec<OperationRecord>) -> Vec<OperationRecord>
pub fn linearize(records: Vec<OperationRecord>) -> Vec<OperationRecord>
Reorder records into a topological order: every record comes after
all of its parents that are in the set (a parent missing from the set
— an incomplete local log — imposes no constraint, the same leniency
the walks have). Among the records ready at any point, the one listed
first in records goes first, so an input that is already
topological comes back unchanged, and the result is a pure function
of the input order.
Sourcepub fn continues_from(records: &[OperationRecord], since: &OpId) -> bool
pub fn continues_from(records: &[OperationRecord], since: &OpId) -> bool
Whether records is exactly a continuation of since: every record
descends from since through parents that are themselves in
records (or are since), so none of them is an ancestor of since
and none has an ancestor outside the set. This is the shape
Self::walk_forward_since returns for a plain fast-forward, and
NOT the shape it returns for a merge: there it also walks the second
parent’s history back to genesis, which is history since already
contains (#1062). Replaying such a set on top of a state computed for
since re-applies old changes over newer ones.
Sourcepub fn walk_forward_since(
&self,
head: &OpId,
since: &OpId,
) -> Result<Option<Vec<OperationRecord>>>
pub fn walk_forward_since( &self, head: &OpId, since: &OpId, ) -> Result<Option<Vec<OperationRecord>>>
Like Self::walk_forward, but bounded: walk from head back
toward genesis and stop as soon as since is reached, without
visiting since’s own parents or including since itself in the
result. Returns oldest-first, suitable for incrementally
extending a transition map already computed as of since.
Returns Ok(None) if since is never reached (not an ancestor
of head — e.g. after a branch reset or a merge that reordered
history): callers should fall back to a full walk_forward in
that case, since there is nothing valid to incrementally extend.
This is the piece walk_forward itself doesn’t provide: its own
limit truncates the result after a full walk_back to genesis
has already completed (see its body above), so it can’t turn an
O(N)-in-total-history walk into an O(ops since a checkpoint) one.
head == since returns Ok(Some(vec![])) without touching the
op log at all.
Sourcepub fn lca(&self, a: &OpId, b: &OpId) -> Result<Option<OpId>>
pub fn lca(&self, a: &OpId, b: &OpId) -> Result<Option<OpId>>
Common ancestor of two op_ids in the DAG.
On tree-shaped histories and chain merges this is the
lowest common ancestor — the closest shared op. On
criss-cross merges (two ops each with two parents from
independent histories) there can be multiple
incomparable common ancestors; this picks one
deterministically (the first hit when traversing b’s
ancestors newest-first), but not via a recursive merge.
None if no shared ancestor exists.
Tier-1 merge in #129 covers linear and tree-shaped
histories; criss-cross resolution is deferred to a
future tier (Git’s recursive strategy is the reference).
Sourcepub fn list_all(&self) -> Result<Vec<OperationRecord>>
pub fn list_all(&self) -> Result<Vec<OperationRecord>>
Every record in the log. Order is whatever the directory
listing produces — undefined and not stable. Used by the
[crate::predicate] evaluator when no narrower candidate
set is available.