Rebasing in Git to maintain history's health
David Sánchez
Posted on June 30, 2020
Have you ever felt that the Git history of your project is becoming a big mess and when you need to find out why a change was made you encounter a lot of small commits that were just adding tiny bits of code that you forgot to add in previous commits? Well, this also happened to me.
I'm a passionate learner so I'm always trying to find out new ways of doing things and questioning the current knowledge I have about a tool. When I found out that the Git history of my project was having a lot of meaningless commits, I started asking myself if there was a better way to add those changes to previously made commits so they can maintain cohesion and in the future, another developer doesn't need to solve the puzzle by searching through multiple commits to finding the reason of the change.
This is when I found about Rebasing in Git. There are two ways that we can use Rebase: as a "replacement" of git merge
and change, drop, and squash previously made commits. Gotcha, you can see that one of the use cases of the Rebase command fits perfectly the use case I'm searching for.
I'm not going to talk about git rebase
as a "replacement" of git merge
in this article, but if you are curious how is that so, I recommend you to read this article from the people at Atlassian where they explain it.
To explain how to do Interactive Rebase in Git I've created this simple example repository that has some meaningless commits that I want to get rid of.
By using tig
we can observe multiple things:
- I'm doing these changes in a separate branch called feature-branch, it's very important that you do not use
git rebase
on public/shared branches as it will overwrite history. This means that if you force-push the changes, anyone else who's also working in your branch will go out of sync. - There are two meaningless commits in my
feature-branch
that only fixes some accents that I forgot to add in the Adding my personal information commit. We will be fusing them 2 with the original commit. - I made them all quickly in my example, but you may extrapolate the things we're going to make here into bigger projects.
Let's do it
First, we'll take the SHA-1 of the oldest commit that we want to retain as-is. In this case, the last commit we want maintaining as-is is the Adding my initial description file commit. This is because we are going to be fusing the two "Forgot ..." commits with the Adding my personal information commit, hence not retaining it as-is.
Now that we have the hash of the commit, we're going to execute the following command which is going to run Rebase in Interactive mode, so we can easily pick which actions we want to perform to the commits.
git rebase -i 6c8e26c^
You may notice two things in the command:
- We're using only a few characters of the hash: Git allows us to refer to a commit by only using few characters from it. You can check out that in Git's documentation.
- The caret at the end: In order to have our current commit available in the rebase command we will need to bring the parent of the 6c8e26c commit. This will let us do the fusion operation between the 3 commits.
Executing this command will open the editor that we have configured in Git with the following text:
pick d7ce763 Adding my personal information
pick b049848 Forgot to add accent in my last name
pick b215b9e Forgot to add accent in my city
pick b544998 Adding software development skills
# Rebase 6c8e26c..b544998 onto b544998 (4 commands)
#
# 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
# b, break = stop here (continue rebase later with 'git rebase --continue')
# 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
By reading at the commands, the best fit of actions for what we want to do is the following:
- Pick the Adding my personal information commit (leave it as-is)
- Fixup the Forgot to add accent in my last name commit
- Fixup the Forgot to add accent in my city commit
- Pick the Adding software development skills commit (leave it as-is)
By using the Fixup action instead of the Squash action, we're telling Git to move the changes of both commits to the above commit (Adding my personal information) and not ask us to rewrite the commit message or append their commit messages to the one we had originally.
This is how our file will look like after making the changes:
pick d7ce763 Adding my personal information
fixup b049848 Forgot to add accent in my last name
fixup b215b9e Forgot to add accent in my city
pick b544998 Adding software development skills
# Rebase 6c8e26c..b544998 onto b544998 (4 commands)
#
# 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
# b, break = stop here (continue rebase later with 'git rebase --continue')
# 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
Now it's only necessary to save and quit the editor and Git will do these operations instantly.
This is the final result of running our rebase command with the fixup.
There are no more meaningless commits, both have been fused with the commit that added the personal information. This way no one has to solve the puzzle by having multiple commits that fix stuff that we mistyped or forgot to do in previous commits that are not yet merged into the stable branch or shared with another coworker.
You can also use this Interactive Rebase to edit something in a previous commit that you just happened to see right now, you just have to use the edit action and Git will take you to that moment in history so you can modify it. The only difference when you finish making the change is that you don't do a git commit
but you do a git commit --amend
.
Conclusion
Maintaining meaningful commits in your Git history is important, it helps other developers flow through changes in a more natural way and waste less time reading meaningless changes that were added just to comply with Git's rule to always add a commit message.
Rebase will help us achieve this goal by giving us tools to traverse the history of our repository and do changes to our commits so we don't need to create a single commit to fix something insignificant but we can edit the commit in which the problem it was introduced.
We may need to take care of rebasing though. It is good that we only use it in branches that are not shared with anyone else and the commits are not merged into a public branch. After our branch is merged in the main branch of the project is not a good idea to rebase it, we'll have to create a new commit.
I hope to read your comments about this method and if you like the post I really appreciate you to share it and add a reaction to it!
Posted on June 30, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.