send-pack, and the receive-pack grammar under it — the half of git that gitoxide does not have.
Verified in gix 0.86: gix-protocol/src/ carries fetch/, ls_refs.rs and
handshake/, and nothing for push. Direction::Push exists only as a
refspec direction. So a git client written on gix has to put this on the
wire itself, and a git server written on gix has to put the other side of
it on the wire itself, and until this crate existed those two were written
twice — once per direction, in two crates, in two different styles, agreeing
only by inspection.
One grammar, both directions
That is the load-bearing idea, and it is why this crate is not called
send-pack alone. Four formats make up the v0 push conversation:
| format | client | server |
|---|---|---|
[capabilities] |
parses the remote's, writes its own | writes its own, parses the client's |
[advertisement] |
[advertisement::parse] |
[advertisement::lines] |
[command] |
[command::lines] |
[command::parse_line] |
[report] |
[report::parse] |
[report::lines] |
Each row is one implementation with two entry points, so an encoder and its decoder cannot drift: there is nothing for them to drift from. A test that round-trips one row exercises both ends of a real conversation rather than one end against a fixture written by the same hand.
Payloads, not packets
Every lines function returns payloads without pkt-line framing, without
a terminating flush-pkt, and without a trailing newline, and every parser
takes them the same way. git frames these sections as text pkt-lines,
<4-hex-length><payload>\n, and the newline belongs to the framer.
That split is what lets a server that already owns a pkt-line framer and a
socket use this crate without paying for a second framing layer: with
default-features = false the whole crate is gix-hash + bstr +
thiserror. The blocking-io feature — on by default, because a client is
the majority consumer — adds [Transport], the pkt-line reader and
[send_pack], framed with [gix_packetline].
Example: what a client does
# fn main() -> Result<(), gunnar_sendpack::Error> {
use gunnar_sendpack::{read_advertisement, send_pack, IoPair, PushCommand, SendPackOptions};
// Two pipes to a remote `git-receive-pack`.
# let (stdout, stdin): (std::fs::File, std::fs::File) = unimplemented!();
let mut transport = IoPair::new(stdout, stdin);
let (reader, _) = { use gunnar_sendpack::Transport as _; transport.io() };
let adv = read_advertisement(reader)?;
let tip = adv.oid_of("refs/heads/main");
# let new_tip = tip;
let commands = vec![PushCommand::update("refs/heads/main", tip, new_tip)];
let report = send_pack(
&mut transport,
&adv,
&commands,
adv.hash_kind,
Some(|w: &mut dyn std::io::Write| Ok(w.write_all(b"PACK...")?)),
&SendPackOptions::default(),
)?;
assert!(report.is_ok(), "{:?}", report.failure_summary());
# Ok(())
# }
Licence
MIT OR Apache-2.0, to match gitoxide's.