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 has_merge_conflicts(&merged) {
return Ok(ReplayOutcome::Conflicts {
paths: conflict_paths(&merged),
});
}
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, '#');
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, &[])
}
fn has_merge_conflicts(merged: &crate::merge_trees::TreeMergeOutput) -> bool {
!merged.conflict_content.is_empty() || merged.index.entries().iter().any(|e| e.stage() != 0)
}
fn conflict_paths(merged: &crate::merge_trees::TreeMergeOutput) -> Vec<String> {
let mut paths: Vec<String> = merged
.conflict_content
.keys()
.map(|k| String::from_utf8_lossy(k).into_owned())
.collect();
paths.extend(
merged
.index
.entries()
.iter()
.filter(|e| e.stage() != 0)
.map(|e| String::from_utf8_lossy(&e.path).into_owned()),
);
paths.sort();
paths.dedup();
paths
}
#[cfg(test)]
mod tests {
use super::*;
use crate::index::{Index, IndexEntry};
use crate::merge_trees::TreeMergeOutput;
use std::collections::BTreeMap;
fn index_entry(path: &str, stage: u8) -> IndexEntry {
let mut entry = IndexEntry {
ctime_sec: 0,
ctime_nsec: 0,
mtime_sec: 0,
mtime_nsec: 0,
dev: 0,
ino: 0,
mode: crate::index::MODE_REGULAR,
uid: 0,
gid: 0,
size: 0,
oid: crate::diff::zero_oid(),
flags: 0,
flags_extended: None,
path: path.as_bytes().to_vec(),
base_index_pos: 0,
};
entry.set_stage(stage);
entry
}
#[test]
fn conflict_paths_unions_text_markers_and_unmerged_index_paths() {
let mut index = Index::new();
index.add_or_replace(index_entry("b.txt", 1));
index.add_or_replace(index_entry("b.txt", 2));
let mut conflict_content = BTreeMap::new();
conflict_content.insert(b"a.txt".to_vec(), crate::diff::zero_oid());
let merged = TreeMergeOutput {
index,
conflict_content,
result_tree: crate::diff::zero_oid(),
touched_paths: std::collections::HashSet::new(),
};
assert_eq!(
conflict_paths(&merged),
vec!["a.txt".to_owned(), "b.txt".to_owned()]
);
}
}