Web DevelopmentPUBLISHED UPDATED 11 MIN READ

I audited my 24 commits. None had a body.

Four commands will tell you whether your commit history is documentation or noise, and you can run all four in under a minute. I ran them against my own repository and the answer was unflattering: 0 of 24 commits had a body, 11 subject lines ran past 72 characters, and the longest was 2,550 characters - a five-paragraph explanation crammed into the field git treats as a one-line summary. This post is that audit. The commands, my real output, the rules the output broke, an honest look at Conventional Commits and at my own numbered 000 - prefix, and the commit template I switched to.

TL;DR

  • Four git commands audit any repository in under a minute: how many commits carry a body, how subject lengths fall against 50/72, the longest subject, and which verb your subjects start with.
  • The rule that matters is not the 50-character limit, it is the blank line: a subject summarises, a body explains, and almost every view git gives you shows only the subject.
  • A numbering prefix like 000 - is a genuinely useful handle, but it is an index, not a structure, and no number can carry the reason a commit exists.

Audit your commit history in four commands

Four commands tell you almost everything about the state of a repository's history: how many commits carry a body, how the subject lines fall against the 50/72 convention, how bad the worst one is, and which word your subjects start with. Run them in the repository you are least proud of.

1. How many commits carry a body at all?

echo "$(git log -z --format='%b' | tr -d '\n' | tr '\0' '\n' | grep -c .) of $(git rev-list --count HEAD)"

2. Where do subject lines fall against 50/72?

git log --format='%s' | awk '{n=length}
  n<=50 {a++} n>50 && n<=72 {b++} n>72 {c++}
  END {printf "  <=50 : %d\n  51-72: %d\n  >72  : %d\n", a, b, c}'

3. How bad is the worst one?

git log --format='%s' | awk '{ if (length > m) m = length } END { print m " characters" }'

4. Which word do your subjects start with?

git log --format='%s' | awk '{print $1}' | sort | uniq -c | sort -rn | head -5

Here is what those four printed for this website's repository, 24 commits deep:

0 of 24

  <=50 : 13
  51-72: 0
  >72  : 11

2550 characters

      4 Updated
      3 Integrated
      2 Made
      2 Fixed
      2 Created

Four readings, in order of how much they hurt.

Not one commit has a body. Not a short one, not a bad one. Zero. Every explanation I have ever written about this project lives on the subject line, which is the field git reserves for a summary.

Nothing sits in the middle. Thirteen subjects fit inside 50 characters and eleven run past 72. The 51-to-72 band, which is where a considered summary usually lands, is empty. That gap is the tell: an empty middle means nobody is editing a message down to fit. Each commit is either a stub written in two seconds or an essay written instead of a body.

The worst is 2,550 characters. For scale, git log --oneline prints one line per commit. Twenty-four commits should be twenty-four rows. In a 120-column terminal mine needs 129 rows, and a single commit accounts for 22 of them:

git log --oneline | awk '{ rows += int(length/120)+1 } END { print rows " rows for " NR " commits" }'
# 129 rows for 24 commits

A one-line-per-commit view that scrolls off the screen has stopped being a view.

Three verb moods in 24 commits. Nineteen subjects are past tense (Updated, Fixed, Integrated), two are imperative (Create projects page), and two are present-tense third person (Replaces, Moves). I did not choose three; I drifted three times and never noticed, because nothing in my workflow ever showed me the list.

That last point is the honest root cause of all of it. None of this happened because I did not know the rules. It happened because I never once looked at my own history as a list. The audit takes forty seconds and I had not run it in twenty-four commits. It is the same failure I hit when a 407-line agent work log stopped being readable: a record nobody reads back quietly stops being true.

Two more worth running

If your repository has more than one contributor, add these:

# subjects that say nothing - the classic filler verbs, alone or nearly alone
git log --format='%s' | grep -icE '^(wip|update|fix|misc|stuff|changes|minor|cleanup)s?\.?$'

# who writes bodies and who does not
git log --format='%an' | sort | uniq -c | sort -rn

