12 Best Practices for Git Commit Messages (With Examples)

12 Git commit message best practices: 50-char imperative subject, why-focused body, trailers, Conventional Commits, fixup commits and CI linting.

Dec 17, 2025 - 15:16
Nov 7, 2026 - 05:16
 0  484
12 Best Practices for Git Commit Messages (With Examples)

A good Git commit message has a short imperative subject line (about 50 characters), a blank line, and a body that explains why the change was made, wrapped near 72 columns. Keep one logical change per commit, add trailers for issues and co-authors, and enforce the format with a commit-msg hook or CI check.

This guide covers one thing: the text you write when you run git commit. It belongs to our Git repository management guide, which is the hub for this series. Branch naming and merge policy are in the Git branching strategies guide, and the commands themselves (git log, git rebase -i, git commit --amend) are explained in 25 Git commands every DevOps engineer must know. Every rule and option below was checked against git-scm.com, conventionalcommits.org and the GitHub and GitLab docs in November 2026.

Key takeaways

  • Git treats everything up to the first blank line as the commit title. Keep it short (the git-commit manual suggests no more than 50 characters) and leave a blank line before the body.
  • Write the subject in the imperative mood: "Add retry to S3 upload", not "Added" or "Adds".
  • Use the body for the reason, the problem and any trade-off. The diff already shows what changed.
  • One logical change per commit makes git revert, git bisect and code review far easier.
  • Trailers such as Closes #123, Co-authored-by: and Signed-off-by: link commits to issues and people in a machine-readable way.
  • If your team wants automated changelogs and version bumps, adopt Conventional Commits and lint it in CI.

Anatomy of a commit message

A commit message has up to three parts: a subject line, an optional body, and optional trailers at the end. The git-commit manual says the text up to the first blank line is the commit title and "that title is used throughout Git", for example in git log --oneline, git shortlog, git rebase -i and the email subject produced by git format-patch.

Retry S3 uploads on 503 SlowDown responses

The nightly backup job failed three times last week because S3
returned 503 SlowDown during peak load. The upload client gave up
after the first error.

Wrap the upload in exponential backoff (5 attempts, max 30s) and
log each retry at WARN so the on-call engineer can see throttling
in the dashboard. A fixed sleep was considered but would slow every
upload, not only the throttled ones.

Closes #482
Co-authored-by: Priya Sharma <[email protected]>

Read the subject alone and you know what the commit does. Read the body and you know why it exists and what the author ruled out. The trailers tell the hosting platform which issue to close and who else wrote the code.

The 12 practices at a glance

#PracticeWhy it mattersQuick check
1Short subject (about 50 chars)Not truncated in logs and UIsgit log --oneline reads cleanly
2Blank line after subjectGit uses it to split title from bodyTitle shows alone in --oneline
3Imperative moodMatches Git's own generated messages"If applied, this commit will ..."
4No trailing period, consistent caseSaves space, looks consistentTeam style guide
5Body explains whyThe diff cannot explain intentProblem, approach, trade-offs
6Wrap body near 72 columnsReadable in terminals and patchesEditor ruler or git log
7One logical change per commitClean revert, bisect and reviewSubject has no "and"
8Link issues with trailersTraceability, auto-closeCloses #123
9Credit people with trailersCorrect attributionCo-authored-by:
10Use a structured convention if you automateChangelogs and SemVer bumpsConventional Commits
11Fix history before merging, not afterNo "fix typo" noise on main--fixup + --autosquash
12Enforce with templates, hooks and CIStandards survive deadlinescommit-msg hook, CI lint

Writing the subject line

1. Keep the subject short, about 50 characters

The git-commit manual recommends "a single short (no more than 50 characters) line summarizing the change". Git's own SubmittingPatches guide calls 50 characters a soft limit. GitHub and most terminals show longer lines, but they cut them off in lists and notifications, so the useful part should come first. If you cannot fit the change in 50 to 60 characters, the commit probably does more than one thing (see practice 7).

Name the thing that changed and the effect: "Cache Terraform providers in CI" is better than "CI improvements". Avoid subjects that only make sense with a ticket open, such as "PROJ-1182".

2. Separate the subject from the body with a blank line

