mike mike - 2 months ago 7
Git Question

Fix history after forced push

I have a bare git repository B, from which I have cloned two repositories C and D. Then I have added some changed to C which were later pushed to B, and finally pulled by D. So everything is synchronized.

Now I want to remove last pushed commit from C so I do following:

$ git reset HEAD^ --hard
$ git push -f

(from http://christoph.ruegg.name/blog/git-howto-revert-a-commit-already-pushed-to-a-remote-reposit.html)

and on D I do:

$ git pull

and I get output as follows:

[mkm@horklum git1]$ git pull
From /home/mkm/projects/git_tests/git1
+ a5d681f...c481973 master -> origin/master (forced update)
Already up-to-date.

git log
gives me the exact same output as before. I would like history on D to be the same as on C. I know from What and where does one potentially lose stuff when git says "forced update"? that last
git pull
merged my history with the changed one on the server, but is it really what should happen? I would like history to be still synchronized everywhere.

I know I can do git revert commit and this is the recomended aproach in such scenerio, but I would like to understand why in case of forced push, history will not remain synchronized everywhere.


Let's try to draw the history of your repository (higher is older):

commit 1   --> o <-- the initial commit
commit N-1 --> o <-- B and C are here now
commit N   --> o <-- D is here

You created some commits (up to commit N) on C, pushed to B, pulled from D. All three repos were in sync with their master branches pointing to commit N. (If your branch is not named master then just put its name instead and read on).

Then you forced the master branch of C to go back to commit N-1 and also checked it out (this is what git reset --hard does).

Also, you forced the master branch of B to go back to commit N-1. This is what git push -f does in this context.

Now, the commit N doesn't exist any more in the B and C repositories.1 It is like you created the commits 1..N-1 on C, pushed them to B, pulled them from D (having B, C and D in sync) and then you created commit N on D. It looks like commit N was never created on B and C.

Then you run git pull on D and nothing changes. This is because in the background git pull runs git fetch followed by git merge.

git fetch retrieves from the remote repository (B) all the reacheable commits that are not already present in the local repo. There are none in this situation; B has a subset of the commits present on D. It also learns about the current position of the branches on B.

git merge finds out that the local repo (D) is one commit ahead of the remote repo (B) and it has nothing to do.

In order to make D look like B you can run git fetch then git reset --hard B/master on D. It will forcibly move the master branch of D where the master branch of B is then will check it out. It basically does what git reset --hard HEAD~1 did on B.

1 This is not entirely true. The commit still exists but it is not accessible using branches. This makes it an orphan commit that will be removed on the next garbage collection. While it still exists in the repository it can be accessed using its hash and it can be recovered by creating a branch that points to it.