181 lines
6.1 KiB
Rust
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)
|
|
}
|