The rule that matters is the blank line

A git commit message has two parts separated by a blank line: a subject that summarises the change, and a body that explains it. Everything else - the 50-character target, the 72-character limit, the imperative mood - exists to serve that split. Get the split right and the rest mostly follows.

The reason so few commits have a body is not laziness. It is that git commit -m "..." physically cannot write one. The -m flag takes a message and puts all of it in the subject, so a habit of -m is a habit of never writing a body. The git commit documentation is explicit about it, and there are two ways out:

# open your editor, write a subject, blank line, then the body
git commit

# or stay on one line: a second -m starts the body
git commit -m "Open content images full size in an overlay" \
           -m "Galleries rendered at layout width, so a dashboard screenshot was unreadable on a phone."

Why the subject carries so much weight

Nearly every view git gives you shows the subject and discards the body. git log --oneline, git shortlog, the todo list in git rebase -i, the one-line summaries in git log --graph, the commit list on GitHub and GitLab. Your body is written for the one person who runs git show; your subject is written for everyone else, forever.

That is where 50 and 72 come from. The pair is usually credited to Tim Pope's 2008 note on git commit messages and reached most people through Chris Beams' guide to writing a git commit message: aim for 50 characters, treat 72 as the ceiling. Both numbers have a concrete reason behind them.

  • 72 is where GitHub gives up. Longer titles are truncated in the commit list and the tail is hidden behind an ellipsis you have to click.
  • 72 also fits a terminal. git log indents the message by exactly four spaces. Run git log -1 | cat -A and count them. A 72-character line plus that indent still lands inside 80 columns with room to spare.
  • 50 is not a limit, it is a forcing function. If the summary will not fit in 50 characters, that is usually a signal that the commit is doing two things.

Imperative mood, and the sentence that settles it

Write the subject as a command: Add, Fix, Move, Remove. Not Added, not Adds. The test that ends the argument is one sentence:

If applied, this commit will _____.

If applied, this commit will add the contact form reads correctly. If applied, this commit will added the contact form does not. Git already writes its own messages this way - Merge branch 'main', Revert "Add the contact form" - so imperative subjects sit consistently alongside the ones git generates for you.

Nineteen of my twenty-four subjects fail that sentence.

Explain why, not what

The diff already tells anyone what changed. It cannot tell them why, what you tried first, or what you deliberately left alone. That is the body's job, and it is the part no tool can reconstruct later.

A body worth writing usually answers three things: what the situation was before, what this commit does about it, and what you chose not to do. If a commit fixes a bug, the body should say what the bug actually did to a user - "the parser assumed at least one token, so an empty file crashed the build" - because in eighteen months that sentence is the only thing standing between the next reader and re-deriving the whole problem from scratch.

Conventional Commits: what it buys and what it costs

Conventional Commits is the most widely adopted commit convention, and version 1.0.0 of the specification fits in one template:

<type>[optional scope]: <description>

[optional body]

[optional footer(s)]

Only two types are mandated: fix for a patch and feat for a feature, mapping onto the PATCH and MINOR levels of semantic versioning. The spec permits others and the commonly used set is build, chore, ci, docs, style, refactor, perf, test and revert. A breaking change is flagged either by a BREAKING CHANGE: footer or by an exclamation mark before the colon, as in feat(api)!: drop the v1 endpoint. Footers follow git's trailer format - a hyphenated token, a separator, a value - so Reviewed-by: Ada and Refs: #219 parse the same way git's own trailers do.

In practice:

fix(auth): reject expired refresh tokens

The refresh endpoint compared the expiry to the token issue time
instead of the current time, so an expired token minted a fresh
access token indefinitely.

Refs: #412
BREAKING CHANGE: refresh responses now return 401 instead of 200
with an error body.

What you actually get

The point of the format is that it is machine-readable. That buys you three concrete things:

  1. A changelog you do not write. Tools group commits by type and produce release notes from the history itself.
  2. Version bumps you do not decide. fix bumps the patch, feat bumps the minor, a breaking change bumps the major, automatically.
  3. A history you can filter. git log --grep '^feat' is a real question with a real answer.

