1use super::auto_push_reconcile::{
18 RECONCILE_FAILED_PREFIX, RECONCILE_LINE_PREFIX, named_destination_push, reconcile_divergent_push,
19};
20use super::auto_push_single_flight::{
22 PushAttempt, PushGit, SingleFlightError, SingleFlightKind, SingleFlightOutcome,
23 resolve_push_coordination_paths, run_query, run_single_flight_push_repeating,
24};
25use super::bounded_push::{LOCK_WAIT_VARIABLE, lock_wait_limit, push_deadline};
27use super::lock_holder_facts::holder_facts;
29use super::lfs_bounded_push::run_push;
31use super::diagnostic_log::debug;
33use super::owner_lock_record::{PublishedOwner, read_owner_lock_record};
35use std::ffi::OsString;
37use std::io::Write;
39
40const TAG: &str = "cli-git";
42
43const REMOTE_LINE_PREFIX: &str = "remote: ";
45
46pub const DETACHED_HEAD_NOTE: &str = "cli-git: HEAD is detached (mid-rebase/cherry-pick or a detached checkout); skipping auto-push.";
48
49pub const PUSH_FAILED_NOTE: &str =
51 "cli-git: commit saved locally, but auto-push to origin failed; run `git push` when ready.";
52
53const FIRST_PUSH_ARGUMENTS: &[&str] = &["push", "--set-upstream", "origin", "HEAD"];
55
56const UPSTREAM_PUSH_ARGUMENTS: &[&str] = &["push"];
58
59#[derive(Clone, Copy, Debug, Eq, PartialEq)]
69pub enum AutoPushOutcome {
70 Skipped,
72 Pushed,
74 Failed,
76}
77
78#[derive(Clone, Debug, Eq, PartialEq)]
88pub struct AutoPushResult {
89 pub outcome: AutoPushOutcome,
91 pub exit_code: i64,
93 pub shown: String,
95 pub joined: bool,
97}
98
99pub fn filter_push_output(output: &str, exit_code: i64) -> String {
109 if exit_code != 0 {
110 return output.to_owned();
111 }
112 return output
113 .split('\n')
114 .filter(|line| {
115 return line.starts_with(REMOTE_LINE_PREFIX) || line.starts_with(RECONCILE_LINE_PREFIX);
116 })
117 .collect::<Vec<&str>>()
118 .join("\n");
119}
120
121fn query_succeeds(git: &PushGit, arguments: &[&str]) -> Result<bool, SingleFlightError> {
131 let argv: Vec<OsString> = arguments.iter().map(OsString::from).collect();
132 return Ok(run_query(git, &argv)?.code == Some(0));
133}
134
135fn read_symbolic_head(git: &PushGit) -> Result<Option<String>, SingleFlightError> {
145 let argv: Vec<OsString> = ["symbolic-ref", "--quiet", "HEAD"]
146 .iter()
147 .map(OsString::from)
148 .collect();
149 let output = run_query(git, &argv)?;
150 if output.code != Some(0) {
151 return Ok(None);
152 }
153 return Ok(Some(
154 super::js_text::trim_javascript(&String::from_utf8_lossy(&output.stdout)).to_owned(),
155 ));
156}
157
158fn run_push_command(git: &PushGit, arguments: &[&str]) -> Result<PushAttempt, SingleFlightError> {
171 let argv: Vec<OsString> = arguments.iter().map(OsString::from).collect();
172 return run_push(git, &argv);
173}
174
175fn waiting_line(git: &PushGit, branch_ref: &str, waited: Option<std::time::Duration>) -> String {
186 let (pid, facts): (Option<i64>, String) = match holder(git, branch_ref) {
187 Some((pid, lock_directory)) => (
188 Some(pid),
189 holder_facts(pid, lock_directory.as_path(), push_deadline(&git.environment)),
190 ),
191 None => (None, String::new()),
192 };
193 let holder: String = match pid {
194 Some(number) => format!("process {number}"),
195 None => String::from("another cli-git process"),
196 };
197 let progress: String = match waited {
199 Some(elapsed) => format!(" ({} seconds so far)", elapsed.as_secs()),
200 None => String::new(),
201 };
202 return format!(
203 "cli-git: waiting for {holder} to finish pushing {branch_ref}{progress}; giving up after {} seconds \
204 ({LOCK_WAIT_VARIABLE} changes this). The commit is already saved.{facts}",
205 lock_wait_limit(&git.environment).as_secs()
206 );
207}
208
209fn holder(git: &PushGit, branch_ref: &str) -> Option<(i64, std::path::PathBuf)> {
216 let paths = resolve_push_coordination_paths(git, branch_ref).ok()?;
217 return match read_owner_lock_record(&paths.lock_directory) {
218 Ok(PublishedOwner::Owner(record)) => Some((record.owner_pid, paths.lock_directory)),
219 Ok(PublishedOwner::Busy) | Err(_) => None,
220 };
221}
222
223fn report_outcome(
234 outcome: &SingleFlightOutcome,
235 landed_oid: &str,
236 branch_ref: &str,
237 err: &mut dyn Write,
238) -> std::io::Result<AutoPushResult> {
239 let joined = matches!(
240 outcome.kind,
241 SingleFlightKind::JoinedPushed | SingleFlightKind::JoinedFailed
242 );
243 let failed = matches!(
244 outcome.kind,
245 SingleFlightKind::Failed | SingleFlightKind::JoinedFailed
246 );
247 let shown = if outcome.kind == SingleFlightKind::JoinedPushed {
248 String::new()
249 } else {
250 filter_push_output(&outcome.output, outcome.exit_code)
251 };
252 if !shown.is_empty() {
253 writeln!(err, "{shown}")?;
254 }
255 if !outcome.covered {
256 writeln!(
257 err,
258 "cli-git: landed commit {landed_oid} is no longer on {branch_ref}; auto-push pushed {} without it.",
259 outcome.tip
260 )?;
261 }
262 if failed && !outcome.output.contains(RECONCILE_FAILED_PREFIX) {
264 writeln!(err, "{PUSH_FAILED_NOTE}")?;
265 }
266 return Ok(AutoPushResult {
267 outcome: if failed {
268 AutoPushOutcome::Failed
269 } else {
270 AutoPushOutcome::Pushed
271 },
272 exit_code: outcome.exit_code,
273 shown,
274 joined,
275 });
276}
277
278enum Planned {
280 Skip(AutoPushResult),
282 Push {
284 branch_ref: String,
286 has_upstream: bool,
288 arguments: Vec<String>,
290 },
291}
292
293fn plan_push(git: &PushGit, err: &mut dyn Write) -> Result<Planned, SingleFlightError> {
305 let has_upstream = query_succeeds(
306 git,
307 &[
308 "rev-parse",
309 "--abbrev-ref",
310 "--symbolic-full-name",
311 "@{upstream}",
312 ],
313 )?;
314 if !has_upstream && !query_succeeds(git, &["remote", "get-url", "origin"])? {
315 debug(TAG, "no upstream and no origin remote; skipping auto-push");
316 return Ok(Planned::Skip(AutoPushResult {
317 outcome: AutoPushOutcome::Skipped,
318 exit_code: 0,
319 shown: String::new(),
320 joined: false,
321 }));
322 }
323 let Some(branch_ref) = read_symbolic_head(git)? else {
324 let _written = writeln!(err, "{DETACHED_HEAD_NOTE}");
326 return Ok(Planned::Skip(AutoPushResult {
327 outcome: AutoPushOutcome::Skipped,
328 exit_code: 0,
329 shown: DETACHED_HEAD_NOTE.to_owned(),
330 joined: false,
331 }));
332 };
333 let named: Option<Vec<String>> = if has_upstream {
334 named_destination_push(git, &branch_ref)?
335 } else {
336 None
337 };
338 let named_arguments: Vec<String> = named.into_iter().flatten().collect();
339 let arguments: Vec<String> = if !named_arguments.is_empty() {
340 named_arguments
341 } else if has_upstream {
342 UPSTREAM_PUSH_ARGUMENTS.iter().map(|argument| { return (*argument).to_owned(); }).collect()
343 } else {
344 FIRST_PUSH_ARGUMENTS.iter().map(|argument| { return (*argument).to_owned(); }).collect()
345 };
346 return Ok(Planned::Push { branch_ref, has_upstream, arguments });
347}
348
349pub fn push_once(git: &PushGit, landed_oid: &str, err: &mut dyn Write) -> Result<AutoPushResult, SingleFlightError> {
360 let (branch_ref, has_upstream, arguments): (String, bool, Vec<String>) = match plan_push(git, err)? {
361 Planned::Skip(result) => return Ok(result),
362 Planned::Push { branch_ref, has_upstream, arguments } => (branch_ref, has_upstream, arguments),
363 };
364 let argument_views: Vec<&str> = arguments.iter().map(String::as_str).collect();
365 debug(TAG, &format!("pushing {landed_oid} with {argument_views:?} on {branch_ref}"));
366 let mut attempt: PushAttempt = run_push_command(git, argument_views.as_slice())?;
367 if has_upstream && attempt.exit_code != 0 {
368 attempt = reconcile_divergent_push(git, &branch_ref, attempt)?;
369 }
370 let failed: bool = attempt.exit_code != 0;
371 let shown: String = filter_push_output(&attempt.output, attempt.exit_code);
372 if !shown.is_empty() {
373 writeln!(err, "{shown}").map_err(SingleFlightError::Io)?;
374 }
375 if failed && !attempt.output.contains(RECONCILE_FAILED_PREFIX) {
376 writeln!(err, "{PUSH_FAILED_NOTE}").map_err(SingleFlightError::Io)?;
377 }
378 return Ok(AutoPushResult {
379 outcome: if failed { AutoPushOutcome::Failed } else { AutoPushOutcome::Pushed },
380 exit_code: attempt.exit_code,
381 shown,
382 joined: false,
383 });
384}
385
386pub fn auto_push(
398 git: &PushGit,
399 landed_oid: &str,
400 err: &mut dyn Write,
401) -> Result<AutoPushResult, SingleFlightError> {
402 let (branch_ref, has_upstream, owned_arguments): (String, bool, Vec<String>) = match plan_push(git, err)? {
403 Planned::Skip(result) => return Ok(result),
404 Planned::Push { branch_ref, has_upstream, arguments } => (branch_ref, has_upstream, arguments),
405 };
406 let push_arguments: &[&str] = &owned_arguments.iter().map(String::as_str).collect::<Vec<&str>>();
407 debug(
408 TAG,
409 &format!("auto-pushing {landed_oid} with {push_arguments:?} on {branch_ref}"),
410 );
411 let coordinated = {
412 let mut push = || {
413 let attempt = run_push_command(git, push_arguments)?;
414 if has_upstream && attempt.exit_code != 0 {
416 return reconcile_divergent_push(git, &branch_ref, attempt);
417 }
418 return Ok(attempt);
419 };
420 let mut on_wait = |waited: Option<std::time::Duration>| {
421 let line = waiting_line(git, &branch_ref, waited);
422 debug(TAG, &line);
423 let _written = writeln!(err, "{line}");
425 };
426 run_single_flight_push_repeating(git, &branch_ref, landed_oid, &mut push, &mut on_wait)
427 };
428 return match coordinated {
429 Ok(outcome) => {
430 report_outcome(&outcome, landed_oid, &branch_ref, err).map_err(SingleFlightError::Io)
431 }
432 Err(error) => {
433 debug(TAG, &format!("auto-push coordination failed: {error}"));
434 let _written = writeln!(
435 err,
436 "cli-git: auto-push of {landed_oid} could not run: {error}"
437 );
438 let _noted = writeln!(err, "{PUSH_FAILED_NOTE}");
439 Ok(AutoPushResult {
440 outcome: AutoPushOutcome::Failed,
441 exit_code: 1,
442 shown: error.to_string(),
443 joined: false,
444 })
445 }
446 };
447}
448
449#[cfg(test)]
451#[path = "auto_push_tests.rs"]
452mod tests;
453
454#[cfg(test)]
456#[path = "auto_push_concurrency_tests.rs"]
457mod concurrency_tests;