use crate::auto_push_destination::{NamedPush, named_push};
use crate::auto_push_reconcile::{RECONCILE_FAILED_PREFIX, RECONCILE_LINE_PREFIX, reconcile_divergent_push};
use crate::auto_push_single_flight::{PushAttempt, PushGit, SingleFlightError, run_query};
use crate::lfs_bounded_push::run_push;
use crate::diagnostic_log::debug;
use std::ffi::OsString;
use std::io::Write;
const TAG: &str = "cli-git";
const REMOTE_LINE_PREFIX: &str = "remote: ";
pub const PUSH_FAILED_NOTE: &str =
"cli-git: commit saved locally, but auto-push to origin failed; run `git push` when ready.";
const REFERENCE_LOCK_REFUSAL: &str = "cannot lock ref";
pub const PUSH_LOCK_RETRIES: u32 = 3;
const PUSH_LOCK_PAUSE: std::time::Duration = std::time::Duration::from_millis(250);
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum AutoPushOutcome {
Skipped,
Pushed,
Failed,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AutoPushResult {
pub outcome: AutoPushOutcome,
pub exit_code: i64,
pub shown: String,
}
pub fn filter_push_output(output: &str, exit_code: i64) -> String {
if exit_code != 0 {
return output.to_owned();
}
return output
.split('\n')
.filter(|line| {
return line.starts_with(REMOTE_LINE_PREFIX) || line.starts_with(RECONCILE_LINE_PREFIX);
})
.collect::<Vec<&str>>()
.join("\n");
}
fn query_succeeds(git: &PushGit, arguments: &[&str]) -> Result<bool, SingleFlightError> {
let argv: Vec<OsString> = arguments.iter().map(OsString::from).collect();
return Ok(run_query(git, &argv)?.code == Some(0));
}
fn run_push_command(git: &PushGit, arguments: &[&str]) -> Result<PushAttempt, SingleFlightError> {
let argv: Vec<OsString> = arguments.iter().map(OsString::from).collect();
return run_push(git, &argv);
}
enum Planned {
Skip(AutoPushResult),
Push {
branch_ref: String,
has_upstream: bool,
arguments: Vec<String>,
},
}
fn skipped_with(note: String, err: &mut dyn Write) -> Planned {
let _written = writeln!(err, "{note}");
return Planned::Skip(AutoPushResult { outcome: AutoPushOutcome::Skipped, exit_code: 0, shown: note });
}
fn plan_named_push(git: &PushGit, branch_ref: &str, err: &mut dyn Write) -> Result<Planned, SingleFlightError> {
let has_upstream: bool = match branch_ref.strip_prefix("refs/heads/") {
Some(name) => {
let upstream_name: String = format!("{name}@{{upstream}}");
query_succeeds(git, &["rev-parse", "--abbrev-ref", "--symbolic-full-name", upstream_name.as_str()])?
}
None => false,
};
if !has_upstream && !query_succeeds(git, &["remote", "get-url", "origin"])? {
debug(TAG, "no upstream and no origin remote; skipping auto-push");
return Ok(Planned::Skip(AutoPushResult { outcome: AutoPushOutcome::Skipped, exit_code: 0, shown: String::new() }));
}
let arguments: Vec<String> = if has_upstream {
let (remote, targets): (String, Vec<String>) = match named_push(git, branch_ref)? {
NamedPush::Push { remote, targets } => (remote, targets),
NamedPush::Matching { remote } => {
return Ok(Planned::Push { branch_ref: branch_ref.to_owned(), has_upstream, arguments: vec![String::from("push"), remote] });
}
NamedPush::Skip(note) => return Ok(skipped_with(note, err)),
};
let mut arguments: Vec<String> = vec![String::from("push"), remote];
arguments.extend(targets.iter().map(|target| { return format!("{branch_ref}:{target}"); }));
arguments
} else {
vec![String::from("push"), String::from("--set-upstream"), String::from("origin"), format!("{branch_ref}:{branch_ref}")]
};
return Ok(Planned::Push { branch_ref: branch_ref.to_owned(), has_upstream, arguments });
}
pub fn push_once(git: &PushGit, branch_ref: &str, landed_oid: &str, err: &mut dyn Write) -> Result<AutoPushResult, SingleFlightError> {
let planned: Planned = plan_named_push(git, branch_ref, err)?;
let (pushed_ref, has_upstream, mut attempt): (String, bool, PushAttempt) = match planned {
Planned::Skip(result) => return Ok(result),
Planned::Push { branch_ref: planned_ref, has_upstream, arguments } => {
let argument_views: Vec<&str> = arguments.iter().map(String::as_str).collect();
debug(TAG, &format!("pushing {landed_oid} with {argument_views:?} on {planned_ref}"));
let mut attempt: PushAttempt = run_push_command(git, argument_views.as_slice())?;
let mut pause: u32 = 0;
while attempt.output.contains(REFERENCE_LOCK_REFUSAL) && pause < PUSH_LOCK_RETRIES {
pause += 1;
std::thread::sleep(PUSH_LOCK_PAUSE * pause);
attempt = run_push_command(git, argument_views.as_slice())?;
}
(planned_ref, has_upstream, attempt)
}
};
if has_upstream && attempt.exit_code != 0 {
attempt = reconcile_divergent_push(git, &pushed_ref, attempt)?;
}
let failed: bool = attempt.exit_code != 0;
let shown: String = filter_push_output(&attempt.output, attempt.exit_code);
if !shown.is_empty() {
writeln!(err, "{shown}").map_err(SingleFlightError::Io)?;
}
if failed && !attempt.output.contains(RECONCILE_FAILED_PREFIX) {
writeln!(err, "{PUSH_FAILED_NOTE}").map_err(SingleFlightError::Io)?;
}
return Ok(AutoPushResult {
outcome: if failed { AutoPushOutcome::Failed } else { AutoPushOutcome::Pushed },
exit_code: attempt.exit_code,
shown,
});
}
#[cfg(test)]
#[path = "auto_push_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "auto_push_semantics_tests.rs"]
mod semantics_tests;