Introduction
Recently, I was proposing exercises for my mentee, to help him learn Kubernetes, I had written a README with the projects he’d work on.
I asked him to push the project to the repo, and later he said he messed it up and the README was gone. The problem? He had force pushed on main 😬.
I did not have the repo cloned locally – since I had only written the README – so I thought I couldn’t recover it.
I knew he would be able to restore it locally, since Git stores every change – even force pushes.
However, I thought: GitHub would also store it, since it is also a Git repository and I remember seeing force pushes logged in PRs (with the previous and after commit hash).
Thus, I went looking out how to recover it, via GitHub.
Recovering commits before a force-push
I saw some posts of recovering the commit using GitHub’s API, however we use an enterprise edition, which I did not know the API endpoint, so I decided to use GitHub’s CLI (gh).
Note: this is also the best way, since you become less coupled with Git’s API changes.
Login to your GitHub instance
gh auth login --hostname <hostname here>
Omit –hostname if you’re using GitHub.com
Get all events of the repository
gh api /repos/<owner>/<repo>/events --method GET | jq > events.json
<owner> is either the organization or the user where the repository is
<repo> is the repository name
Find the commit hash
Find the payload.before of the guilty commit in the json. (hint: look for a PushEvent)
Example:
{
"id": "1337",
"type": "PushEvent",
...
"payload": {
...
"ref": "refs/heads/main",
"head": "1234redacted",
"before": "0000redacted",
"commits": [
{
"sha": "1234redacted",
"author": {
"email": "mentee@evilcorp.com",
"name": "Mentee"
},
"message": "feat: add project code",
...
}
]
},
...
},
Create a branch on the old commit
gh api /repos/<owner>/<repo>/git/refs -f "ref=refs/heads/chore/recovered" -f "sha=0000redacted" --method POST
This will create a branch named chore/recovered on the commit before the force push. (the refs/heads part makes it create a branch)
And you have restored the changes! Now you only need to combine both what’s on main and in this branch, by rebasing it.
Rebase the recovered branch onto main
- Clone locally:
git clone <repo-url> - Rebase the “recovered” branch on GitHub onto main:
git rebase origin/chore/recovered - Force push to main (I have an excuse to do it):
git push -f origin main - Delete the “recovered” branch:
git push origin -d chore/recovered
That’s it!
Important Notice!
You must inform everyone from your team of this, because if someone pulls main locally, it will have a weird behavior.
For it to be fixed, everyone should re-create the branch on their side:
git fetch --all
git checkout -b temp
git branch -D main
git checkout -b main origin/main
git branch -D temp
This will re-create the local main branch from what’s on GitHub. (the temp branch is to allow us to delete the local main branch)
Conclusion
In this post you’ve learned how to recover a commit from an accidental force push – be it on main or any other branch.
You saw how to create a branch from the last lost commit and rebase main to include both the lost and the new commits. Finally, we saw how our colleagues can also fix their branch locally.
As you can see, mentoring someone is always a mutual learning experience. (or duplex, as he once said 😂)