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