When I first started professionally programming in a team, one of the first articles I had to read and memorize was How to Write a Git Commit Message by Chris Beams.
And since then I used his method of writing imperative style, short and clear commit messages.
His recommendation is really a must read for any newbie to git. Additionally, consistency is important! Look at what style your team uses. Team members who appreciate attention to details (read: OCD freaks like me) will thank you.
If it doesn't seem that important right away, you might think differently when you'll have to browse the git commit history. Keeping messages concise and consistent can significantly speed up this process.
My assumption on your git workflow
Before diving in the specifics on writing and organizing git commits, I'll assume that you and your team are using a central branch main
. Main is protected from force pushing. All off the feature and fixup branches are created from that main branch. When work is done on the feature branch, a pull request is opened, reviewed and merged to main.
Craft Pull Requests That Rock: Learn git rebase
Git rebase whips your commit history into a clean, linear masterpiece. No more messy PR's – just your beautiful commits, sparkling and up-to-date with the main branch.
But this great tool is often avoided...
Is Git Rebase Scary?
Many colleagues are afraid of using the git rebase command. And there's a good reason for being scared. Many things can go wrong and I recommend this excellent read by Julia Evans.
In the past I've dealt with all those git rebase mishaps:
🚫 Commit messages from base branch get duplicated on featured branch with different sha.
🚫 Co-authored commit with original commiter is wrong.
🚫 Having to go through many merge conflict resolutions is a code smell!
Should git rebase be avoided?
Not at all, but if you're a newbie, ask your more experienced teammate to "pair rebase" with you. This is the kind of thing where practice makes perfect.
How to safeguard your branch before git rebase?
Create a backup branch before doing the rebase. If anything goes wrong, there's a backup:
git checkout -b your-feature-backup
There are 2 different sets of rebases that I use.
- Rebasing onto main branch - It may go wrong.
- Self rebasing if I need to squash many commits into one, rename a commit or rarely edit a past commit. You don't need to be afraid of this one.
Rebasing onto main branch
Go back to your branch and fetch main:
git fetch origin && git rebase -i origin/main
Ideally, your new commits should be stacked on top of main commits. When you open a PR, only your own commits will show.
But what if you have a merge conflict resolution that you need to go through?
Before anything, rebase your commits and squash them into one. Then you don't have to go through merge resolution multiple times per each commit.
Merge conflicts will be something that you'll need to deal with from time to time.
This is ok as long as there are not that many changes to go through. Maybe you and your colleague were working on the same piece of code and hence the merge conflict...
With more merge conflict resolutions, more chances it will go wrong.
If you are not sure it's going right just git rebase --abort
.
Sometimes what helps is git soft resetting your feature branch commit and git stashing it. In that case, when you do git stash pop
, choosing what's changed seems clearer.
What if you messed up anyway?
Remember what I said at the beginning: you have your backup branch.
If you don't it's still ok. Everything in git is reversable.
Use a git reflog
where you can see the exact flow of your git rebase. Next to your git rebase workflow there's a sha and just do the git reset --hard sha
DO NOT GIT FORCE PUSH TO SHARED BRANCHES LIKE DEV OR MAIN
Instead of force pushing use --force-with-lease
. This option will only proceed if the remote matches your local version of the remote branch.
This way, if someone else has pushed changes while you were planning to force-push, you will be alerted instead of accidentally overwriting their work.
If you only have a single commit and you want to push to a volatile branch like dev, you can just git cherry-pick <your_single_commit_sha>
and git push origin dev
.
Merge often, limit branches to a single feature
Merging often reduces merge conflicts and limiting branches to a single feature makes PR reviews more enjoyable.
Special Keywords in GitHub Commits
These keywords can automatically perform actions like closing issues, referencing pull requests, and even skipping CI builds on Github, or issue trackers like Jira and Linear.
- Close Issue as Completed -
Fix <issue_code>
,Fixes #123
,Resolve #123
. It's case insensitive, works for closing Github issues (#123) or your issue tracker like Jira or Linear. - Reference issue -
<Some_verb> <What> references #123
orrelated to #123
will link a commit to an existing issue. - Skip Continuous Integration -
[skip ci]
or[ci skip]
will skip any continuous integration pipeline if it's configured on Github. - Work in progress commit - any commit containing a
wip
keyword indicates to your team members it's a work in progress and will be overwritten.
Optionally, nitpicking it more
This is optional because it's more important for repos like libraries, frameworks, apps where managing versions and releases is of utmost importance.
Use Conventional Commits
In addition to writing git commits in Chris Beam's style, using a conventional style commits might help you automate things, like autoupdate Jira issues, autoclose Github issues, create changelogs, automate version bumps etc.
You don't need to think, hey is this release a patch or a minor?!
Library will make that decision for you based on what kind of commit history is there from the last tagged release.
There are plenty solutions out there, but commitizen is like a standard. I like using the python version. I like my repo related scripts to be either bash or python.
Continuous integration pipelines are 90% of the time Linux based. Minimum OSes like Alpine or Slim have lighter images, which means faster CI completion.
Git commit linting - is it necessary?
Commit linting involves applying a set of rules or guidelines to the messages included with each commit to a repository. These rules can dictate the format, style, and content of commit messages, ensuring they remain consistent and informative. Common practices include starting messages with a capital letter, referencing issue tracker codes (such as JIRA, Linear, or GitHub issues), and using a specific syntax to describe the commit’s purpose.
I don't know what to recommend here. I'm not sure if using a commit lint is an overkill.
I have to admit if you've seen my repositories, I only use the Chris Beams style of commits followed with an issue tracker code (jira, linear, github whatever...).
I don't even take care if I write with first letter lowercase or uppercase.
My usual workflow when I work on a feature branch is make a bunch of "wip [skip ci]" commits and once I'm done to make a PR, I do a rebase and logically organize my commits.
Maybe it makes sense to include a commit lint if you were enforcing conventional commits but I'd do it rather as a CI pipeline that will prevent merge to main anything that doesn't fit the desired format.
Not as a local pre-commit hook, that really is an overkill. Imagine you need to push something fast for your other teammate to pull...
Lazy Person's Commit Linter
If you're interested, I've got a lazy person's commit linter for you. The linter checks for special keywords not to end up in main. I called it lazy because commit linting can get much more complicated and there are much advanced tools for commit linting.
Create a new file in your repository within the .github/workflows
directory, such as check-commits.yml
.
name: Check Commits
on:
pull_request:
branches:
- main
jobs:
check-commits:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Validate commit messages
run: |
git fetch origin ${{ github.base_ref }} ${{ github.head_ref }}
pattern='^.*(\bwip\b|\[skip ci\]|merge.+into|signed-off).*$'
commit_messages=$(git log origin/${{ github.base_ref }}..origin/${{ github.head_ref }} --pretty=format:%s)
echo "$commit_messages" | while read line; do
if [[ "${line,,}" =~ $pattern ]]; then
echo "Invalid commit message: '$line'"
exit 1
fi
done
This check will run when you do a pull request to main branch. It will fail if any of your commit messages contain a single word like wip, [skip ci], merge commits, and signed off commits. Link to gist.github.com
Conclusion
So, that's the gist of commits! I've covered how to write clear commit messages (thanks, Chris Beams!), the magic of rebasing (don't be scared!), and those super-helpful keywords for GitHub commits.
Speaking of avoiding disasters, remember the golden rule: NEVER FORCE PUSH to shared branches!
For those who like things extra neat and tidy, there's even fancy stuff like conventional commits and commit linters.
If you're just starting out, focus on clear commit messages and don't be afraid to ask your teammates for help.
And that's not all, here's an infographic for you and below a link to my YT video!