A Practical Guide to Micro-Commits

How do you practice small batches — micro-commits — as part of your daily workflow? Let me show you.

By Niko Heikkilä / November 5, 2022 / ☕️ 4 minutes read

In software development, there's a lot of talk about small vs big batches. However, that talk mostly comes from the Lean methodologies perspective, which, while essential to learn, is not digestible for all.

How do you practice small batches — micro-commits — as part of your daily workflow? Let me show you.

If you'd rather skip straight to the source code, you can find it here.

Tim Ottinger of Industrial Logic has written a great piece about micro-commits. It's one of those articles that changed how I see Git and version control for good. Not only as a sequence of commits pushed to the remote but as a means of saving and loading a working state. People familiar with fast-paced video games know what I speak of.

"A micro-commit is a tiny commit. It consists of the changes necessary to do one tightly-scoped change. Maybe it's just a file reformat. Maybe it's just a variable rename. It could be the addition of one loop or one statement. It might involve a new microtest and just enough code to make it pass."

Working with micro-commits requires tremendous self-discipline. So, unsurprisingly, you should couple it with a practice that allows you to work with small batches verifying that each minimal change leaves your codebase in a working state: micro-tests and test-driven development.

That being said, before working with micro-commits, you should be experienced in writing practical automated tests that are essentially FIRST: fast, isolated, repeatable, self-verifying, and timely.

When I work with micro-commits, I typically follow the steps below.

  1. Set the goal
  2. If tests pass, save the progress; otherwise, revert to a last known good state
  3. Wrap up the task

To facilitate it, I've created a few custom Git commands. Few people know that you can extend Git in powerful ways when you create custom scripts named according to the pattern git-{{command}} and place the files in your $PATH, where they become accessible by invoking git command in your shell.

The animation below1 shows how I work with micro-commits.

A GIF recording of my Git workflow in terminalPicture: A GIF recording of my Git workflow in terminal
Click for a larger version.

Does this feel oddly familiar? You're correct because it's an application of Kent Beck's famous test && commit || revert workflow. Let's break down the steps you saw above.

git goal

Before starting to work, I must figure out what I want to do. This is an excellent opportunity to create a new empty commit — a topic — stating my goal.

Later on, as you will see, I can squash my work to this commit and retain a clean version history.

Bash
1echo "Setting up goal for: '$message'"
2git commit --allow-empty --no-verify -m "$message" > /dev/null 2>&1

Additionally, I'm using the --no-verify flag to skip pre-commit hooks, which I usually have set up in my projects. Running them for an empty changeset would be redundant.

git save

At this point, I have completed a minimal slice of my work. Following the TDD practice, it's typically all tests passing (green phase). This is the best time to save the progress if I screw it up.

Bash
1echo "Committing recent changes with message: '$message'"
2git add -A > /dev/null 2>&1
3git commit --no-verify -m "$message" > /dev/null 2>&1

For staging files, I'm using the -A flag without specifying the path to ensure all files in the entire working tree are updated. Next, I'm once again skipping the hooks and committing the changes. I default the commit message to something simple like "quicksave" or "wip" because it doesn't matter now.

git load

Alas! After continuing my work, I notice I have made a tragic mistake, and my tests are no longer passing. Unfortunately, time has passed, and I can't fix forward quickly, so it's only logical to roll back to the last known good state.

Bash
1echo "Reverting to the last known good state at: '$last_commit'"
2git reset --hard > /dev/null 2>&1
3git clean -fd > /dev/null 2>&1

First, I discard the changes made, and then I clean the repository of any additional files or directories I've created.

git done

Finally, after a few save runs and all tests passing, I'm happy with my changes and ready to share them with others.

To retain a clean history and eliminate the work-in-progress commits, I want to squash those to my goal. There are many ways to achieve this, but I've accustomed to resetting the state softly, staging everything, and amending the changes to the last commit, which is now my goal. I also have the opportunity to reword my commit message if I have a better one in mind.

One caveat to avoid is creating too large commits when wrapping up. Sometimes there is a chance that I set a too ambitious goal, and squashing the micro-commits would result in a massive chunk challenging to review. In that case, I'll squash only some micro-commits and adjust my goal by rebasing. Fortunately, I tend to keep my goals relatively small, so I don't need to do it often.

There's also the option of not squashing if I like it. For those cases, I can simply reword the quicksaves while wrapping up or pass an argument to the git save command.

Bash
1git reset --soft "$(git history)"
2git add -A
3git commit --amend
4echo "Feature committed. Run 'git push' to share with others."

The git history command above is beneficial. It uses fzf — a powerful interactive fuzzy finder — to help me get the commit SHA from my goal. I could have saved the desired SHA to an environment variable while setting the goal, but I prefer to view my commit history before squashing so I know I'm going in the right direction.

fzf also has an excellent way of running an arbitrary command for a given line. Here I'm using git show accompanied by diff-so-fancy to view the contents of that commit on the right-hand side of my screen.

Bash
1git log --oneline --pretty='%h %s' \
2| fzf --preview='git show --color=always {+1} | diff-so-fancy' \
3| cut -d ' ' -f 1

Conclusion

Just because Git steers you to work a certain way doesn't mean you have to follow it by the book. Git is a potent tool, and I've found it best to crank its benefits to the extreme like we do it in Extreme Programming.

Creating a micro-commit every few minutes ensures a stable pace, and you can always revert to safety in case of errors. If you try it out, you may notice a significant amount of stress related to Git usage simply vanishes.

Footnotes

  1. I created this helpful GIF using VHS, which is a digital tape recorder for your command-line.

Back to postsEdit PageView History