Introduction

Git push rejected non-fast-forward errors occur when your local branch has diverged from the remote branch, and Git prevents overwriting remote commits that don't exist in your local history. This is Git's safety mechanism to prevent accidental data loss when multiple developers push to the same branch, when you push from multiple machines, or after rebasing local commits. The error message rejected non-fast-forward indicates the remote branch has commits your local branch doesn't have, and a simple push would lose those commits. The fix requires understanding branch topology, choosing between merge and rebase integration strategies, knowing when force push is safe, and implementing team workflows that prevent divergence. This guide provides production-proven troubleshooting for Git push rejection scenarios across individual development, team collaboration, and CI/CD pipeline contexts.

Symptoms

  • error: failed to push some refs to 'origin'
  • hint: Updates were rejected because the tip of your current branch is behind
  • hint: its remote counterpart is ahead of your branch
  • hint: Merge the remote changes before pushing again
  • hint: See the 'Note about fast-forwards' in 'git help push'
  • CI/CD pipeline fails at push step with non-fast-forward rejection
  • ! [rejected] main -> main (non-fast-forward)
  • Git GUI shows "Would overwrite" warning
  • Push succeeds locally but fails on shared repository
  • Protected branch push rejected even with force

Common Causes

  • Remote branch has commits not in your local branch (divergence)
  • You pulled with merge instead of rebase, creating divergent history
  • Teammate pushed to same branch while you were working
  • You pushed from different machines with different commit history
  • You rebased local commits, rewriting commit hashes
  • CI/CD pipeline auto-commits to branch while you have local changes
  • Hotfix applied directly to remote branch (production fix)
  • Squash merge on remote changed commit history
  • You reset local branch to older commit without pulling first
  • Protected branch rules block force push

Step-by-Step Fix

### 1. Diagnose branch divergence

Check the relationship between local and remote:

```bash # Check current status git status # Shows: "Your branch is behind 'origin/main' by X commits" # Or: "Your branch and 'origin/main' have diverged"

# See commit difference git log --oneline --left-right --cherry-pick HEAD...origin/main

# Output shows: # < abc123 Commits only in local (will be pushed) # > def456 Commits only in remote (would be lost)

# Visualize divergence git log --oneline --graph --all --decorate

# Count commits difference git rev-list --count HEAD ^origin/main # Local ahead git rev-list --count origin/main ^HEAD # Remote ahead ```

Understand the divergence pattern:

``` # Pattern 1: Only remote ahead (you're behind) # A-B-C-D (origin/main) # \ # A-B-C (main) # Fix: Pull and merge/rebase, then push

# Pattern 2: Only local ahead (should fast-forward) # A-B-C (main) # \ # A-B-C-D-E (origin/main) # This shouldn't happen - check git config # Fix: git config push.default matching

# Pattern 3: True divergence (both have unique commits) # D-E (origin/main unique) # / # A-B # \ # C-F (main unique) # Fix: Merge or rebase to combine histories ```

### 2. Pull and integrate remote changes

Safe integration with merge:

```bash # Pull remote changes and merge git pull origin main

# This creates a merge commit combining both histories # A-B-C-D (origin/main) # \ \ # E-F-G (main after merge)

# Resolve any merge conflicts if they occur # Then push git push origin main

# Or with explicit merge strategy git pull --no-rebase origin main ```

Integration with rebase (cleaner history):

```bash # Pull remote changes and rebase git pull --rebase origin main

# This replays your commits on top of remote # Before: # A-B-C-D (origin/main) # \ # E-F (main)

# After rebase: # A-B-C-D-E'-F' (main rebased)

# Resolve conflicts during rebase: # 1. Fix conflicted files # 2. git add <files> # 3. git rebase --continue # 4. Repeat until rebase completes

# Then push git push origin main ```

### 3. Safe force push patterns

When force push is appropriate:

