use std::path::Path;
use serde::Serialize;
use void_core::cid;
use void_core::crypto::{self, MAGIC_V1};
use void_core::metadata::parse_commit;
use void_core::store::{FsStore, ObjectStoreExt};
use crate::context::{load_signing_key, open_repo, signing_key_exists};
use crate::output::{run_command, CliError, CliOptions};
#[derive(Debug)]
pub struct ReparentArgs {
pub commit_cid: String,
pub new_parent_cids: Vec<String>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ReparentOutput {
pub old_cid: String,
pub new_cid: String,
pub parents: Vec<String>,
pub signed: bool,
}
pub fn run(cwd: &Path, args: ReparentArgs, opts: &CliOptions) -> Result<(), CliError> {
run_command("reparent", opts, |ctx| {
let repo = open_repo(cwd)?;
let vault = repo.vault();
let objects_dir = repo.void_dir().join("objects");
let store = FsStore::new(objects_dir)
.map_err(|e| CliError::internal(format!("failed to open object store: {}", e)))?;
ctx.progress(format!("Loading commit {}...", args.commit_cid));
let parsed_cid = cid::parse(&args.commit_cid)
.map_err(|e| CliError::invalid_args(format!("invalid commit CID: {}", e)))?;
let encrypted_blob: crypto::EncryptedCommit = store
.get_blob(&parsed_cid)
.map_err(|e| CliError::not_found(format!("failed to load commit: {}", e)))?;
let encrypted = encrypted_blob.as_bytes();
let original_nonce: Option<crypto::KeyNonce> =
if encrypted.len() > 20 && encrypted.starts_with(MAGIC_V1) {
crypto::KeyNonce::from_bytes(&encrypted[4..20])
} else {
None
};
let (decrypted, _nonce) = vault.unseal_commit_with_nonce(&encrypted)
.map_err(|e| CliError::internal(format!("failed to decrypt commit: {}", e)))?;
let mut commit = parse_commit(&decrypted)
.map_err(|e| CliError::internal(format!("failed to parse commit: {}", e)))?;
let new_parent_bytes: Vec<Vec<u8>> = args
.new_parent_cids
.iter()
.map(|cid_str| {
let parent = cid::parse(cid_str).map_err(|e| {
CliError::invalid_args(format!("invalid parent CID '{}': {}", cid_str, e))
})?;
Ok(cid::to_bytes(&parent))
})
.collect::<Result<Vec<_>, CliError>>()?;
ctx.progress("Reparenting commit...");
commit.parents = new_parent_bytes.into_iter().map(void_core::crypto::CommitCid::from_bytes).collect();
commit.author = None;
commit.signature = None;
let signed = if signing_key_exists() {
let signing_key = load_signing_key()?;
commit.sign(&signing_key);
true
} else {
false
};
let mut new_bytes = Vec::new();
ciborium::into_writer(&commit, &mut new_bytes)
.map_err(|e| CliError::internal(format!("failed to serialize commit: {}", e)))?;
let nonce = original_nonce.unwrap_or_else(crypto::generate_key_nonce);
let new_encrypted = vault.seal_commit_with_nonce(&new_bytes, &nonce)
.map_err(|e| CliError::internal(format!("failed to encrypt commit: {}", e)))?;
let new_cid = store
.put_blob(&new_encrypted)
.map_err(|e| CliError::internal(format!("failed to store commit: {}", e)))?;
let new_cid_str = new_cid.to_string();
if !ctx.use_json() {
ctx.info(format!("Old CID: {}", args.commit_cid));
ctx.info(format!("New CID: {}", new_cid_str));
ctx.info(format!(
"Parents: {}",
if args.new_parent_cids.is_empty() {
"(none - initial commit)".to_string()
} else {
args.new_parent_cids.join(", ")
}
));
ctx.info(format!("Signed: {}", signed));
}
Ok(ReparentOutput {
old_cid: args.commit_cid,
new_cid: new_cid_str,
parents: args.new_parent_cids,
signed,
})
})
}