Skip to main content

ralph/git/commit/
rebase_push.rs

1//! Rebase-aware push helpers.
2//!
3//! Purpose:
4//! - Implement bounded retry logic for pushing with non-fast-forward recovery.
5//!
6//! Responsibilities:
7//! - Detect non-fast-forward push failures.
8//! - Rebase onto the appropriate upstream reference and retry.
9//! - Set local upstream tracking when remote-only branches already exist.
10//!
11//! Scope:
12//! - Retry-oriented push orchestration only.
13//! - Lower-level git commands live in sibling modules.
14//!
15//! Usage:
16//! - Re-exported as `crate::git::push_upstream_with_rebase`.
17//!
18//! Invariants/assumptions:
19//! - Retries stay bounded.
20//! - Upstream fallback targets use `origin/<current-branch>` when explicit tracking is absent.
21
22use 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
32/// Push HEAD to upstream, rebasing on non-fast-forward rejections.
33///
34/// If the branch has no upstream yet, this will create one via `git push -u origin HEAD`.
35/// When the push is rejected because the remote has new commits, this will:
36/// - `git fetch origin --prune`
37/// - `git rebase <upstream>`
38/// - retry push with a bounded number of attempts
39pub 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}