After you become familiar with git, it becomes easy to fall into a rut of using the same commands in the same way to the same ends. For me, for almost all of my career, this workflow looked something like this

  1. Make a small change
  2. Run git diff compulsively
  3. Repeat steps 1 & 2 until satisfied with a small changeset
  4. Run git add .
  5. Run gcam (if you use the zsh git plugin ), or, git commit -m with some moderately helpful message like git commit -m "Fixed bug in user model"
  6. Has it been a minute? If so, merge your trunk branch back in git merge main to get the most recent changes
  7. Repeat steps 1-6 until the assignment is finished
  8. Run gpsup, or, git push --set-upstream origin $(git_current_branch)
  9. Create a pull request from the provided link
  10. Merge with a merge commit

I got really good at doing this really fast. And because I am paranoid about losing my progress, I committed changes a lot, resulting in PRs with 20, 30, maybe even 40 commits to merge in. On the one hand, this has always kept me in the running for ā€œMost Commitsā€ awards at work (even though Iā€™ll be the first to admit that number of commits says way more about my workflow than my output or actual value/production šŸ˜…). On the other though, my commits were largely meaningless, unhelpful, and due to the nature of merging trunk back in, they were completely nonlinear.

I vaguely knew that rebase was a way to help with that, but honestly I was afraid of it! I had used it once or twice before with devastating results and had abandoned it wholesale. Not until a co-worker walked me through rebase workflows did I begin to see the light and change my own workflow to create a more linear history.

I should pause and mention that learning the difference between merging and rebasing is outside the scope of this article. Lots of better written blogs on the internet will tell you what you need to do know about that. I mostly want to talk about my own journey towards using rebase, and maybe give a few helpful hints about my new workflow using rebase.

Telling A Story #

Why does it matter? git rebase or git merge ā€” if all of your changes are going to make their way onto main and onto production, why should it matter if your commit history is linear or nonlinear?

At the end of the day, itā€™s about telling a story with your git commits. This brings us to one of the greatest ironies of being a software developer: Coding is a creative endeavor used to accomplish deterministic ends. In other words, the process of writing code is more like writing a song or concerto than it is like assembling an Ikea bookshelf ā€” it requires back and forth, writing and deleting, and lots of discovery. Like writing a song, there are an infinite number of ways to accomplish the same ends. But thatā€™s just it: there are ends that need accomplishing. At the end of the day, the code we write has to do the thing we need it to do, regardless of what it looks like. And, letā€™s not forget, code is run on computers which are ruthlessly deterministic. Creatively forging solutions for an unfeeling machine is a marriage of left and right brain unlike anything else Iā€™ve ever experienced.

The problem comes when I use commits that I authored when I was in my creative flow to tell others how that code accomplishes the deterministic ends that I wrote it to accomplish. I shouldnā€™t expect others to understand how I came to the solution I did, but I should expect them to be able to see a logical linking of corresponded changes.

Using git rebase allows me to re-write and re-group my commits in a way that makes more sense to people whose only concern are the deterministic solutions that the code provides. In this way, it allows me to tell a more easily understandable story about why I made the changes I did.

Since this realization, Iā€™ve actually come to take a great deal of pride in my commit messages. I feel like anybody who now stumbles on my PRs or who sees my name as an author when they run git log will be able to understand why I made the changes I did in the order I did .

Going From Workflow to Toolbox #

So what is my workflow now? Refreshingly, I donā€™t use the same workflow on each and every branch. Instead, git rebase has given me a box full of tools to use in the right situation. Letā€™s go over some of these tools!

git rebase -i #

This is the workhorse of the toolbox. I use this command lots of different ways but mainly (1) whenever I need to bring in changes from main, and (2) when Iā€™m about ready to push up my branch to origin and I want to clean up and regroup my commits

Running git rebase -i main on this dummy repo Iā€™ve created gives you a screen that looks like this:

rebase1

As I just mentioned, this is the command I use when Iā€™m getting ready to clean up my commits to tell a story. Normally, once Iā€™m in the interactive ( -i ) git window, Iā€™ll follow this workflow:

  1. Reorder commits that are related. This is a good opportunity to move your fixes typo or linting fixes commit to be closer to the original line it edits, but itā€™s also an opportunity group together changes by file or by domain. For example, if I were building out a frontend that called a new controller, I might ping and pong between the front and back ends ā€” build out a REST method, commit, then build out the corresponding UI, commit, then build out the next REST method, commit, etc. But maybe I decide that it would be better to group together all my controller changes and then all my frontend changes? This is the time to do that
  2. Once my commits are in a good order, Iā€™ll decide which ones I want to keep. The commands I use most commonly here are fixup, which will meld that commit with the previous one, reword, if I want to give a more comprehensive commit message for a commit, and occasionally drop if the commit is for changes that I ended up deleting later anyways. Continuing with the previous example, maybe instead of having multiple commits ( Added create method to controller, Added delete method to controller) I decide I want just one big Complete CRUD methods on users_controller. I could fixup all those previous commits and then reword it to reflect those changes

At this point, grouping my frontend and backend changes and rewording those commits to reflect all the changes, my terminal should look something like this:

rebase2

  1. Run :wq and let git do itā€™s thing! If youā€™ve chosen to reword any commits, at this point git will stop along its rebase process and let you rewrite the commit

git commit ā€”fixup abc123def #

This is a little hack to make life easier when you finally decide to git rebase. Letā€™s say I add my name to the README and this commit produces the hash 02ba0ca. Later I learn that I have spelled my name wrong. Instead of writing a new commit and then remembering where it should go and where to fixup -it when I rebase, I can tell git to associate it with the commit that I will ultimately want to fixup it on by making the change, staging the commits, then running git commit --fixup 02ba0ca

Now if I run git log you see something kind of different:

rebase3

It doesnā€™t rebase and fixup for you ā€” it commits a placeholder. The real magic comes when youā€™re ready to rebase. If you run git rebase -i --autosquash main youā€™ll see that Git automatically sets that commit to fixup to the commit you indicated:

rebase4

If you find yourself doing this alot, you can run Which enables you to drop the --autosquash flag when you rebase.

git undo #

This is my hackiest of all hacks with rebase, and I donā€™t use it that often, but often enough to include here. Sometimes if my commits are a real mess, I actually want to just start from scratch with all my changes uncommitted and unstaged and pick and choose changes to commit in the order I want.

When I go this route, I follow this workflow

  1. Run git rebase -i and fixup ALL my commits to 1 single commit (you could squash them too)
  2. Run git reset --soft HEAD^ ā€” this undoes the last commit but keeps your changes
  3. Run git reset to unstage all the changes

This gives you a blank canvas to work with. You can then use a GUI editor like Tower to select bits and pieces to stage then commit, or if youā€™re hardcore, you could run git add --patch and do it from the command line.


Hopefully you can use these tools to tell the story of your code, and enjoy it like I have.

Happy coding!