use std::process::Stdio; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::process::Command; use tokio_stream::wrappers::ReceiverStream; use crate::bare::GitBare; use crate::pb::PackfileChunk; const MAX_PACK_OBJECTS_STDERR_BYTES: u64 = 64 * 1024; impl GitBare { /// Pack objects using git-pack-objects --stdout. /// /// Unary request → server-streaming response. /// The returned stream yields `PackfileChunk` chunks as pack data is produced. pub async fn pack_objects( &self, request: crate::pb::PackObjectsRequest, ) -> Result>, tonic::Status> { let bare_dir = self.bare_dir.clone(); let bare_dir_str = bare_dir.to_string_lossy().into_owned(); tracing::info!( repo = %bare_dir_str, "spawning git pack-objects subprocess" ); let (tx, rx) = tokio::sync::mpsc::channel(8); tokio::spawn(async move { let opts = request.options.as_ref(); let has_wants = opts.is_some_and(|o| !o.wants.is_empty()); let mut args = vec![ "--git-dir".to_string(), bare_dir_str, "pack-objects".to_string(), "--stdout".to_string(), ]; // --all is mutually exclusive with explicit revision selection. if !has_wants { args.push("--all".into()); } else { args.push("--revs".into()); } if opts.is_some_and(|o| o.thin_pack) { args.push("--thin".into()); } if opts.is_some_and(|o| !o.use_bitmaps) { args.push("--no-use-bitmaps".into()); } if opts.is_some_and(|o| o.delta_base_offset) { args.push("--delta-base-offset".into()); } let stdin_data = match generate_pack_input(&request) { Ok(data) => data, Err(err) => { let _ = tx .send(Err(tonic::Status::invalid_argument(err.to_string()))) .await; return; } }; let mut child = match Command::new("git") .args(&args) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn() { Ok(c) => c, Err(e) => { let _ = tx .send(Err(tonic::Status::internal(format!( "failed to spawn git pack-objects: {e}" )))) .await; return; } }; let mut stdin = child.stdin.take(); let mut stdout = child.stdout.take(); let mut stderr = child.stderr.take(); let stdin_task = async move { if let Some(mut stdin) = stdin.take() { let _ = stdin.write_all(&stdin_data).await; } }; let stdout_task = { let tx = tx.clone(); async move { if let Some(mut stdout) = stdout.take() { let mut buf = vec![0u8; 65536]; loop { match stdout.read(&mut buf).await { Ok(0) => break, Ok(n) => { if tx .send(Ok(PackfileChunk { data: buf[..n].to_vec(), })) .await .is_err() { break; } } Err(e) => { let _ = tx .send(Err(tonic::Status::internal(format!( "read error: {e}" )))) .await; break; } } } } } }; let stderr_task = { let tx = tx.clone(); async move { if let Some(stderr) = stderr.take() { let mut stderr = stderr.take(MAX_PACK_OBJECTS_STDERR_BYTES); let mut s = String::new(); if stderr.read_to_string(&mut s).await.is_ok() && !s.is_empty() { let _ = tx.send(Err(tonic::Status::internal(s))).await; } } } }; tokio::join!(stdin_task, stdout_task, stderr_task); match child.wait().await { Ok(status) if !status.success() => { let _ = tx .send(Err(tonic::Status::internal( "git pack-objects exited with error", ))) .await; } Err(e) => { let _ = tx .send(Err(tonic::Status::internal(format!("wait error: {e}")))) .await; } _ => {} } }); Ok(ReceiverStream::new(rx)) } } fn generate_pack_input( req: &crate::pb::PackObjectsRequest, ) -> Result, crate::error::GitError> { let mut input = String::new(); if let Some(opts) = req.options.as_ref() { for want in &opts.wants { crate::sanitize::validate_oid_hex(&want.hex)?; input.push_str(&format!("{}\n", want.hex)); } for have in &opts.haves { crate::sanitize::validate_oid_hex(&have.hex)?; input.push_str(&format!("^{}\n", have.hex)); } } Ok(input.into_bytes()) }