Something went wrong. Try again.
⚘ use your pds as a git remote if you want to ⚘
Something went wrong. Try again.
7.0 kB · 206 lines
Rust
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207//! Git bundle operations.//!//! Wraps `git bundle` CLI commands to create and apply bundles.//! Bundles are the serialization format used to store repository//! snapshots as PDS blobs.
use std::path::Path;use tokio::process::Command;
/// Result of creating a bundle, including the raw bytes and metadata/// needed to build a `BundleEntry`.#[derive(Debug)]pub struct CreatedBundle { /// raw bundle file bytes pub data: Vec<u8>, /// commit SHAs that the receiver must already have pub prerequisites: Vec<String>, /// commit SHAs this bundle provides up to (the tip commits) pub tips: Vec<String>,}
/// Creates a full bundle containing the entire repository history.////// Equivalent to `git bundle create <file> --all`. The bundle includes/// all branches and tags, and has no prerequisites.pub async fn create_full_bundle(repo_path: &Path) -> Result<CreatedBundle, String> { let tmp = tempfile::NamedTempFile::new().map_err(|e| format!("failed to create temp file: {}", e))?; let bundle_path = tmp.path();
// create a full bundle with all refs let output = Command::new("git") .args(["bundle", "create", bundle_path.to_str().unwrap(), "--all"]) .current_dir(repo_path) .output() .await .map_err(|e| format!("failed to run git bundle create: {}", e))?;
if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(format!("git bundle create failed: {}", stderr.trim())); }
let data = tokio::fs::read(bundle_path) .await .map_err(|e| format!("failed to read bundle file: {}", e))?;
// get the tip commits (all branch heads) let tips = get_branch_tips(repo_path).await?;
Ok(CreatedBundle { data, prerequisites: vec![], tips, })}
/// Creates an incremental bundle containing only commits since the given SHAs.////// `refs` is the list of refs to include (e.g. `["refs/heads/main"]`)./// `since_commits` are the prerequisite commits the receiver must already/// have — the bundle contains everything reachable from `refs` that is/// not reachable from `since_commits`.pub async fn create_incremental_bundle( repo_path: &Path, refs: &[&str], since_commits: &[&str],) -> Result<CreatedBundle, String> { let tmp = tempfile::NamedTempFile::new().map_err(|e| format!("failed to create temp file: {}", e))?; let bundle_path = tmp.path();
// build args: git bundle create <file> <refs...> ^<since...> let mut args = vec![ "bundle".to_string(), "create".to_string(), bundle_path.to_str().unwrap().to_string(), ];
for r in refs { args.push(r.to_string()); } for sha in since_commits { args.push(format!("^{}", sha)); }
let output = Command::new("git") .args(&args) .current_dir(repo_path) .output() .await .map_err(|e| format!("failed to run git bundle create: {}", e))?;
if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(format!( "git bundle create (incremental) failed: {}", stderr.trim() )); }
let data = tokio::fs::read(bundle_path) .await .map_err(|e| format!("failed to read bundle file: {}", e))?;
// resolve ref names to their current SHAs for the tips let tips = resolve_refs_to_shas(repo_path, refs).await?;
Ok(CreatedBundle { data, prerequisites: since_commits.iter().map(|s| s.to_string()).collect(), tips, })}
/// Applies a bundle to a repository.////// Writes the bundle bytes to a temp file and runs `git bundle unbundle`./// The caller should set up refs afterward if needed.pub async fn apply_bundle(repo_path: &Path, bundle_bytes: &[u8]) -> Result<(), String> { let tmp = tempfile::NamedTempFile::new().map_err(|e| format!("failed to create temp file: {}", e))?; let bundle_path = tmp.path();
tokio::fs::write(bundle_path, bundle_bytes) .await .map_err(|e| format!("failed to write bundle to temp file: {}", e))?;
let output = Command::new("git") .args(["bundle", "unbundle", bundle_path.to_str().unwrap()]) .current_dir(repo_path) .output() .await .map_err(|e| format!("failed to run git bundle unbundle: {}", e))?;
if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(format!("git bundle unbundle failed: {}", stderr.trim())); }
Ok(())}
/// Verifies that a bundle's prerequisites are satisfied by the local repo.////// Returns Ok(true) if the bundle can be applied, Ok(false) if/// prerequisites are missing.pub async fn verify_bundle(repo_path: &Path, bundle_bytes: &[u8]) -> Result<bool, String> { let tmp = tempfile::NamedTempFile::new().map_err(|e| format!("failed to create temp file: {}", e))?; let bundle_path = tmp.path();
tokio::fs::write(bundle_path, bundle_bytes) .await .map_err(|e| format!("failed to write bundle to temp file: {}", e))?;
let output = Command::new("git") .args(["bundle", "verify", bundle_path.to_str().unwrap()]) .current_dir(repo_path) .output() .await .map_err(|e| format!("failed to run git bundle verify: {}", e))?;
Ok(output.status.success())}
/// Returns the current tip commit SHAs for all local branches.async fn get_branch_tips(repo_path: &Path) -> Result<Vec<String>, String> { let output = Command::new("git") .args(["for-each-ref", "--format=%(objectname)", "refs/heads/"]) .current_dir(repo_path) .output() .await .map_err(|e| format!("failed to run git for-each-ref: {}", e))?;
if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(format!("git for-each-ref failed: {}", stderr.trim())); }
let stdout = String::from_utf8_lossy(&output.stdout); let tips: Vec<String> = stdout.lines().map(|l| l.trim().to_string()).collect(); Ok(tips)}
/// Resolves ref names to their current commit SHAs.async fn resolve_refs_to_shas(repo_path: &Path, refs: &[&str]) -> Result<Vec<String>, String> { let mut shas = Vec::with_capacity(refs.len()); for r in refs { let output = Command::new("git") .args(["rev-parse", r]) .current_dir(repo_path) .output() .await .map_err(|e| format!("failed to run git rev-parse {}: {}", r, e))?;
if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(format!("git rev-parse {} failed: {}", r, stderr.trim())); }
let sha = String::from_utf8_lossy(&output.stdout).trim().to_string(); shas.push(sha); } Ok(shas)}