```bash # ONLY force push when: # 1. You're the only one using the branch # 2. You rebased your own feature branch # 3. You understand the consequences

# NEVER force push to: # - main/master branches # - Shared development branches # - Protected branches # - Release branches others depend on

# Safe force push with lease (Git 1.8.5+) git push --force-with-lease origin main

# --force-with-lease checks that remote hasn't changed # If someone else pushed, it fails instead of overwriting # Much safer than --force

# Force push specific branch only git push --force-with-lease origin feature-branch

# Set force-with-lease as default (safer) git config --global push.default force-with-lease ```

Force push after rebase:

```bash # You rebased your feature branch git rebase -i HEAD~5 # Interactive rebase

# Commits were rewritten (new hashes) # Normal push will fail git push origin feature-branch # ! [rejected]

# Safe force push (you own this branch) git push --force-with-lease origin feature-branch

# Tell teammates if they have this branch # They need to re-clone or reset: # git fetch origin # git reset --hard origin/feature-branch ```

### 4. Handle protected branch rejection

Branch protection overrides force push:

```bash # Error: GH006: Protected branch update failed # Rules require review, CI, or specific permissions

# GitHub protected branch rules: # - Require pull request review # - Require status checks # - Require signed commits # - Restrict who can push

# Fix: Use pull request instead of direct push git push origin main # Rejected

# Create pull request gh pr create --title "Fix: ..." --body "Changes: ..." # Or use GitHub/GitLab web UI

# For urgent fixes, use emergency bypass: # 1. Create hotfix branch # 2. Cherry-pick commits # 3. Create PR with expedited review # 4. Merge with bypass approval ```

GitLab protected branches:

```bash # GitLab branch protection levels: # - No one: Branch cannot be pushed # - Maintainers: Only Maintainer+ can push # - Developers + Maintainers: Most common

# Check protection level: # Settings -> Repository -> Protected Branches

# If you're not authorized: # 1. Request Maintainer role # 2. Create merge request # 3. Ask authorized person to merge ```

### 5. Recover from accidental force push

If you force pushed and lost commits:

```bash # Find lost commits with reflog git reflog

# Output: # abc123 HEAD@{0}: reset: moving to HEAD~2 # def456 HEAD@{1}: commit: Important feature # ghi789 HEAD@{2}: commit: Another commit

# The reset at @{0} is where commits were lost # Restore to before reset git reset --hard HEAD@{1}

# Or restore specific commit git checkout -b recovery-branch def456

# Push recovered branch git push --force-with-lease origin main ```

Recover from remote:

```bash # If teammate has the lost commits: # Ask them to push a recovery branch

# Teammate runs: git checkout -b recovery-branch <lost-commit-hash> git push origin recovery-branch

# You run: git fetch origin recovery-branch git checkout recovery-branch git merge recovery-branch # Or rebase ```

GitHub/GitLab commit recovery:

```bash # Lost commits may still be in GitHub/GitLab # Check: # GitHub: https://github.com/:user/:repo/network # GitLab: https://gitlab.com/:user/:repo/-/network

# Or use GitHub API to find orphaned commits curl -H "Authorization: token $GITHUB_TOKEN" \ https://api.github.com/repos/:user/:repo/git/refs/heads

# Find commit by message or author gh api /repos/:user/:repo/commits \ --jq '.[] | select(.commit.message | contains("lost commit message"))' ```

### 6. Fix CI/CD pipeline push failures

GitHub Actions push:

```yaml # Problematic: May fail with non-fast-forward - name: Push changes run: | git config user.name "github-actions" git config user.email "github-actions@github.com" git commit -m "Auto-commit changes" git push origin main

# Fixed: Pull before push - name: Push changes run: | git config user.name "github-actions" git config user.email "github-actions@github.com" git commit -m "Auto-commit changes" git pull --rebase origin main git push origin main

# Or use force-with-lease for auto-commits - name: Push changes (force) env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | git config user.name "github-actions" git config user.email "github-actions@github.com" git commit -m "Auto-commit changes" git push --force-with-lease origin main ```

GitLab CI push:

yaml # Fixed push in CI push_changes: script: - git config user.name "gitlab-ci" - git config user.email "gitlab-ci@gitlab.com" - git commit -m "Auto-commit" || echo "Nothing to commit" - git pull --rebase origin $CI_COMMIT_BRANCH - git push origin $CI_COMMIT_BRANCH rules: - if: $CI_COMMIT_BRANCH == "main"

