Skip to main content

push_branch

Function push_branch 

Source
pub fn push_branch(
    tx: &dyn Transport,
    store: &ObjectStore,
    branch: &str,
    tip: Hash,
    condition: RefWriteCondition,
) -> Result<(), DispatchError>
Expand description

Push one branch: upload one or more delta-compressed packs carrying every object reachable from tip that the remote lacks — split across multiple packs when the plan’s payload exceeds a single pack’s cap (issue #831) — durably advertise them as one node on the refs/mkit/packmap/<branch> metadata ref, then CAS-write refs/heads/<branch> under condition.

Objects already present at the remote’s current tip are never re-sent (identical-object dedup), and changed FastCDC chunks are delta-encoded against the prior version the remote already holds when that saves bytes (see mkit_core::transfer::plan_pack). The pack is keyed by its own BLAKE3 digest (SPEC-PACKFILE §7) — required because the digest- checking storage server rejects a delta stored under the reconstructed object’s hash.

The packmap is advanced and confirmed before the branch ref moves: if the packmap can’t be durably established the push aborts without touching the head, so the head never points past a packmap that fails to reconstruct it (even under concurrent pushers to the same branch).

Plans the pack FIRST (diffing against the remote’s current tip). A no-op push (empty plan — the remote already holds this closure) takes the cheap head-only path and walks NO packmap chain (mkit #521 perf). Only when the plan is non-empty does it resolve the branch’s current packmap chain depth (walking it exactly once, see packmap::probe_chain) and, if the chain would grow past the re-baseline threshold (#406, see packmap::rebaseline_depth) AND the transport’s advance_refs is transactional (Transport::supports_atomic_advance, mkit #521) AND the head write is CAS-conditioned (a force push’s Any head condition takes the safe append path — an Any condition makes even an atomic transport fall back to the ordered two-PUT advance_refs, so a reset there is not safe), re-plans as a full closure (diffs against no remote tip) and carries that decision down to advance_packmap as ChainAction::ResetSelfContained so it resets the chain to a single fresh node instead of appending to it — bounding clone cost, which otherwise grows with chain length.

On a transport WITHOUT transactional advance_refs (the default used by file/S3/SSH/memory), crossing the threshold never triggers a reset: the default advance_refs commits the packmap write before the head CAS, and a reset (unlike an append) is not a superset of the prior chain, so a lost head-CAS race after a committed reset would strand the (unmoved) head pointing at a commit the packmap can no longer reconstruct. Such a transport keeps appending — ChainAction::Append — past the threshold; packmap::MAX_PACK_CHAIN_DEPTH (the pure runaway/cycle guard) remains the only bound on chain growth there, unchanged by this gate.

A chain read that fails with DispatchError::PackChainInvalid, or a missing prior packmap (first push), is left alone here — depth is only defined for a resolvable chain, and a broken chain already has its own reset path in advance_packmap (the broken-chain escape hatch, gated on self_contained alone, independent of this transactional-advance gate — see ChainAction::Append’s doc comment).

The already-resolved chain from this probe (when not discarded by a re-baseline decision) is threaded into advance_packmap so its first CAS attempt does not have to walk the chain a second time (#521 perf fix).

On a CAS failure (TransportError::RefConflict) this returns DispatchError::NonFastForwardPush so callers can render an actionable fetch-then-retry hint. Does NOT touch local remote-tracking refs — the caller decides when to advance them.