What it costs

The honest counterweight is that all three benefits require a tool that consumes the format. If nothing generates your changelog and nothing bumps your version, Conventional Commits is a naming ceremony with no payoff. Three failure modes show up repeatedly:

  • chore: becomes a landfill. Anything that is not obviously a feature or a fix gets filed there, and the type stops carrying information.
  • Scopes invented per commit are noise. A scope is only useful if it comes from a fixed, agreed list. Freehand scopes produce feat(ui), feat(frontend) and feat(components) for the same directory.
  • The type is not a reason. fix(auth): tells you the category. It still does not tell you why the bug existed. The body does that, and the prefix can quietly feel like it has already done the explaining.

Choosing

Conventional Commits

A sequential prefix

No convention

Automated changelog

Yes

No

No

Automated version bump

Yes

No

No

Filterable by intent

Yes, by type

No

No

Portable handle outside the repo

Weak

Strong

None

Setup cost

Tooling and a hook

None

None

Fails when ignored

Loudly, CI blocks it

Quietly, a gap appears

Silently

The row that decides it for most people is the first. Adopt Conventional Commits when something downstream consumes it. Otherwise pick a convention you will keep, and spend the effort on the body instead.

The 000 - convention, honestly assessed

My own convention is a zero-padded sequence number: 000 - Initialized app, then 001, 002, up to 023 today. It appears in no standard. It is not in Conventional Commits, not in git's own SubmittingPatches, and I could not find it documented as a named convention anywhere. It is a house style, and it is worth being clear about what a house style can and cannot do.

What it genuinely buys me

A handle that survives history rewriting. Short hashes do not. Rebase a branch and every hash changes; squash two commits and one of them stops existing. 013 still means the same change in a client message, a task description and a note written three months ago. That portability outside the repository is the strongest argument for it and it is not a small one.

A gap you can see. If a number is missing, something was squashed, dropped or never pushed. A missing hash is invisible; a missing number is a hole in a sequence.

A way to label a split change. Two of my commits are 012 and 013, the same Sanity integration cut in half, with the second subject starting Part 2:. That integration is what renders the case studies on this site today. The numbers made the pairing legible at a glance. Conventional Commits has no equivalent - two feat(sanity): commits look unrelated in a list.

A tiny amount of friction in the right place. Choosing the next number means looking at the last one, which means looking at the log. That is more attention than most people give their history.

What it does not buy me

It is an index, not a structure. It sorts and it names; it cannot hold a reason. And that is not a neutral limitation - I think it is the direct cause of the audit result at the top of this post.

A numbered prefix makes the subject line feel like a form field. There is a number, a separator, and a space where the description goes. The space has no bottom to it, so when I had five paragraphs to say about the image overlay work, I typed all five into that space. The commit ends up 2,550 characters long and reads, in git log --oneline, as twenty-two rows of prose with a number in front of it.

Conventional Commits would not have saved me from that, incidentally. feat(images): ... followed by 2,500 characters is exactly as broken. The convention on the front of the subject was never the problem. The absent blank line was.

So the number stays. It has earned its place and swapping it for feat: would cost me the handle and buy me automation I do not run. What changes is what comes after it.

The template I am switching to

The fix keeps the number, caps the subject at 72 characters, and moves every explanation below a blank line. Here is the worst commit in my history, before and after.

Before, all 2,550 characters of it on the subject line:

016 - Made content images on the project and blog detail routes open full size in an overlay, because a screenshot of a dashboard rendered at card width is unreadable on a phone, and the previous hand-rolled overlay had no focus trap, so it was replaced with Base UI's modal Dialog which supplies Escape, focus movement, the trap and focus return, and the thumbnail stays at the call site so it is still server-rendered and present in the initial HTML, and images inside a Link are excluded because a button inside an anchor is invalid...

After:

016 - Open content images full size in an overlay

Case study galleries and in-post figures rendered at their layout
width, so a screenshot of a dashboard was unreadable on a phone.

