//! Export a committed tree or literal subtree as tar, without an index or worktree. use crate::{Result, repo::error}; use std::{io::Write, path::Path}; pub(crate) fn archive( repo: &gix::Repository, revision: &str, path: Option<&str>, output: Option<&Path>, ) -> Result<()> { if let Some(path) = path { crate::repo::validate_path(path)?; } let root = crate::local::tree(repo, revision)?; let writer: Box = match output { Some(path) => { Box::new(std::fs::File::create(path).map_err(|e| error("archive output", e))?) } None => Box::new(std::io::stdout().lock()), }; let mut tar = tar::Builder::new(writer); fn walk( repo: &gix::Repository, id: gix::ObjectId, prefix: &str, filter: Option<&str>, tar: &mut tar::Builder>, ) -> Result { let tree = repo.find_tree(id).map_err(|e| error("archive tree", e))?; let mut found = filter.is_none(); for entry in tree.iter() { let entry = entry.map_err(|e| error("archive entry", e))?; let path = format!("{prefix}{}", entry.filename()); let selected = filter.is_none_or(|f| path == f || path.starts_with(&format!("{f}/"))); let ancestor = filter.is_some_and(|f| f.starts_with(&format!("{path}/"))); if !selected && !ancestor { continue; } found |= selected; if entry.mode().is_tree() { if selected { let mut header = tar::Header::new_gnu(); header.set_entry_type(tar::EntryType::Directory); header.set_mode(0o755); header.set_size(0); tar.append_data(&mut header, &path, std::io::empty()) .map_err(|e| error("archive directory", e))?; } found |= walk( repo, entry.oid().to_owned(), &format!("{path}/"), filter, tar, )?; } else if selected && !entry.mode().is_commit() { let object = repo .find_blob(entry.oid()) .map_err(|e| error("archive blob", e))?; let mut header = tar::Header::new_gnu(); header.set_mode(entry.mode().kind() as u32 & 0o777); if entry.mode().is_link() { header.set_entry_type(tar::EntryType::Symlink); header.set_size(0); header .set_link_name( std::str::from_utf8(&object.data) .map_err(|e| error("archive link", e))?, ) .map_err(|e| error("archive link", e))?; tar.append_data(&mut header, &path, std::io::empty()) .map_err(|e| error("archive link", e))?; } else { header.set_size(object.data.len() as u64); tar.append_data(&mut header, &path, object.data.as_slice()) .map_err(|e| error("archive file", e))?; } } } Ok(found) } if !walk(repo, root, "", path, &mut tar)? { return Err(error("archive", "path did not match any files")); } tar.finish().map_err(|e| error("archive finish", e)) }