choreo-daemon 0.1.0

Agentic coding assistant — daemon, TUI, and bridges
use crate::tools::{ToolError, truncate_tool_output};
use schemars::JsonSchema;
use serde::Deserialize;
use std::fmt::Write as _;

use super::{
    append_command_output, current_branch_name, describe_head, normalize_nonempty_argument,
    open_repo, repo_work_dir_display, run_git_command, yes_no,
};

#[derive(Debug, Deserialize, JsonSchema)]
pub struct GitPushArgs {
    pub repo_path: Option<String>,
    pub remote: String,
    pub branch: Option<String>,
    pub set_upstream: Option<bool>,
    pub force_with_lease: Option<bool>,
    pub dry_run: Option<bool>,
}

pub fn execute_git_push_tool(
    args: &GitPushArgs,
    working_dir: Option<&std::path::Path>,
) -> Result<String, ToolError> {
    let output = git_push_impl(
        args.repo_path.as_deref(),
        &args.remote,
        args.branch.as_deref(),
        args.set_upstream.unwrap_or(false),
        args.force_with_lease.unwrap_or(false),
        args.dry_run.unwrap_or(false),
        working_dir,
    )?;
    Ok(truncate_tool_output(&output))
}

fn git_push_impl(
    repo_path: Option<&str>,
    remote: &str,
    branch: Option<&str>,
    set_upstream: bool,
    force_with_lease: bool,
    dry_run: bool,
    working_dir: Option<&std::path::Path>,
) -> Result<String, ToolError> {
    let repo = open_repo(repo_path, working_dir)?;
    let remote = normalize_nonempty_argument(remote, "remote")?;
    let branch = match branch {
        Some(branch) => normalize_nonempty_argument(branch, "branch")?.to_string(),
        None => current_branch_name(&repo)?,
    };

    let mut args = vec!["push".to_string()];
    if dry_run {
        args.push("--dry-run".to_string());
    }
    if set_upstream {
        args.push("--set-upstream".to_string());
    }
    if force_with_lease {
        args.push("--force-with-lease".to_string());
    }
    args.push(remote.to_string());
    args.push(branch.clone());

    let output = run_git_command(&repo, &args)?;
    let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
    let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();

    if !output.status.success() {
        let mut out = String::new();
        writeln!(&mut out, "repository: {}", repo_work_dir_display(&repo)).ok();
        writeln!(&mut out, "head: {}", describe_head(&repo)?).ok();
        writeln!(&mut out, "remote: {remote}").ok();
        writeln!(&mut out, "branch: {branch}").ok();
        writeln!(&mut out, "dry_run: {}", yes_no(dry_run)).ok();
        writeln!(&mut out, "set_upstream: {}", yes_no(set_upstream)).ok();
        writeln!(&mut out, "force_with_lease: {}", yes_no(force_with_lease)).ok();
        writeln!(&mut out, "result: push failed").ok();
        append_command_output(&mut out, "stdout", &stdout);
        append_command_output(&mut out, "stderr", &stderr);
        return Err(ToolError::Other(out.trim_end().to_string()));
    }

    let mut out = String::new();
    writeln!(&mut out, "repository: {}", repo_work_dir_display(&repo)).ok();
    writeln!(&mut out, "head: {}", describe_head(&repo)?).ok();
    writeln!(&mut out, "remote: {remote}").ok();
    writeln!(&mut out, "branch: {branch}").ok();
    writeln!(&mut out, "dry_run: {}", yes_no(dry_run)).ok();
    writeln!(&mut out, "set_upstream: {}", yes_no(set_upstream)).ok();
    writeln!(&mut out, "force_with_lease: {}", yes_no(force_with_lease)).ok();
    writeln!(
        &mut out,
        "result: {}",
        if dry_run {
            "dry run complete"
        } else {
            "pushed"
        }
    )
    .ok();
    append_command_output(&mut out, "stdout", &stdout);
    append_command_output(&mut out, "stderr", &stderr);
    Ok(out.trim_end().to_string())
}

pub fn describe_git_push_invocation(args: &GitPushArgs) -> String {
    let mut parts = vec![format!("Pushing to `{}`.", args.remote)];
    if let Some(ref branch) = args.branch {
        parts.push(format!(" Branch: `{}`.", branch));
    }
    if args.set_upstream.unwrap_or(false) {
        parts.push(" Setting upstream.".to_string());
    }
    if args.force_with_lease.unwrap_or(false) {
        parts.push(" Force-with-lease.".to_string());
    }
    if args.dry_run.unwrap_or(false) {
        parts.push(" Dry run.".to_string());
    }
    parts.concat()
}

pub(crate) struct GitPush;

define_tool!(
    GitPush,
    "git_push",
    "Push to a Git remote branch.",
    GitPushArgs,
    execute_git_push_tool,
    "git",
    describe_git_push_invocation
);