securegit 0.8.5

Zero-trust git replacement with 12 built-in security scanners, LLM redteam bridge, universal undo, durable backups, and a 50-tool MCP server
Documentation
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)?;

    // Log the undo itself
    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)?;

    // Refuse to force-checkout if the working tree has uncommitted changes
    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."
        );
    }

    // Restore HEAD
    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)?;
            // Ensure the ref points to the right commit
            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)?;
    }

    // Restore branches
    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();

    // Delete branches not in snapshot
    for name in &current_branches {
        if !snapshot.branches.contains_key(name) {
            if let Ok(mut branch) = repo.find_branch(name, git2::BranchType::Local) {
                let _ = branch.delete();
            }
        }
    }

    // Create/update branches from snapshot
    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); // force = true overwrites
        }
    }

    // Restore tags
    let current_tags: Vec<String> = repo
        .tag_names(None)?
        .iter()
        .flatten()
        .map(|s| s.to_string())
        .collect();

    for name in &current_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);
        }
    }

    // Restore index if we have a tree OID
    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()?;
        }
    }

    // Checkout HEAD to sync working tree
    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(())
}