Wraps content images in ImageZoom, which resolves its labels on the
server and hands them to a Base UI modal Dialog. That supplies
Escape, focus movement, the focus trap and focus return, which the
previous hand-rolled overlay did not have.

The thumbnail stays at the call site and stays server-rendered, so
it is still in the initial HTML.

Excludes images inside a Link: a button inside an anchor is invalid
HTML, and listing covers are teasers rather than content.

Same information. The first version is unreadable in every tool that shows a commit list. The second is a 48-character summary anyone can scan, with the full reasoning one keystroke away in git show.

Making it the default

Three changes, and only the first one really matters.

Stop reaching for -m. Point git at a template and run a bare git commit, so the editor opens with the shape already in front of you:

git config --global commit.template ~/.gitmessage ~/.gitmessage

, where every line is a comment git strips before saving:

# NNN - Subject in the imperative, 72 characters maximum
#
# Before: what the situation was
# Now:    what this commit does about it
# Not:    what was deliberately left alone
#
# Refs: #issue

Let a hook catch the long ones. A commit-msg hook is eight lines and it fails closed, which is the whole point - a convention nothing enforces is a preference. Git's hooks documentation lists the rest of them:

#!/bin/sh
# .git/hooks/commit-msg - reject an over-long subject
subject=$(head -n 1 "$1")
if [ ${#subject} -gt 72 ]; then
  echo "Subject is ${#subject} characters. The limit is 72." >&2
  echo "Move the explanation below a blank line." >&2
  exit 1
fi

Make it executable with chmod +x .git/hooks/commit-msg. Hooks are not committed by default, so either keep it in the repository and point core.hooksPath at that directory, or expect to reinstall it after every clone.

Fix what you can still fix. The most recent message is one command away:

git commit --amend

Older ones need an interactive rebase, changing pick to reword on the lines you want to edit:

git rebase -i HEAD~5

Both rewrite history and change hashes, so on a shared branch this needs coordination before you force-push. I am not rewriting my own twenty-four; the audit is more useful standing as it is. Running it against a codebase you inherited rather than one you wrote is a different job, and one I take on as technical consulting.

What you get

  • A history you can read as a list. Twenty-four commits should take twenty-four rows, and after this they do.
  • Reasons that survive. The body outlives the pull request, the chat thread and the platform the code is hosted on.
  • A summary that fits everywhere. Under 72 characters means no ellipsis on GitHub and no wrapping in a terminal.
  • A rule that enforces itself. The hook fails the commit. A guideline in a README does not.

None of this required a new tool, and it is not really about numbering versus feat: either. Both of those are prefixes, and a prefix was never the missing piece. What was missing was one blank line and the paragraph underneath it.

A number tells you where a commit sits. Only a body tells you why it exists.

Ogtay Iskandarov

Designer and full-stack developer running klauzzdcode, a one-person studio in Baku. Freelance since 2023, I ship products from Figma to deploy and write down what survives contact with production.

STORY →

FAQ

How long should a git commit subject line be?

Aim for 50 characters and treat 72 as the hard limit. GitHub truncates longer titles with an ellipsis, and git indents messages by four spaces in the log, so 72 still fits an 80-column terminal. Anything longer belongs in the body.

Does the commit body matter if the pull request already explains the change?

Yes. A pull request lives on a platform you might leave; the body lives in the repository. git log and git blame read the commit, not the PR, and a squash merge routinely discards the discussion that explained the change.

Is numbering commits like "000 - " an actual convention?

No published standard defines it. It is a house style, and a useful one: the number is a short handle that survives rebases and reads the same in a chat message. It is an index, though, not a replacement for a commit body.

Should a solo developer use Conventional Commits?

Only if something consumes the format. It pays for itself when a tool generates a changelog or a version bump from your history. Without that you get the ceremony and none of the automation. The subject and body split pays off either way.

Can I fix commit messages I have already pushed?

Yes, but it rewrites history. git commit --amend fixes the most recent message and git rebase -i with reword fixes older ones. Both change the commit hashes, so coordinate with anyone sharing the branch before you force-push.