use crate::{GitError, GitResult, RepoHandle};
use tokio::process::Command;
use tokio::time::Duration;
#[derive(Debug, Clone)]
pub struct PushOpts {
pub remote: String,
pub refspecs: Vec<String>,
pub force: bool,
pub tags: bool,
pub timeout_secs: Option<u64>,
}
impl Default for PushOpts {
fn default() -> Self {
Self {
remote: "origin".to_string(),
refspecs: Vec::new(),
force: false,
tags: false,
timeout_secs: None,
}
}
}
#[derive(Debug, Clone)]
pub struct PushResult {
pub commits_pushed: usize,
pub tags_pushed: usize,
pub warnings: Vec<String>,
}
pub async fn push(repo: &RepoHandle, opts: PushOpts) -> GitResult<PushResult> {
let work_dir = repo
.raw()
.workdir()
.ok_or_else(|| GitError::InvalidInput("Repository has no working directory".to_string()))?
.to_path_buf();
let PushOpts {
remote,
refspecs,
force,
tags,
timeout_secs,
} = opts;
let timeout_duration = Duration::from_secs(timeout_secs.unwrap_or(300));
let mut cmd = Command::new("git");
cmd.current_dir(&work_dir);
cmd.arg("push");
cmd.env("GIT_TERMINAL_PROMPT", "0");
cmd.env("LC_ALL", "C");
cmd.env("LANG", "C");
cmd.stdout(std::process::Stdio::piped());
cmd.stderr(std::process::Stdio::piped());
if force {
cmd.arg("--force");
}
if tags {
cmd.arg("--tags");
}
cmd.arg(&remote);
for refspec in &refspecs {
cmd.arg(refspec);
}
let mut child = cmd.spawn().map_err(GitError::Io)?;
let status = tokio::select! {
result = child.wait() => {
result.map_err(GitError::Io)?
}
() = tokio::time::sleep(timeout_duration) => {
let _ = child.kill().await;
return Err(GitError::InvalidInput(format!("Push operation timed out after {} seconds", timeout_secs.unwrap_or(300))));
}
};
use tokio::io::AsyncReadExt;
let mut stdout_data = Vec::new();
let mut stderr_data = Vec::new();
if let Some(mut stdout) = child.stdout.take() {
let _ = stdout.read_to_end(&mut stdout_data).await;
}
if let Some(mut stderr) = child.stderr.take() {
let _ = stderr.read_to_end(&mut stderr_data).await;
}
let output = std::process::Output {
status,
stdout: stdout_data,
stderr: stderr_data,
};
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(GitError::InvalidInput(format!("Push failed: {stderr}")));
}
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
let combined = format!("{stdout}\n{stderr}");
let commits_pushed = combined
.lines()
.filter(|line| {
let trimmed = line.trim_start();
if !trimmed.contains(" -> ") {
return false;
}
if trimmed.starts_with('!')
|| trimmed.starts_with("error:")
|| trimmed.contains("[rejected]")
{
return false;
}
trimmed.starts_with(|c: char| c.is_ascii_hexdigit())
|| trimmed.starts_with("* [new")
|| trimmed.starts_with('+')
})
.count();
let tags_pushed = if tags && output.status.success() {
1 } else if output.status.success() && refspecs.iter().any(|r| r.contains("refs/tags/")) {
refspecs.iter().filter(|r| r.contains("refs/tags/")).count()
} else {
0
};
let mut warnings = Vec::new();
if force {
warnings.push("Force push executed".to_string());
}
Ok(PushResult {
commits_pushed,
tags_pushed,
warnings,
})
}
pub async fn push_current_branch(repo: &RepoHandle, remote: &str) -> GitResult<PushResult> {
push(
repo,
PushOpts {
remote: remote.to_string(),
refspecs: Vec::new(),
force: false,
tags: false,
timeout_secs: None,
},
)
.await
}
pub async fn push_tags(repo: &RepoHandle, remote: &str) -> GitResult<PushResult> {
push(
repo,
PushOpts {
remote: remote.to_string(),
refspecs: Vec::new(),
force: false,
tags: true,
timeout_secs: None,
},
)
.await
}
pub async fn delete_remote_tag(repo: &RepoHandle, remote: &str, tag_name: &str) -> GitResult<()> {
let work_dir = repo
.raw()
.workdir()
.ok_or_else(|| GitError::InvalidInput("Repository has no working directory".to_string()))?
.to_path_buf();
let tag_name = tag_name.strip_prefix("refs/tags/").unwrap_or(tag_name);
if tag_name.is_empty() {
return Err(GitError::InvalidInput(
"Tag name cannot be empty".to_string(),
));
}
if tag_name.contains("..") {
return Err(GitError::InvalidInput(format!(
"Invalid tag name: {tag_name}"
)));
}
if tag_name.starts_with('/') {
return Err(GitError::InvalidInput(format!(
"Invalid tag name: {tag_name}"
)));
}
let remote = remote.to_string();
let tag_name_owned = tag_name.to_string();
let timeout_duration = Duration::from_secs(300);
let mut cmd = Command::new("git");
cmd.current_dir(&work_dir);
cmd.env("GIT_TERMINAL_PROMPT", "0"); cmd.arg("push");
cmd.arg(&remote);
cmd.arg("--delete");
cmd.arg(format!("refs/tags/{tag_name_owned}"));
cmd.stdout(std::process::Stdio::piped());
cmd.stderr(std::process::Stdio::piped());
let mut child = cmd.spawn().map_err(GitError::Io)?;
let status = tokio::select! {
result = child.wait() => {
result.map_err(GitError::Io)?
}
() = tokio::time::sleep(timeout_duration) => {
let _ = child.kill().await;
return Err(GitError::InvalidInput("Delete remote tag operation timed out after 300 seconds".to_string()));
}
};
use tokio::io::AsyncReadExt;
let mut stdout_data = Vec::new();
let mut stderr_data = Vec::new();
if let Some(mut stdout) = child.stdout.take() {
let _ = stdout.read_to_end(&mut stdout_data).await;
}
if let Some(mut stderr) = child.stderr.take() {
let _ = stderr.read_to_end(&mut stderr_data).await;
}
let output = std::process::Output {
status,
stdout: stdout_data,
stderr: stderr_data,
};
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(GitError::InvalidInput(format!(
"Failed to delete remote tag '{tag_name_owned}': {stderr}"
)));
}
Ok(())
}
pub async fn delete_remote_branch(
repo: &RepoHandle,
remote: &str,
branch_name: &str,
) -> GitResult<()> {
let work_dir = repo
.raw()
.workdir()
.ok_or_else(|| GitError::InvalidInput("Repository has no working directory".to_string()))?
.to_path_buf();
let branch_name = branch_name
.strip_prefix("refs/heads/")
.unwrap_or(branch_name);
if branch_name.is_empty() {
return Err(GitError::InvalidInput(
"Branch name cannot be empty".to_string(),
));
}
if branch_name.contains("..") {
return Err(GitError::InvalidInput(format!(
"Invalid branch name: {branch_name}"
)));
}
if branch_name.starts_with('/') {
return Err(GitError::InvalidInput(format!(
"Invalid branch name: {branch_name}"
)));
}
let remote = remote.to_string();
let branch_name_owned = branch_name.to_string();
let timeout_duration = Duration::from_secs(300);
let mut cmd = Command::new("git");
cmd.current_dir(&work_dir);
cmd.env("GIT_TERMINAL_PROMPT", "0"); cmd.arg("push");
cmd.arg(&remote);
cmd.arg("--delete");
cmd.arg(format!("refs/heads/{branch_name_owned}"));
cmd.stdout(std::process::Stdio::piped());
cmd.stderr(std::process::Stdio::piped());
let mut child = cmd.spawn().map_err(GitError::Io)?;
let status = tokio::select! {
result = child.wait() => {
result.map_err(GitError::Io)?
}
() = tokio::time::sleep(timeout_duration) => {
let _ = child.kill().await;
return Err(GitError::InvalidInput("Delete remote branch operation timed out after 300 seconds".to_string()));
}
};
use tokio::io::AsyncReadExt;
let mut stdout_data = Vec::new();
let mut stderr_data = Vec::new();
if let Some(mut stdout) = child.stdout.take() {
let _ = stdout.read_to_end(&mut stdout_data).await;
}
if let Some(mut stderr) = child.stderr.take() {
let _ = stderr.read_to_end(&mut stderr_data).await;
}
let output = std::process::Output {
status,
stdout: stdout_data,
stderr: stderr_data,
};
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(GitError::InvalidInput(format!(
"Failed to delete remote branch '{branch_name_owned}': {stderr}"
)));
}
Ok(())
}
pub async fn check_remote_branch_exists(
repo: &RepoHandle,
remote: &str,
branch_name: &str,
) -> GitResult<bool> {
let work_dir = repo
.raw()
.workdir()
.ok_or_else(|| GitError::InvalidInput("Repository has no working directory".to_string()))?
.to_path_buf();
let branch_name = branch_name
.strip_prefix("refs/heads/")
.unwrap_or(branch_name);
if branch_name.is_empty() {
return Err(GitError::InvalidInput(
"Branch name cannot be empty".to_string(),
));
}
let remote = remote.to_string();
let branch_name_owned = branch_name.to_string();
let refspec = format!("refs/heads/{branch_name_owned}");
let timeout_duration = Duration::from_secs(30);
let mut cmd = Command::new("git");
cmd.current_dir(&work_dir);
cmd.env("GIT_TERMINAL_PROMPT", "0"); cmd.arg("ls-remote");
cmd.arg("--heads"); cmd.arg(&remote);
cmd.arg(&refspec);
cmd.stdout(std::process::Stdio::piped());
cmd.stderr(std::process::Stdio::piped());
let mut child = cmd.spawn().map_err(GitError::Io)?;
let status = tokio::select! {
result = child.wait() => {
result.map_err(GitError::Io)?
}
() = tokio::time::sleep(timeout_duration) => {
let _ = child.kill().await;
return Err(GitError::InvalidInput("ls-remote operation timed out after 30 seconds".to_string()));
}
};
use tokio::io::AsyncReadExt;
let mut stdout_data = Vec::new();
let mut stderr_data = Vec::new();
if let Some(mut stdout) = child.stdout.take() {
let _ = stdout.read_to_end(&mut stdout_data).await;
}
if let Some(mut stderr) = child.stderr.take() {
let _ = stderr.read_to_end(&mut stderr_data).await;
}
let output = std::process::Output {
status,
stdout: stdout_data,
stderr: stderr_data,
};
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(GitError::InvalidInput(format!(
"ls-remote failed: {stderr}"
)));
}
let stdout = String::from_utf8_lossy(&output.stdout);
Ok(!stdout.trim().is_empty())
}
pub async fn check_remote_tag_exists(
repo: &RepoHandle,
remote: &str,
tag_name: &str,
) -> GitResult<bool> {
let work_dir = repo
.raw()
.workdir()
.ok_or_else(|| GitError::InvalidInput("Repository has no working directory".to_string()))?
.to_path_buf();
let tag_name = tag_name.strip_prefix("refs/tags/").unwrap_or(tag_name);
if tag_name.is_empty() {
return Err(GitError::InvalidInput(
"Tag name cannot be empty".to_string(),
));
}
let remote = remote.to_string();
let tag_name_owned = tag_name.to_string();
let refspec = format!("refs/tags/{tag_name_owned}");
let timeout_duration = Duration::from_secs(30);
let mut cmd = Command::new("git");
cmd.current_dir(&work_dir);
cmd.env("GIT_TERMINAL_PROMPT", "0"); cmd.arg("ls-remote");
cmd.arg("--tags"); cmd.arg(&remote);
cmd.arg(&refspec);
cmd.stdout(std::process::Stdio::piped());
cmd.stderr(std::process::Stdio::piped());
let mut child = cmd.spawn().map_err(GitError::Io)?;
let status = tokio::select! {
result = child.wait() => {
result.map_err(GitError::Io)?
}
() = tokio::time::sleep(timeout_duration) => {
let _ = child.kill().await;
return Err(GitError::InvalidInput("ls-remote operation timed out after 30 seconds".to_string()));
}
};
use tokio::io::AsyncReadExt;
let mut stdout_data = Vec::new();
let mut stderr_data = Vec::new();
if let Some(mut stdout) = child.stdout.take() {
let _ = stdout.read_to_end(&mut stdout_data).await;
}
if let Some(mut stderr) = child.stderr.take() {
let _ = stderr.read_to_end(&mut stderr_data).await;
}
let output = std::process::Output {
status,
stdout: stdout_data,
stderr: stderr_data,
};
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(GitError::InvalidInput(format!(
"ls-remote failed: {stderr}"
)));
}
let stdout = String::from_utf8_lossy(&output.stdout);
Ok(!stdout.trim().is_empty())
}