use crate::refs::resolve_ref;
use crate::repo::Repository;
use crate::state::{resolve_head, HeadState};
use super::apply::{apply_rewrite, verify_rewrite_plan};
use super::error::HistoryError;
use super::last::{read_history_last, HistoryLastRecord};
use super::types::{ApplyOptions, UndoOutcome};
pub fn undo_last_rewrite(
repo: &Repository,
opts: &ApplyOptions,
dry_run: bool,
) -> Result<UndoOutcome, HistoryError> {
let record = read_history_last(repo)?.ok_or(HistoryError::NoUndoRecord)?;
verify_record_still_valid(repo, &record)?;
let plan = record.undo_plan();
verify_rewrite_plan(repo, &plan)?;
let head_change = plan.head_change();
if dry_run {
let head_after = head_change
.map(|(_, new)| new)
.or(record.head_old_commit)
.ok_or_else(|| HistoryError::UndoRecordRead {
message: "record missing head commit for dry-run".into(),
})?;
return Ok(UndoOutcome {
ref_updates: plan.ref_updates.clone(),
head: head_after,
head_change,
dry_run: true,
});
}
let mut apply_opts = opts.clone();
apply_opts.record_operation = Some("undo".into());
apply_opts.reflog_message = "history undo".into();
let applied = apply_rewrite(repo, &plan, &apply_opts)?;
Ok(UndoOutcome {
ref_updates: applied.ref_updates,
head: applied.head,
head_change,
dry_run: false,
})
}
fn verify_record_still_valid(
repo: &Repository,
record: &HistoryLastRecord,
) -> Result<(), HistoryError> {
for update in &record.ref_updates {
let live = match resolve_ref(repo.git_dir(), &update.name) {
Ok(oid) => oid,
Err(_) => {
return Err(HistoryError::UndoStale {
name: update.name.clone(),
})
}
};
if live != update.new {
return Err(HistoryError::UndoStale {
name: update.name.clone(),
});
}
}
if let Some(expected) = record.head_new_commit {
let head = match resolve_head(repo.git_dir()) {
Ok(state) => state,
Err(_) => {
return Err(HistoryError::UndoStale {
name: record.head_ref.clone().unwrap_or_else(|| "HEAD".into()),
})
}
};
let live_commit = match head {
HeadState::Branch { oid, .. } => oid,
HeadState::Detached { oid } => Some(oid),
HeadState::Invalid => None,
};
if live_commit != Some(expected) {
let label = record.head_ref.clone().unwrap_or_else(|| "HEAD".into());
return Err(HistoryError::UndoStale { name: label });
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::history::types::{HeadEffect, PlannedRefUpdate};
use crate::objects::ObjectId;
#[test]
fn undo_plan_swaps_refs_and_head_trees() {
let record = HistoryLastRecord {
operation: "drop".into(),
head_ref: Some("refs/heads/main".into()),
head_old_commit: Some(
ObjectId::from_hex("0000000000000000000000000000000000000001").unwrap(),
),
head_new_commit: Some(
ObjectId::from_hex("0000000000000000000000000000000000000002").unwrap(),
),
head_old_tree: Some(
ObjectId::from_hex("0000000000000000000000000000000000000003").unwrap(),
),
head_new_tree: Some(
ObjectId::from_hex("0000000000000000000000000000000000000004").unwrap(),
),
ref_updates: vec![PlannedRefUpdate {
name: "refs/heads/main".into(),
old: ObjectId::from_hex("0000000000000000000000000000000000000001").unwrap(),
new: ObjectId::from_hex("0000000000000000000000000000000000000002").unwrap(),
}],
};
let plan = record.undo_plan();
assert_eq!(plan.ref_updates[0].old, record.ref_updates[0].new);
assert_eq!(plan.ref_updates[0].new, record.ref_updates[0].old);
assert_eq!(
plan.head,
HeadEffect {
commit_changes: true,
old_commit: record.head_new_commit,
head_ref: record.head_ref.clone(),
old_tree: record.head_new_tree,
new_tree: record.head_old_tree,
}
);
}
}