use crate::auto_push_reconcile::{Destination, Integrated, Reconcile, integrate_and_push};
use crate::auto_push_single_flight::{PushAttempt, PushGit, SingleFlightError, run_query};
use crate::action::PushForward;
use crate::child_environment::environment_with_overlay;
use crate::command_push::stackable_option_tokens;
use crate::diagnostic_log::debug;
use crate::lfs_interactive::{InteractiveUpload, upload_first};
use crate::stacked_merge_push::gated_merge_push;
use crate::manual_push_probe::probe_manual_push_updates;
use crate::worktree_copy_forward::{ForwardedRun, run_forwarded_git};
use std::ffi::OsString;
use std::io::Write;
const TAG: &str = "cli-git";
pub const STACKED_PUSH_MARKER: &str = "CLI_GIT_STACKED_PUSH";
pub type MergePush<'a> =
&'a dyn Fn(&PushGit, &[OsString], &[OsString], &[String]) -> Result<i32, SingleFlightError>;
pub struct StackRequest<'a> {
pub git: &'a PushGit,
pub global_prefix: &'a [OsString],
pub merge_push: MergePush<'a>,
pub region: &'a [OsString],
}
fn checked_out_branch(git: &PushGit) -> Option<String> {
let arguments: Vec<OsString> = ["symbolic-ref", "--quiet", "HEAD"]
.iter()
.map(OsString::from)
.collect();
let output = run_query(git, &arguments).ok()?;
if output.code != Some(0) {
return None;
}
let name = String::from_utf8_lossy(&output.stdout).trim().to_owned();
if name.is_empty() {
return None;
}
return Some(name);
}
pub fn stack_failed_push(
request: &StackRequest<'_>,
failed_code: i64,
err: &mut dyn Write,
) -> Option<i32> {
let tokens: Vec<OsString> = stackable_option_tokens(request.region)?;
let git = request.git;
let updates = match probe_manual_push_updates(git, request.global_prefix, request.region) {
Ok(found) => found,
Err(error) => {
debug(
TAG,
&format!("the failed push's updates could not be read: {}", error.message),
);
return None;
}
};
let [update] = updates.as_slice() else {
return None;
};
let local = update.local_oid.clone()?;
update.remote_oid.as_ref()?;
if !update.remote_ref.starts_with("refs/heads/") {
return None;
}
let branch_ref: Option<String> = checked_out_branch(git);
let plan = Reconcile {
destination: Destination {
remote: update.remote_name.clone(),
merge_ref: update.remote_ref.clone(),
},
local,
branch_ref: branch_ref.as_deref(),
label: "push",
explicit: true,
};
let mut push_merge = |destination: &Destination, merge_oid: &str| -> Result<PushAttempt, SingleFlightError> {
let code: i32 = (request.merge_push)(
git,
request.global_prefix,
&tokens,
&[destination.remote.clone(), format!("{merge_oid}:{}", destination.merge_ref)],
)?;
return Ok(PushAttempt {
exit_code: i64::from(code),
output: String::new(),
});
};
let rejected = PushAttempt {
exit_code: failed_code,
output: String::new(),
};
match integrate_and_push(git, &plan, rejected, &mut push_merge) {
Ok(Integrated::NotDivergent(_)) => return None,
Ok(Integrated::Finished(attempt)) => {
let text = attempt.output.trim();
if !text.is_empty() {
let _written = writeln!(err, "{text}");
}
return Some(i32::try_from(attempt.exit_code).unwrap_or(1));
}
Err(error) => {
debug(TAG, &format!("stacking the failed push stopped: {error}"));
return None;
}
}
}
pub fn run_stacked_push(forward: &PushForward) -> i32 {
let (Some(prefix), Some(region)) = (
forward.arguments.get(..forward.subcommand_index),
forward.arguments.get(forward.subcommand_index + 1..),
) else {
return 1;
};
if forward.lfs
&& let Some(code) = upload_first(&InteractiveUpload {
prefix,
region,
real_git: forward.real_git.as_path(),
environment: forward.environment.as_slice(),
overlay: forward.overlay.as_slice(),
settings: forward.lfs_settings.as_deref(),
})
{
return code;
}
let code: i32 = match run_forwarded_git(
forward.real_git.as_path(),
forward.arguments.as_slice(),
forward.overlay.as_slice(),
forward.copy.as_deref(),
) {
ForwardedRun::Ran(0) => return 0,
ForwardedRun::Ran(code) => code,
ForwardedRun::Failed { stderr, code } => {
let _written = std::io::stderr().write_all(stderr.as_bytes());
return code;
}
};
if !forward.stack {
return code;
}
let Ok(cwd) = std::env::current_dir() else {
return code;
};
let git = PushGit {
real_git: forward.real_git.clone(),
cwd,
environment: environment_with_overlay(forward.environment.as_slice(), forward.overlay.as_slice()),
lfs: false,
};
let request = StackRequest {
git: &git,
global_prefix: prefix,
merge_push: &gated_merge_push,
region,
};
return stack_failed_push(&request, i64::from(code), &mut std::io::stderr()).unwrap_or(code);
}
#[cfg(test)]
#[path = "push_stacking_tests.rs"]
mod tests;