Skip to main content

GitOps

Trait GitOps 

Source
pub trait GitOps: Send + Sync {
Show 14 methods // Required methods fn put(&self, pack: &[u8], refs: &[RefUpdate]) -> Result<TxId>; fn put_pack(&self, bytes: &[u8]) -> Result<TxId>; fn put_refs(&self, updates: &[RefUpdate]) -> Result<TxId>; fn get(&self, oid: Oid<'_>) -> Result<Option<Stored>>; fn has(&self, oid: Oid<'_>) -> Result<bool>; fn size(&self, oid: Oid<'_>) -> Result<Option<u64>>; fn extents(&self, oids: &[Oid<'_>]) -> Result<Vec<Option<Extent>>>; fn refs(&self) -> Result<Vec<RefRow>>; fn update_ref( &self, name: &str, old: Option<Oid<'_>>, new: Option<Oid<'_>>, ) -> Result<TxId>; fn put_refs_cas(&self, edits: &[RefCas<'_>]) -> Result<TxId>; fn reachable( &self, want: &[Oid<'_>], have: &[Oid<'_>], ) -> Result<Vec<Vec<u8>>>; fn gc(&self) -> Result<GcReport>; // Provided methods fn put_pack_observed( &self, bytes: &[u8], progress: &dyn PackProgress, ) -> Result<TxId> { ... } fn publishes_pack_progress(&self) -> bool { ... }
}
Expand description

The eleven. Typed, backend-neutral, and the only entry point a git server needs into storage.

Implemented by znippy’s GitStore (Arrow-IPC) and by storage-git-gix (gix). No method invents storage, an index, a durability contract or a concurrency mechanism; if one looks like it does, that is the bug. Send + Sync is a supertrait because every consumer shares one store across threads and cannot do otherwise. gunnar’s RepoStore is Send + Sync — the server holds one per repository and serves many connections from it at once — so a RepoStore holding an Arc<dyn GitOps> does not compile without this (E0277 at every implementor). The traits GitOps replaced, ObjectStore and RefStore, both carried it; dropping it here was an oversight of the extraction, not a decision.

It costs the implementers nothing: both already satisfy it. The Arrow arm is built around a per-account indexer thread and declares ArchiveWrite: Send + Sync and ObjectAbsorb: Send + Sync itself; the gix arm shares a pooled odb handle across workers. The alternative — spelling dyn GitOps + Send + Sync at every use site — is viral, and a consumer who forgets it gets a different type rather than an error at the definition.

Required Methods§

Source

fn put(&self, pack: &[u8], refs: &[RefUpdate]) -> Result<TxId>

One push: pack bytes and ref updates, durable before this returns.

The order inside it is the contract: the pack’s bytes durable first, then the refs that point into them. A crash between the two leaves objects nobody points at (a GC reclaims them); the reverse order leaves a ref pointing at objects that are not there, which no later pass repairs.

Source

fn put_pack(&self, bytes: &[u8]) -> Result<TxId>

The pack half of put, on its own.

Source

fn put_refs(&self, updates: &[RefUpdate]) -> Result<TxId>

The ref half of put, on its own.

Source

fn get(&self, oid: Oid<'_>) -> Result<Option<Stored>>

The stored bytes of one object.

Source

fn has(&self, oid: Oid<'_>) -> Result<bool>

Is this object here? The negotiation call, made a thousand at a time.

Source

fn size(&self, oid: Oid<'_>) -> Result<Option<u64>>

Post-resolution size.

Source

fn extents(&self, oids: &[Oid<'_>]) -> Result<Vec<Option<Extent>>>

Byte extents, in bulk — the wire path.

Source

fn refs(&self) -> Result<Vec<RefRow>>

The current ref namespace.

Source

fn update_ref( &self, name: &str, old: Option<Oid<'_>>, new: Option<Oid<'_>>, ) -> Result<TxId>

Compare-and-swap one ref.

Source

fn put_refs_cas(&self, edits: &[RefCas<'_>]) -> Result<TxId>

Apply every edit or none. The git push --atomic primitive.

The third shape, not a replacement: put_refs (batch, no CAS) and update_ref (CAS, one ref) both stay, and both have callers that want exactly what they are.

Returns Err naming the first edit whose old did not match, and applies nothing. That error carries a RefRejection, recoverable with RefRejection::of — receive-pack must be able to tell a client which ref lost the race and what was there instead, and it must never do that by reading a message.

§Every expectation is checked BEFORE anything is applied — S-023

Not a stylistic preference; it is the one implementation note this method carries, and it is a real defect found in a real backend. gix’s file ref store short-circuits an edit whose new value equals the value the reference already holds: it rewrites the expectation to MustExistAndMatch(current) and never evaluates the one the caller wrote. For an old: None — must not exist — that turns a create that must fail into a silent success, so “exactly one creator wins”, the property receive-pack arbitrates two racing pushes with, was not true on the only backend that survives a restart.

So an implementation compares against one snapshot of the namespace, taken once, before the batch reaches the backend’s own writer.

§There is no third state

An empty batch is a no-op that succeeds, not an error: a deletions-free push with nothing to apply calls this, and refusing it would make the caller special-case the empty case at every site.

Source

fn reachable(&self, want: &[Oid<'_>], have: &[Oid<'_>]) -> Result<Vec<Vec<u8>>>

want minus have, the object closure a fetch must send.

Source

fn gc(&self) -> Result<GcReport>

Reachability, drop the dead index rows, then compact.

Provided Methods§

Source

fn put_pack_observed( &self, bytes: &[u8], progress: &dyn PackProgress, ) -> Result<TxId>

put_pack, saying how far it has got while it is still getting there.

§Why this is a defaulted sibling and not a parameter on put_pack

A caller that wants no progress must not have to say so, and — far more importantly — no existing implementer must have to change. Every backend, bench harness and test double in three repositories implements GitOps; a new required method, or a new argument on an existing one, would be a compile error in all of them for a feature none of them owes. With a default they keep compiling and simply publish nothing, which is the honest answer for a store that has no per-object loop to hang a count on.

§What “honest” means here, and what a store must not do

The count is the caller’s numerator, and the only thing on the system that knows it is the code that is already splitting the pack. Recovering it anywhere else means inflating every entry a second time — a second pack decoder, which LAW 5 forbids and which would be a slower and separately-wrong copy of the one that exists.

So an implementer either has an entry loop and reports from inside it, or it reports nothing. What it must never do is fabricate the shape: publishing (0, N) and then (N, N) around a call that reports nothing is indistinguishable, to a console, from a store that genuinely counts — and that is precisely the defect this method exists to close.

An implementer that overrides this must also answer publishes_pack_progress with true, or the conformance suite will pass it vacuously and the override will go unchecked for ever.

Source

fn publishes_pack_progress(&self) -> bool

Does put_pack_observed actually publish? false for the default implementation.

This exists for LAW 2, and only for LAW 2: a store that publishes nothing and a store whose publishing is broken are the same store as far as any assertion can tell, because both produce an empty series. The conformance suite therefore asks first — a true is held to the property, a false passes by name, and neither can be mistaken for the other in a report.

Dyn Compatibility§

This trait is dyn compatible.

In older versions of Rust, dyn compatibility was called "object safety".

Implementors§