This is the one formatting rule Git itself depends on. Without the blank line, Git treats the whole paragraph as the title, so git log --oneline and git rebase -i show a run-on line. Using git commit -m "subject" for small commits is fine. For anything that needs explanation, run git commit without -m so your editor opens, or pass a second -m, which Git adds as a separate paragraph.

# Subject and body from the command line (each -m becomes a paragraph)
git commit -m "Pin base image to node:22-alpine" \
  -m "Unpinned :latest pulled a new major version and broke npm ci."

# Open the editor and show the staged diff below the message
git commit -v

git commit -v places the diff under the message template. Git removes it before saving, but it lets you describe what you actually staged instead of what you remember changing.

3. Use the imperative mood

Write the subject as a command: "Add", "Fix", "Remove", "Rename". Git's SubmittingPatches guide puts it directly: describe your changes in imperative mood, "make xyzzy do frotz" instead of "[This patch] makes xyzzy do frotz". Git's own messages follow the same pattern, for example "Merge branch 'feature'" and "Revert ...".

A quick test: the subject should complete the sentence "If applied, this commit will ...". "If applied, this commit will fix race in token refresh" works. "If applied, this commit will fixed race" does not.

4. Drop the trailing period and pick one capitalisation rule

A subject line is a title, so the full stop adds nothing and costs a character. On capitalisation, conventions differ. Many teams capitalise the first word ("Fix ..."). The Git project itself uses an area prefix and a lowercase word after it ("doc: clarify ..."), and Conventional Commits usually reads "fix(auth): handle expired token". The specification says its parts are not case-sensitive except BREAKING CHANGE, and recommends being consistent. Choose one style, write it in CONTRIBUTING.md, and stick to it.

Writing the body

5. Explain why, not what

The diff shows which lines changed. It cannot show the bug report, the failed alternative or the constraint that forced a strange-looking line. Git's SubmittingPatches guide asks the body to explain the problem with the current code, justify why the change solves it, mention alternatives that were considered and dropped, and record decisions made during review. It also asks for messages that make sense without external links: summarise the discussion instead of pasting only a URL.

A useful order for the body is:

  1. The problem or the trigger (what was wrong, how it showed up).
  2. The approach and why it was chosen.
  3. Side effects, risks or follow-up work.

Six months later, someone running git blame on that line will read this paragraph instead of guessing.

6. Wrap the body near 72 columns

Git does not wrap text for you. git log indents the message by four spaces, and patches sent by email are read in fixed-width terminals, so a body wrapped at around 72 characters stays readable everywhere. Most editors can show a ruler or wrap automatically for Git commit files. Lists and code snippets inside the body are fine; keep them indented and short.

What goes into each commit

7. Make each commit one logical change

An atomic commit contains one self-contained change that builds and passes tests on its own. If your subject needs the word "and" ("Fix login redirect and update README and bump eslint"), split it. Small commits make three things easier:

  • Revert: git revert undoes exactly one change without dragging unrelated edits with it.
  • Bisect: git bisect lands on a small commit, so the cause of a regression is obvious.
  • Review: reviewers can read the branch commit by commit.

Use git add -p to stage only the hunks that belong together, and commit formatting-only changes separately from logic changes so the real diff is not buried.

Trailers: linking work and people

8. Link issues with closing keywords and trailers

Both major platforms can close issues from commit messages:

  • GitHub recognises close, closes, closed, fix, fixes, fixed, resolve, resolves and resolved, followed by an issue reference such as Closes #10 or Fixes octo-org/octo-repo#100. A keyword in a commit message closes the issue when the commit is merged into the default branch.
  • GitLab's default pattern accepts forms of Close, Fix, Resolve and Implement (for example Closes #123 or Implements group/project#123). The issue closes when the commit reaches the default branch. Text such as "Related to #5" only links; it does not close.

Put these references at the end of the message as trailers, not in the subject. For Jira or another tracker, a plain trailer such as Refs: PROJ-1182 keeps the ID searchable with git log --grep="PROJ-1182" without wasting subject characters.

9. Credit people with Co-authored-by and Signed-off-by

