When developing it's encouraged to commit often and use good commit messages, but sometimes it can git messy.
Imagine we got this feature branch with some undesired commits. We retrace the steps taken to see what happened. This example is a bit contrived but please bare with me.
There's a bit of typing to be done. You can copy from this gist into your terminal to avoid having to type it all in.
$ git init
$ echo "A bunch of project files ..." > project.txt
$ git add project.txt
$ git commit -m "Initial commit"
Moving on the the feature branch ...
$ git checkout -b new-killer-feature
Switched to a new branch 'new-killer-feature'
$ echo "A lot of coding being done... looking god." > component.txt
$ git add component.txt
$ git commit -m "Add first component"
Fixing the typo (good not god) but we're in a flow so we add it the wrong commit by mistake.
$ echo "A lot of coding being done... looking good." > component.txt
$ echo "More code here ..." > anotherComponent.txt
$ git add .
$ git commit -m "Add another component" -m "With a really long descriptive message..." -m "With details and important information..."
$ echo "Creates code for business logic ... and fn -> 100/Oops" > businessLogic.txt
$ git add businessLogic.txt
$ git commit -m "Add business logic"
$ echo "Add a third component ..." > thirdComponent.txt
$ git add thirdComponent.txt
$ git commit -m "Add third component"
We stop and try the code and it's not working, we need to fix the oops.
$ echo "Add for business logic ... fn -> 100/5" > businessLogic.txt
$ git add businessLogic.txt
$ git commit -m "Fix error in business logic"
It should be a 4 not 5, obviously.
$ echo "Add business logic ... fn -> 100/4" > businessLogic.txt
$ git add businessLogic.txt
$ git commit -m "Fix business rule"
So what happened
First a typo found its way into our component, when it later got fixed it was staged and added to the wrong commit. Then some erroneous business logic got committed, a third component were added before the error was detected. The error got fixed as well as some business rules.
This is how our branch looks now:
git --no-pager log --oneline
4dc57a6 (HEAD -> new-killer-feature) Fix business rule
0ea4f69 Fix error in business logic
2449165 Add third component
6f750fc Add business logic
f09f39e Add another component
a59d39a Add first component
34f47d9 (master) Initial commit
Maybe it's not that bad? Could we get away with it? Probably! But remember this is over simplified, a real world example might be messier.
So let's continue and see what we can do about it.
Interactive rebase to the rescue
With interactive rebase we can rewrite the history. So we issue the command git rebase -i <branch>
. We use the master branch, it hasn't been modified so there should be no conflicts going forward.
$ git rebase -i master
This should open a dialog like the one below.
pick a59d39a Add first component
pick f09f39e Add another component
pick 6f750fc Add business logic
pick 2449165 Add third component
pick 0ea4f69 Fix error in business logic
pick 4dc57a6 Fix business rule
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) using shell
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# . create a merge commit using the original merge commit's
# . message (or the oneline, if no original merge commit was
# . specified). Use -c <commit> to reword the commit message.
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
#
# Note that empty commits are commented out
Notice the comments (#) above, it describes the available commands and we are going to use some of them.
Now if you remember the typo fix, it was added to Add another component, but the typo was done in Add first component so the fix should really be part of that commit.
So the first thing we need to do is to modify the Add another component, so we change it from pick to edit. (#1)
Then we have the two fixes for the business logic, it would be great if they could be apart of the original commit. So we move them up right below the Add business logic and change from pick to fixup. (#2)
pick a59d39a Add first component
edit f09f39e Add another component (#1)
pick 6f750fc Add business logic
fixup 0ea4f69 Fix error in business logic (#2)
fixup 4dc57a6 Fix business rule (#2)
pick 2449165 Add third component
When we save and exit the commands above will be executed from top to bottom, at the edit
command it will stop so we can make changes.
$ git --no-pager log --oneline
f09f39e (HEAD) Add another component
a59d39a Add first component
34f47d9 (master) Initial commit
The HEAD is pointing to Add another component, now let's do a reset on that commit.
$ git reset HEAD~
$ git --no-pager log --oneline
a59d39a (HEAD) Add first component
34f47d9 (master) Initial commit
Run git status
and it should show component.txt (modified)
and anotherComponent.txt (untracked)
, also notice how the HEAD now points to Add first component.
The component.txt (modified)
holds the typo fix and HEAD is pointing to the commit where the typo happened in the first place. So to undo it we can simply stage and amend like so:
$ git add component.txt
$ git commit --amend
We still need the handle the anotherComponent.txt
though and the git reset HEAD~
command undid the commit associated with this file.
This is not ideal because that commit message held a lot of important information that we don't want (or remember) to retype. So how do we fix this?
There's a neat feature we can use, it's called ORIG_HEAD. It's the previous state of HEAD after a possible dangerous operation which git reset HEAD~
qualifies as. So what we do:
$ git add anotherComponent.txt
$ git commit -c ORIG_HEAD
The -c
flag above is saying re-edit message
, so we get the previous message we had in HEAD and use it as the commit message.
That's it! We undid the typo altogether when we amending the fix to Add first component and Add another component has only relevant files/changes.
Now we can continue the rebase process ...
$ git rebase --continue
Successfully rebased and updated refs/heads/new-killer-feature.
$ git --no-pager log --oneline
aee1f72 (HEAD -> new-killer-feature) Add third component
cebe196 Add business logic
5a2573d Add another component
ab87670 Add first component
34f47d9 (master) Initial commit
The two fixup
command got executed, those changes got applied to Add business logic as if those errors never happened.
Is it dangerous?
Git rebase is powerful tool, it's also easy screw things up if you don't know what you're doing, so let's screw it up a bit together. We are in the new-killer-feature
branch just as we left it.
$ git rebase -i master
pick c14c5e0 Add first component
pick 8b6f9f2 Add another component
pick fe055bf Add business logic
pick 85963e5 Add third component
Now we remove all but the first commit.
pick c14c5e0 Add first component
Now we're in pickle right? Well not really, because we can use git reflog
and go back to just before we did our mistake (after we finished our last rebase).
$ git --no-pager reflog
9e51b2e (HEAD -> new-killer-feature) HEAD@{0}: rebase -i (finish): returning to refs/heads/new-killer-feature
9e51b2e (HEAD -> new-killer-feature) HEAD@{1}: rebase -i (pick): Add third component
b8a72f8 (master) HEAD@{2}: rebase -i (start): checkout master
85963e5 HEAD@{3}: rebase -i (finish): returning to refs/heads/new-killer-feature
...
$ git reset --hard 85963e5
Note: You'll get your own hashes (change 85963e5 to your own)
And we're back in business. Now I encourage you to learn more about rebase because there's a lot of stuff I've not covered in this post.
Top comments (0)