use std::fs;
use crate::change_id::{change_id, ChangeId, ChangeIdPolicy};
use crate::config::ConfigSet;
use crate::error::{Error, Result};
use crate::merge_file::MergeFavor;
use crate::merge_trees::{
merge_trees_three_way, TreeMergeConflictPresentation, WhitespaceMergeOptions,
};
use crate::objects::{parse_commit, CommitData, ObjectId, ObjectKind};
use crate::porcelain::checkout::checkout_between_trees;
use crate::porcelain::commit::{stamp_change_id_on_new_commit, write_commit_object};
use crate::porcelain::revert::merge_commit_message_for_revert;
use crate::porcelain::tree_merge_checkout::apply_clean_tree_merge_to_worktree;
use crate::porcelain::worktree_guard::ensure_worktree_clean_for_pick;
use crate::refs::{
resolve_ref, should_autocreate_reflog_with_config, update_branch_for_commit_with_config,
BranchCommitRefLock, BranchCommitRefUpdate,
};
use crate::repo::Repository;
use crate::state::{resolve_head, HeadState};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReplayDirection {
Pick,
Revert,
}
#[derive(Debug, Clone)]
pub struct ReplayRequest {
pub commit: ObjectId,
pub direction: ReplayDirection,
pub committer: String,
pub message_override: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ReplayOutcome {
Committed {
oid: ObjectId,
tree: ObjectId,
change_id: Option<ChangeId>,
},
Conflicts {
paths: Vec<String>,
},
Empty,
AlreadyApplied,
}
pub fn replay_commit(repo: &Repository, req: &ReplayRequest) -> Result<ReplayOutcome> {
ensure_worktree_clean_for_pick(repo)?;
let (refname, head_oid) = match resolve_head(repo.git_dir())? {
HeadState::Branch {
refname,
oid: Some(oid),
..
} => (refname, oid),
HeadState::Branch { .. } => return Err(Error::UnbornHead),
HeadState::Detached { .. } => return Err(Error::DetachedHead),
HeadState::Invalid => {
return Err(Error::Message("HEAD is in an unknown state".into()));
}
};
if req.direction == ReplayDirection::Pick && req.commit == head_oid {
return Err(Error::ReplaySourceAtHead);
}
let source_obj = repo.odb().read(&req.commit)?;
let source = parse_commit(&source_obj.data)?;
if source.parents.len() > 1 {
return Err(Error::MergeCommit { oid: req.commit });
}
let head_tree = commit_tree(repo, &head_oid)?;
let (base_tree, ours_tree, theirs_tree) =
merge_trees_for_direction(repo, req.direction, &source, head_tree)?;
if req.direction == ReplayDirection::Pick && base_tree == source.tree {
return Ok(ReplayOutcome::Empty);
}
let presentation = TreeMergeConflictPresentation {
df_directory_ours_commit: Some(head_oid),
df_directory_theirs_commit: Some(req.commit),
..TreeMergeConflictPresentation::default()
};
let merged = merge_trees_three_way(
repo,
base_tree,
ours_tree,
theirs_tree,
MergeFavor::default(),
WhitespaceMergeOptions::default(),
None,
presentation,
)?;
if crate::operation::tree_merge_has_conflicts(&merged) {
let conflicts = crate::operation::conflicts_from_tree_merge(&merged);
let paths: Vec<String> = conflicts.iter().map(|c| c.path.clone()).collect();
return Ok(ReplayOutcome::Conflicts { paths });
}
let new_tree = merged.result_tree;
if new_tree == head_tree {
return Ok(ReplayOutcome::AlreadyApplied);
}
let config = repo.config()?;
let message = commit_message(req, &source)?;
let author = match req.direction {
ReplayDirection::Pick => source.author.clone(),
ReplayDirection::Revert => req.committer.clone(),
};
let mut commit_data = CommitData {
tree: new_tree,
parents: vec![head_oid],
author,
committer: req.committer.clone(),
author_raw: Vec::new(),
committer_raw: Vec::new(),
encoding: None,
message,
raw_message: None,
extra_headers: Vec::new(),
};
let policy = ChangeIdPolicy::from_config(config.as_ref());
stamp_change_id_on_new_commit(repo, &policy, &mut commit_data)?;
let new_oid = write_commit_object(repo, &commit_data, None)?;
let subject = commit_data.message.lines().next().unwrap_or("").trim_end();
let reflog_msg = match req.direction {
ReplayDirection::Pick => format!("cherry-pick: {subject}"),
ReplayDirection::Revert => format!("revert: {subject}"),
};
let ref_update = BranchCommitRefUpdate {
branch_ref: &refname,
expected_old: Some(head_oid),
new_oid,
identity: &req.committer,
reflog_message: &reflog_msg,
};
let index_snapshot = fs::read(repo.index_path()).ok();
if crate::refs::store::open_ref_store(repo.git_dir())?.format()
!= crate::refs::store::RefStorageFormat::Files
{
let current = resolve_ref(repo.git_dir(), &refname).ok();
crate::refs::verify_branch_commit_cas(Some(head_oid), current, &refname)?;
apply_clean_tree_merge_to_worktree(repo, &head_tree, &merged)?;
if let Err(err) =
update_branch_for_commit_with_config(repo.git_dir(), &ref_update, config.as_ref())
{
undo_prepared_branch_commit_reflogs(repo.git_dir(), &refname, config.as_ref());
restore_worktree_after_failed_ref_commit(
repo,
&head_tree,
&new_tree,
index_snapshot.as_deref(),
)?;
return Err(err);
}
} else {
let mut branch_ref_lock =
BranchCommitRefLock::acquire(repo.git_dir(), &refname, Some(head_oid))?;
branch_ref_lock.write_reflogs(repo.git_dir(), &ref_update, &head_oid, config.as_ref())?;
apply_clean_tree_merge_to_worktree(repo, &head_tree, &merged)?;
if let Err(err) = branch_ref_lock.finish_ref_commit(repo.git_dir(), &ref_update) {
undo_prepared_branch_commit_reflogs(repo.git_dir(), &refname, config.as_ref());
restore_worktree_after_failed_ref_commit(
repo,
&head_tree,
&new_tree,
index_snapshot.as_deref(),
)?;
return Err(err);
}
}
Ok(ReplayOutcome::Committed {
oid: new_oid,
tree: new_tree,
change_id: change_id(&commit_data),
})
}
fn undo_prepared_branch_commit_reflogs(
git_dir: &std::path::Path,
branch_ref: &str,
config: &ConfigSet,
) {
if should_autocreate_reflog_with_config(config, git_dir, branch_ref) {
let _ = crate::reflog::truncate_last_reflog_line(git_dir, branch_ref);
}
if should_autocreate_reflog_with_config(config, git_dir, "HEAD") {
let _ = crate::reflog::truncate_last_reflog_line(git_dir, "HEAD");
}
}
fn restore_worktree_after_failed_ref_commit(
repo: &Repository,
head_tree: &ObjectId,
checked_out_tree: &ObjectId,
index_snapshot: Option<&[u8]>,
) -> Result<()> {
checkout_between_trees(repo, Some(checked_out_tree), head_tree)?;
if let Some(bytes) = index_snapshot {
fs::write(repo.index_path(), bytes)?;
}
Ok(())
}
fn merge_trees_for_direction(
repo: &Repository,
direction: ReplayDirection,
source: &CommitData,
head_tree: ObjectId,
) -> Result<(ObjectId, ObjectId, ObjectId)> {
let parent_tree = if let Some(parent) = source.parents.first() {
commit_tree(repo, parent)?
} else {
empty_tree(repo)?
};
let source_tree = source.tree;
Ok(match direction {
ReplayDirection::Pick => (parent_tree, head_tree, source_tree),
ReplayDirection::Revert => (source_tree, head_tree, parent_tree),
})
}
fn commit_message(req: &ReplayRequest, source: &CommitData) -> Result<String> {
if let Some(msg) = &req.message_override {
let mut message = msg.trim().to_owned();
if !message.is_empty() && !message.ends_with('\n') {
message.push('\n');
}
return Ok(message);
}
match req.direction {
ReplayDirection::Pick => Ok(source.message.clone()),
ReplayDirection::Revert => {
let (title, body) =
merge_commit_message_for_revert(source, req.commit, false, '#', None);
Ok(format!("{title}\n{body}"))
}
}
}
fn commit_tree(repo: &Repository, commit_oid: &ObjectId) -> Result<ObjectId> {
let obj = repo.odb().read(commit_oid)?;
if obj.kind != ObjectKind::Commit {
return Err(Error::CorruptObject(format!(
"expected commit, got {}",
obj.kind.as_str()
)));
}
Ok(parse_commit(&obj.data)?.tree)
}
fn empty_tree(repo: &Repository) -> Result<ObjectId> {
repo.odb().write(ObjectKind::Tree, &[])
}