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::auth;
use crate::auth::SecureString;
use crate::cli::UI;
use crate::graphrag;
use anyhow::{bail, Result};
use std::path::Path;

pub struct PushOptions<'a> {
    pub set_upstream: bool,
    pub force: bool,
    pub tags: bool,
    pub all: bool,
    pub token: Option<&'a SecureString>,
    pub ssh_key: Option<&'a Path>,
}

pub fn execute(
    path: &Path,
    remote_name: &str,
    branch: Option<&str>,
    opts: PushOptions,
    ui: &UI,
) -> Result<()> {
    let repo = crate::ops::open_repo(path)?;

    let prefix = if opts.force { "+" } else { "" };
    let mut refspecs: Vec<String> = Vec::new();
    let mut resolved_branch: Option<String> = None;

    if opts.all {
        let branches = repo.branches(Some(git2::BranchType::Local))?;
        for branch_result in branches {
            let (b, _) = branch_result?;
            if let Some(name) = b.name()? {
                refspecs.push(format!("{}refs/heads/{}:refs/heads/{}", prefix, name, name));
            }
        }
        // Capture current branch for set_upstream
        if let Ok(head) = repo.head() {
            resolved_branch = head.shorthand().map(|s| s.to_string());
        }
    } else if !opts.tags {
        // Push a specific branch or tag (or the current branch)
        let ref_name = if let Some(b) = branch {
            b.to_string()
        } else {
            if repo.head_detached()? {
                bail!(
                    "You are not currently on any branch.\n\
                     To push, specify the branch name: securegit push <remote> <branch>"
                );
            }
            let head = repo.head()?;
            head.shorthand()
                .ok_or_else(|| anyhow::anyhow!("Could not determine current branch name"))?
                .to_string()
        };

        // Detect if the argument is a tag (when no local branch of that name exists)
        let is_tag = repo
            .find_branch(&ref_name, git2::BranchType::Local)
            .is_err()
            && repo
                .find_reference(&format!("refs/tags/{}", ref_name))
                .is_ok();

        if is_tag {
            refspecs.push(format!(
                "{}refs/tags/{}:refs/tags/{}",
                prefix, ref_name, ref_name
            ));
        } else {
            refspecs.push(format!(
                "{}refs/heads/{}:refs/heads/{}",
                prefix, ref_name, ref_name
            ));
            resolved_branch = Some(ref_name);
        }
    } else if let Some(b) = branch {
        // --tags with an explicit branch: push the branch too
        let branch_name = b.to_string();
        refspecs.push(format!(
            "{}refs/heads/{}:refs/heads/{}",
            prefix, branch_name, branch_name
        ));
        resolved_branch = Some(branch_name);
    }

    if opts.tags {
        let tag_names = repo.tag_names(None)?;
        for tag in tag_names.iter().flatten() {
            refspecs.push(format!("{}refs/tags/{}:refs/tags/{}", prefix, tag, tag));
        }
    }

    if refspecs.is_empty() {
        bail!("Nothing to push.");
    }

    let mut remote = repo.find_remote(remote_name)?;

    let host = remote
        .url()
        .and_then(|u| url::Url::parse(u).ok())
        .and_then(|u| u.host_str().map(|h| h.to_string()));
    let mut callbacks = auth::build_git2_callbacks(opts.token, opts.ssh_key, host);
    callbacks.push_transfer_progress(|_current, _total, _bytes| {});

    let mut push_opts = git2::PushOptions::new();
    push_opts.remote_callbacks(callbacks);

    let refspec_strs: Vec<&str> = refspecs.iter().map(|s| s.as_str()).collect();
    remote.push(&refspec_strs, Some(&mut push_opts))?;

    let remote_url = remote.url().map(|s| s.to_string());
    ui.info(format!("To {}", remote_url.as_deref().unwrap_or("")));

    if let Some(ref branch_name) = resolved_branch {
        ui.field("Branch", format!("{} -> {}", branch_name, branch_name));
    }
    if opts.tags {
        ui.info("[tags pushed]");
    }
    if opts.all {
        ui.info("[all branches pushed]");
    }

    // Set upstream tracking if requested
    if opts.set_upstream {
        if let Some(ref branch_name) = resolved_branch {
            let mut local_branch = repo.find_branch(branch_name, git2::BranchType::Local)?;
            let upstream_name = format!("{}/{}", remote_name, branch_name);
            local_branch.set_upstream(Some(&upstream_name))?;
            ui.success(format!(
                "Branch '{}' set up to track '{}' from '{}'.",
                branch_name, branch_name, remote_name
            ));
        }
    }

    // Trigger GraphRAG indexing after push
    let branch_for_graphrag = resolved_branch.as_deref();
    graphrag::client::trigger_indexing_sync(path, remote_url.as_deref(), branch_for_graphrag);

    // Trigger auto-backup to configured destinations
    crate::ops::backup::trigger_auto_backup(path);

    Ok(())
}