use crate::cli::UI;
use crate::ops::oplog;
use anyhow::Result;
use std::path::Path;
pub fn execute(path: &Path, list: bool, op_id: Option<&str>, count: usize, ui: &UI) -> Result<()> {
if list {
return display_oplog(path, count, ui);
}
if let Some(id) = op_id {
return undo_by_id(path, id, ui);
}
undo_last(path, ui)
}
fn undo_last(path: &Path, ui: &UI) -> Result<()> {
let entries = oplog::read_oplog(path)?;
let entry = entries
.first()
.ok_or_else(|| anyhow::anyhow!("No operations to undo. Operation log is empty."))?;
ui.info(format!(
"Undoing: {} — {}",
entry.command, entry.description
));
restore_snapshot(path, &entry.before)?;
let _ = oplog::with_oplog(path, "undo", &format!("undo {}", entry.command), || {
Ok::<(), anyhow::Error>(())
});
ui.success(format!(
"Done. Reverted to state before '{}'",
entry.command
));
Ok(())
}
fn undo_by_id(path: &Path, op_id: &str, ui: &UI) -> Result<()> {
let entries = oplog::read_oplog(path)?;
let entry = entries
.iter()
.find(|e| e.id == op_id || e.id.starts_with(op_id))
.ok_or_else(|| anyhow::anyhow!("No operation found with ID '{}'", op_id))?
.clone();
ui.info(format!(
"Undoing: {} — {}",
entry.command, entry.description
));
restore_snapshot(path, &entry.before)?;
let _ = oplog::with_oplog(
path,
"undo",
&format!(
"undo {} ({})",
entry.command,
&entry.id[..8.min(entry.id.len())]
),
|| Ok::<(), anyhow::Error>(()),
);
ui.success(format!(
"Done. Reverted to state before '{}'",
entry.command
));
Ok(())
}
fn restore_snapshot(path: &Path, snapshot: &oplog::RepoSnapshot) -> Result<()> {
let repo = crate::ops::open_repo(path)?;
let statuses = repo.statuses(None)?;
let dirty = statuses.iter().any(|s| {
s.status().intersects(
git2::Status::WT_MODIFIED
| git2::Status::WT_DELETED
| git2::Status::WT_RENAMED
| git2::Status::WT_TYPECHANGE
| git2::Status::INDEX_NEW
| git2::Status::INDEX_MODIFIED
| git2::Status::INDEX_DELETED
| git2::Status::INDEX_RENAMED
| git2::Status::INDEX_TYPECHANGE,
)
});
if dirty {
anyhow::bail!(
"Working tree has uncommitted changes. Commit or stash them before undoing.\n\
Hint: use 'securegit stash save' to save your changes first."
);
}
if let Some(ref head_ref) = snapshot.head_ref {
if let Some(ref head_oid) = snapshot.head_oid {
let oid = git2::Oid::from_str(head_oid)?;
repo.reference(head_ref, oid, true, "securegit undo")?;
repo.set_head(head_ref)?;
}
} else if let Some(ref head_oid) = snapshot.head_oid {
let oid = git2::Oid::from_str(head_oid)?;
repo.set_head_detached(oid)?;
}
let current_branches: Vec<String> = repo
.branches(Some(git2::BranchType::Local))?
.filter_map(|b| b.ok())
.filter_map(|(b, _)| b.name().ok().flatten().map(|s| s.to_string()))
.collect();
for name in ¤t_branches {
if !snapshot.branches.contains_key(name) {
if let Ok(mut branch) = repo.find_branch(name, git2::BranchType::Local) {
let _ = branch.delete();
}
}
}
for (name, oid_str) in &snapshot.branches {
let oid = git2::Oid::from_str(oid_str)?;
if let Ok(commit) = repo.find_commit(oid) {
let _ = repo.branch(name, &commit, true); }
}
let current_tags: Vec<String> = repo
.tag_names(None)?
.iter()
.flatten()
.map(|s| s.to_string())
.collect();
for name in ¤t_tags {
if !snapshot.tags.contains_key(name) {
let _ = repo.tag_delete(name);
}
}
for (name, oid_str) in &snapshot.tags {
let oid = git2::Oid::from_str(oid_str)?;
if repo.find_reference(&format!("refs/tags/{}", name)).is_err() {
let obj = repo.find_object(oid, None)?;
let _ = repo.tag_lightweight(name, &obj, true);
}
}
if let Some(ref tree_oid_str) = snapshot.index_tree_oid {
let tree_oid = git2::Oid::from_str(tree_oid_str)?;
if let Ok(tree) = repo.find_tree(tree_oid) {
let mut index = repo.index()?;
index.read_tree(&tree)?;
index.write()?;
}
}
if let Some(ref head_oid) = snapshot.head_oid {
let oid = git2::Oid::from_str(head_oid)?;
if let Ok(commit) = repo.find_commit(oid) {
let mut checkout = git2::build::CheckoutBuilder::new();
checkout.force();
repo.checkout_tree(commit.as_object(), Some(&mut checkout))?;
}
}
Ok(())
}
fn display_oplog(path: &Path, count: usize, ui: &UI) -> Result<()> {
let entries = oplog::read_oplog(path)?;
if entries.is_empty() {
ui.info("No operations recorded yet");
return Ok(());
}
ui.section("Operation History (newest first)");
for entry in entries.iter().take(count) {
let short_id = &entry.id[..12.min(entry.id.len())];
let head_change = match (&entry.before.head_oid, &entry.after.head_oid) {
(Some(before), Some(after)) if before != after => {
format!(
" {}..{}",
&before[..7.min(before.len())],
&after[..7.min(after.len())]
)
}
_ => String::new(),
};
ui.list_item(format!(
"{} {} {}{} — {}",
short_id, entry.timestamp, entry.command, head_change, entry.description
));
}
if entries.len() > count {
ui.info(format!(
"... and {} more (use -n {} to see more)",
entries.len() - count,
entries.len()
));
}
Ok(())
}