thin-fetch 0.1.0

Low-level Git crate to fetch objects with high granularity
Documentation
use crate::{Error, Parcel, WithError as _, bail};

use super::{Repo, parse_response};

#[derive(Debug, Default, Clone)]
pub struct Capabilities {
    pub agent: Option<String>,
    pub ls_refs: Vec<String>,
    pub fetch: Vec<String>,
    pub object_format: Option<String>,
}

impl Repo {
    /// # Errors
    pub fn discover(&self) -> Result<Capabilities, Error> {
        let agent = ureq::Agent::config_builder()
            .http_status_as_error(false)
            .build()
            .new_agent();

        let res = agent
            .get(format!("{}/info/refs?service=git-upload-pack", self.url))
            .header("Git-Protocol", "version=2")
            .call()
            .with_err_msg("could not send HTTP request")?;

        let mut res = parse_response(res)?;
        res.ignore_end = true;
        let parcel: Parcel = res.collect::<Result<Parcel, Error>>()?;

        let mut sections = parcel.sections.into_iter();

        let header = sections.next().with_err_msg("header not found")?;
        let expected_header = ["# service=git-upload-pack\n"];
        if &header != expected_header.as_slice() {
            bail!("unexpected header: {header:?}");
        }

        let caps = sections.next().with_err_msg("capabilities not found")?;

        let raw_caps = caps.chunks.into_iter();
        let mut caps = Capabilities::default();

        for cap in raw_caps {
            let cap = str::from_utf8(&cap).with_err_msg("could not decode capability as utf-8")?;
            let line = cap.trim_end_matches('\n');

            if let Some(value) = line.strip_prefix("agent=") {
                caps.agent.replace(value.to_owned());
            }

            if let Some(value) = line.strip_prefix("ls-refs=") {
                caps.ls_refs.extend(value.split(' ').map(str::to_owned));
            }

            if let Some(value) = line.strip_prefix("fetch=") {
                caps.fetch.extend(value.split(' ').map(str::to_owned));
            }

            if let Some(value) = line.strip_prefix("object-format=") {
                caps.object_format.replace(value.to_owned());
            }
        }

        Ok(caps)
    }
}