Trailers are Token: value lines in the last paragraph of the message. Two are worth knowing:

  • Co-authored-by: name <email>: GitHub shows every listed co-author on the commit. Add one line per person after a blank line.
  • Signed-off-by:: added by git commit -s. Projects that use the Developer Certificate of Origin (DCO), such as the Linux kernel, require it. A sign-off is a statement about the right to contribute; it is not a cryptographic signature. Signing commits uses git commit -S and a GPG or SSH key.
# Add trailers without editing the message by hand
git commit -s \
  --trailer "Co-authored-by: Priya Sharma <[email protected]>" \
  --trailer "Refs: PROJ-1182"

# Read trailers back, for example in a release script
git log -1 --format="%(trailers:key=Refs,valueonly)"

The --trailer option and git interpret-trailers keep the format correct, which matters when scripts or platforms parse these lines.

Conventions and automation

10. Adopt Conventional Commits when you automate releases

Conventional Commits 1.0.0 is a small specification layered on top of the rules above. The subject becomes <type>[optional scope]: <description>. The specification defines two types: feat (a new feature) and fix (a bug fix). Teams commonly add others such as docs, refactor, test, ci and chore, which the spec allows but does not require.

feat(api)!: return 404 instead of 200 for missing users

Clients that relied on an empty 200 body must now handle 404.

BREAKING CHANGE: GET /users/{id} returns 404 when the user does not exist
Refs: PROJ-1290

Breaking changes are marked with ! right before the colon, with a BREAKING CHANGE: footer (which must be uppercase), or both. The format maps to Semantic Versioning: fix means a PATCH release, feat means a MINOR release, and any breaking change means a MAJOR release. That mapping is why release tools can generate changelogs and version numbers from history.

CommitMeaningSemVer effect
fix(parser): handle empty YAML documentsBug fixPATCH (1.4.2 to 1.4.3)
feat(cli): add --dry-run flagNew featureMINOR (1.4.3 to 1.5.0)
feat(api)!: drop v1 endpointsBreaking changeMAJOR (1.5.0 to 2.0.0)
docs: fix install command in READMEDocs onlyNone by default

Do not adopt it only because it looks tidy. If nobody generates changelogs or versions from commits, a plain imperative subject is enough, and the type prefix eats into your 50 characters.

11. Clean up history before you merge, not after

Review fixes ("address comments", "fix typo", "oops") are normal on a feature branch. They should not land on the main branch. Git has built-in support for folding them into the commit they correct:

# Record a fix that belongs to an earlier commit on this branch
git commit --fixup=a1b2c3d

# Change only the message of an earlier commit
git commit --fixup=reword:a1b2c3d

# Before merging, fold fixups into their targets
git rebase -i --autosquash origin/main

--fixup creates a commit titled fixup! <original subject>, and git rebase --autosquash moves it next to its target and squashes it. Set git config --global rebase.autoSquash true to make that the default. Only rewrite commits that nobody else has built on; once a branch is shared, add new commits instead (the Git commands guide covers safe rebasing and --force-with-lease).

If your repository uses squash merging, the pull request title and description often become the final commit message. On GitHub you can set the default squash message to the pull request title, the title plus commit details, or the title plus description. In that case, apply the subject and body rules to the pull request itself.

12. Enforce the format with templates, hooks and CI

Standards written only in a wiki fade under deadline pressure. Use three layers, from gentle to strict:

  1. Template: git config commit.template .gitmessage pre-fills the editor with a reminder of the format. Lines starting with # are stripped.
  2. Local hook: a commit-msg hook receives the path of the message file and can reject the commit by exiting non-zero. Developers can skip it with --no-verify, so treat it as fast feedback, not control. Commit shared hooks in a folder such as .githooks and point Git at it with git config core.hooksPath .githooks.
  3. Server or CI check: GitLab Premium and Ultimate offer push rules named "Require expression in commit messages" and "Reject expression in commit messages". On any platform, a CI job can lint the commits in a merge request.
