use crate::git::object::ObjectType;
use anyhow::{bail, Context, Result};
use std::collections::HashSet;
use std::io::{BufRead, BufReader, Read, Write};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::time::Instant;
use tracing::debug;
use super::{fetch_progress_interval, RemoteHelper};
impl RemoteHelper {
pub(super) fn git_batch_check_objects<'a>(
&self,
oids: impl Iterator<Item = &'a str>,
) -> Result<HashSet<String>> {
let mut existing = HashSet::new();
let oids: Vec<_> = oids.collect();
const BATCH_SIZE: usize = 1000;
for chunk in oids.chunks(BATCH_SIZE) {
let mut child = Command::new("git")
.args(["cat-file", "--batch-check=%(objectname)"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.context("Failed to spawn git cat-file")?;
{
let stdin = child.stdin.as_mut().context("Failed to open stdin")?;
for oid in chunk {
writeln!(stdin, "{}", oid)?;
}
}
let output = child
.wait_with_output()
.context("Failed to read git cat-file output")?;
for line in String::from_utf8_lossy(&output.stdout).lines() {
let line = line.trim();
if line.len() == 40 && !line.contains(' ') {
existing.insert(line.to_string());
}
}
}
Ok(existing)
}
pub(super) fn git_dir_path() -> PathBuf {
PathBuf::from(std::env::var("GIT_DIR").unwrap_or_else(|_| ".git".to_string()))
}
pub(super) fn write_git_object_to_dir(git_dir: &Path, oid: &str, data: &[u8]) -> Result<()> {
if oid.len() < 3 {
bail!("Invalid object id: {}", oid);
}
let (dir_name, file_name) = oid.split_at(2);
let obj_dir = git_dir.join("objects").join(dir_name);
std::fs::create_dir_all(&obj_dir).context("Failed to create object directory")?;
let obj_path = obj_dir.join(file_name);
if obj_path.exists() {
return Ok(());
}
std::fs::write(&obj_path, data).context("Failed to write git object")?;
debug!("Wrote git object {} as loose object", oid);
Ok(())
}
pub(super) fn list_objects_to_push(
&self,
sha: &str,
exclude: &[String],
) -> Result<Vec<String>> {
self.list_objects_for_shas(&[sha.to_string()], exclude)
}
pub(super) fn list_objects_for_shas(
&self,
include: &[String],
exclude: &[String],
) -> Result<Vec<String>> {
if include.is_empty() {
return Ok(Vec::new());
}
let mut command = Command::new("git");
command.arg("rev-list").arg("--objects");
for sha in include {
command.arg(sha);
}
if !exclude.is_empty() {
command.arg("--not");
for sha in exclude {
command.arg(sha);
}
}
let output = command.output()?;
if !output.status.success() {
bail!("Failed to list objects");
}
let mut seen = HashSet::new();
let mut objects = Vec::new();
for line in String::from_utf8_lossy(&output.stdout).lines() {
if let Some(oid) = line.split_whitespace().next() {
let oid = oid.to_string();
if seen.insert(oid.clone()) {
objects.push(oid);
}
}
}
Ok(objects)
}
pub(super) fn read_git_objects_batch(
&self,
oids: &[String],
) -> Result<Vec<(ObjectType, Vec<u8>)>> {
if oids.is_empty() {
return Ok(Vec::new());
}
let total = oids.len();
let mut results = Vec::with_capacity(total);
let progress_interval = fetch_progress_interval();
let mut last_progress = Instant::now();
const BATCH_SIZE: usize = 100;
for (batch_idx, batch) in oids.chunks(BATCH_SIZE).enumerate() {
let mut child = Command::new("git")
.args(["cat-file", "--batch"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()?;
let mut stdin = child
.stdin
.take()
.ok_or_else(|| anyhow::anyhow!("Failed to open stdin"))?;
let stdout = child
.stdout
.take()
.ok_or_else(|| anyhow::anyhow!("Failed to open stdout"))?;
for oid in batch {
writeln!(stdin, "{}", oid)?;
}
drop(stdin);
let mut reader = BufReader::new(stdout);
for (i, oid) in batch.iter().enumerate() {
let mut header = String::new();
reader.read_line(&mut header)?;
let header = header.trim();
let parts: Vec<&str> = header.split_whitespace().collect();
if parts.len() < 3 {
bail!("Object not found or invalid header for {}: {}", oid, header);
}
let obj_type = match parts[1] {
"blob" => ObjectType::Blob,
"tree" => ObjectType::Tree,
"commit" => ObjectType::Commit,
"tag" => ObjectType::Tag,
_ => bail!("Unknown object type: {}", parts[1]),
};
let size: usize = parts[2].parse()?;
let mut content = vec![0u8; size];
reader.read_exact(&mut content)?;
let mut newline = [0u8; 1];
reader.read_exact(&mut newline)?;
results.push((obj_type, content));
let done = batch_idx * BATCH_SIZE + i + 1;
if done == total || last_progress.elapsed() >= progress_interval {
eprintln!(" Reading objects: {}/{}", done, total);
last_progress = Instant::now();
}
}
child.wait()?;
}
Ok(results)
}
}