ralph/git/commit/
rebase_push.rs1use std::path::Path;
23
24use crate::git::current_branch;
25use crate::git::error::GitError;
26
27use super::upstream::{
28 is_ahead_of_ref, is_ahead_of_upstream, push_upstream, push_upstream_allow_create, rebase_onto,
29 reference_exists, set_upstream_to, upstream_ref,
30};
31
32pub fn push_upstream_with_rebase(repo_root: &Path) -> Result<(), GitError> {
40 const MAX_PUSH_ATTEMPTS: usize = 4;
41
42 let branch = current_branch(repo_root).map_err(GitError::Other)?;
43 let fallback_upstream = format!("origin/{}", branch);
44 let ahead = match is_ahead_of_upstream(repo_root) {
45 Ok(ahead) => ahead,
46 Err(GitError::NoUpstream) | Err(GitError::NoUpstreamConfigured) => {
47 if reference_exists(repo_root, &fallback_upstream)? {
48 is_ahead_of_ref(repo_root, &fallback_upstream)?
49 } else {
50 true
51 }
52 }
53 Err(err) => return Err(err),
54 };
55
56 if !ahead {
57 if upstream_ref(repo_root).is_err() && reference_exists(repo_root, &fallback_upstream)? {
58 set_upstream_to(repo_root, &fallback_upstream)?;
59 }
60 return Ok(());
61 }
62
63 let mut last_non_fast_forward: Option<GitError> = None;
64 for _attempt in 0..MAX_PUSH_ATTEMPTS {
65 let push_result = match push_upstream(repo_root) {
66 Ok(()) => return Ok(()),
67 Err(GitError::NoUpstream) | Err(GitError::NoUpstreamConfigured) => {
68 push_upstream_allow_create(repo_root)
69 }
70 Err(err) => Err(err),
71 };
72
73 match push_result {
74 Ok(()) => return Ok(()),
75 Err(err) if is_non_fast_forward_error(&err) => {
76 let upstream = match upstream_ref(repo_root) {
77 Ok(upstream) => upstream,
78 Err(_) => fallback_upstream.clone(),
79 };
80 rebase_onto(repo_root, &upstream)?;
81 if !is_ahead_of_ref(repo_root, &upstream)? {
82 if upstream_ref(repo_root).is_err() {
83 set_upstream_to(repo_root, &upstream)?;
84 }
85 return Ok(());
86 }
87 last_non_fast_forward = Some(err);
88 }
89 Err(err) => return Err(err),
90 }
91 }
92
93 Err(last_non_fast_forward
94 .unwrap_or_else(|| GitError::PushFailed("rebase-aware push exhausted retries".to_string())))
95}
96
97fn is_non_fast_forward_error(err: &GitError) -> bool {
98 let GitError::PushFailed(detail) = err else {
99 return false;
100 };
101 let lower = detail.to_lowercase();
102 lower.contains("non-fast-forward")
103 || lower.contains("non fast-forward")
104 || lower.contains("fetch first")
105 || lower.contains("rejected")
106 || lower.contains("updates were rejected")
107}