use std::path::{Path, PathBuf}; use anyhow::{anyhow, bail}; use gix::{NestedProgress, Progress, worktree::archive}; pub struct Options { pub format: Option, pub files: Vec<(String, String)>, pub prefix: Option, pub add_paths: Vec, } pub fn stream( repo: gix::Repository, destination_path: &Path, rev_spec: Option<&str>, mut progress: impl NestedProgress, Options { format, prefix, add_paths, files, }: Options, ) -> anyhow::Result<()> { let format = format.map_or_else(|| format_from_ext(destination_path), Ok)?; let object = repo.rev_parse_single(rev_spec.unwrap_or("HEAD"))?.object()?; let (modification_date, tree) = fetch_rev_info(object)?; let start = std::time::Instant::now(); let (mut stream, index) = repo.worktree_stream(tree)?; if !add_paths.is_empty() { let root = gix::path::realpath( repo.workdir() .ok_or_else(|| anyhow!("Adding files requires a worktree directory that contains them"))?, )?; for path in add_paths { stream.add_entry_from_path(&root, &gix::path::realpath(&path)?, repo.object_hash())?; } } for (path, content) in files { stream.add_entry(gix::worktree::stream::AdditionalEntry { id: gix::hash::Kind::Sha1.null(), mode: gix::object::tree::EntryKind::Blob.into(), relative_path: path.into(), source: gix::worktree::stream::entry::Source::Memory(content.into()), }); } let mut entries = progress.add_child("entries"); entries.init(Some(index.entries().len()), gix::progress::count("entries")); let mut bytes = progress.add_child("written"); bytes.init(None, gix::progress::bytes()); let mut file = gix::progress::Write { inner: std::io::BufWriter::with_capacity(128 * 1024, std::fs::File::create(destination_path)?), progress: &mut bytes, }; repo.worktree_archive( stream, &mut file, &mut entries, &gix::interrupt::IS_INTERRUPTED, gix::worktree::archive::Options { format, tree_prefix: prefix.map(gix::bstr::BString::from), modification_time: modification_date.unwrap_or_else(|| { std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .map(|d| d.as_secs()) .unwrap_or_default() as gix::date::SecondsSinceUnixEpoch }), }, )?; entries.show_throughput(start); bytes.show_throughput(start); Ok(()) } fn fetch_rev_info( object: gix::Object<'_>, ) -> anyhow::Result<(Option, gix::ObjectId)> { Ok(match object.kind { gix::object::Kind::Commit => { let commit = object.into_commit(); (Some(commit.committer()?.seconds()), commit.tree_id()?.detach()) } gix::object::Kind::Tree => (None, object.id), gix::object::Kind::Tag => fetch_rev_info(object.peel_to_kind(gix::object::Kind::Commit)?)?, gix::object::Kind::Blob => bail!("Cannot derive commit or tree from blob at {}", object.id), }) } fn format_from_ext(path: &Path) -> anyhow::Result { Ok(match path.extension().and_then(std::ffi::OsStr::to_str) { None => bail!("Cannot derive archive format from a file without extension"), Some("tar") => archive::Format::Tar, Some("gz") => archive::Format::TarGz { compression_level: None, }, Some("zip") => archive::Format::Zip { compression_level: None, }, Some("stream") => archive::Format::InternalTransientNonPersistable, Some(ext) => bail!("Format for extension '{ext}' is unsupported"), }) }