Working Better with Git for a Clear History

Your teammate worked on a few improvements in all forms of the company’s website. At some point you, also a programmer, are asked for a code review.

This is the feature branch’s commit history you get in the Pull Request:

[290xx26] Resolve merge conflicts
[9efxxf2] Refactor event listener for form fields
[5d9xx5a] Update snapshots
[948xxfa] Update dispatch event
[f5xxea1] WIP
[f8xxaae] Revert change
[49xxf55e] Revert changes
[02xxdf1] Update snapshots
[21xx329] Pass down prop
[28xxa865] Fix onChange event and add minimal design  in form
[cfxx37c] U[date snapshots
[cfxx36c] Update form to handle onChange event for autofill
[242xx25] Fix another bug with onChange
[f7xx738] Update form component
[09xx868] Update snapshots

It seems like a lot, when actually those improvements are simply 1) fixing a bug with event handling and 2) introducing a minimal style to them. It just that it takes time to visualise that.

Indeed, committing often to a branch is a good practice[1] and commits are supposed to be low-level rather than huge. However, a commit is applicable when you have a meaningful, self-contained batch of work to log to the history – and updating Jest snapshots is not it.

How about the following history?

[9efxxf2] Refactor event listener for form fields
[cfxx37c] Add minimal design in form
[cfxx36c] Update form to handle onChange event for autofill

That history communicates in a clear way what in the codebase has been changed in order to get the improvements done. If you want to know how it’s changed, it’s just a matter of checking out a particular commit.

Why a clear history matters

Apart from facilitating code reviews, since reviewers could grasp the context of the changes right at the first glimpse, a clear Git history is healthy for the project.

When commits started to reflect one’s workflow rather than the work done itself, the history turns into a mess of both meaningful and meaningless logs, hard to navigate through and undo changes (since commits are “checkpoints” of work). It’s highly likely that, as time goes by, developers will stop caring about the source code history as the powerful resource it is.

The final Git history should reflect your work, not the way you worked.

You begin to lose the benefit of source code management with a messy history, which was supposed to reflect how the codebase evolved over time.

A better relationship with Git

There are very common comprehensible reasons – yet not excuses at all! – for developers to unnecessarily commit. I can think of:

  1. “I had to update snapshots/fix unit test/code formatting/a typo in something I introduced.”
  2. “I had to merge master.”
  3. “I wanted to save my work.”
  4. “I don’t necessarily follow a clear flow – I am productive when I work in iterative way and usually do a lot of things at once.”

They can all be worked out by developing a better relationship with Git itself. That might take a while at first, but when it’s part of your daily workflow, you barely think about it at all. (I promise!)

First, let’s talk Git rebasing

Git rebase is a very powerful tool to reorganise your commits. It does a lot, so I won’t get deep into it.

The command git rebase has an important tool you should get comfortable with: interactive rebase. Let’s say we want to reorganise the latest 3 commits:

git rebase --interactive HEAD~3

HEAD points to the current branch in your Git repository. You can use @ as an alias as well: @~3, or the commit hash.

Interactive rebase will provide you a text interface with the list of the commits (in this case, within the range HEAD~3…HEAD) that are about to be rebased and actions you can apply to them:

pick [242xx25] Fix another bug with onChange
pick [f7xx738] Update form component
pick [09xx868] Update snapshots

# Rebase 242xx25..09xx868 onto 242xx25
# Commands:
#  p, pick = use commit
#  r, reword = use commit, but edit the commit message
#  e, edit = use commit, but stop for amending
#  s, squash = use commit, but meld into previous commit
#  f, fixup = like "squash", but discard this commit's log message
#  x, exec = run command (the rest of the line) using shell
# 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

pick just means that commit is included. It's the default.

Editing that file (either by applying actions to commits or reordering them) and closing it will reapply those commits to the branch. We’ll explore some scenarios where that is useful.

Rebase instead of merge

The branch that you branched from has been updated (let’s call it master), so you need to fetch latest changes and merge that into yours with git pull. Instead of merging and introducing an ugly new commit for that, you can rebase and pretend that nothing ever happened in the first place.

git pull origin master --rebase

"Why isn't that the default then?", you might ask. I'd guess they didn't want to make a feature that rewrites history part of the default behaviour.

Squash commits when you can

You need to fix a typo, update a test, or include anything else that should have actually been part of a commit previously introduced. You can create a new commit and later squash it to the initial one with rebase.

git commit -m "Update contact form tests"
git rebase -i HEAD~2 # act on top of latest 2 commits

Interactive rebase will show up, and you can mark that “Update contact form tests” as the commit to be squashed into a previous one by changing pick to s (squash).

The squashed commit has to come exactly before the commit you want to squash into, so you might have to some reordering.

Fix up and save time

