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

/// Print the current branch name. Empty output if HEAD is detached.
pub fn show_current(path: &Path) -> Result<()> {
    let repo = crate::ops::open_repo(path)?;
    if let Ok(head) = repo.head() {
        if head.is_branch() {
            if let Some(name) = head.shorthand() {
                println!("{}", name);
            }
        }
    }
    Ok(())
}

pub fn list(path: &Path, show_all: bool, ui: &UI) -> Result<()> {
    let repo = crate::ops::open_repo(path)?;
    let filter = if show_all {
        None
    } else {
        Some(git2::BranchType::Local)
    };

    let branches = repo.branches(filter)?;
    let head = repo.head().ok();
    let head_name = head
        .as_ref()
        .and_then(|h| h.shorthand().map(|s| s.to_string()));

    for branch in branches {
        let (branch, branch_type) = branch?;
        let name = branch.name()?.unwrap_or("").to_string();
        let is_current = head_name.as_deref() == Some(&name);

        match branch_type {
            git2::BranchType::Remote => {
                ui.list_item(format!("remotes/{}", name));
            }
            git2::BranchType::Local => {
                let (hash, summary) = match branch.get().peel_to_commit() {
                    Ok(commit) => (
                        short_oid(&commit.id()),
                        commit.summary().unwrap_or("").to_string(),
                    ),
                    Err(_) => ("???????".to_string(), String::new()),
                };
                ui.branch_item(&name, &hash, &summary, is_current);
            }
        }
    }

    Ok(())
}

pub fn list_compact(path: &Path, show_all: bool) -> Result<String> {
    let repo = crate::ops::open_repo(path)?;
    let filter = if show_all {
        None
    } else {
        Some(git2::BranchType::Local)
    };
    let branches = repo.branches(filter)?;
    let head = repo.head().ok();
    let head_name = head
        .as_ref()
        .and_then(|h| h.shorthand().map(|s| s.to_string()));

    let mut local = Vec::new();
    let mut remote_only = Vec::new();

    for branch in branches {
        let (branch, branch_type) = branch?;
        let name = branch.name()?.unwrap_or("").to_string();
        let is_current = head_name.as_deref() == Some(&name);

        match branch_type {
            git2::BranchType::Remote => {
                let short = name.split('/').skip(1).collect::<Vec<_>>().join("/");
                if !local.iter().any(|(n, _): &(String, bool)| *n == short) {
                    remote_only.push(name);
                }
            }
            git2::BranchType::Local => {
                local.push((name, is_current));
            }
        }
    }

    let mut lines = Vec::new();
    for (name, current) in &local {
        if *current {
            lines.push(format!("* {}", name));
        } else {
            lines.push(format!("  {}", name));
        }
    }
    if !remote_only.is_empty() {
        let count = remote_only.len();
        let shown: Vec<&str> = remote_only.iter().map(|s| s.as_str()).take(10).collect();
        lines.push(format!("remote-only ({}):", count));
        for r in &shown {
            lines.push(format!("  {}", r));
        }
        if count > 10 {
            lines.push(format!("  ... +{} more", count - 10));
        }
    }

    Ok(lines.join("\n"))
}

pub fn create(path: &Path, name: &str, start_point: Option<&str>, ui: &UI) -> Result<()> {
    oplog::with_oplog(path, "branch", &format!("create '{}'", name), || {
        create_inner(path, name, start_point, ui)
    })
}

fn create_inner(path: &Path, name: &str, start_point: Option<&str>, ui: &UI) -> Result<()> {
    let repo = crate::ops::open_repo(path)?;

    let commit = if let Some(sp) = start_point {
        let obj = repo.revparse_single(sp)?;
        obj.peel_to_commit()?
    } else {
        repo.head()?.peel_to_commit()?
    };

    repo.branch(name, &commit, false)?;
    ui.success(format!("Created branch '{}'", name));

    Ok(())
}

pub fn delete(path: &Path, name: &str, force: bool, ui: &UI) -> Result<()> {
    oplog::with_oplog(path, "branch", &format!("delete '{}'", name), || {
        delete_inner(path, name, force, ui)
    })
}

fn delete_inner(path: &Path, name: &str, force: bool, ui: &UI) -> Result<()> {
    let repo = crate::ops::open_repo(path)?;
    let mut branch = repo.find_branch(name, git2::BranchType::Local)?;

    if !force && !branch.is_head() {
        let head = repo.head()?.peel_to_commit()?;
        let branch_commit = branch.get().peel_to_commit()?;
        let merge_base = repo.merge_base(head.id(), branch_commit.id());
        if merge_base.ok() != Some(branch_commit.id()) {
            bail!(
                "Branch '{}' is not fully merged. Use -D to force delete.",
                name
            );
        }
    }

    branch.delete()?;
    ui.success(format!("Deleted branch '{}'", name));

    Ok(())
}

pub fn rename(path: &Path, old_name: &str, new_name: &str, force: bool, ui: &UI) -> Result<()> {
    oplog::with_oplog(
        path,
        "branch",
        &format!("rename '{}' to '{}'", old_name, new_name),
        || rename_inner(path, old_name, new_name, force, ui),
    )
}

fn rename_inner(path: &Path, old_name: &str, new_name: &str, force: bool, ui: &UI) -> Result<()> {
    let repo = crate::ops::open_repo(path)?;
    let mut branch = repo.find_branch(old_name, git2::BranchType::Local)?;
    branch.rename(new_name, force)?;
    ui.success(format!("Renamed branch '{}' to '{}'", old_name, new_name));
    Ok(())
}