gitoxide-core 0.14.0

The library implementating all capabilities of the gitoxide CLI
Documentation
use std::{
    io,
    path::PathBuf,
    sync::{atomic::AtomicBool, Arc},
};

use git_repository::{
    hash::ObjectId,
    objs::bstr::{BString, ByteSlice},
    odb::pack,
    protocol,
    protocol::{
        fetch::{Action, Arguments, LsRefsAction, Ref, Response},
        transport,
        transport::client::Capabilities,
    },
    Progress,
};

use crate::{remote::refs::JsonRef, OutputFormat};

pub const PROGRESS_RANGE: std::ops::RangeInclusive<u8> = 1..=3;

pub struct Context<W> {
    pub thread_limit: Option<usize>,
    pub format: OutputFormat,
    pub should_interrupt: Arc<AtomicBool>,
    pub out: W,
    pub object_hash: git_repository::hash::Kind,
}

struct CloneDelegate<W> {
    ctx: Context<W>,
    directory: Option<PathBuf>,
    refs_directory: Option<PathBuf>,
    ref_filter: Option<&'static [&'static str]>,
    wanted_refs: Vec<BString>,
}
static FILTER: &[&str] = &["HEAD", "refs/tags", "refs/heads"];

fn remote_supports_ref_in_want(server: &Capabilities) -> bool {
    server
        .capability("fetch")
        .and_then(|cap| cap.supports("ref-in-want"))
        .unwrap_or(false)
}

impl<W> protocol::fetch::DelegateBlocking for CloneDelegate<W> {
    fn prepare_ls_refs(
        &mut self,
        server: &Capabilities,
        arguments: &mut Vec<BString>,
        _features: &mut Vec<(&str, Option<&str>)>,
    ) -> io::Result<LsRefsAction> {
        if server.contains("ls-refs") {
            arguments.extend(FILTER.iter().map(|r| format!("ref-prefix {}", r).into()));
        }
        Ok(if self.wanted_refs.is_empty() {
            LsRefsAction::Continue
        } else {
            LsRefsAction::Skip
        })
    }

    fn prepare_fetch(
        &mut self,
        version: transport::Protocol,
        server: &Capabilities,
        _features: &mut Vec<(&str, Option<&str>)>,
        _refs: &[Ref],
    ) -> io::Result<Action> {
        if !self.wanted_refs.is_empty() && !remote_supports_ref_in_want(server) {
            return Err(io::Error::new(
                io::ErrorKind::Other,
                "Want to get specific refs, but remote doesn't support this capability",
            ));
        }
        if version == transport::Protocol::V1 {
            self.ref_filter = Some(FILTER);
        }
        Ok(Action::Continue)
    }

    fn negotiate(
        &mut self,
        refs: &[Ref],
        arguments: &mut Arguments,
        _previous_response: Option<&Response>,
    ) -> io::Result<Action> {
        if self.wanted_refs.is_empty() {
            for r in refs {
                let (path, id) = r.unpack();
                match self.ref_filter {
                    Some(ref_prefixes) => {
                        if ref_prefixes.iter().any(|prefix| path.starts_with_str(prefix)) {
                            arguments.want(id);
                        }
                    }
                    None => arguments.want(id),
                }
            }
        } else {
            for r in &self.wanted_refs {
                arguments.want_ref(r.as_ref())
            }
        }
        Ok(Action::Cancel)
    }
}

#[cfg(feature = "blocking-client")]
mod blocking_io {
    use std::{io, io::BufRead, path::PathBuf};

    use git_repository::{
        bstr::BString,
        protocol,
        protocol::fetch::{Ref, Response},
        Progress,
    };

    use super::{receive_pack_blocking, CloneDelegate, Context};
    use crate::net;

    impl<W: io::Write> protocol::fetch::Delegate for CloneDelegate<W> {
        fn receive_pack(
            &mut self,
            input: impl BufRead,
            progress: impl Progress,
            refs: &[Ref],
            _previous_response: &Response,
        ) -> io::Result<()> {
            receive_pack_blocking(
                self.directory.take(),
                self.refs_directory.take(),
                &mut self.ctx,
                input,
                progress,
                refs,
            )
        }
    }

    pub fn receive<P: Progress, W: io::Write>(
        protocol: Option<net::Protocol>,
        url: &str,
        directory: Option<PathBuf>,
        refs_directory: Option<PathBuf>,
        wanted_refs: Vec<BString>,
        progress: P,
        ctx: Context<W>,
    ) -> anyhow::Result<()> {
        let transport = net::connect(url.as_bytes(), protocol.unwrap_or_default().into())?;
        let delegate = CloneDelegate {
            ctx,
            directory,
            refs_directory,
            ref_filter: None,
            wanted_refs,
        };
        protocol::fetch(
            transport,
            delegate,
            protocol::credentials::helper,
            progress,
            protocol::FetchConnection::TerminateOnSuccessfulCompletion,
        )?;
        Ok(())
    }
}

#[cfg(feature = "blocking-client")]
pub use blocking_io::receive;

#[cfg(feature = "async-client")]
mod async_io {
    use std::{io, io::BufRead, path::PathBuf};

    use async_trait::async_trait;
    use futures_io::AsyncBufRead;
    use git_repository::{
        bstr::{BString, ByteSlice},
        odb::pack,
        protocol,
        protocol::fetch::{Ref, Response},
        Progress,
    };

    use super::{print, receive_pack_blocking, write_raw_refs, CloneDelegate, Context};
    use crate::{net, OutputFormat};

