1use super::auto_push_reconcile::{Destination, Integrated, Reconcile, integrate_and_push};
22use super::auto_push_single_flight::{PushAttempt, PushGit, SingleFlightError, run_query};
24use super::action::PushForward;
26use super::child_environment::environment_with_overlay;
28use super::forwarding::git_command;
30use super::commit_command::effective_directory;
32use super::command_push::stackable_option_tokens;
34use super::diagnostic_log::debug;
36use super::lfs_push_precheck::lfs_refusal;
38use super::lfs_upload::run_upload_interactive;
39use super::lfs_upload_plan::plan_upload;
40use super::stacked_merge_push::gated_merge_push;
42use super::manual_push_probe::probe_manual_push_updates;
44use super::worktree_copy_forward::{ForwardedRun, run_forwarded_git};
46use std::ffi::OsString;
48use std::io::Write;
50
51const TAG: &str = "cli-git";
53
54pub const STACKED_PUSH_MARKER: &str = "CLI_GIT_STACKED_PUSH";
56
57pub type MergePush<'a> =
59 &'a dyn Fn(&PushGit, &[OsString], &[OsString], &[String]) -> Result<i32, SingleFlightError>;
60
61pub struct StackRequest<'a> {
68 pub git: &'a PushGit,
70 pub global_prefix: &'a [OsString],
72 pub merge_push: MergePush<'a>,
75 pub region: &'a [OsString],
77}
78
79fn checked_out_branch(git: &PushGit) -> Option<String> {
89 let arguments: Vec<OsString> = ["symbolic-ref", "--quiet", "HEAD"]
90 .iter()
91 .map(OsString::from)
92 .collect();
93 let output = run_query(git, &arguments).ok()?;
94 if output.code != Some(0) {
95 return None;
96 }
97 let name = String::from_utf8_lossy(&output.stdout).trim().to_owned();
98 if name.is_empty() {
99 return None;
100 }
101 return Some(name);
102}
103
104pub fn stack_failed_push(
117 request: &StackRequest<'_>,
118 failed_code: i64,
119 err: &mut dyn Write,
120) -> Option<i32> {
121 let tokens: Vec<OsString> = stackable_option_tokens(request.region)?;
122 let git = request.git;
123 let updates = match probe_manual_push_updates(git, request.global_prefix, request.region) {
124 Ok(found) => found,
125 Err(error) => {
126 debug(
127 TAG,
128 &format!("the failed push's updates could not be read: {}", error.message),
129 );
130 return None;
131 }
132 };
133 let [update] = updates.as_slice() else {
134 return None;
135 };
136 let local = update.local_oid.clone()?;
137 update.remote_oid.as_ref()?;
138 if !update.remote_ref.starts_with("refs/heads/") {
139 return None;
140 }
141 let branch_ref: Option<String> = checked_out_branch(git);
142 let plan = Reconcile {
143 destination: Destination {
144 remote: update.remote_name.clone(),
145 merge_ref: update.remote_ref.clone(),
146 },
147 local,
148 branch_ref: branch_ref.as_deref(),
149 label: "push",
150 explicit: true,
151 };
152 let mut push_merge = |destination: &Destination, merge_oid: &str| -> Result<PushAttempt, SingleFlightError> {
153 let code: i32 = (request.merge_push)(
154 git,
155 request.global_prefix,
156 &tokens,
157 &[destination.remote.clone(), format!("{merge_oid}:{}", destination.merge_ref)],
158 )?;
159 return Ok(PushAttempt {
160 exit_code: i64::from(code),
161 output: String::new(),
162 });
163 };
164 let rejected = PushAttempt {
165 exit_code: failed_code,
166 output: String::new(),
167 };
168 match integrate_and_push(git, &plan, rejected, &mut push_merge) {
169 Ok(Integrated::NotDivergent(_)) => return None,
170 Ok(Integrated::Finished(attempt)) => {
171 let text = attempt.output.trim();
172 if !text.is_empty() {
173 let _written = writeln!(err, "{text}");
175 }
176 return Some(i32::try_from(attempt.exit_code).unwrap_or(1));
177 }
178 Err(error) => {
179 debug(TAG, &format!("stacking the failed push stopped: {error}"));
180 return None;
181 }
182 }
183}
184
185pub fn run_stacked_push(forward: &PushForward) -> i32 {
195 let (Some(prefix), Some(region)) = (
196 forward.arguments.get(..forward.subcommand_index),
197 forward.arguments.get(forward.subcommand_index + 1..),
198 ) else {
199 return 1;
200 };
201 if forward.lfs {
204 if let Some(refusal) = lfs_refusal(prefix, region, forward.environment.as_slice()) {
205 let _written = std::io::stderr().write_all(refusal.as_bytes());
207 return 1;
208 }
209 let mut query = |query_arguments: &[&str]| -> Option<String> {
210 let mut arguments: Vec<OsString> = prefix.to_vec();
211 arguments.extend(query_arguments.iter().map(OsString::from));
212 let output =
213 git_command(forward.real_git.as_path(), arguments.as_slice(), forward.overlay.as_slice()).output().ok()?;
214 if !output.status.success() {
215 return None;
216 }
217 return Some(String::from_utf8_lossy(output.stdout.as_slice()).trim().to_owned());
218 };
219 let directory: std::path::PathBuf = effective_directory(std::env::current_dir().unwrap_or_default().as_path(), prefix);
220 if let Some(plan) = plan_upload(prefix, region, forward.environment.as_slice(), directory.as_path(), &mut query)
221 && let Some(code) = run_upload_interactive(
222 &plan,
223 prefix,
224 directory.as_path(),
225 forward.real_git.as_path(),
226 forward.environment.as_slice(),
227 forward.overlay.as_slice(),
228 &mut std::io::stderr(),
229 )
230 {
231 return code;
232 }
233 }
234 let code: i32 = match run_forwarded_git(
235 forward.real_git.as_path(),
236 forward.arguments.as_slice(),
237 forward.overlay.as_slice(),
238 forward.copy.as_deref(),
239 ) {
240 ForwardedRun::Ran(0) => return 0,
241 ForwardedRun::Ran(code) => code,
242 ForwardedRun::Failed { stderr, code } => {
243 let _written = std::io::stderr().write_all(stderr.as_bytes());
245 return code;
246 }
247 };
248 if !forward.stack {
249 return code;
250 }
251 let Ok(cwd) = std::env::current_dir() else {
252 return code;
253 };
254 let git = PushGit {
255 real_git: forward.real_git.clone(),
256 cwd,
257 environment: environment_with_overlay(forward.environment.as_slice(), forward.overlay.as_slice()),
258 lfs: false,
259 };
260 let request = StackRequest {
261 git: &git,
262 global_prefix: prefix,
263 merge_push: &gated_merge_push,
264 region,
265 };
266 return stack_failed_push(&request, i64::from(code), &mut std::io::stderr()).unwrap_or(code);
267}
268
269#[cfg(test)]
271#[path = "push_stacking_tests.rs"]
272mod tests;