use anyhow::Result;
use colored::Colorize;
use std::time::Duration;
use crate::error_code;
pub(super) fn retry_transient<F>(label: &str, mut op: F) -> Result<()>
where
F: FnMut() -> Result<()>,
{
const MAX_ATTEMPTS: u32 = 4;
let mut delay = Duration::from_secs(1);
for attempt in 1..=MAX_ATTEMPTS {
match op() {
Ok(()) => {
if attempt > 1 {
tracing::info!(
"{}",
format!(" ✓ {label} succeeded on attempt {attempt}/{MAX_ATTEMPTS}")
.green()
);
}
return Ok(());
}
Err(err) => {
let transient = is_transient_git_error(&err);
if !transient || attempt == MAX_ATTEMPTS {
return Err(err);
}
tracing::warn!(
"{}",
format!(
" ⚠ {label} attempt {attempt}/{MAX_ATTEMPTS} failed (transient): {err}; \
retrying in {}s",
delay.as_secs()
)
.yellow()
);
std::thread::sleep(delay);
delay = delay.saturating_mul(2);
}
}
}
unreachable!("the final attempt returns rather than falling out of the loop")
}
const TERMINAL: &[&str] = &[
"non-fast-forward",
"branch protection",
"rejected by remote",
"authentication failed",
"permission denied",
"repository not found",
];
const TRANSIENT: &[&str] = &[
"connection refused",
"connection reset",
"connection closed",
"connection timed out",
"failed to connect",
"could not resolve host",
"could not resolve proxy",
"temporarily unavailable",
"network is unreachable",
"network is down",
"network error",
"timed out",
"broken pipe",
"rst_stream",
"remote end hung up",
"early eof",
"ssl handshake",
"ssl connect error",
"ssl_read",
"ssl_write",
"sslv3",
"tls handshake",
"gnutls_handshake",
"certificate verify failed",
"bad gateway",
"service unavailable",
"gateway timeout",
"internal server error",
"secondary rate limit",
"rate limit exceeded",
"fatal error in commit_refs",
"object is no commit object",
"no commit object",
"class=invalid",
"object not found",
"odb",
];
const TRANSIENT_STATUS: &[&str] = &["502", "503", "504"];
fn mentions_http_status(chain: &str, code: &str) -> bool {
[
"error: ",
"http ",
"http/1.1 ",
"http/2 ",
"status ",
"status code ",
"code ",
]
.iter()
.any(|prefix| chain.contains(&format!("{prefix}{code}")))
}
pub(super) fn is_transient_git_error(err: &anyhow::Error) -> bool {
let chain = err
.chain()
.map(|e| e.to_string().to_lowercase())
.collect::<Vec<_>>()
.join(" ");
if TERMINAL.iter().any(|phrase| chain.contains(phrase)) {
return false;
}
if TRANSIENT.iter().any(|phrase| chain.contains(phrase)) {
return true;
}
TRANSIENT_STATUS
.iter()
.any(|code| mentions_http_status(&chain, code))
}
pub fn is_push_rejected_error(err: &anyhow::Error) -> bool {
let rejected_code = error_code::GIT_PUSH_REJECTED.to_string();
err.chain().any(|cause| {
let raw = cause.to_string();
if raw == rejected_code {
return true;
}
let msg = raw.to_lowercase();
msg.contains("rebase conflict")
|| msg.contains("push declined due to repository rule")
|| msg.contains("non-fast-forward")
|| msg.contains("non-fastforward")
|| msg.contains("not fast forward")
|| msg.contains("fetch first")
|| msg.contains("stale info")
|| msg.contains("already exist on remote pointing to a different commit")
|| msg.contains("expected branch to point to")
})
}