    #[async_trait(?Send)]
    impl<W: io::Write + Send + 'static> protocol::fetch::Delegate for CloneDelegate<W> {
        async fn receive_pack(
            &mut self,
            input: impl AsyncBufRead + Unpin + 'async_trait,
            progress: impl Progress,
            refs: &[Ref],
            _previous_response: &Response,
        ) -> io::Result<()> {
            receive_pack_blocking(
                self.directory.take(),
                self.refs_directory.take(),
                &mut self.ctx,
                futures_lite::io::BlockOn::new(input),
                progress,
                &refs,
            )
        }
    }

    pub async fn receive<P: Progress, W: io::Write + Send + 'static>(
        protocol: Option<net::Protocol>,
        url: &str,
        directory: Option<PathBuf>,
        refs_directory: Option<PathBuf>,
        wanted_refs: Vec<BString>,
        progress: P,
        ctx: Context<W>,
    ) -> anyhow::Result<()> {
        let transport = net::connect(url.as_bytes(), protocol.unwrap_or_default().into()).await?;
        let mut delegate = CloneDelegate {
            ctx,
            directory,
            refs_directory,
            ref_filter: None,
            wanted_refs,
        };
        blocking::unblock(move || {
            futures_lite::future::block_on(protocol::fetch(
                transport,
                delegate,
                protocol::credentials::helper,
                progress,
                protocol::FetchConnection::TerminateOnSuccessfulCompletion,
            ))
        })
        .await?;
        Ok(())
    }
}

#[cfg(feature = "async-client")]
pub use self::async_io::receive;

#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]
pub struct JsonBundleWriteOutcome {
    pub index_kind: pack::index::Version,
    pub index_hash: String,

    pub data_hash: String,
    pub num_objects: u32,
}

impl From<pack::index::write::Outcome> for JsonBundleWriteOutcome {
    fn from(v: pack::index::write::Outcome) -> Self {
        JsonBundleWriteOutcome {
            index_kind: v.index_kind,
            num_objects: v.num_objects,
            data_hash: v.data_hash.to_string(),
            index_hash: v.index_hash.to_string(),
        }
    }
}

#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]
pub struct JsonOutcome {
    pub index: JsonBundleWriteOutcome,
    pub pack_kind: pack::data::Version,

    pub index_path: Option<PathBuf>,
    pub data_path: Option<PathBuf>,

    pub refs: Vec<JsonRef>,
}

impl JsonOutcome {
    pub fn from_outcome_and_refs(v: pack::bundle::write::Outcome, refs: &[Ref]) -> Self {
        JsonOutcome {
            index: v.index.into(),
            pack_kind: v.pack_kind,
            index_path: v.index_path,
            data_path: v.data_path,
            refs: refs.iter().cloned().map(Into::into).collect(),
        }
    }
}

fn print_hash_and_path(out: &mut impl io::Write, name: &str, id: ObjectId, path: Option<PathBuf>) -> io::Result<()> {
    match path {
        Some(path) => writeln!(out, "{}: {} ({})", name, id, path.display()),
        None => writeln!(out, "{}: {}", name, id),
    }
}

fn print(out: &mut impl io::Write, res: pack::bundle::write::Outcome, refs: &[Ref]) -> io::Result<()> {
    print_hash_and_path(out, "index", res.index.index_hash, res.index_path)?;
    print_hash_and_path(out, "pack", res.index.data_hash, res.data_path)?;
    writeln!(out)?;
    crate::remote::refs::print(out, refs)?;
    Ok(())
}

fn write_raw_refs(refs: &[Ref], directory: PathBuf) -> std::io::Result<()> {
    let assure_dir_exists = |path: &BString| {
        assert!(!path.starts_with_str("/"), "no ref start with a /, they are relative");
        let path = directory.join(git_features::path::from_byte_slice_or_panic_on_windows(path));
        std::fs::create_dir_all(path.parent().expect("multi-component path")).map(|_| path)
    };
    for r in refs {
        let (path, content) = match r {
            Ref::Symbolic { path, target, .. } => (assure_dir_exists(path)?, format!("ref: {}", target)),
            Ref::Peeled { path, tag: object, .. } | Ref::Direct { path, object } => {
                (assure_dir_exists(path)?, object.to_string())
            }
        };
        std::fs::write(path, content.as_bytes())?;
    }
    Ok(())
}

fn receive_pack_blocking<W: io::Write>(
    mut directory: Option<PathBuf>,
    mut refs_directory: Option<PathBuf>,
    ctx: &mut Context<W>,
    input: impl io::BufRead,
    progress: impl Progress,
    refs: &[Ref],
) -> io::Result<()> {
    let options = pack::bundle::write::Options {
        thread_limit: ctx.thread_limit,
        index_kind: pack::index::Version::V2,
        iteration_mode: pack::data::input::Mode::Verify,
        object_hash: ctx.object_hash,
    };
    let outcome =
        pack::Bundle::write_to_directory(input, directory.take(), progress, &ctx.should_interrupt, None, options)
            .map_err(|err| io::Error::new(io::ErrorKind::Other, err))?;

    if let Some(directory) = refs_directory.take() {
        write_raw_refs(refs, directory)?;
    }

    match ctx.format {
        OutputFormat::Human => drop(print(&mut ctx.out, outcome, refs)),
        #[cfg(feature = "serde1")]
        OutputFormat::Json => {
            serde_json::to_writer_pretty(&mut ctx.out, &JsonOutcome::from_outcome_and_refs(outcome, refs))?
        }
    };
    Ok(())
}