Hello! One of the most common problems I see folks struggling with in Git iswhen a local branch (like main
) and a remote branch (maybe also calledmain
) have diverged.
There are two things that make this situation hard:
- If you’re not used to interpreting git’s error messages, it’s nontrivial toeven realize that your
main
has diverged from the remotemain
(gitwill often just give you an intimidating but generic error message like! [rejected] main -> main (non-fast-forward) error: failed to push some refs to 'github.com:jvns/int-exposed'
) - Once you realize that your branch has diverged from the remote
main
, thereno single clear way to handle it (what you need to do depends on thesituation and your git workflow)
So let’s talk about a) how to recognize when you’re in a situation where a localbranch and remote branch have diverged and b) what you can do about it! Here’s aquick table of contents:
- what does “diverged” mean?
- recognizing when branches are diverged
- way 1: git status
- way 2: git push
- way 3: git pull
- there’s no one solution
- solution 1.1: git pull –rebase
- solution 1.2: git pull –no-rebase
- solution 2.1: git push –force
- solution 2.2: git push –force-with-lease
- solution 3: git reset –hard origin/main
Let’s start with what it means for 2 branches to have “diverged”.
what does “diverged” mean?
If you have a local main
and a remote main
, there are 4 basic configurations:
1: up to date. The local and remote main
branches are in the exact same place. Something like this:
a - b - c - d ^ LOCAL ^ REMOTE
2: local is behind
Here you might want to git pull
. Something like this:
a - b - c - d - e ^ LOCAL ^ REMOTE
3: remote is behind
Here you might want to git push
. Something like this:
a - b - c - d - e ^ REMOTE ^ LOCAL
4: they’ve diverged :(
This is the situation we’re talking about in this blog post. It looks something like this:
a - b - c - d - e \ ^ LOCAL -- f ^ REMOTE
There’s no one recipe for resolving this (how you want to handle it depends onthe situation and your git workflow!) but let’s talk about how to recognizethat you’re in that situation and some options for how to resolve it.
recognizing when branches are diverged
There are 3 main ways to tell that your branch has diverged.
way 1: git status
The easiest way to is to run git fetch
and then git status
. You’ll get a message something like this:
$ git fetch$ git statusOn branch mainYour branch and 'origin/main' have diverged, <-- here's the relevant line!and have 1 and 2 different commits each, respectively. (use "git pull" to merge the remote branch into yours)
way 2: git push
When I run git push
, sometimes I get an error like this:
$ git pushTo github.com:jvns/int-exposed ! [rejected] main -> main (non-fast-forward)error: failed to push some refs to 'github.com:jvns/int-exposed'hint: Updates were rejected because the tip of your current branch is behindhint: 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 doesn’t always mean that my local main
and the remote main
havediverged (it could just mean that my main
is behind), but for me it oftenmeans that. So if that happens I might run git fetch
and git status
tocheck.
way 3: git pull
If I git pull
when my branches have diverged, I get this error message:
$ git pullhint: You have divergent branches and need to specify how to reconcile them.hint: You can do so by running one of the following commands sometime beforehint: your next pull:hint:hint: git config pull.rebase false # mergehint: git config pull.rebase true # rebasehint: git config pull.ff only # fast-forward onlyhint:hint: You can replace "git config" with "git config --global" to set a defaulthint: preference for all repositories. You can also pass --rebase, --no-rebase,hint: or --ff-only on the command line to override the configured default perhint: invocation.fatal: Need to specify how to reconcile divergent branches.
This is pretty clear about the issue (“you have divergent branches”).
git pull
doesn’t always spit out this error message though when your branches have diverged: it depends on howyou configure git. The three other options I’m aware of are:
- if you set
git config pull.rebase false
, it’ll automatically start merging the remotemain
- if you set
git config pull.rebase true
, it’ll automatically start rebasing onto the remotemain
- if you set
git config pull.ff only
, it’ll exit with the errorfatal: Not possible to fast-forward, aborting.
Now that we’ve talked about some ways to recognize that you’re in a situationwhere your local branch has diverged from the remote one, let’s talk about whatyou can do about it.
there’s no one solution
There’s no “best” way to resolve branches that have diverged – it reallydepends on your workflow for git and why the situation is happening.
I use 3 main solutions, depending on the situation:
- I want to keep both sets of changes on
main
. To do this, I’ll rungitpull --rebase
. - The remote changes are useless and I want to overwrite them. To do this,I’ll run
git push --force
- The local changes are useless and I want to overwrite them. To do this, I’llrun
git reset --hard origin/main
Here are some more details about all 3 of these solutions.
solution 1.1: git pull --rebase
This is what I do when I want to keep both sets of changes. It rebases main
onto the remote main
branch. I mostly use this in repositories where I’mdoing all of my work on the main
branch.
You can configure git config pull.rebase true
, to do this automatically everytime, but I don’t because sometimes I actually want to use solutions 2 or 3(overwrite my local changes with the remote, or the reverse). I’d rather bewarned “hey, these branches have diverged, how do you want to handle it?” anddecide for myself if I want to rebase or not.
solution 1.2: git pull --no-rebase
This starts a merge between the local
and remote main
. Here you’ll need to:
- Run
git pull --no-rebase
. This starts a merge and (if it succeeds) opens a text editor so that you can confirm that you want to commit the merge - Save the file in your text editor.
I don’t have too much to say about this because I’ve never done it. I alwaysuse rebase instead. That’s a personal workflow choice though, lots of people have verylegitimate reasons to avoid rebase.
solution 2.1: git push --force
Sometimes I know that the work on the remote main
is actually useless and Ijust want to overwrite it with whatever is on my local main
.
I do this pretty often on private repositories where I’m the only committer,for example I might:
git push
some commits- belatedly decide I want to change the most recent commit
- make the changes and run
git commit --amend
- run
git push --force
Of course, if the repository has many different committers, force-pushing inthis way can cause a lot of problems. On shared repositories I’ll usuallyenable github branch protectionso that it’s impossible to force push.
solution 2.2: git push --force-with-lease
I’ve still never actually used git push --force-with-lease
, but I’ve seen alot of people recommend it as an alternative to git push --force
that makessure that nobody else has changed the branch since the last time you pushed orfetched, so that you don’t accidentally blow their changes away.
Seems like a good option. I did notice that --force-with-lease
isn’tfoolproof though – for example this git committalks about how if you use VSCode’s autofetching feature to continuously git fetch
,then --force-with-lease
won’t help you.
Apparently now Git also has --force-with-lease --force-if-includes
(documented here),which I think checks the reflog to make sure that you’ve already integrated theremote branch into your branch somehow. I still don’t totally understand thisbut I found this stack overflow conversationhelpful.
solution 3.1: git reset --hard origin/main
You can use this as the reverse of git push --force
(since there’s no git pull --force
). I do this when I know thatmy local work shouldn’t be there and I want to throw it away and replace itwith whatever’s on the remote branch.
For example, I might do this if I accidentally made a commit to main
thatactually should have been on new branch. In that case I’ll also create a newbranch (new-branch
in this example) to store my local work on the main
branch, so it’s not really being thrown away.
Fixing that problem looks like this:
git checkout main# 1. create `new-branch` to store my workgit checkout -b new-branch # 2. go back to the `main` branch I messed upgit checkout main # 3. make sure that my `origin/main` is up to dategit fetch # 4. double check to make sure I don't have any uncomitted # work because `git reset --hard` will blow it away git status # 5. force my local branch to match the remote `main` # NOTE: replace `origin/main` with the actual name of the# remote/branch, you can get this from `git status`.git reset --hard origin/main
This “store your work on main
on a new branch and then git reset --hard
” pattern canalso be useful if you’re not sure yet how to solve the conflict, since mostpeople are more used to merging 2 local branches than dealing with merging aremote branch.
As always git reset --hard
is a dangerous action and you can permanently loseyour uncommitted work. I always run git status
first to make sure I don’thave any uncommitted changes.
Some alternatives to using git reset --hard
for this:
- check out some other branch and run
git branch -f main origin/main
. - check out some other branch and run
git fetch origin main:main --force
that’s all!
I’d never really thought about how confusing the git push
and git pull
error messages can be if you’re not used to reading them.