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 practice1 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:
- “I had to update snapshots/fix unit test/code formatting/a typo in something I introduced.”
- “I had to merge master.”
- “I wanted to save my work.”
- “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.
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
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
--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
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”):
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
git add --patch
That’s another interface you’ll have to get comfortable with. You can also try out its
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.
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
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.
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 clients2 – they are powerful and simplify visualising branches and its commits, especially when it gets tough3.
I hope you have better, more sane, work days after that!
Commit Often, Perfect Later, Publish Once: Git Best Practices by Seth Robertson.
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.