Files
sausage/userland/git/src/cherry_pick.rs
T

181 lines
6.1 KiB
Rust

//! 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<String>)> {
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::<std::collections::BTreeSet<_>>()
.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<String>,
resume: bool,
abort: bool,
quiet: bool,
) -> Result<i32> {
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)
}