use crate::candidate_batch::{BatchReply, ObjectKind, read_batch_reply};
use crate::candidate_error::{CandidateError, CandidateFailure};
use crate::start_retry::RetryStart;
use crate::candidate_object::ObjectId;
use crate::forwarding::git_command;
use crate::child_watch::{ChildWatch, WatchTerms, reader_subject, watched_timeout_text};
use std::collections::HashMap;
use std::ffi::OsString;
use std::io::{BufReader, Write};
use std::path::Path;
use std::process::{Child, ChildStdin, ChildStdout, Stdio};
use std::rc::Rc;
pub const OBJECT_MISSING_PREFIX: &str = "cli-git could not read candidate content: Git has no object ";
struct ReaderPipes {
input: ChildStdin,
output: BufReader<ChildStdout>,
}
pub struct ObjectReader {
child: Child,
pipes: Option<ReaderPipes>,
blobs: HashMap<ObjectId, Rc<[u8]>>,
watch: ChildWatch,
}
fn reader_ended(detail: &str) -> CandidateError {
return CandidateError::new(
CandidateFailure::ReaderEnded,
format!(
"cli-git could not read a Git object: the git cat-file --batch process {detail}. \
Run `git cat-file --batch-check` in this repository to see Git's own diagnostic."
)
.as_str(),
);
}
pub fn start_object_reader(
real_git: &Path,
global_prefix: &[OsString],
overlay: &[(OsString, OsString)],
) -> Result<ObjectReader, CandidateError> {
return start_object_reader_with(real_git, global_prefix, overlay, WatchTerms::from_environment());
}
pub fn start_object_reader_with(
real_git: &Path,
global_prefix: &[OsString],
overlay: &[(OsString, OsString)],
terms: WatchTerms,
) -> Result<ObjectReader, CandidateError> {
let mut arguments: Vec<OsString> = global_prefix.to_vec();
arguments.push(OsString::from("cat-file"));
arguments.push(OsString::from("--batch"));
let started: std::io::Result<Child> = git_command(real_git, arguments.as_slice(), overlay)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn_retrying();
let mut child: Child = match started {
Ok(spawned) => spawned,
Err(error) => {
return Err(CandidateError::new(
CandidateFailure::GitNotStarted,
format!(
"cli-git could not start git cat-file --batch to read Git objects: {error}."
)
.as_str(),
));
}
};
let (Some(input), Some(output)) = (child.stdin.take(), child.stdout.take()) else {
let _ = child.kill();
let _ = child.wait();
return Err(reader_ended("started without its request and reply pipes"));
};
let watch: ChildWatch = ChildWatch::start(reader_subject().as_str(), child.id(), terms).kept_as("git cat-file --batch").within(crate::wait_ceilings::OBJECT_STEP_CEILING);
return Ok(ObjectReader {
child,
pipes: Some(ReaderPipes {
input,
output: BufReader::new(output),
}),
blobs: HashMap::new(),
watch,
});
}
impl ObjectReader {
fn request(&mut self, name: &[u8]) -> Result<BatchReply, CandidateError> {
self.watch.begin();
let reply: Result<BatchReply, CandidateError> = self.exchange(name);
self.watch.end();
if reply.is_err() && self.watch.expired() {
self.pipes = None;
return Err(CandidateError::new(
CandidateFailure::ReaderEnded,
format!(
"cli-git could not read a Git object: {}",
watched_timeout_text(reader_subject().as_str(), self.watch.limit())
)
.as_str(),
));
}
return reply;
}
fn exchange(&mut self, name: &[u8]) -> Result<BatchReply, CandidateError> {
let Some(pipes) = self.pipes.as_mut() else {
return Err(reader_ended("was closed after an earlier failure"));
};
let mut line: Vec<u8> = name.to_vec();
line.push(b'\n');
if let Err(error) = pipes.input.write_all(line.as_slice()) {
self.pipes = None;
return Err(reader_ended(
format!("stopped accepting requests ({error})").as_str(),
));
}
match read_batch_reply(&mut pipes.output, name) {
Ok(reply) => return Ok(reply),
Err(error) => {
self.pipes = None;
return Err(error);
}
}
}
pub fn blob(&mut self, object: &ObjectId) -> Result<Rc<[u8]>, CandidateError> {
if let Some(bytes) = self.blobs.get(object) {
return Ok(Rc::clone(bytes));
}
let reply: BatchReply = self.request(object.as_str().as_bytes())?;
match reply {
BatchReply::Missing => {
return Err(CandidateError::new(
CandidateFailure::ObjectMissing,
format!(
"{OBJECT_MISSING_PREFIX}{}. \
Run `git fsck` to check the object store.",
object.as_str()
)
.as_str(),
));
}
BatchReply::Found { kind, bytes, .. } => {
if kind != ObjectKind::Blob {
return Err(CandidateError::new(
CandidateFailure::ObjectKindUnexpected,
format!(
"cli-git could not read candidate content: object {} is not a blob.",
object.as_str()
)
.as_str(),
));
}
let shared: Rc<[u8]> = Rc::from(bytes);
self.blobs.insert(object.clone(), Rc::clone(&shared));
return Ok(shared);
}
}
}
pub fn head_commit(&mut self) -> Result<Option<ObjectId>, CandidateError> {
match self.request(b"HEAD")? {
BatchReply::Missing => return Ok(None),
BatchReply::Found { object, kind, .. } => {
if kind != ObjectKind::Commit {
return Err(CandidateError::new(
CandidateFailure::ObjectKindUnexpected,
"cli-git could not list candidate files: HEAD does not name a commit.",
));
}
return Ok(Some(object));
}
}
}
}
impl Drop for ObjectReader {
fn drop(&mut self) {
self.pipes = None;
let _ = self.child.wait();
}
}
#[cfg(test)]
#[path = "candidate_reader_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "candidate_reader_failure_tests.rs"]
mod failure_tests;