Over the last few months I've been serving as a coordinators for the ngOfficeUIFabric project where we are building directives for Angular 1.x for the Office UI Fabric controls. We have adopted some practices and patterns, most borrowed from other projects that have helped us in managing the project. This post is one of a few in a How We Do It series of posts to share some of our guidelines that may help others.
Those contributing to the project are doing so on their own time. Anyone is welcome to contribute and not be saddled with the burden of a lot of rules. My goal has always been to put things in place that make it easy for new people to get involved, to make sure the management of the project and tedious stuff is stuff contributors don't have to worry about.
Check out the next post in the series: ngOfficeUIFabric - How We Do It - Handling Pull Requests
In this post I will share how we keep a very clean commit log, one that is self-documenting, one that is very easy to read and quickly identify when and where stuff happened. How do we do this? Let's look at what we aim for and compare it to what we do not want.
Commit Log Nirvana
Take a look at this log from our project (taken from SmartGit):
Notice a nice clean linear list of commits, each description is brief and to the point? You know exactly what is in each commit, you know what it does by the prefix and what it addresses.
Check out the expanded view if you run
Notice how after the one-liner description shows up first, then you see the verbose description and what issues the commit references? Easy to read and consume.
Commit Log Pandora
Now, look at a log that, IMHO, gives you no insight into the project (taken from SmartGit). I picked a project that I didn't put any guidelines in place... you might recognize it, but that's not the point... and this isn't throwing stones in glass houses. Notice my name is all over that ugliness... I did it!
Yuk! There are a ton of merge commits and you can't tell what each one does! You have to follow the swim lane to find what it did and that takes concentration.
Commit Message Guidelines
The first step to a clean commit log is good commit messages. The idea is for the message to include be more than just a quick message the developer has to enter before saying their job is done. Rather, a commit message should be more like a journal entry that can be used in creating great docs and convey to other developers what happened.
You can see our commit message guidelines, inspired from the Angular Material project, here: Coding » Commit Messages
This starts with a brief header line that explains what the kind of change is, what it affected, and a short message to what it is about. Using just this single line you someone should be able to find exactly what they are looking for by issuing a
git log --oneline or browsing the commit log on GitHub.
We use a convention of
type(scope): subject. The type could be things like
feat for a new feature,
fix for a bug fix or
chore which is used for project management stuff like build updates. Check the docs for the complete list. After that there is an optional reference to the scope of the commit that refers to what it addresses. This might be blank, or it might reference a specific component in the project (or an Angular directive in our case). Finally the header then contains a brief message, the subject of the change.
This first header line should be concise, not verbose, so it will not wrap or get truncated in any tool. We shoot for nothing greater than 75 characters. for the full header line length.
After an empty line, we then include a verbose message that explains the commit. This can include code (n markdown format) or anything else that conveys what the commit is all about. If there are breaking changes, we make sure they are spelled out here including how to change your code. Here's a good example of a commit that does that: e4ca786... notice how the breaking change is indented & the code changes are easily readable.
Referencing Issues in Commit Messages
The last part of the commit message is a footer line that references any issues related to the commit. If the commit closes an issue, we include
Closes #123. Why? First, this makes it easy to track which issue the commit addresses. If it does not close the issue, we reference it (
References #123.). It very well may close or reference multiple issues too, like this one.
The other reason for including the reference like this is that it's automatically linked within the commit message to the actual issue if you use the format
#123. Further more, GitHub also adds an entry to the issue that cross references the commit. Check out issue #9... if you scroll down you'll see links automatically added by GitHub to specific commits where the issue has been referenced.
Let's take a minute to look at a great commit message... first line is a to the point summary, then an explanation of what it does followed by the issues it touches...
Avoid GitHub's Merge Commits
When a pull request is submitted in GitHub and you use GitHub's handy, enticing, big green Merge pull request button, do you know what it really does?
From my POV, it makes big dirty commits. You get a commit with a message in the format of Merge pull request #123 from username/branch... look at the commit Pandora picture above... do those help at all? From my POV, no... not a single bit. What you want is to avoid these messages and don't push the big green button!
Instead, click the link command line instructions and merge it locally. The process is fairly simple and can even be automated. Generally speaking you are going to merge the changes into a new branch on your cloned copy of the main repo on your computer, rebase the branch on your local dev branch (this is the step that gets rid of the merge commit) and once all is good, switch back to dev and rebase dev off the local PR branch.
Confused? My next post in this series will dive into this process more but as a preview, you can see how we do it in our docs: MERGE-REQUESTS. Heck, it's even something you can automate like I do on every PR I evaluate: check-pr.sh.
More on PR's in my next post...
Single Responsibility Commits
This should be the simplest thing to grok: every single commit should do one thing, and one thing only. A commit shouldn't fix multiple bugs and add a feature with some extra refactoring! Do one thing. At times we have commits that don't meet this so when the PR is submitted, we break it up.
A good example of this was PR201. Looking at the history you can see the PR was submitted with a single commit... but you can see where I stepped in and broke it up into three separate commits.
Breaking up a commit is easy... if it's the most recent one, just run
git reset HEAD~ or try your google-fu: git break up commit. Once it's broken up you can then selectively commit the files that it included in multiple commits.
You can even set the author of those commits to the original commit author by rewriting history... more on that in a moment.
We've all done this with git or any source control tool. You think you're done with your work only to find "oh shoot there's a topo" so you have a "fix typo" commit. Ugh... that wasn't ideal. While you can fix it by appending to the most recent commit (
git commit --amend), sometimes PRs come across with a bunch of commits for one thing.
We had one such example in PR197. See how there are a handful of commits that don't really talk about the commit message?
Since this PR involved adding a new feature, we really want all those commits squashed to a single commit. This is actually very easy to do. Each git app has a different way of handling this, but I'll show you the command line way as I think it's the easiest way to understand.
What you want to do is use the rebase command in git, specifically the interactive one. This can be scary so make sure you have a backup as you can really foul stuff up. Let's say you have a mess of commits in the last 10 commit entries. Let's run the following to tell git "I want to cleanup the commit log, so open a list of these commits and let me tweak them one by one":
git rebase -i HEAD~10
Your text editor of choice will open with the following and while this is open, the command line is waiting for you to save & close this file:
If you close the editor, nothing happens. The commits are listed in the order of oldest at the top, newest at the bottom. Let's say I wanted to make the last three commits one single commit. What I would do is replace the word
pick for commits
cb7089f with the word
squash. The former ignores the commit messages in the two commits I'm merging into commit
8365586 whereas the latter appends their messages to
I usually also like to have a chance to review the commit message for the one I'm merging stuff into, so I would replace
pick on commit
8365586 with the word
reword. When I save my changes & close, the text editor will reopen, but this time I'll see the commit message for
8365586. Here I can make any change I like and then save & close. Git will then make all the changes I wanted.
Last but not least, how do you rewrite history in a commit log? I've already shown how to squash commits, but there's so much more you can do. From the screenshot above you can see that you can even delete or reorder commits... that comes in handy quite a bit, but you can quickly get into a strange state. No worries... if you ever get there when rebasing, just run a
git rebase --abort to throw your hands up and start over.
You can do so much to rewrite history... there's even a dedicated page on it for the git book: 7.6 Git Tools - Rewriting History.
The most common thing I usually do involves updating the author of a commit. I need to do that when I break up a commit so it isn't tied to me, rather it's tied to the author. This is easy... I'll just point you to THE answer on it that I always go back to on StackOverflow: Change the Author of a Commit in Git - Using Interactive Rebase.
Keep in mind when you do changes like this, if you are affecting existing commits you've pushed to GitHub, you can repush them to GitHub by overwriting the history on GitHub... just do a normal push, but add the force flag (
-f) when pushing. Keep in mind this also changes the SHA for the commit so if you have links to specific commits, they will change.
So there you have it... my take on keeping a clean commit log. Why do this? There are so many reasons related to usability, but if you have a good pattern, it can even help with automation.
We haven't set it up yet, but soon I plan to dynamically generate our changelog by running a script that will read our commit log to build the changelog on each release. Why not? The history is right there as well as all the documentation for breaking changes! There's a slick node utility you can use to do this for you called NPMJS: conventional-changelog. The Angular Material team is using this and we will to.
But first, I've got a big task ahead of me: I need to update our old commit messages to adopt everything I've said in this post because we (ahem... me) didn't follow it for the first 70+ commits. That's my task :) ngOfficeUIFabric - Rewrite our commit history.
Next up in this series... ngOfficeUIFabric - How We Do It - Handling Pull Requests!