git_policy_cli/
push_stacking.rs1use super::auto_push_reconcile::{Destination, Integrated, Reconcile, integrate_and_push};
20use super::auto_push_single_flight::{PushAttempt, PushGit, SingleFlightError, run_query};
22use super::start_retry::RetryStart;
24use super::action::PushForward;
26use super::child_environment::environment_with_overlay;
28use super::command_push::stackable_option_tokens;
30use super::diagnostic_log::debug;
32use super::lfs_pre_push::with_lfs_hook;
34use super::manual_push_probe::probe_manual_push_updates;
36use super::worktree_copy_forward::{ForwardedRun, run_forwarded_git};
38use std::ffi::OsString;
40use std::io::Write;
42use std::path::Path;
44
45const TAG: &str = "cli-git";
47
48pub const STACKED_PUSH_MARKER: &str = "CLI_GIT_STACKED_PUSH";
50
51pub struct StackRequest<'a> {
58 pub git: &'a PushGit,
60 pub own_executable: &'a Path,
62 pub global_prefix: &'a [OsString],
64 pub region: &'a [OsString],
66}
67
68fn checked_out_branch(git: &PushGit) -> Option<String> {
78 let arguments: Vec<OsString> = ["symbolic-ref", "--quiet", "HEAD"]
79 .iter()
80 .map(OsString::from)
81 .collect();
82 let output = run_query(git, &arguments).ok()?;
83 if output.code != Some(0) {
84 return None;
85 }
86 let name = String::from_utf8_lossy(&output.stdout).trim().to_owned();
87 if name.is_empty() {
88 return None;
89 }
90 return Some(name);
91}
92
93pub fn stack_failed_push(
106 request: &StackRequest<'_>,
107 failed_code: i64,
108 err: &mut dyn Write,
109) -> Option<i32> {
110 let tokens: Vec<OsString> = stackable_option_tokens(request.region)?;
111 let git = request.git;
112 let updates = match probe_manual_push_updates(git, request.global_prefix, request.region) {
113 Ok(found) => found,
114 Err(error) => {
115 debug(
116 TAG,
117 &format!("the failed push's updates could not be read: {}", error.message),
118 );
119 return None;
120 }
121 };
122 let [update] = updates.as_slice() else {
123 return None;
124 };
125 let local = update.local_oid.clone()?;
126 update.remote_oid.as_ref()?;
127 if !update.remote_ref.starts_with("refs/heads/") {
128 return None;
129 }
130 let branch_ref: Option<String> = checked_out_branch(git);
131 let plan = Reconcile {
132 destination: Destination {
133 remote: update.remote_name.clone(),
134 merge_ref: update.remote_ref.clone(),
135 },
136 local,
137 branch_ref: branch_ref.as_deref(),
138 label: "push",
139 explicit: true,
140 };
141 let mut push_merge = |destination: &Destination, merge_oid: &str| -> Result<PushAttempt, SingleFlightError> {
142 let status = std::process::Command::new(request.own_executable)
143 .current_dir(&git.cwd)
144 .args(request.global_prefix)
145 .arg("push")
146 .args(&tokens)
147 .arg(&destination.remote)
148 .arg(format!("{merge_oid}:{}", destination.merge_ref))
149 .env_clear()
150 .envs(git.environment.iter().map(|(name, value)| {
151 return (name, value);
152 }))
153 .env(STACKED_PUSH_MARKER, "1")
154 .status_retrying()
155 .map_err(SingleFlightError::Io)?;
156 return Ok(PushAttempt {
157 exit_code: i64::from(status.code().unwrap_or(1)),
158 output: String::new(),
159 });
160 };
161 let rejected = PushAttempt {
162 exit_code: failed_code,
163 output: String::new(),
164 };
165 match integrate_and_push(git, &plan, rejected, &mut push_merge) {
166 Ok(Integrated::NotDivergent(_)) => return None,
167 Ok(Integrated::Finished(attempt)) => {
168 let text = attempt.output.trim();
169 if !text.is_empty() {
170 let _written = writeln!(err, "{text}");
172 }
173 return Some(i32::try_from(attempt.exit_code).unwrap_or(1));
174 }
175 Err(error) => {
176 debug(TAG, &format!("stacking the failed push stopped: {error}"));
177 return None;
178 }
179 }
180}
181
182pub fn run_stacked_push(forward: &PushForward) -> i32 {
192 let first_arguments: Vec<OsString> = match forward.lfs_pre_push.as_deref() {
194 Some(executable) => with_lfs_hook(forward.arguments.as_slice(), executable),
195 None => forward.arguments.clone(),
196 };
197 let code: i32 = match run_forwarded_git(
198 forward.real_git.as_path(),
199 first_arguments.as_slice(),
200 forward.overlay.as_slice(),
201 forward.copy.as_deref(),
202 ) {
203 ForwardedRun::Ran(0) => return 0,
204 ForwardedRun::Ran(code) => code,
205 ForwardedRun::Failed { stderr, code } => {
206 let _written = std::io::stderr().write_all(stderr.as_bytes());
208 return code;
209 }
210 };
211 let (Ok(cwd), Ok(own_executable)) = (std::env::current_dir(), std::env::current_exe()) else {
212 return code;
213 };
214 let git = PushGit {
215 real_git: forward.real_git.clone(),
216 cwd,
217 environment: environment_with_overlay(forward.environment.as_slice(), forward.overlay.as_slice()),
218 lfs_pre_push: None,
219 };
220 let Some(prefix) = forward.arguments.get(..forward.subcommand_index) else {
221 return code;
222 };
223 let Some(region) = forward.arguments.get(forward.subcommand_index + 1..) else {
224 return code;
225 };
226 let request = StackRequest {
227 git: &git,
228 own_executable: own_executable.as_path(),
229 global_prefix: prefix,
230 region,
231 };
232 return stack_failed_push(&request, i64::from(code), &mut std::io::stderr()).unwrap_or(code);
233}
234
235#[cfg(test)]
237#[path = "push_stacking_tests.rs"]
238mod tests;