//! Bundle applicator for restoring snapshots to git repositories. //! //! Uses `git bundle unbundle` to apply pack data to a bare repository. use std::path::{Path, PathBuf}; pub struct BundleApplicator { pub repo_path: PathBuf, } impl BundleApplicator { pub fn new(repo_path: PathBuf) -> Self { Self { repo_path } } pub fn apply_bundle(&self, data: &[u8]) -> Result<(), String> { let mut child = std::process::Command::new("git") .args([ "--git-dir", &self.repo_path.to_string_lossy(), "bundle", "unbundle", "-", ]) .stdin(std::process::Stdio::piped()) .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::piped()) .spawn() .map_err(|e| format!("spawn git bundle unbundle: {e}"))?; use std::io::Write; if let Some(ref mut stdin) = child.stdin { stdin .write_all(data) .map_err(|e| format!("write bundle: {e}"))?; } let output = child .wait_with_output() .map_err(|e| format!("wait bundle: {e}"))?; if !output.status.success() { return Err(String::from_utf8_lossy(&output.stderr).into_owned()); } Ok(()) } /// Apply bundle from a file path (for streaming writes). pub fn apply_bundle_from_file(&self, path: &Path) -> Result<(), String> { let file = std::fs::File::open(path).map_err(|e| format!("open bundle file: {e}"))?; let mut child = std::process::Command::new("git") .args([ "--git-dir", &self.repo_path.to_string_lossy(), "bundle", "unbundle", "-", ]) .stdin(std::process::Stdio::piped()) .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::piped()) .spawn() .map_err(|e| format!("spawn git bundle unbundle: {e}"))?; // Stream file contents to stdin in a background thread let mut stdin = child.stdin.take().ok_or("no stdin")?; let file_handle = file; let writer = std::thread::spawn(move || -> Result<(), String> { use std::io::{Read, Write}; let mut reader = std::io::BufReader::new(file_handle); let mut buf = vec![0u8; 65536]; loop { match reader.read(&mut buf) { Ok(0) => break, Ok(n) => { stdin .write_all(&buf[..n]) .map_err(|e| format!("write to stdin: {e}"))?; } Err(e) => return Err(format!("read bundle file: {e}")), } } Ok(()) }); let output = child .wait_with_output() .map_err(|e| format!("wait bundle: {e}"))?; // Wait for writer thread let _ = writer.join().map_err(|_| "writer thread panicked")?; if !output.status.success() { return Err(String::from_utf8_lossy(&output.stderr).into_owned()); } Ok(()) } }