Resolving Git ‘rejected non-fast-forward’ Errors on macOS Local Branches

Fix Git 'rejected non-fast-forward' errors on macOS from divergent history. Learn to safely resolve branch conflicts using merge, rebase, or `push --force-with-lease`.


Fix Git 'rejected non-fast-forward' errors on macOS from divergent history. Learn to safely resolve branch conflicts using merge, rebase, or `push –force-with-lease`.

Welcome, fellow developer! Encountering a rejected non-fast-forward error when pushing to a remote Git repository on your macOS local environment is a common roadblock. This guide, crafted by an expert SysAdmin with years of DevOps experience, will demystify this error and provide robust, step-by-step solutions to get your commits pushed successfully while maintaining a clean, accurate project history.

Symptom & Error Signature

When attempting to push your local changes to a remote Git repository, you might see an error message similar to this in your terminal:

$ git push origin main
To https://github.com/your-org/your-repo.git
 ! [rejected]        main -> main (non-fast-forward)
error: failed to push some refs to 'https://github.com/your-org/your-repo.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

This output clearly indicates that your push was rejected because your local branch's history has diverged from the remote branch, preventing a "fast-forward" update.

Root Cause Analysis

The rejected non-fast-forward error fundamentally means that your local branch's commit history is not a direct linear extension of the remote branch's history. Git's default behavior is to only allow "fast-forward" pushes, where the remote branch can simply move its pointer forward to your new commit(s) without any merging or rewriting of history. When this isn't possible, Git rejects the push to prevent unintended loss of work or corruption of shared history.

The primary reasons for this divergence include:

  1. Remote Changes: The most common cause is that other team members have pushed new commits to the remote branch (e.g., origin/main) since the last time you pulled from it. Your local branch is now "behind" the remote, and your local commits are built on an older state of the repository.
  2. Local History Rewriting: You might have locally rewritten history on a branch that has already been pushed to the remote. This can happen through actions like:
    • git rebase: Reordering, combining, or editing commits.
    • git commit --amend: Changing the last commit.
    • git reset --hard or --soft: Moving the branch pointer, potentially discarding or staging commits. If you've rewritten history locally and then try to push, Git sees a completely different history for what it thought was the same branch.
  3. Accidental Commits: You might have committed sensitive files or temporary changes and then reverted them locally, but the remote still holds the original state.

Step-by-Step Resolution

To resolve this error, you need to reconcile your local branch's history with the remote's. There are generally two safe strategies: merging or rebasing. A third, more aggressive strategy, force pushing, is reserved for specific, cautious scenarios.

1. Assess Your Current Branch State

Before making any changes, it's crucial to understand the state of your local and remote branches.

# Ensure you are on the correct branch
git status

# Fetch the latest remote changes without merging
git fetch origin

# Visualize your local and remote branch history
# This helps identify divergence points
git log --oneline --graph --decorate --all

Look for origin/main (or your target remote branch) in the git log output. If HEAD -> main is not directly ahead of origin/main, you have a divergence.

2. Strategy A: Merge Remote Changes (Recommended for collaboration)

Merging is the safest and most common approach, especially when working on shared branches. It integrates remote changes into your local branch by creating a new "merge commit".

  1. Pull Remote Changes: This command performs a git fetch followed by a git merge of origin/main into your current local branch.

    git pull origin main
    
  2. Resolve Conflicts (if any): If there are conflicting changes between your local commits and the remote commits, Git will pause the merge and prompt you to resolve them.

    # Open conflicting files in your editor
    # Look for "<<<<<<< HEAD", "=======", ">>>>>>> <commit_hash>" markers
    # Edit the files to combine the changes as desired
    

    After resolving conflicts in each file:

    # Stage the resolved files
    git add .
    
    # Complete the merge commit. Git will provide a default message.
    git commit
    

    Carefully review and test your code after resolving conflicts to ensure all necessary changes from both sides are correctly integrated and the application functions as expected.

  3. Push Your Merged Changes: Once the merge is complete and conflicts are resolved, your local history is now a fast-forward of the remote. You can now push.

    git push origin main
    

3. Strategy B: Rebase Local Changes (For a cleaner, linear history)

Rebasing re-applies your local commits on top of the remote branch's latest history. This results in a linear history without merge commits, which many developers prefer for feature branches.

DO NOT REBASE A SHARED BRANCH THAT OTHERS HAVE ALREADY PULLED! Rebasing rewrites history. If others have based their work on your old commits, rebasing will cause significant problems for them and require complex recovery. Only rebase branches you're working on locally, or feature branches that have not yet been shared/pulled by teammates.

  1. Start the Rebase:

    git rebase origin/main
    
  2. Resolve Conflicts (if any): During a rebase, conflicts are resolved commit-by-commit as Git attempts to re-apply each of your local commits onto the new base.

    # Open conflicting files in your editor and resolve them
    # After resolving:
    git add .
    
    # Continue the rebase.
    # This applies the current patch and moves to the next.
    git rebase --continue
    

    Repeat this process for each commit where conflicts arise. If you need to stop the rebase at any point:

    # To abort the rebase and return to the state before it started:
    git rebase --abort
    
  3. Push Your Rebased Changes: Once the rebase is complete, your local branch's history is a direct extension of the remote's, making it a fast-forward push.

    git push origin main
    

4. Strategy C: Force Push (Use with Extreme Caution!)

Force pushing (git push --force or git push --force-with-lease) is a powerful command that overwrites the remote branch's history with your local history, regardless of divergence. This should only be used in very specific scenarios, typically when you have intentionally rewritten history on a branch that you are certain no one else has pulled.

NEVER use git push --force on shared main/master branches or any branch others are actively working on. Doing so will erase their work and create immense difficulties for the team. This command is primarily for correcting mistakes on your personal, unshared feature branches immediately after pushing.

  1. Identify the Need: This strategy is appropriate if, for example, you just pushed a commit with a typo, immediately did a git commit --amend or git rebase -i HEAD~1 to fix it locally, and know for a fact that no one else has pulled your original incorrect commit.

  2. Use git push --force-with-lease: This is the safer alternative to git push --force. It performs a force push only if the remote branch is in the state you expect it to be. If someone else pushed to the remote branch before your force push, force-with-lease will fail, preventing accidental overwrites.

    git push --force-with-lease origin main
    

    If you are absolutely certain and understand the implications (e.g., on a personal feature branch you own), you can use the more aggressive:

    git push --force origin main
    

    Always communicate with your team if you intend to use any form of force pushing, even on feature branches, to avoid potential conflicts and confusion.

By understanding the root causes and applying the appropriate strategy—merging for collaborative work, rebasing for clean history on personal branches, or carefully force-pushing when rewriting unshared history—you can effectively manage your Git workflow and avoid the dreaded rejected non-fast-forward error.