I have a bad commit from a long time ago, I want to remove it completely from git history as if it never happened. I know the commit id let's say 1f020. I have tried git rebase and remove it , but there is so many conflicts when rebasing that it is not possible that way. That commit is a simple 1 line change of code and pushing some files not related to the project. So I want to write that 1 line code change and commit it, then replace somehow replace this new commit with the one a long time ago.
There are many ways to rewrite history with git. Use git commit --amend to change your latest log message. Use git commit --amend to make modifications to the most recent commit. Use git rebase to combine commits and modify history of a branch.
The replace command lets you specify an object in Git and say "every time you refer to this object, pretend it's a different object". This is most commonly useful for replacing one commit in your history with another one without having to rebuild the entire history with, say, git filter-branch .
The git commit –amend command lets you modify your last commit. You can change your log message and the files that appear in the commit. The old commit is replaced with a new commit which means that when you amend your old commit it will no longer be visible in the project history.
Changing the latest Git commit message If the message to be changed is for the latest commit to the repository, then the following commands are to be executed: git commit --amend -m "New message" git push --force repository-name branch-name.
If the offending commit is in a private repository, what you want to do is not a big deal. Rewriting published git history is irritating for your collaborators, so be sure removing this line is worth the cost.
The git-rebase
documentation has a helpful passage.
git rebase [...] [<--onto newbase>] [<upstream>] [<branch>]
A range of commits could also be removed with rebase. If we have the following situation:
E---F---G---H---I---J topicA
then the command
git rebase --onto topicA~5 topicA~3 topicA
would result in the removal of commits F and G:
E---H'---I'---J' topicA
This is useful if F and G were flawed in some way, or should not be part of
topicA
. Note that the argument to--onto
and the upstream parameter can be any valid commit-ish.
Assuming your history is linear and the offending commit is in your master branch, you can adapt the example above by running
git rebase --onto 1f020~ 1f020 master
For hairier situations, use interactive rebase. You may find it helpful to follow along with an example that merges two commits, but instead of marking the commit with s
for squash, remove the entire line to remove the commit from your history.
This is slightly complex but anyway, here is how it goes:
detach head and move to commit just AFTER that bad commit. use git log to find out the next commit after 1f020.
git checkout <SHA1-for-commit-just-after-bad-commit-1f020>
move HEAD to commit just BEFORE that bad commit, but leave the index and working tree as it is
git reset --soft <SHA1-for-just-previous-to-bad-commit-1f020>
Redo the commit just AFTER that bad commit re-using the commit message, but now on top of commit just BEFORE that bad commit. thus, removing that bad commit
git commit -C <SHA1-for-commit-just-after-bad-commit-1f020>
Re-apply everything from the commit just AFTER that bad commit onwards onto this new place
git rebase --onto HEAD <SHA1-for-commit-just-after-bad-commit-1f020> master
So, what you want is to:
Remove the bad commit from history, and
Not have to redo all the merges that came after the deleted commit. I.e. each commit that descends from the deleted commit should be kept as-is, only minimally changed to not include the changes erased from history.
Solutions based on git rebase --onto
and git rebase -i
fail on the 2nd point, because they require redoing all the merges that happened later. However, it is theoretically possible to simply recreate all these other commits as if the offending commit had never happened, provided that the bad commit is small enough that reverting it from its successors itself create conflicts.
As Michael and others pointed out, it is highly inadvisable to do this. It's also a major undertaking that is almost certainly not worth the effort. But, for education value, here is an outline of a comprehensive solution that accomplishes the goal:
back up the repository.
use git rev-list
to generate a list of commits that starts with the bad commit and leads up to all the branch heads from which the commit is reachable.
initialize an empty map to map old commits to new ones.
for each commit in the list, do the following:
git write-tree
git commit-tree
to create a new commit with the new tree, old commit message, and parents translated from old parents using the above map (if some parents are not in the map, just use the old parents)go through tags and tag objects, and recreate them to point to the new commits, consulting the map.
go through branch heads and call git update-ref
to point them to the new commits, consulting the map.
If the one-line change (and the unnecessary files) introduced by the commit doesn't conflict with the changes from later commits, this process could be completely automated, and does not require you to manually redo all the merges and conflicts from the later commits.
The downside is that it still requires a rewrite of all the later commits, invalidating them if they had been shared elsewhere, and invalidating commit references in commit messages, such as those produced by git revert
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With