Here's a scene that plays out on every Android team at least once. Someone pushes a commit with the message fix. Another one says changes. A third says asdfjkl. Three weeks later there's a production bug, you're doing git blame, and the trail goes cold at misc updates.
Sound familiar? That's not a tools problem — that's a communication problem. And Conventional Commits fixes it.
It's a dead-simple specification that brings structure to your commit messages. Readable history, automated changelogs, semantic versioning, and cleaner PRs — all from adding a few words before your commit description. And with pre-commit hooks, you enforce it automatically so nobody on your team can skip it even if they wanted to.
Let's set it up from scratch — including Android Studio, because yes, the IDE bypasses hooks by default and nobody tells you that.
Why Bother? The Real Benefits
Beyond just looking tidy, a structured commit history is actually useful. Here's what you get:
- Instant context — anyone reading the history knows what changed, why, and what type of change it was
- Automated changelogs — tools like
standard-versioncan generate a full changelog from your commit history automatically - Semantic versioning —
featbumps a minor version,fixbumps a patch,BREAKING CHANGEbumps major - Better PR reviews — reviewers understand the intent of each commit before reading the diff
- CI/CD triggers — GitHub Actions workflows can be triggered or filtered based on commit type
Bad vs Good Commit Messages
| ❌ Bad | ✅ Good (Conventional) |
|---|---|
fix stuff |
fix(auth): [ANDR-123] resolve null crash on login |
wip |
feat(profile): [ANDR-456] add avatar upload support |
updated files |
refactor(network): simplify retrofit client setup |
asdfgh |
test(viewmodel): add unit tests for StateFlow updates |
changes |
chore(deps): update kotlin to 2.0.0 |
The Format — Simpler Than It Looks
There are only three required parts. Everything else is optional:
<type>[optional scope]: [TICKET-ID] <short description> [optional body — explains what and why, not how] [optional footer — breaking changes, references]
Real-world examples from an Android project:
# Feature with scope and ticket reference feat(login): [ANDR-101] add biometric authentication support # Bug fix with body explaining the root cause fix(network): [ANDR-202] handle timeout on slow connections Increased OkHttp read timeout from 10s to 30s. Root cause: API response time increased after server migration. # Breaking change feat(api): [ANDR-303] rename getUserData() to fetchUser() BREAKING CHANGE: getUserData() has been removed. Migrate all call sites to use fetchUser() instead. # Revert with SHA references revert: let us never again speak of the noodle incident Refs: 676104e, a215868 # Chore — no ticket needed for dependency updates chore(deps): update gradle wrapper to 8.7
All Commit Types
| Type | Version Bump | When to Use | Android Example |
|---|---|---|---|
feat |
Minor | New feature added | feat(ui): add dark mode toggle |
fix |
Patch | Bug fix | fix(crash): resolve NPE in MainActivity |
docs |
Patch | Documentation only | docs: update README setup steps |
style |
Patch | Formatting, whitespace, no logic change | style: apply ktlint formatting |
refactor |
Patch | Code change, no bug fix or feature | refactor(vm): extract repository layer |
perf |
Patch | Performance improvement | perf(list): use DiffUtil in RecyclerView |
test |
Patch | Adding or fixing tests | test(api): add unit tests for RetrofitClient |
build |
Patch | Build system, dependencies | build(deps): upgrade AGP to 8.4.0 |
ci |
Patch | CI config changes | ci: add release workflow for tags |
chore |
Patch | Other maintenance tasks | chore: update .gitignore for Android |
revert |
Patch | Revert a previous commit | revert: feat(login) add biometric auth |
Pull Request Guidelines
The same discipline that applies to commits should apply to PRs. A well-structured PR title makes reviews faster and your CI/CD pipeline smarter:
# PR title format <type>(<scope>): [TICKET-ID] <short description> # Examples feat(auth): [ANDR-101] Add biometric login support fix(crash): [ANDR-202] Fix NPE on profile screen rotation refactor(network): [ANDR-303] Migrate from Volley to Retrofit
A good PR description should include:
- What — a short summary of the change
- Why — the ticket/issue context
- How to test — steps for the reviewer to verify
- Screenshots — for UI changes
Enforcing It Automatically - No More Policing Your Team
Relying on team discipline to follow a commit format is a losing battle. Someone will always be in a hurry. Pre-commit hooks solve this by rejecting non-compliant commits at the source — before they ever reach the remote. No code review, no Slack message, no awkward "hey can you fix your commit message" conversation.
Step 1: Install pre-commit via Homebrew
# Install brew install pre-commit # Verify installation pre-commit --version
Step 2: Install the hooks in your project
# Run from your project root pre-commit install -t commit-msg -t pre-commit -t pre-push --allow-missing-config
Step 3: Create .pre-commit-config.yaml
Add this file to your project root. It uses updated hook versions compatible with current tooling:
# .pre-commit-config.yaml
default_stages: [commit, push]
repos:
# Commitlint — enforces conventional commit format
- repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook
rev: v9.5.0
hooks:
- id: commitlint
stages: [commit-msg]
additional_dependencies:
- "@commitlint/config-conventional"
- conventional-changelog-conventionalcommits
# Common pre-commit checks
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: check-json # Validates JSON files
- id: check-merge-conflict # Catches unresolved merge conflicts
- id: detect-private-key # Prevents committing private keys
- id: no-commit-to-branch # Prevents direct commits to main/develop
args: ['--branch', 'main', '--branch', 'develop']
# Gradle Spotless — enforces code formatting
- repo: https://github.com/jguttman94/pre-commit-gradle
rev: v0.3.0
hooks:
- id: gradle-spotless
args: ['-w', '--wrapper']
Step 4: Create commitlint.config.js
Add this to your project root. Customize issuePrefixes to match your Jira/Linear ticket format:
// commitlint.config.js
module.exports = {
extends: ["@commitlint/config-conventional"],
parserPreset: {
parserOpts: {
// Add your project's ticket prefixes here
issuePrefixes: ['ANDR-', 'FEAT-', 'BUG-', 'CHORE-']
}
},
rules: {
"body-leading-blank": [1, "always"],
"footer-leading-blank": [1, "always"],
"header-max-length": [2, "always", 72],
"scope-case": [2, "always", "lower-case"],
"subject-case": [2, "never", ["sentence-case", "start-case", "pascal-case", "upper-case"]],
"subject-empty": [2, "never"],
"subject-full-stop": [2, "never", "."],
"type-case": [2, "always", "lower-case"],
"type-empty": [2, "never"],
"type-enum": [2, "always", [
"build", "chore", "ci", "docs",
"feat", "fix", "perf", "refactor",
"revert", "style", "test"
]]
}
};
Step 5: Commit the config files
git add .pre-commit-config.yaml commitlint.config.js git commit -m "chore: add conventional commits pre-commit hooks"
What it looks like when it works
A passing commit:
> git commit -m "fix: [ANDR-123] resolve crash on rotation" Check JSON...................................................Passed Check for merge conflicts....................................Passed Detect private key...........................................Passed commitlint...................................................Passed [main 3a4f2d1] fix: [ANDR-123] resolve crash on rotation
A rejected commit:
> git commit -m "fixed stuff" commitlint...................................................Failed - hook id: commitlint - exit code: 1 ⧗ input: fixed stuff ✖ subject may not be empty [subject-empty] ✖ type may not be empty [type-empty] ✖ found 2 problems, 0 warnings
Android Studio - The Trap Nobody Warns You About
Here's the gotcha that trips up almost every Android team. You spend 30 minutes setting up pre-commit hooks, everyone runs pre-commit install, and you feel good about it. Then a teammate commits bug fixed directly from the Android Studio dialog — and nothing stops them.
Android Studio's Git commit dialog uses its own Git implementation (JGit) which bypasses pre-commit hooks by default. All that enforcement? Silently skipped. Here's how to fix it.
Option 1: Enable "Run Git hooks" in Android Studio
This is the simplest fix. It makes Android Studio respect your pre-commit hooks:
- Open Android Studio → Settings (or Preferences on macOS)
- Go to Version Control → Git
- Check the option "Run Git hooks"
- Click Apply → OK
Now when you commit from the Android Studio dialog, it will trigger your pre-commit hooks exactly as if you committed from the terminal.
pre-commit install.
Option 2: Set a Commit Message Template
Even with hooks enabled, a commit template pre-fills the format in the Android Studio commit dialog — helping developers write correct messages without memorising the format.
Step 1 — Create the template file:
# Create ~/.gitmessage file touch ~/.gitmessage
Add this content to ~/.gitmessage:
# <type>(<scope>): [TICKET-ID] <short description> # # Types: feat | fix | docs | style | refactor | perf | test | build | ci | chore | revert # # Examples: # feat(login): [ANDR-101] add biometric authentication # fix(crash): [ANDR-202] resolve NPE on profile screen # chore(deps): update kotlin to 2.0.0 # # Body (optional — explain WHAT and WHY, not HOW): # # # Footer (optional — BREAKING CHANGE or Refs): #
Step 2 — Register the template globally:
git config --global commit.template ~/.gitmessage
Android Studio automatically picks up the global git config. The next time you open the commit dialog, the template will be pre-filled as a guide.
For project-level template (applies to all contributors who clone the repo):
# Store template in repo touch .gitmessage # Configure in project's local git config git config commit.template .gitmessage # Commit the template file git add .gitmessage git commit -m "chore: add commit message template"
Prerequisites — Node.js Required for commitlint
commitlint is an npm package. The pre-commit config installs it automatically via additional_dependencies, but Node.js must be installed on your machine first — Android developers may not have it by default.
# Check if Node.js is installed node --version # If not installed, install via Homebrew (macOS/Linux) brew install node # Or download from nodejs.org (Windows) # https://nodejs.org/en/download
A Note on the Gradle Spotless Hook
The gradle-spotless hook in the .pre-commit-config.yaml enforces code formatting using Spotless. However, Spotless must be configured in your build.gradle first — the hook will fail with an error if Spotless isn't set up in your project.
Add Spotless to your build.gradle (app):
plugins {
id 'com.diffplug.spotless' version '6.25.0'
}
spotless {
kotlin {
target '**/*.kt'
ktlint('1.2.1')
trimTrailingWhitespace()
endWithNewline()
}
kotlinGradle {
target '**/*.kts'
ktlint('1.2.1')
}
}
If you don't want to use Spotless yet, simply remove the gradle-spotless block from your .pre-commit-config.yaml.
A Few Things That'll Save You Headaches
Skip hooks when needed
Sometimes you genuinely need to bypass the hook — for example a quick WIP save on a personal branch. Use --no-verify:
# Bypass all pre-commit hooks for this commit git commit --no-verify -m "wip: temp checkpoint"
--no-verify skips ALL hooks including merge conflict detection and private key checks. Never use it on shared branches.
Keep hook versions up to date
Run this periodically to update all hook versions in your .pre-commit-config.yaml to the latest:
pre-commit autoupdate
Run hooks in GitHub Actions CI
Pre-commit hooks run on the developer's machine. To also enforce them in CI as a safety net, add this step to your GitHub Actions workflow:
# In your PR checks workflow - name: Run pre-commit hooks uses: pre-commit/action@v3.0.1
This runs all your pre-commit hooks against every file changed in the PR, catching anything that slipped through locally.
Rules Worth Tattooing on Your Keyboard
- Keep the subject line under 72 characters — it's the limit enforced by most Git tools and exactly what commitlint checks. Long subjects get truncated in GitHub's UI.
- Use the imperative mood — write "add feature" not "added feature" or "adding feature". Think of it as a command: "this commit will add feature".
- One logical change per commit — don't bundle a bug fix and a refactor into the same commit. If you need two types, make two commits. This makes reverting and bisecting much easier.
- Reference ticket IDs — always include your project management ticket (Jira, Linear, GitHub Issue) in the commit. This creates a traceable link between code changes and requirements.
- Commit the config files with the whole team — pre-commit hooks only work if every team member runs
pre-commit installafter cloning. Document this in your project's README setup instructions. - Use
BREAKING CHANGEfooter carefully — this triggers a major version bump in automated versioning. Only use it when an API change genuinely breaks existing consumers.
Frequently Asked Questions
What is the Conventional Commits specification?
Conventional Commits is a lightweight convention for commit messages. It defines a structured format of type(scope): description that makes commit history readable, enables automated changelog generation, and supports semantic versioning. See conventionalcommits.org for the full spec.
What is the difference between feat and feature commit types?
They are essentially the same — feat is the official Conventional Commits type. Some teams also allow feature as an alias. In commitlint you can add both to the type-enum rule to support both spellings.
Do pre-commit hooks work on Windows?
Yes — install via pip install pre-commit since Homebrew is macOS/Linux only. After installing, run pre-commit install from your project directory. All hooks including commitlint work cross-platform.
Can I use Conventional Commits without pre-commit hooks?
Yes — it's just a format, you can follow it manually. However, without hooks compliance depends entirely on team discipline. Pre-commit hooks are strongly recommended for teams to ensure consistent enforcement across all contributors.
- Format:
type(scope): [TICKET] description - feat → minor bump, fix → patch bump, BREAKING CHANGE → major bump
- Use imperative mood — "add feature" not "added feature"
- Keep subject line under 72 characters
- Enforce with pre-commit + commitlint — no manual policing needed
- Always reference ticket IDs — links code to requirements
- Install pre-commit on Windows via pip, macOS/Linux via Homebrew
Very helpful. Thanks for sharing
ReplyDelete