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§
Sourcefn put(&self, pack: &[u8], refs: &[RefUpdate]) -> Result<TxId>
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.
Sourcefn has(&self, oid: Oid<'_>) -> Result<bool>
fn has(&self, oid: Oid<'_>) -> Result<bool>
Is this object here? The negotiation call, made a thousand at a time.
Sourcefn extents(&self, oids: &[Oid<'_>]) -> Result<Vec<Option<Extent>>>
fn extents(&self, oids: &[Oid<'_>]) -> Result<Vec<Option<Extent>>>
Byte extents, in bulk — the wire path.
Sourcefn update_ref(
&self,
name: &str,
old: Option<Oid<'_>>,
new: Option<Oid<'_>>,
) -> Result<TxId>
fn update_ref( &self, name: &str, old: Option<Oid<'_>>, new: Option<Oid<'_>>, ) -> Result<TxId>
Compare-and-swap one ref.
Sourcefn put_refs_cas(&self, edits: &[RefCas<'_>]) -> Result<TxId>
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.
Provided Methods§
Sourcefn put_pack_observed(
&self,
bytes: &[u8],
progress: &dyn PackProgress,
) -> Result<TxId>
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.
Sourcefn publishes_pack_progress(&self) -> bool
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".