Intro
By now I'm sure many of you are familiar with the Github Flow.
Before I introduce some clever git commands to help maintain a clean git history, it's helpful to review some good git principles.
If that link is not available the 5 rules are:
- Make commits as small as possible
- Write meaningful commit messages
- Rebase always
- Don’t squash everything
- Commit instead of commenting out
Alright, now that we have that out of the way let's look at a common situation.
Let's assume the following as a starting point for the suggested workflow in this post:
Preconditions
You want a clean Git history
You don't want to rewrite your git history consistently
Situation
# coding the component
$ git commit -m "add some component"
# missed an edge-case
$ git commit -m "fix missing null check"
# add component to service
$ git commit -m "add component to service"
# updating the component after service tests found a bug
$ git commit -m "fix component due to silly bug"
$ rubocop
# fix linting errors
$ git commit -m "fix linting errors"
# team members do a code review on the PR / MR
$ git commit -m "fixes based on PR comments"
Now this history isn't uncommon (in fact I can't think of many PRs of real substance that don't go through something like this).
This will result in a Git history that looks like this
fixes based on PR comments
fix linting errors
fix component due to silly bug
add component to service
fix missing null check
add some component
But is that really reflective of the work? We don't want the commit history to be littered with "fix linting" commits.
This is actually representative of the work.
add component to service
add some component
Solution
Let's go about these "fixes" in a different way.
We'll use the familiar command git commit
but with an interesting flag called (appropriately) fixup
.
# coding the component
$ git commit -m "add some component"
# missed an edge-case
$ git commit --fixup <COMMIT HASH OF THE COMMIT "add some component">
# add component to service
$ git commit -m "add component to service"
# updating the component after service tests found a bug
$ git commit --fixup <COMMIT HASH OF THE COMMIT "add component to service">
$ rubocop
# fix linting errors
$ git commit --fixup <COMMIT HASH OF THE COMMIT "add component to service">
# team members do a code review on the PR / MR
$ git commit --fixup <COMMIT HASH OF THE COMMIT "add component to service">
Now if we look at the git log we'll see
fixup! add component to service
fixup! add component to service
fixup! add component to service
add component to service
fixup! add some component
add some component
Instead of regularly committing (and also writing a commit message) we use the --fixup
flag to create fixup commits.
Now, before merging the merge request, we issue an interactive rebase with the --autosquash
flag.
Note:
GitLab marks MRs that include fixup commits as WIP, so
merging is prevented if you haven't squashed.
$ git rebase --interactive --autosquash \
<COMMIT HASH OF THE COMMIT "add some component">~1
We'll enter interactive rebase mode with the relevant commits already marked for fixup:
fixup afa6970 fixup! add component to service
fixup 1da5d7c fixup! add component to service
fixup c998b08 fixup! add component to service
pick d9731b5 add component to service
fixup 33d6080 fixup! add some component
pick 73e8a6a add some component
The resulting Git history looks like this:
add component to service
add some component
The final result is a very consistent and readable Git history, where every other developer knows what happens, without knowing about every bug on the journey to this final code.
Summary
In summary, the following two Git commands are necessary to make use of the Git fixup workflow:
$ git commit --fixup <COMMIT THAT NEEDS FIXING>
$ git rebase -i --autosquash <FIRST COMMIT THAT NEEDS FIXING>~1
Top comments (4)
Love this
Very useful, thanks for sharing! 😊
Doesn't rebasing like this (after having pushed, eg for a PR) require a "push - -force" in the end since you rewrote public history in order to do the fixups?
Yes. But... hopefully, you work on a (almost) private feature branch...
Most rebase operations alter the history of the branch, and therefore require a push --force. But that will grow into a habit soon. And as a consequence, this is why you should protect your master branch to not allow it.