pub struct Options { pub patterns: Vec, pub verbatim: bool, pub max_count: usize, } pub(super) mod function { use std::{borrow::Cow, path::Path}; use anyhow::{Context, bail}; use gix::{object::tree::EntryKind, objs::FindExt}; use super::Options; pub fn git_to_sh( output_dir: &Path, repo_dir: &Path, name: &str, committish: &str, mut out: impl std::io::Write, Options { patterns, verbatim, max_count, }: Options, ) -> anyhow::Result<()> { let repo = gix::open(repo_dir)?; let commit = repo.rev_parse_single(committish)?.object()?.try_into_commit()?; let assets = output_dir.join(name); std::fs::create_dir_all(&assets)?; let mut commits = Vec::new(); let mut tree_buf = Vec::new(); let mut current = 0; for entry in commit.id().ancestors().first_parent_only().all()? { let entry = entry?; let commit = entry.id().object()?.into_commit(); commits.push((commit.id, commit.message_raw_sloppy().to_owned())); let index = repo.index_from_tree(&commit.tree_id()?)?; tree_buf.clear(); write_tree_as_update_index_format(&repo, &index, &mut tree_buf, &assets, verbatim, patterns.clone())?; let tree_file = assets.join(format!("{}.tree", commit.id)); std::fs::write(tree_file, &tree_buf)?; current += 1; if current >= max_count { break; } } writeln!( &mut out, "# The following is to be executed in the receiving git repository" )?; writeln!(&mut out, "ROOT=to-be-specified-by-user")?; writeln!(&mut out, "index=.git/index")?; writeln!(&mut out, "git hash-object -w -t blob -- $ROOT/{name}/*.blob")?; for (commit_id, commit_msg) in commits.iter().rev() { writeln!(&mut out, "rm \"$index\"")?; writeln!( &mut out, "git update-index --index-info < \"$ROOT/{name}/{commit_id}.tree\"" )?; let commit_msg_file = assets.join(format!("{commit_id}.msg")); std::fs::write(commit_msg_file, commit_msg)?; writeln!(&mut out, "git commit --allow-empty -F \"$ROOT/{name}/{commit_id}.msg\"")?; } Ok(()) } fn write_tree_as_update_index_format( repo: &gix::Repository, index: &gix::index::State, out: &mut dyn std::io::Write, output_dir: &Path, verbatim: bool, patterns: Vec, ) -> anyhow::Result<()> { let mut blob_buf = Vec::new(); let mut specs = repo.pathspec( true, // TODO: ideally this could accept patterns already. patterns.clone().into_iter().map(|p| p.to_bstring()), true, index, gix::worktree::stack::state::attributes::Source::IdMapping, )?; for (rela_path, entry) in specs.index_entries_with_paths(index).into_iter().flatten() { if rela_path.contains(&b'\n') { bail!( "Entry at '{rela_path}' contained a newline, which currently can't be encoded. Preferred newlines over NULL separation." ) } let (blob_id, blob_data) = match entry.mode.to_tree_entry_mode() { None => { bail!("Couldn't interpret mode of tree entry at '{rela_path}'") } Some(mode) => match mode.kind() { EntryKind::Tree => { unreachable!("Can't have trees in indices") } EntryKind::Blob | EntryKind::BlobExecutable => { let obj = repo.objects.find(&entry.id, &mut blob_buf)?; if verbatim { (entry.id, Cow::Borrowed(&blob_buf)) } else { let data = std::str::from_utf8(obj.data).with_context(|| { format!("Entry at '{rela_path}' was not valid UTF8 and can't be remapped") })?; let mapped = crate::commands::copy_royal::remapped(data); ( gix::objs::compute_hash( repo.object_hash(), gix::object::Kind::Blob, mapped.as_bytes(), )?, Cow::Owned(mapped.into()), ) } } EntryKind::Link => { repo.objects.find(&entry.id, &mut blob_buf)?; (entry.id, Cow::Borrowed(&blob_buf)) } EntryKind::Commit => continue, }, }; let blob_path = output_dir.join(format!("{blob_id}.blob")); std::fs::write(blob_path, blob_data.as_ref())?; writeln!(out, "{mode:06o} {blob_id}\t{rela_path}", mode = entry.mode)?; } Ok(()) } }