thin-fetch 0.1.0

Low-level Git crate to fetch objects with high granularity
Documentation
use ureq::BodyReader;

use crate::{
    Command, Error, HashKind, PackDecoder, WithError, bail,
    command::Execute,
    parcel::{ParcelDecoder, ParcelItem},
};

/// Fetch remote objects.
///
/// ## Example
///
/// ```
/// # use thin_fetch::command::Fetch;
/// # use thin_fetch::HashKind;
/// # let want = "HEAD";
/// # let have = "awawa";
/// # let blobs = false;
/// let fetch = Fetch {
///     want: vec![want.to_owned()],
///     have: vec![have.to_owned()],
///     filter: if blobs {
///         None
///     } else {
///         Some("blob:none".to_owned())
///     },
///     deepen: None,
///     hash_kind: HashKind::Sha1,
/// };
/// ```
#[derive(Default, Debug)]
pub struct Fetch {
    pub want: Vec<String>,
    pub have: Vec<String>,
    pub filter: Option<String>,
    pub deepen: Option<usize>,
    pub hash_kind: HashKind,
    pub include_tag: bool,
}

impl From<&Fetch> for Command {
    fn from(value: &Fetch) -> Self {
        let mut cmd = Command::new("fetch").object_format(value.hash_kind);

        for want in &value.want {
            cmd = cmd.arg(format!("want {want}"));
        }
        for have in &value.have {
            cmd = cmd.arg(format!("have {have}"));
        }

        if let Some(filter) = &value.filter {
            cmd = cmd.arg(format!("filter {filter}"));
        }
        if let Some(deepen) = &value.deepen {
            cmd = cmd.arg(format!("deepen {deepen}"));
        }
        if value.include_tag {
            cmd = cmd.arg("include-tag");
        }

        cmd = cmd.arg("done");

        cmd
    }
}

impl Execute for Fetch {
    type Output = PackDecoder<FetchDecoder<BodyReader<'static>>>;

    fn execute(&self, repo: &crate::Repo) -> Result<Self::Output, Error> {
        let parcel = repo.stream(&self.into())?;
        let fetch = FetchDecoder::new(parcel);
        let pack = PackDecoder::new(fetch, self.hash_kind)?;
        Ok(pack)
    }
}

#[derive(PartialEq, Eq)]
enum FetchMode {
    Initial,
    Packfile,
    ShallowInfo,
}

pub struct FetchDecoder<R: std::io::Read> {
    mode: FetchMode,
    pack_data: Vec<u8>,
    reader: ParcelDecoder<R>,
}

impl<R: std::io::Read> FetchDecoder<R> {
    fn new(reader: ParcelDecoder<R>) -> Self {
        Self {
            mode: FetchMode::Initial,
            pack_data: vec![],
            reader,
        }
    }

    fn read_data(&mut self) -> Result<Vec<u8>, Error> {
        let mut data = vec![];

        while data.is_empty() {
            let item = self.reader.get_one().with_err_msg("read one item");
            if item == Ok(ParcelItem::End) {
                return Ok(vec![]);
            }
            data = self.parse_data(item)?;
        }

        Ok(data)
    }

    fn parse_data(&mut self, item: Result<ParcelItem, Error>) -> Result<Vec<u8>, Error> {
        match item? {
            ParcelItem::Data(data) if self.mode == FetchMode::Initial => {
                let header = str::from_utf8(&data).with_err_msg("could not decode header")?;
                match header {
                    "packfile\n" => {
                        self.mode = FetchMode::Packfile;
                        Ok(vec![])
                    }
                    "shallow-info\n" => {
                        self.mode = FetchMode::ShallowInfo;
                        Ok(vec![])
                    }
                    hdr => {
                        bail!("unknown header: {hdr}");
                    }
                }
            }
            ParcelItem::Data(_data) if self.mode == FetchMode::ShallowInfo => {
                Ok(vec![])
            }
            ParcelItem::Data(data) if self.mode == FetchMode::Packfile => {
                let stream = data.first().with_err_msg("stream empty")?;

                let data = &data[1..];

                match *stream {
                    1 => Ok(data.to_vec()),
                    2 => {
                        let msg = str::from_utf8(data)
                            .with_err_msg("unexpected non-utf8 data in status stream")?;
                        print!("{msg}");
                        Ok(vec![])
                    }
                    3 => {
                        let msg = str::from_utf8(data)
                            .with_err_msg("unexpected non-utf8 data in error stream")?;
                        bail!("remote error: {msg}");
                    }
                    n => {
                        bail!("unknown stream: {n}");
                    }
                }
            }
            ParcelItem::Data(_data) => {
                todo!()
            }
            ParcelItem::Separator => {
                self.mode = FetchMode::Initial;
                Ok(vec![])
            }
            ParcelItem::End => {
                bail!("unexpected data end");
            }
        }
    }
}

impl<R: std::io::Read> std::io::Read for FetchDecoder<R> {
    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
        let data = if self.pack_data.is_empty() {
            self.read_data()?
        } else {
            std::mem::take(&mut self.pack_data)
        };

        if data.is_empty() {
            return Ok(0);
        }

        if data.len() > buf.len() {
            let src = &data[0..buf.len()];
            let rest = &data[buf.len()..];
            buf.copy_from_slice(src);
            self.pack_data.extend_from_slice(rest);
            return Ok(buf.len());
        }

        let dst = &mut buf[0..data.len()];
        dst.copy_from_slice(&data);
        Ok(data.len())
    }
}

impl<R: std::io::Read> std::io::BufRead for FetchDecoder<R> {
    fn fill_buf(&mut self) -> std::io::Result<&[u8]> {
        if self.pack_data.is_empty() {
            let data = self.read_data()?;
            self.pack_data.extend_from_slice(&data);
        }

        Ok(&self.pack_data)
    }

    fn consume(&mut self, amount: usize) {
        self.pack_data.drain(0..amount);
    }
}