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