Conventional Commits for Android — Write Better Git Commit Messages Every Time

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.

Conventional Commits Git commit guidelines

Photo by Yancy Min on Unsplash — clean git history starts with clean commit messages

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-version can generate a full changelog from your commit history automatically
  • Semantic versioningfeat bumps a minor version, fix bumps a patch, BREAKING CHANGE bumps major
  • Better PR reviews — reviewers understand the intent of each commit before reading the diff
  • CI/CD triggersGitHub 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

Conventional Commits type examples

Conventional Commits format examples in practice

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.

⚠️ Important: If your team uses Android Studio's Git UI without the fixes below, pre-commit hooks will not run — developers can commit any message format and it will go through unchecked.

Option 1: Enable "Run Git hooks" in Android Studio

This is the simplest fix. It makes Android Studio respect your pre-commit hooks:

  1. Open Android Studio → Settings (or Preferences on macOS)
  2. Go to Version Control → Git
  3. Check the option "Run Git hooks"
  4. 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.

✅ Recommended: This is the preferred option for teams. Ask every team member to enable this setting after running 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"
💡 Best of both: Use Option 1 (Run Git hooks) to enforce the format automatically, and Option 2 (commit template) to guide developers as they type. Together they cover both enforcement and discoverability.

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"
⚠️ Use sparingly. --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 install after cloning. Document this in your project's README setup instructions.
  • Use BREAKING CHANGE footer 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.

📝 Summary
  • 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

Pragnesh Ghoda

A forward-thinking developer offering more than 8 years of experience building, integrating, and supporting android applications for mobile and tablet devices on the Android platform. Talks about #kotlin and #android

1 Comments

Please let us know about any concerns or query.

Previous Post Next Post

Contact Form