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::refstore;
use crate::ops::utils::short_oid;
use anyhow::{bail, Result};
use git2::Repository;
use serde::{Deserialize, Serialize};
use std::path::Path;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StackEntry {
    pub branch: String,
    pub parent_branch: String,
    pub stack_name: String,
    pub position: u32,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StackMetadata {
    pub name: String,
    pub base_branch: String,
    pub branches: Vec<String>,
    pub created_at: String,
}

fn stack_meta_ref(name: &str) -> String {
    format!("refs/stack-metadata/__stack__{}", name)
}

fn branch_meta_ref(branch: &str) -> String {
    format!("refs/stack-metadata/{}", branch)
}

/// Create a new stack anchored at the current branch.
pub fn new_stack(path: &Path, name: &str, ui: &UI) -> Result<()> {
    let repo = crate::ops::open_repo(path)?;

    let head = repo.head()?;
    let base_branch = head
        .shorthand()
        .ok_or_else(|| anyhow::anyhow!("Cannot create stack: HEAD is detached"))?
        .to_string();

    // Check if stack already exists
    let meta_ref = stack_meta_ref(name);
    if refstore::read_json_ref::<StackMetadata>(&repo, &meta_ref)?.is_some() {
        bail!("Stack '{}' already exists.", name);
    }

    let metadata = StackMetadata {
        name: name.to_string(),
        base_branch: base_branch.clone(),
        branches: Vec::new(),
        created_at: crate::ops::oplog::now_iso8601_pub(),
    };

    refstore::write_json_ref(&repo, &meta_ref, &metadata)?;

    ui.success(format!(
        "Created stack '{}' based on '{}'",
        name, base_branch
    ));
    Ok(())
}

/// Push a new branch onto the top of the current stack.
pub fn push_branch(path: &Path, branch_name: Option<&str>, ui: &UI) -> Result<()> {
    let repo = crate::ops::open_repo(path)?;

    // Find which stack the current branch belongs to
    let current_branch = repo
        .head()?
        .shorthand()
        .map(|s| s.to_string())
        .ok_or_else(|| anyhow::anyhow!("HEAD is detached"))?;

    let (stack_name, mut metadata) = find_stack_for_branch(&repo, &current_branch)?;

    // Generate branch name if not provided
    let new_branch = if let Some(name) = branch_name {
        name.to_string()
    } else {
        format!("{}/{}", stack_name, metadata.branches.len() + 1)
    };

    // Create the new branch from current HEAD
    let head_commit = repo.head()?.peel_to_commit()?;
    repo.branch(&new_branch, &head_commit, false)?;

    // Create stack entry for the new branch
    let entry = StackEntry {
        branch: new_branch.clone(),
        parent_branch: current_branch.clone(),
        stack_name: stack_name.clone(),
        position: metadata.branches.len() as u32,
    };

    refstore::write_json_ref(&repo, &branch_meta_ref(&new_branch), &entry)?;

    // Update stack metadata
    metadata.branches.push(new_branch.clone());
    refstore::write_json_ref(&repo, &stack_meta_ref(&stack_name), &metadata)?;

    // Checkout the new branch
    let obj = repo
        .find_branch(&new_branch, git2::BranchType::Local)?
        .get()
        .peel(git2::ObjectType::Commit)?;
    repo.checkout_tree(&obj, None)?;
    repo.set_head(&format!("refs/heads/{}", new_branch))?;

    ui.success(format!(
        "Pushed '{}' onto stack '{}'",
        new_branch, stack_name
    ));
    Ok(())
}

/// Show status of the current stack.
pub fn status(path: &Path, ui: &UI) -> Result<()> {
    let repo = crate::ops::open_repo(path)?;

    let current_branch = repo
        .head()?
        .shorthand()
        .map(|s| s.to_string())
        .unwrap_or_default();

    let (stack_name, metadata) = match find_stack_for_branch(&repo, &current_branch) {
        Ok(result) => result,
        Err(_) => {
            ui.info("Not in a stack. Use 'securegit stack new <name>' to create one.");
            return Ok(());
        }
    };

    ui.section(&format!("Stack: {}", stack_name));
    ui.field("base", &metadata.base_branch);
    ui.blank();

    let branch_count = metadata.branches.len();
    for (i, branch) in metadata.branches.iter().enumerate() {
        let is_last = i == branch_count - 1;

        // Count commits ahead of parent
        let entry = refstore::read_json_ref::<StackEntry>(&repo, &branch_meta_ref(branch))?;
        let ahead = if let Some(entry) = &entry {
            count_ahead(&repo, &entry.parent_branch, branch).unwrap_or(0)
        } else {
            0
        };

        let is_current = branch == &current_branch;
        let branch_display = if is_current {
            format!("{} *", branch)
        } else {
            branch.to_string()
        };

        let plural = if ahead == 1 { "" } else { "s" };
        ui.tree_item(
            is_last,
            format!("{}. {} ({} commit{})", i + 1, branch_display, ahead, plural),
        );
    }

    Ok(())
}

/// Cascade rebase the entire stack from base to tip.
pub fn rebase_stack(path: &Path, ui: &UI) -> Result<()> {
    let repo = crate::ops::open_repo(path)?;

    let current_branch = repo
        .head()?
        .shorthand()
        .map(|s| s.to_string())
        .unwrap_or_default();

    let (stack_name, metadata) = find_stack_for_branch(&repo, &current_branch)?;

    ui.info(format!(
        "Rebasing stack '{}' ({} branches)...",
        stack_name,
        metadata.branches.len()
    ));

    let mut prev_branch = metadata.base_branch.clone();
    for branch in &metadata.branches {
        ui.info(format!("Rebasing '{}' onto '{}'", branch, prev_branch));

        // Checkout the branch
        let branch_ref = repo.find_branch(branch, git2::BranchType::Local)?;
        let obj = branch_ref.get().peel(git2::ObjectType::Commit)?;
        repo.checkout_tree(&obj, None)?;
        repo.set_head(&format!("refs/heads/{}", branch))?;

        // Rebase onto parent
        let upstream_obj = repo.revparse_single(&prev_branch)?;
        let upstream_commit = upstream_obj.peel_to_commit()?;
        let upstream_annotated = repo.find_annotated_commit(upstream_commit.id())?;

        let mut rebase = repo.rebase(None, Some(&upstream_annotated), None, None)?;
        let sig = repo.signature()?;

        while rebase.next().is_some() {
            let index = repo.index()?;
            if index.has_conflicts() {
                ui.warning(format!(
                    "Conflict in '{}'. Resolve and run 'securegit stack rebase' again.",
                    branch
                ));
                rebase.abort()?;
                // Restore to the branch we were on
                let obj = repo
                    .find_branch(&current_branch, git2::BranchType::Local)?
                    .get()
                    .peel(git2::ObjectType::Commit)?;
                repo.checkout_tree(&obj, None)?;
                repo.set_head(&format!("refs/heads/{}", current_branch))?;
                bail!("Stack rebase stopped due to conflicts in '{}'", branch);
            }
            rebase.commit(None, &sig, None)?;
        }

        rebase.finish(None)?;
        ui.success("Done");

        prev_branch = branch.clone();
    }

    // Restore original branch
    let obj = repo
        .find_branch(&current_branch, git2::BranchType::Local)?
        .get()
        .peel(git2::ObjectType::Commit)?;
    repo.checkout_tree(&obj, None)?;
    repo.set_head(&format!("refs/heads/{}", current_branch))?;

    ui.success(format!("Stack '{}' rebased successfully", stack_name));
    Ok(())
}

/// Remove the current branch from the stack.
pub fn pop_branch(path: &Path, ui: &UI) -> Result<()> {
    let repo = crate::ops::open_repo(path)?;

    let current_branch = repo
        .head()?
        .shorthand()
        .map(|s| s.to_string())
        .ok_or_else(|| anyhow::anyhow!("HEAD is detached"))?;

    let (stack_name, mut metadata) = find_stack_for_branch(&repo, &current_branch)?;

    // Must be the last branch in the stack
    if metadata.branches.last().map(|s| s.as_str()) != Some(&current_branch) {
        bail!(
            "Can only pop the top of the stack. '{}' is not the top branch.",
            current_branch
        );
    }

    // Find the parent branch to switch to
    let entry = refstore::read_json_ref::<StackEntry>(&repo, &branch_meta_ref(&current_branch))?
        .ok_or_else(|| anyhow::anyhow!("No stack entry for '{}'", current_branch))?;

    let parent = entry.parent_branch.clone();

    // Remove from stack
    metadata.branches.retain(|b| b != &current_branch);
    refstore::write_json_ref(&repo, &stack_meta_ref(&stack_name), &metadata)?;
    refstore::delete_ref(&repo, &branch_meta_ref(&current_branch))?;

    // Switch to parent
    let obj = repo
        .find_branch(&parent, git2::BranchType::Local)?
        .get()
        .peel(git2::ObjectType::Commit)?;
    repo.checkout_tree(&obj, None)?;
    repo.set_head(&format!("refs/heads/{}", parent))?;

    ui.success(format!(
        "Popped '{}' from stack '{}'. Now on '{}'.",
        current_branch, stack_name, parent
    ));
    Ok(())
}

/// Log all commits in the stack grouped by branch.
pub fn log_stack(path: &Path, ui: &UI) -> Result<()> {
    let repo = crate::ops::open_repo(path)?;

    let current_branch = repo
        .head()?
        .shorthand()
        .map(|s| s.to_string())
        .unwrap_or_default();

    let (_stack_name, metadata) = find_stack_for_branch(&repo, &current_branch)?;

    let mut prev_branch = metadata.base_branch.clone();
    for branch in &metadata.branches {
        ui.section(branch);

        // Show commits in this branch not in parent
        if let Ok(Some(branch_oid)) = repo
            .find_branch(branch, git2::BranchType::Local)
            .map(|b| b.get().target())
        {
            if let Ok(Some(parent_oid)) = repo
                .find_branch(&prev_branch, git2::BranchType::Local)
                .map(|b| b.get().target())
            {
                let mut revwalk = repo.revwalk()?;
                revwalk.push(branch_oid)?;
                revwalk.hide(parent_oid)?;

                for oid in revwalk.flatten() {
                    if let Ok(commit) = repo.find_commit(oid) {
                        ui.log_oneline(&short_oid(&oid), commit.summary().unwrap_or(""));
                    }
                }
            }
        }

        prev_branch = branch.clone();
    }

    Ok(())
}

// --- Internal helpers ---

fn find_stack_for_branch(repo: &Repository, branch_name: &str) -> Result<(String, StackMetadata)> {
    // First check if this branch has a stack entry
    if let Some(entry) = refstore::read_json_ref::<StackEntry>(repo, &branch_meta_ref(branch_name))?
    {
        let metadata =
            refstore::read_json_ref::<StackMetadata>(repo, &stack_meta_ref(&entry.stack_name))?
                .ok_or_else(|| {
                    anyhow::anyhow!("Stack '{}' metadata not found", entry.stack_name)
                })?;
        return Ok((entry.stack_name, metadata));
    }

    // Check if this branch is a base for any stack
    let refs = refstore::list_refs_with_prefix(repo, "refs/stack-metadata/__stack__")?;
    for ref_name in &refs {
        if let Some(metadata) = refstore::read_json_ref::<StackMetadata>(repo, ref_name)? {
            if metadata.base_branch == branch_name {
                return Ok((metadata.name.clone(), metadata));
            }
        }
    }

    bail!("Branch '{}' is not part of any stack.", branch_name)
}

fn count_ahead(repo: &Repository, base: &str, branch: &str) -> Result<usize> {
    let base_oid = repo
        .find_branch(base, git2::BranchType::Local)?
        .get()
        .target()
        .ok_or_else(|| anyhow::anyhow!("No OID for '{}'", base))?;

    let branch_oid = repo
        .find_branch(branch, git2::BranchType::Local)?
        .get()
        .target()
        .ok_or_else(|| anyhow::anyhow!("No OID for '{}'", branch))?;

    let (ahead, _) = repo.graph_ahead_behind(branch_oid, base_oid)?;
    Ok(ahead)
}