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
);