Recovering from git push fuckup

For some of my projects the central Git server is hosted with gitolite for a long time now. Some time ago I forked a repo bss (example name) to a new repo gee, where both should keep the history of bss, but head in a different direction, including different tags. While doing changes on both repos in different branches I still need to merge an old branch of bss to the recent branch of gee, but usually not in the other direction.

To not accidentally break things, my working copies of both projects only have one remote, the one designated for the original or the fork. For merge or cherry-pick operations in one or the other directions I have a special working copy with both remotes configured. Today after a cherry-pick from gee to bss I messed up pushing, and ended up with gee/master pushed to bss/master, which was wrong as hell, and cold sweat appeared on my head. Damn.

So I had to rewind bss/master to a known good state, but on the server.
WARNING: Make 100% sure you know what you do here to prevent DATA LOSS or your colleagues being mad at you or both.

First try was with the obvious git push --force which was denied by gitolite right away. If you do not know why force pushing usually is a bad thing, stop reading here and learn why it is a bad thing.

Did I warn you about possible data loss?

Second try usually works with branches which are not the default branch, like this (assuming I messed up origin/mybranch and want to push a different version):

$ git push --delete origin mybranch
$ git push --set-upstream origin mybranch

This won’t work with the default branch however with this error message:

$ git push --delete origin master
remote: error: By default, deleting the current branch is denied, because the next
remote: 'git clone' won't result in any file checked out, causing confusion.
remote:
remote: You can set 'receive.denyDeleteCurrent' configuration variable to
remote: 'warn' or 'ignore' in the remote repository to allow deleting the
remote: current branch, with or without a warning message.
remote:
remote: To squelch this message, you can set it to 'refuse'.
remote: error: refusing to delete the current branch: refs/heads/master
To ssh://git.example.com/bss
 ! [remote rejected]   master (deletion of the current branch prohibited)
Fehler: Fehler beim Versenden einiger Referenzen nach 'ssh://git.example.com/bss'

This is not a gitolite problem, but Git itself complains here. Why? For bare repositories Git has a concept of a default branch. This is the branch you get when cloning a repo without specifying a branch. It is marked by the ref named HEAD. Now the idea to recover is like this:

  1. create a new temporary branch on the same changeset you want to have as master on the server
  2. push that branch to the server
  3. change the default branch on the server
  4. do the push –delete / push dance from above (with master now not being the default branch anymore)
  5. change the server’s default branch back
  6. delete the temporary branch both local and remote

In recent Git changing the default branch is done with git symbolic-ref and that would have to be executed on the gitolite server.

Thankfully on recent gitolite V3 you can execute that through SSH, and I learned this from stackoverflow. These were the commands I used (including output), with main being my temporary branch (see above):

$ ssh git@git.example.com symbolic-ref bss HEAD
refs/heads/master
$ ssh git@git.example.com symbolic-ref bss HEAD refs/heads/main
$ ssh git@git.example.com symbolic-ref bss HEAD                
refs/heads/main

Now this is the time to delete master on the remote and push it again from a known good state. Then switch back the default branch.

$ ssh git@git.example.com symbolic-ref bss HEAD refs/heads/master
$ ssh git@git.example.com symbolic-ref bss HEAD                  
refs/heads/master

Everything fine again now. Wrote this for my future self.

Leave a Reply

Your email address will not be published. Required fields are marked *