### 7. Prevent divergence with workflow changes

Team workflow best practices:

```bash # 1. Always pull before starting work git checkout main git pull --rebase origin main

# 2. Use feature branches for all work git checkout -b feature/my-feature

# 3. Keep feature branch updated git fetch origin git rebase origin/main

# 4. Push feature branch (first time) git push -u origin feature/my-feature

# 5. Create PR, get review, merge # Don't push directly to main

# 6. After PR merge, clean up git branch -d feature/my-feature git checkout main git pull --rebase origin main ```

Git configuration for safer pushes:

```bash # Set safe defaults globally git config --global push.default current git config --global push.forceWithLease true git config --global pull.rebase true git config --global fetch.prune true

# Or per repository cd my-repo git config push.default current git config push.forceWithLease true git config pull.rebase true

# Show divergence in status git config --global status.showUntrackedFiles all ```

### 8. Handle rebase-induced rejections

After rebasing, push fails:

```bash # You rebased onto updated main git rebase origin/main

# Commit hashes changed, normal push fails git push origin feature-branch # ! [rejected]

# Safe force push (your feature branch) git push --force-with-lease origin feature-branch

# If teammate has your old branch: # 1. Tell them you rebased # 2. They run: git fetch origin git reset --hard origin/feature-branch ```

Rebase abort and recovery:

```bash # Rebase went wrong, abort git rebase --abort

# You're back to pre-rebase state # Try merge instead git pull origin main

# Or try rebase with different options git pull --rebase --strategy-option=theirs origin main ```

### 9. Resolve complex divergence scenarios

Multiple divergent branches:

```bash # Your local main and multiple feature branches diverged

# Step 1: Fix main first git checkout main git fetch origin git reset --hard origin/main # If you don't need local main commits

# Step 2: Rebase feature branches onto fixed main git checkout feature-1 git rebase main

# Step 3: Force push feature branches you own git push --force-with-lease origin feature-1

# Step 4: Delete abandoned branches git branch -D old-feature-branch ```

Cherry-pick recovery:

```bash # Remote has commits you need, but merge/rebase won't work

# Find remote commits git log origin/main --oneline

# Cherry-pick specific commits git checkout main git cherry-pick abc123 git cherry-pick def456

# Push now (fast-forward if only cherry-picked) git push origin main ```

### 10. Monitor and alert on push failures

CI/CD monitoring:

```yaml # GitHub Actions - notify on push failure - name: Push changes id: push run: | git push origin main || echo "failed"

  • name: Notify on failure
  • if: steps.push.outcome == 'failure'
  • run: |
  • curl -X POST $SLACK_WEBHOOK \
  • -d '{"text":"Push failed on main - possible divergence"}'
  • `

Git hook for pre-push validation:

```bash # .git/hooks/pre-push #!/bin/bash

# Check for divergence before push remote=$(git rev-parse --symbolic-full-name --verify @{push}) if [ -n "$remote" ]; then merge_base=$(git merge-base HEAD "$remote") if [ "$merge_base" != "$remote" ]; then echo "Error: Branch has diverged from remote" echo "Run 'git pull --rebase' before pushing" exit 1 fi fi

exit 0 ```

Prevention

  • Always pull before starting work and before pushing
  • Use feature branches, never push directly to main
  • Configure push.forceWithLease as default
  • Set pull.rebase = true for cleaner history
  • Use protected branches with PR requirements
  • Implement pre-push hooks to catch divergence
  • Communicate with team before rebasing shared branches
  • Document force push procedures in team runbook
  • Use CI/CD for automated pushes instead of manual
  • Monitor push failure metrics in pipelines
  • **Permission denied (publickey)**: SSH key authentication failed
  • **Remote: Repository not found**: Wrong URL or no access
  • **fatal: Could not read from remote repository**: Network/auth issue
  • **error: RPC failed; curl 55**: Large push timeout
  • **pre-receive hook declined**: Server-side hook rejected push