gunnar-sendpack 1.1.0

git's receive-pack wire format, both ends: the send-pack conversation gitoxide does not have, plus the server-side encoders for the same grammar. Plumbing only, no gunnar types.
Documentation
//! What the client asks for, and what it does when the remote will not give it.
//!
//! # What is refused rather than silently downgraded
//!
//! A client that asks for `atomic` and gets a non-atomic push has done
//! something worse than failing: it has reported success for a guarantee it did
//! not get. [`negotiate`] therefore **errors** when a requested capability was
//! not advertised, for `atomic`, `push-options` and `delete-refs`. A silent
//! downgrade is a green that could never go red.

use gix_hash::Kind;

use crate::advertisement::{object_format_name, Advertisement};
use crate::command::PushCommand;
use crate::error::{Error, Result};

/// What the client asks for.
#[derive(Debug, Clone)]
pub struct SendPackOptions {
    /// The `agent=` string. Identifies the client in the remote's logs.
    pub agent: String,
    /// Require all-or-nothing across the whole command list. **Errors** if the
    /// remote does not advertise `atomic`.
    pub atomic: bool,
    /// Ask for `side-band-64k`, so the remote's hook output and errors arrive
    /// rather than vanishing.
    pub side_band: bool,
    /// Ask the remote to be quiet about progress.
    pub quiet: bool,
    /// `push-options`, delivered after the command list. **Errors** if the
    /// remote does not advertise `push-options`.
    pub push_options: Vec<String>,
    /// Prefer `report-status-v2` when advertised.
    pub report_status_v2: bool,
}

impl Default for SendPackOptions {
    fn default() -> Self {
        SendPackOptions {
            agent: format!("{}/{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")),
            atomic: false,
            side_band: true,
            quiet: false,
            push_options: Vec::new(),
            report_status_v2: true,
        }
    }
}

/// **May the pack this push writes use `OBJ_OFS_DELTA` entries?**
///
/// # One predicate, two readers, because it was one decision made twice
///
/// `ofs-delta` on receive-pack means *"I may send you deltas named by distance
/// backwards rather than by object id"*. It is a thing the CLIENT says, and
/// gunnar's client never said it: [`negotiate`] had no `ofs-delta` arm at all,
/// while `gunnar-client`'s push built `WriteOptions::new(kind)` — whose
/// `ofs_delta` defaults to `true` — and `write_pack_deltified` duly emitted
/// `OBJ_OFS_DELTA` headers. The encoding was used and never negotiated.
///
/// It is not a measured wrong answer against stock git: `index-pack` decodes
/// `OBJ_OFS_DELTA` whether or not the capability was sent, so nothing breaks
/// today. That is exactly what makes it this class rather than a bug — a
/// behaviour the wire does not declare, which happens to be tolerated. A
/// receive-pack entitled to refuse what it never offered would be within its
/// rights, and gunnar would be the one in the wrong.
///
/// So both halves read this, and neither can be changed alone: the capability
/// line comes from here and so does the entry header. The same shape
/// `gunnar_wire::WantPolicy::advertises` has on the serving side, where one
/// written-out predicate decides both what `check_wants` will serve and what
/// the v0 advertisement promises.
pub fn ofs_delta_agreed(adv: &Advertisement) -> bool {
    adv.capabilities.has("ofs-delta")
}

/// The capabilities the client will send, given what the remote offered.
///
/// Errors rather than downgrades when a requested guarantee is not on offer.
/// See the module docs.
pub fn negotiate(
    adv: &Advertisement,
    commands: &[PushCommand],
    local_hash_kind: Kind,
    opts: &SendPackOptions,
) -> Result<Vec<String>> {
    let remote = &adv.capabilities;
    let mut out: Vec<String> = Vec::new();

    if local_hash_kind != adv.hash_kind {
        return Err(Error::protocol(format!(
            "the local repository is {local_hash_kind:?} but the remote is {:?}; \
             git cannot push between object formats",
            adv.hash_kind
        )));
    }

    if remote.has("report-status-v2") && opts.report_status_v2 {
        out.push("report-status-v2".to_string());
    } else if remote.has("report-status") {
        out.push("report-status".to_string());
    }
    // With neither, the remote says nothing at all after the pack and the push
    // is unverifiable. That is allowed by the protocol; it is recorded so the
    // caller can see the report is empty by design rather than by failure.

    if opts.side_band && remote.has("side-band-64k") {
        out.push("side-band-64k".to_string());
    }
    // Said because the pack says it: `ofs_delta_agreed` is what the writer's
    // entry headers are chosen from too, so this line and the encoding cannot
    // disagree. There is no `SendPackOptions` knob for it — a client that
    // declined `ofs-delta` while still writing `OBJ_OFS_DELTA` is the defect
    // this closes, and a knob is one more way to reopen it.
    if ofs_delta_agreed(adv) {
        out.push("ofs-delta".to_string());
    }
    if opts.quiet && remote.has("quiet") {
        out.push("quiet".to_string());
    }
    if opts.atomic {
        if !remote.has("atomic") {
            return Err(Error::protocol(
                "atomic push was requested but the remote does not advertise `atomic`; \
                 refusing to push non-atomically under an atomic request",
            ));
        }
        out.push("atomic".to_string());
    }
    if !opts.push_options.is_empty() {
        if !remote.has("push-options") {
            return Err(Error::protocol(
                "push options were given but the remote does not advertise `push-options`",
            ));
        }
        out.push("push-options".to_string());
    }
    if commands.iter().any(PushCommand::is_delete) && !remote.has("delete-refs") {
        return Err(Error::protocol(
            "the push deletes a ref but the remote does not advertise `delete-refs`",
        ));
    }
    // Only echo `object-format` when the remote raised the subject; older
    // servers reject an unknown capability outright.
    if remote.has("object-format") {
        out.push(format!(
            "object-format={}",
            object_format_name(local_hash_kind)?
        ));
    }
    if remote.has("agent") {
        out.push(format!("agent={}", opts.agent));
    }
    Ok(out)
}