use std::process::Stdio;
use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
use tokio::process::{Child, ChildStdin, ChildStdout, Command};
use tokio::sync::Mutex;
use crate::git::{GitError, piped};
type MakeCommand = Box<dyn Fn() -> Command + Send + Sync>;
pub struct BlobReader {
make: MakeCommand,
batch: Mutex<Option<Batch>>,
}
struct Batch {
_child: Child,
stdin: ChildStdin,
stdout: BufReader<ChildStdout>,
}
impl Batch {
fn spawn(make: &MakeCommand) -> Result<Self, GitError> {
let mut cmd = make();
cmd.args(["cat-file", "--batch"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null());
let mut child = cmd.spawn()?;
let stdin = piped(child.stdin.take(), "stdin")?;
let stdout = BufReader::new(piped(child.stdout.take(), "stdout")?);
Ok(Self {
_child: child,
stdin,
stdout,
})
}
async fn read(&mut self, name: &str) -> Result<Option<Vec<u8>>, GitError> {
self.stdin.write_all(format!("{name}\n").as_bytes()).await?;
self.stdin.flush().await?;
let mut header = String::new();
if self.stdout.read_line(&mut header).await? == 0 {
return Err(GitError::Parse("cat-file exited".into()));
}
let header = header.trim_end();
if header.ends_with(" missing") || header.ends_with(" ambiguous") {
return Ok(None);
}
let size: usize = header
.rsplit(' ')
.next()
.and_then(|s| s.parse().ok())
.ok_or_else(|| GitError::Parse(format!("cat-file header {header:?}")))?;
let mut data = vec![0u8; size.saturating_add(1)];
self.stdout.read_exact(&mut data).await?;
data.pop(); Ok(Some(data))
}
}
impl BlobReader {
pub(crate) fn spawn(
make: impl Fn() -> Command + Send + Sync + 'static,
) -> Result<Self, GitError> {
let make: MakeCommand = Box::new(make);
let batch = Batch::spawn(&make)?;
Ok(Self {
make,
batch: Mutex::new(Some(batch)),
})
}
pub async fn read(&self, name: &str) -> Result<Option<Vec<u8>>, GitError> {
if name.contains(['\n', '\r']) {
return Err(GitError::Parse(format!("bad object name {name:?}")));
}
let mut slot = self.batch.lock().await;
let mut batch = match slot.take() {
Some(batch) => batch,
None => Batch::spawn(&self.make)?,
};
let result = batch.read(name).await;
if result.is_ok() {
*slot = Some(batch);
}
result
}
}