Using the --fixup flag along with the commit hash of the previous commit will mark a commit as a fix of an existing one. Then, when you rebase with --autosquash, they will… well, automatically get squashed.

Let’s say we have fixed a typo in the commit i2r923:

git commit --fixup i2r923
git rebase --autosquash 9ef00f2  # one commit before above

You can configure Git to use the --autosquash flag by default when rebasing. I do that so you can check my dotfiles if you're curious.

Quite often, you’ll know how far you’re traveling from the current commit (HEAD). Instead of specifying hashes, you can just use HEAD as a reference to previous commits:

git commit --fixup HEAD            # fix 1 commit before HEAD
git rebase --autosquash -i HEAD~1  # squash latest 2 commits

Stash to save your work in progress

You want to check or change other branch’s files, but don’t want to commit unfinished work either. You can stash your work in progress (“WIP”):

git stash

That stores everything from your working directory into stash. It’s so useful in my day-to-day work that I honestly often cry using it.

On how to get your work back, name your WIPs, or stash specific files, refer to the docs on Git staging. No way I am better at explaining than the documentation itself!

Commit selected chunks of your work

Remember the “I work in a very messy way, iterating between features” excuse? This helps. (It helped me yesterday when I refactored and introduced something new to it – should not be done altogether! –  at the same time without noticing.)

Besides only adding a specific file to your commit with git add, you may want to add only a chunk of the file’s code to a commit. That can be achieved with the --patch flag:

git add --patch

That’s another interface you’ll have to get comfortable with. You can also try out its --interactive option.

Using VS Code?

VS Code has a powerful built-in Git editor that allows you to add chunks of code to a commit. To stage specific lines to commit later, open VS Code’s Git editor, select the lines of code you feel should go into a commit, and “Stage selected changes” by right-clicking on it.

Showing how to use 'Stage Selected Changes' in VS Code
Changed the whole thing but want to commit only a chunk of it? VS Code has got you covered.

The Command Palette also has a “Git: Stage selected changes” option.

Rename commit messages for meaning

You had a look at the final history and found out your commit messages need some love. To change your latest commit (HEAD), you can just do git commit --amend; for other commits, you’ll have to rebase.

Let’s say you want to fix the latest 5 commits:

git rebase -i @~5

That will open up the interactive rebase you’re already familiar with. Find the commit you want, change pick to e (edit), and save and close the file; after Git rewinds to that commit, edit its message with git commit --amend, and run git rebase --continue when you’re done.

Got a clear history? Push it!

Commit Often, Perfect Later, Publish Once.[1:1]

Make sure that your branch has got a clear, readable and meaningful history…

git log --oneline

Then push it into the remote repository!

git push origin head

If you have already pushed (Y THO???!), you can (carefully) use the force option (-f) to rewrite the remote history.

Extra: Fixing the first history

Remember the first history? We can fix it. It’s definitely harder to change it after it’s done, but totally doable with rebase -i. A possible solution would be:

squash [290xx26] Resolve merge conflicts
pick [9efxxf2] Refactor event listener for form fields
squash [5d9xx5a] Update snapshots
squash [948xxfa] Update dispatch event
delete [f5xxea1] WIP
delete [f8xxaae] Revert change
delete [49xxf55e] Revert changes
squash [02xxdf1] Update snapshots
squash [21xx329] Pass down prop
pick [28xxa865] Fix onChange event and add minimal design  in form
squash [cfxx37c] U[date snapshots
pick [cfxx36c] Update form to handle onChange event for autofill
fixup [242xx25] Fix another bug with onChange
squash [f7xx738] Update form component
squash [09xx868] Update snapshots

Be mindful about your decisions and make sure you are not losing work along the way. I deleted commits unused commits (WIP then reverted), squashed “fix” and “update tests” commits, and then picked only those that are meaningful batches of work.

I could have also split Fix onChange event and add minimal design in form into two separate commits… But that’s for a future post.

Final considerations

Nowadays, I can say for sure that Git helps me to work better. Those techniques you’ve learned are all part of my daily workflow.

There is always something I don’t know about Git though, so I keep exploring. I recommend you do to do the same, and you can start right from your command line:

git help -w reset

Finally, if somehow you don’t feel comfortable with moving around your work from the command line, I’d recommend Git GUI clients[2] – they are powerful and simplify visualising branches and its commits, especially when it gets tough[3].

I hope you have better, more sane, work days after that!

  1. Commit Often, Perfect Later, Publish Once: Git Best Practices by Seth Robertson. ↩︎ ↩︎

  2. Some clients I can think of: GitHub’s Git client (free), Git Kraken (free), Git Tower (paid). ↩︎

  3. It will get tough, because source code management is not easy. Anyhow, it’s always tougher when you care about things anyway. Easiest thing is not to care about anything at all. ↩︎

Tags: git