#!/bin/sh
# .githooks/commit-msg : reject subjects longer than 72 chars or ending with a period
subject=$(head -n1 "$1")
if [ ${#subject} -gt 72 ]; then
  echo "commit-msg: subject is ${#subject} chars; keep it under 72 (aim for 50)." >&2
  exit 1
fi
case "$subject" in
  *.) echo "commit-msg: remove the trailing period from the subject." >&2; exit 1 ;;
esac

For Conventional Commits, commitlint is a common choice. Install it with npm install -D @commitlint/cli @commitlint/config-conventional, add a config that extends @commitlint/config-conventional, and run it in the pipeline on the merge request range:

# GitHub Actions step (checkout must use fetch-depth: 0)
- name: Lint commit messages
  if: github.event_name == 'pull_request'
  run: npx commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose

Our post on why Git hooks are useful for enforcing standards goes deeper into client-side and server-side hooks.

Before and after: rewriting weak messages

Weak messageProblemBetter subject
fixed stuffPast tense, says nothingFix null pointer when cart is empty
WIPNot a finished changeFold into the real commit with --fixup
Update deployment.yamlRepeats the file listRaise API memory limit to 512Mi after OOM kills
PROJ-1182Needs a ticket to understandAdd audit log for role changes + Refs: PROJ-1182
Changes to login, tests, and readme.Three changes, trailing periodThree commits, one per change
Added a new feature that allows users to export their reports as CSV files from the dashboardToo long, past tenseAdd CSV export to report dashboard

Reading history: why the effort pays back

Good messages turn git log into a searchable record. A few queries that only work when messages are consistent:

# Every feature since the last release tag
git log v1.4.0..HEAD --oneline --grep="^feat"

# Find the commit that mentions a ticket
git log --grep="PROJ-1182" --format="%h %an %s"

# Short summary per author for release notes
git shortlog -sn v1.4.0..HEAD

For interview preparation on topics like these, see our Git and GitHub interview questions. If your team is still deciding who owns the standard, read who should be involved in defining Git commit standards.

Practise with real pipelines

Commit conventions make the most sense when you see them drive a pipeline: a lint job that fails a merge request, a tag that triggers a release, a changelog built from feat and fix commits. Our GitLab training and DevOps training cover these workflows hands-on, in the classroom in Kharadi, Pune or live online across India.

Frequently asked questions

How long should a Git commit subject line be?

Aim for 50 characters. The git-commit manual recommends no more than 50, and Git's contributor guide calls it a soft limit. Going to 60 or so is acceptable; beyond 72, tools start truncating and the commit probably covers more than one change.

Why should commit messages use the imperative mood?

It describes what the commit does when applied, and it matches messages Git generates itself, such as "Merge branch" and "Revert". A simple test is whether the subject completes "If applied, this commit will ...".

Should every commit follow Conventional Commits?

Only if you use it. Conventional Commits pays off when tools generate changelogs or Semantic Versioning bumps from history. If nothing reads the type prefixes, a clear imperative subject and a good body are enough.

What is the difference between Signed-off-by and a signed commit?

Signed-off-by is a text trailer added with git commit -s, often used for the Developer Certificate of Origin. A signed commit carries a cryptographic GPG or SSH signature added with git commit -S, which platforms can verify.

Can a commit message close an issue automatically?

Yes. On GitHub, keywords such as Closes #10 or Fixes #10 close the issue when the commit is merged into the default branch. GitLab supports Close, Fix, Resolve and Implement forms with the same default-branch condition.

How do I fix a bad commit message?

For the latest unpushed commit, run git commit --amend. For an older commit on your own branch, use git commit --fixup=reword:<commit> and then git rebase -i --autosquash. Avoid rewriting commits already on a shared branch.

Should I write commit messages if we squash merge?

Yes, for review. The squash commit often takes the pull request title and description, so write those with the same rules. Good branch commits still help reviewers read the change step by step.

How do I enforce commit message rules for a team?

Combine a commit template, a shared commit-msg hook set through core.hooksPath, and a CI job (for example commitlint) that checks every merge request. GitLab Premium also offers commit message push rules.

References

What's Your Reaction?

Like Like 0
Dislike Dislike 0
Love Love 0
Funny Funny 0
Angry Angry 0
Sad Sad 0
Wow Wow 0
Mridul I am a passionate technology enthusiast with a strong focus on DevOps, Cloud Computing, and Cybersecurity. Through my blogs at DevOps Training Institute, I aim to simplify complex concepts and share practical insights for learners and professionals. My goal is to empower readers with knowledge, hands-on tips, and industry best practices to stay ahead in the ever-evolving world of DevOps.