Is there anyway to undo git push -f?

There are a few ways to find out the original HEAD before the push:

Terminal scrollback

If you’re lucky enough to have the terminal open still, there will be some output when the push was made that looks like this:

...
To user@host:repo.git
 + abcdef0...1234567 HEAD -> branchname (forced update)

Here, abcdef0 was the previous HEAD (your A) and 1234567 was what you forced it to be instead.

git reflog

The output of git reflog tells you the chronological history of what you did. You basically want to go back to the line in question (where you had checked out your branch before the changes) and grab the commit ID from the first column.

The most helpful command here is git reflog show remotes/origin/branchname. This should show you your forced update (1234567) and the previous commit ID (abcdef0) as the top two lines.

Previous reference

A couple of commit references might be useful here. These are basically just references to different points on the reflog:

  • @{1} (or branchname@{1}, if you are not on that branch) is the prior value of that reference. Only works if you haven’t done any other commits to your local branch. (But @{2}, @{3} etc will allow you to go further back.)
  • Similarly, remotes/origin/branchname@{1} will be the prior value of the ref on the remote. Only works if someone else hasn’t pushed to the remote. (Same point about @{n} above.)

Checking you’ve got the correct ID

If you want to confirm that the ID you’ve grabbed from one of the above methods is correct, simply check it out:

git checkout abcdef0

and have a look around. If the git log looks familiar (I also recommend tig for browsing your repository, and you can even run tig abcdef0 to look at the log from a given commit, which won’t affect your reflog), then you can be confident that you’re resetting to the right place.

Resetting to the previous state

Once you have the previous commit ID, you can reset to that and force push again:

git checkout branchname # if you're not on it already
git reset --hard abcdef0
git push -f

or just:

git push -f origin abcdef0:branchname

This will restore the state of the branch to the state before the forced push. (Quick note: that first snippet will update your local branch as well as the remote; the second one only updates the remote.)

What’s the impact?

Whenever you force push, it can cause an issue for other users who have branches checked out.

If you force push by accident and then force push back to what it was before, and nobody’s had a chance to fetch or pull, you might be OK.

If others have pulled the branch since you force pushed, and you force push again to a previous commit, then they will run into problems when subsequently updating (they probably already ran into problems the first time, and this will make things worse).

If they’ve not made any commits to their local branch, they can either just delete and re-checkout (after a git fetch to make sure they have up-to-date references), or do the following:

git fetch
git checkout branchname # if you're not on it already
git reset origin/branchname

If they have made local commits, then they will need to rebase those changes onto the correct history (and possibly resolve any conflicts):

git fetch
git checkout branchname # if you're not on it already
git rebase --onto origin/branchname 1234567

The above means “replay all commits after 1234567 (the incorrect head) on top of origin/branchname (the correct head).”


Concept of divergence

To answer how git decides whether a remote and local branch have diverged, consider these two graphs of commits:

                      B
              o---o---o
             /
o---o---o---o
            A
                      D
              o---o---o
             /
o---o---o---o---o---o E
            C

In the top diagram, intuitively B is ahead of A, or more precisely B contains A.

In the bottom diagram, neither D nor E contains the other; they have both diverged, in this case from a common ancestor C.

If you want to merge B into A, then a fast-forward merge will work, which pretty simply updates the ref of A to that of B:

                      B
              o---o---o
             /        A
o---o---o---o

If you want to merge D into E (or vice-versa) then a fast-forward merge is not allowed: you must create a merge commit:

                      D
              o---o---o
             /         \
o---o---o---o---o---o---o E
            C

Alternatively, you could rebase your changes, which takes all the commits between C and E and replays them on D:

                      D
              o---o---o---o---o E'
             /
o---o---o---o---o---o
            C       E

I’ve shown the original E and the rebased E' here to demonstrate that rebasing usually results in a divergence from the original state.

Notice how I’ve been talking generally about branches here, rather than specifically about remote/local versions of a given branch. The concepts are the same however; the only difference being that a push (which is a merge of a local to a remote) must be a fast-forward merge.

If the local and remote have diverged, you must first pull, either rebasing the local changes on top of the new remote, or by creating a merge commit. In both cases, the local is now ahead of the remote, which means a push becomes possible again.

Leave a Comment