{{text-cta}}
It is well known that using Git’s push --force command is strongly discouraged and considered destructive. However, to me, it seemed very strange to put all my trust in Git with my projects and at the same time completely avoid using one of its popular commands. This led me to research why is this command considered to be so harmful? Why does it even exist in the first place? and what happens under the hood?
In this tutorial, I will share my discoveries so you too can understand the usage and impact of this command on your project, learn new, safer alternatives, and master the skills of restoring a broken branch. You will be surprised how the ‘force’ is actually with you 🙌🏻
The push command
To understand how git works under the hood we need to take a step back and examine how Git stores its data. For Git everything is about commits, a commit is an object that includes several keys such as a unique id, a pointer to the snapshot of the staged content and pointers to the commits that came directly before that commit. A branch for that matter is nothing but a pointer to a single commit.
What git push does is basically -
- Copies all the commits that exist in the local branch
- Integrates the histories by forwarding the remote branch to reference the new commit, also called Fast forward ref.
Fast forward ref
Fast forward is simply forwarding the current commit ref of the branch. When our changes are pushed Git automatically searches for a linear path from the current ref to the target commit ref.
If there is an ancestor commit that exists in the remote and not in local(i.e someone updated the remote and we are not up to date) Git won’t be able to find a linear path between the commits and git push will fail.
When to use the --force
Altering commit history and rewriting commits that have already been pushed can be done using git rebase, git squash and git commit --amend, but be warned my friends that these mighty commands don’t just alter the commits — they replace all the commits, creating new ones entirely. Therefore a simple git push will fail and we will have to bypass the “fast forward” rule.
Enter --force.
This option overrides the “fast forward” restriction and matches our local branch to the remote branch. The force flag allows us to order Git “do it anyway”. Whenever we change our history or whenever we want to push changes that are in consists with the remote branch we should use push --force.
Simple scenario
Let’s say Lilly and Bob are developers working on the same feature branch, Lilly completed her tasks and pushed her changes. After a while, Bob also finished his work but before pushing his changes he had noticed some changes had been added. In order to keep the tree clean, he performed a rebase and push --force the rebased branch. Unfortunately, not being updated to the remote branch Bob accidentally erased all the records of Lilly’s changes 😰.
One of the common mistakes using this command is when Bob forgets to update (git pull) his local tracked branch, in this case, using the force might cause Bob a lot of trouble. you must wonder why? Well… force pushes the changes with no regard to the state of the tracked branch, therefore commits might get lost in the process. Shame on you Bob!
Alternative: push — force-with-lease
The --force option has a not so famous relative called -- force-with-lease, which enables us to push --force our changes with a guarantee that we won’t overwrite somebody else’s changes. On default, --force-with-lease will refuse to update branch unless the remote-tracking branch and the remote branch points to the same commit ref. Pretty great right? It becomes even better(!!) you can specify -- force-with-lease exactly which commit, branch or ref to compare to. -- force-with-lease gives you the flexibility to override new commits on your remote branch whilst protecting your old commit history. It’s the same force but with a life vest.
{{text-cta}}
Guide: How to deal with destructive --force
You are without a doubt a responsible developer but I bet it happened to you at least once, that you or one of your teammates accidentally ran git push --force into an important branch that should never be messed with and Oops! In the blink of an eye, everybody’s latest work is now lost.
No need to panic! If you are very lucky someone else who is working on the same code pulled a recent version of the branch just before you broke it. If so, all you have to do is to ask him/her to --force push their recent changes!
But even if you are not that lucky you are still lucky enough to find this tutorial. 👍🏻
1. You were the last person to push before the mistake? 😱
☝🏻 First DO NOT close your terminal.
🥺 Second, go to your teammates and confess your sins.
🗣 Finally, make sure no one messes with the repo for the next couple of minutes because you have some work to do.
Go back to your station. In the output of the git push --force command in your terminal look for the line that resembles this one:
The first group of symbols(which look like a commit SHA prefix) is the key to fixing this.
d02c26f is your last good commit to the branch before you inflicted damages. Your only option is to fight fire with fire and push --force this commit back to the branch on top of the bad one:
Congratulations! You saved the day! 🥳
2. I accidentally --force pushed to my repo, and I want to go back to the previous version. What do I do? 😩
Imagine working on a feature branch, you pulled some changes, created a few commits and completed your part of the feature and pushed your changes up to the main repository. Then you squashed the commits into one, using git rebase --i and pushed again using push --force. But something bad happened and you want to restore your branch to the way it was before the rebase -i. Now, the great thing about Git is that it is very best to never lose data, so the version of the repository before the rebase is still available.
In this case, we’ll use the git reflog command which outputs a detailed history of the repository. For every “update” we do in our local repository, Git creates a reference log entry. git reflog command outputs these ref-logs which are stored in our local git repository. git reflog outputs all the actions that have changed the tips of branches and other references in the local repository, including switching branches and rebases(the tip of the branch called HEAD, and it’s a symbolic reference to the currently active branch. it’s only a symbolic reference since a branch is a pointer to a commit).
Here is a simple reflog that shows the scenario I described above:
The notation HEAD@{number} is the position of HEAD at “number” of changes ago. So HEAD@{0} is HEAD where HEAD is now and HEAD@{4} is HEAD four steps ago. We can see from the reflog above that HEAD@{4} is where we need to go to restore the branch to where it was before the rebase and 0c2d866ab is the commit ID for that commit. So to restore test-branch to the state we want, we’ll reset the branch
and force push again to restore the repository on to where it was before.
📝 General recovery
Anytime you want to restore your branch to the previous version after you push --forced follow this general recovery solution template:
- Get the previous commit via terminal, refLog…
- Create a branch or reset to the previous commit
- Push --force
☝🏻notice: If you created a new branch don’t forget reset the branch so it will be synced with the remote by running the following command:
3. Restore push --force deleted branch
Let’s say…
😎 You own a repository.
🤓 You had a developer that wrote a project for you.
😡 For some reason, the developer got angry.
😈 The developer decided to delete all the branches, and push --force a commit with the message “Ha Ha The project was here”.
🏃♂️ The developer escaped from the country, with no way to contact or find him.
😭 Leaving you without any code and you have never cloned the repo before.
First thing first — you need to find a previous commit. Sadly, in this case, using git log won’t help because the only commit the branch points to is “Ha Ha The project was here” without any related commits. In this case, you have to find deleted commits that aren’t directly linked to by any child commit, branch, tag or other references. Fortunately, these orphan commits are stored in the git’s database and they can be found using the powerful git fsck command
These commits are also called Dangling commits, and according to the docs, simple git GC would remove dangling commits if they are 2 weeks old. Now, having our dangling commits all we have left to do is to find the one previous commit before the damages were done and to follow the General Recovery steps we learned before. 🤟🏻
🛡 Alternative: Protected branches
As the old saying goes:
"The difference between a smart person and a clever person is that a smart person knows how to get out of trouble that a clever person wouldn’t have gotten into in the first place.”
if you wish to completely avoid push --force, GitHub and GitLab offer a very cool feature called Protected Branches, which allows you to mark any branch as protected so no one will be able to push — force it (you can also set admin preferences for that matter like admin permissions).
🌸 Summary
Hopefully, you now understand when you need to add the --force option and what are the risks of using it. Remember, the --force is there for you, it’s only a bypass and like every bypass, you should use it with care.
May the --force be with you 🙏🏻.
Learn from Nana, AWS Hero & CNCF Ambassador, how to enforce K8s best practices with Datree
Headingajsdajk jkahskjafhkasj khfsakjhf
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros elementum tristique. Duis cursus, mi quis viverra ornare, eros dolor interdum nulla, ut commodo diam libero vitae erat. Aenean faucibus nibh et justo cursus id rutrum lorem imperdiet. Nunc ut sem vitae risus tristique posuere.