//! Single-commit cherry-pick uses Git's CHERRY_PICK_HEAD and MERGE_MSG for continuation. use crate::{Result, branches, checkout, local, output, repo::error, staging}; use gix::bstr::ByteSlice; pub(crate) fn merge_trees( repo: &gix::Repository, base: gix::ObjectId, ours: gix::ObjectId, theirs: gix::ObjectId, current: &str, other: &str, ) -> Result<(gix::ObjectId, gix::index::State, Vec)> { let local = crate::merge::merge_repository(repo)?; let labels = gix::merge::blob::builtin_driver::text::Labels { ancestor: None, current: Some(current.into()), other: Some(other.into()), }; let mut outcome = local .merge_trees( base, ours, theirs, labels, local .tree_merge_options() .map_err(|e| error("merge options", e))?, ) .map_err(|e| error("merge trees", e))?; let tree = outcome .tree .write() .map_err(|e| error("merge tree", e))? .detach(); let mut index = gix::index::State::from_tree(&tree, &repo.objects, Default::default()) .map_err(|e| error("merge index", e))?; outcome.index_changed_after_applying_conflicts( &mut index, Default::default(), gix::merge::tree::apply_index_entries::RemovalMode::Prune, ); index.sort_entries(); let conflicts = index .entries() .iter() .filter(|e| e.stage_raw() != 0) .map(|e| e.path(&index).to_str_lossy().into_owned()) .collect::>() .into_iter() .collect(); Ok((tree, index, conflicts)) } fn clear(repo: &gix::Repository) -> Result<()> { for name in ["CHERRY_PICK_HEAD", "MERGE_MSG"] { let path = repo.git_dir().join(name); if path.exists() { std::fs::remove_file(path).map_err(|e| error("cherry-pick cleanup", e))?; } } Ok(()) } pub(crate) fn run( repo: &gix::Repository, revision: Option, resume: bool, abort: bool, quiet: bool, ) -> Result { if abort { if !repo.git_dir().join("CHERRY_PICK_HEAD").exists() { return Err(error("cherry-pick abort", "no cherry-pick in progress")); } let old = std::fs::read_to_string(repo.git_dir().join("ORIG_HEAD")) .map_err(|e| error("cherry-pick abort", e))?; local::reset(repo, old.trim(), &[], false, true, true)?; clear(repo)?; return Ok(0); } let id = if resume { let id = std::fs::read_to_string(repo.git_dir().join("CHERRY_PICK_HEAD")) .map_err(|e| error("cherry-pick continue", e))?; branches::resolve(repo, id.trim())? } else { crate::merge::ensure_idle(repo)?; crate::merge::clean(repo)?; branches::resolve(repo, &crate::required(revision, "cherry-pick revision")?)? }; let old = branches::resolve(repo, "HEAD")?; let original = repo .find_commit(id) .map_err(|e| error("cherry-pick commit", e))?; let decoded = original .decode() .map_err(|e| error("cherry-pick decode", e))?; let subject = decoded .message .to_str_lossy() .lines() .next() .unwrap_or("") .to_owned(); let tree = if resume { staging::tree_from_index(repo, &staging::load(repo)?)? } else { let base = match decoded.parents.first() { Some(id) => local::tree(repo, &id.to_string())?, None => repo .write_object(&gix::objs::Tree { entries: vec![] }) .map_err(|e| error("empty tree", e))? .detach(), }; let ours = local::tree(repo, "HEAD")?; let (tree, index, conflicts) = merge_trees( repo, base, ours, decoded.tree(), "HEAD", &format!("{} ({subject})", &id.to_string()[..7]), )?; checkout::write_tree_to_worktree(repo, Some(ours), tree, false)?; staging::publish(repo, index)?; if !conflicts.is_empty() { std::fs::write(repo.git_dir().join("CHERRY_PICK_HEAD"), format!("{id}\n")) .map_err(|e| error("cherry-pick state", e))?; std::fs::write(repo.git_dir().join("ORIG_HEAD"), format!("{old}\n")) .map_err(|e| error("cherry-pick state", e))?; std::fs::write(repo.git_dir().join("MERGE_MSG"), decoded.message.as_bytes()) .map_err(|e| error("cherry-pick state", e))?; output::conflicts(&conflicts, false); return Ok(1); } tree }; let commit = gix::objs::Commit { tree, parents: vec![old].into_iter().collect(), author: decoded .author() .map_err(|e| error("pick author", e))? .to_owned() .map_err(|e| error("pick author", e))?, committer: staging::signature(None, None)?, encoding: decoded.encoding.map(ToOwned::to_owned), message: decoded.message.to_owned(), extra_headers: vec![], }; let new = repo .write_object(&commit) .map_err(|e| error("cherry-pick write", e))? .detach(); staging::update_head( repo, Some(old), new, &staging::signature(None, None)?, &format!("cherry-pick: {subject}"), )?; clear(repo)?; if !quiet { let head = crate::head_info(repo)?; println!( "[{} {}] {subject}", head.branch .unwrap_or_else(|| "detached HEAD".into()) .trim_start_matches("refs/heads/"), &new.to_string()[..7] ); output::author_date(&repo.find_commit(new).map_err(|e| error("pick output", e))?)?; output::commit_stat(&crate::changes::diff(crate::changes::DiffParams { repo: repo.workdir().unwrap().to_string_lossy().into_owned(), from: Some(old.to_string()), to: Some(new.to_string()), staged: None, path: None, context: None, })?); } Ok(0) }