If I use push -f to overlap the last commit, namely A, on remote repository, and others have pulled A before I pushed.
Anyway to undo this in case not to cause trouble for others?
Is it useful to push -f an original one to “pretend” that nothing has been touched?
Or
How git decides whether the local repo diverges from the remote tracking one ?
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:
Here,
abcdef0was the previous HEAD (yourA) and1234567was what you forced it to be instead.git reflogThe output of
git reflogtells 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}(orbranchname@{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.)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:
and have a look around. If the
git loglooks familiar (I also recommend tig for browsing your repository, and you can even runtig abcdef0to 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:
or just:
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 fetchto make sure they have up-to-date references), or do the following:If they have made local commits, then they will need to rebase those changes onto the correct history (and possibly resolve any conflicts):
The above means "replay all commits after
1234567(the incorrect head) on top oforigin/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:
In the top diagram, intuitively
Bis ahead ofA, or more preciselyBcontainsA.In the bottom diagram, neither
DnorEcontains the other; they have both diverged, in this case from a common ancestorC.If you want to merge
BintoA, then a fast-forward merge will work, which pretty simply updates the ref ofAto that ofB:If you want to merge
DintoE(or vice-versa) then a fast-forward merge is not allowed: you must create a merge commit:Alternatively, you could rebase your changes, which takes all the commits between
CandEand replays them onD:I’ve shown the original
Eand the rebasedE'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.