Committing with Intention: The Art of a Good Commit

Committing with Intention: The Art of a Good Commit

Reading Time: 5 minutes

Part 2 of the Git Mastery Series
Part 1: How Git Actually Thinks | Part 3: Branching Without Fear →

Six months into a project, you’re hunting a bug. You run git log and the history looks like this:

a3f8c9d fix
1b4e7a2 update
c5d2f8a wip
9d3e1f4 asdf
7a2b5c8 final
4f1e8d6 final2
2c9a3b7 ok now it works

Who wrote this? You did. And now you have no idea what any of these commits contain without opening each one individually.

Compare that to this:

a3f8c9d fix(auth): handle expired JWT tokens on refresh
1b4e7a2 feat(cart): add quantity update on product page
c5d2f8a refactor(api): extract payment service into dedicated class
9d3e1f4 fix(checkout): prevent duplicate order on double-click submit

Same code. Same history. The second version is documentation. The first is noise.

The commit isn’t just a save point. It’s a message to the next person reading this code — which is almost always future you.


What an Atomic Commit Actually Means

“Atomic commit” is one of those terms that gets thrown around without explanation. Here’s what it means in practice: one commit, one reason to exist.

Not one file. Not one function. One reason. A refactor and a bug fix are two reasons, even if they touched the same file. A feature and its tests are one reason — the test is part of the feature. Adding a dependency and using it are one reason — the usage doesn’t work without the dependency.

The test for atomicity: can you describe what this commit does in one sentence, without using “and”?

  • ✅ “Fix the null pointer exception on empty cart checkout”
  • ✅ “Add email validation to the registration form”
  • ❌ “Fix cart bug and update user model and add some tests”

The second type isn’t just harder to describe — it’s harder to revert, harder to cherry-pick, harder to understand in code review. Every “and” in a commit description is a sign that the commit should be split.


The Staging Area Is Your Drafting Table

Most developers use git add . for everything and never think about the staging area. This is like writing an entire email in one shot instead of drafting it — it works, but you’re not using the tool the way it was designed.

The staging area exists so you can compose exactly what you want in the next commit, regardless of what’s in your working directory. You might have three different changes across five files, and you want to commit them separately. The staging area lets you do that.

Stage specific files:

git add src/auth/login.php
git add src/auth/logout.php
git commit -m "feat(auth): add login and logout handlers"

git add src/user/profile.php
git commit -m "feat(user): add profile page"

Stage specific lines within a file — this is the one most developers don’t know exists:

git add -p src/user/model.php

This opens an interactive prompt that walks through each change in the file and asks what to do with it:

@@ -15,6 +15,10 @@ class User extends Model
     protected $fillable = ['name', 'email'];
+
+    public function orders() {
+        return $this->hasMany(Order::class);
+    }
+
     public function profile() {

Stage this hunk [y,n,q,a,d,/,e,?]?

Type y to stage that chunk, n to skip it. You can even press e to edit the exact lines you want to stage.

Once you use git add -p regularly, you stop ending up with commits that contain three things that should have been separate. You start thinking in commits while you code rather than after.


Conventional Commits: A Lightweight Standard That Pays Off

Conventional Commits is a simple format that looks like this:

type(scope): description

Optional longer body explaining what and why,
not how (the code shows how).

Optional footer: Closes #123

Common types: feat, fix, refactor, docs, test, chore, perf.

You don’t need a tool to enforce this. You just need the habit. What it buys you:

Scannable history. feat vs fix vs refactor gives you instant context when reading git log. You can tell at a glance whether a release was mostly new features or mostly fixes.

Automated changelogs. Tools like semantic-release and conventional-changelog parse commit messages to generate release notes automatically. Your commit messages become your release documentation.

Clearer intent in code review. A PR with commits like feat(payment): add Razorpay integration, test(payment): add unit tests for webhook handler, docs(payment): add setup guide tells the reviewer exactly what to expect in each commit.

Real examples:

git commit -m "feat(auth): add OTP login via Fast2SMS"
git commit -m "fix(cart): prevent negative quantity on decrement"
git commit -m "refactor(api): extract HTTP client into service layer"
git commit -m "chore(deps): upgrade Laravel from 10 to 11"
git commit -m "perf(images): lazy load product images on listing page"

These aren’t impressive for their complexity. They’re impressive for their clarity. A new developer joining the team can read three months of history and understand what the product has been doing.


Writing the Commit Body: When and Why

The subject line (the -m part) covers the what. The body covers the why. Most commits don’t need a body. But when context matters — when a decision isn’t obvious, when you’re fixing something counterintuitive, when future-you will need context — the body is where that lives.

git commit

This opens your editor. Write the subject, leave a blank line, then write the body:

fix(payment): retry failed Razorpay webhooks on 5xx errors

Razorpay occasionally returns 503 on their webhook endpoint during
high load. Without retry logic, missed webhooks left orders stuck
in "pending" state with no automated resolution path.

Added exponential backoff (3 retries, 2s/4s/8s delays). If all
retries fail, the webhook is queued for manual review.

Closes #247

Six months from now, the person debugging this code will read that and understand not just what changed, but why. That person is almost certainly you.

The rule of thumb: if you had to explain this change in a code review, write that explanation in the commit body. Future developers deserve the same context your reviewer got.


Interactive Rebase: Cleaning Up Before You Share

Here’s a common situation: you’ve been working on a feature for a day. Your commits look like this:

wip: halfway through auth refactor
fix typo
add missing return statement  
actually fix the auth refactor
remove debug logs

This is fine as a work-in-progress history. It’s not fine to merge into main. Before opening a pull request, clean this up with interactive rebase:

git rebase -i HEAD~5

This opens an editor showing the last 5 commits:

pick a3f8c9d wip: halfway through auth refactor
pick 1b4e7a2 fix typo
pick c5d2f8a add missing return statement
pick 9d3e1f4 actually fix the auth refactor
pick 7a2b5c8 remove debug logs

Change pick to squash (or s) to fold commits together, reword (or r) to edit a message, drop (or d) to remove a commit entirely:

reword a3f8c9d wip: halfway through auth refactor
squash 1b4e7a2 fix typo
squash c5d2f8a add missing return statement
squash 9d3e1f4 actually fix the auth refactor
squash 7a2b5c8 remove debug logs

Save, close, and Git walks you through editing the final commit message. The result: one clean commit with a proper message, ready to merge.

One rule: only do this on commits you haven’t pushed to a shared branch yet. Rewriting history that others have pulled creates conflicts for them. On your local branch before a PR? Rebase freely. On a branch others are working from? Don’t touch history.


The Commit as a Unit of Communication

Here’s the mindset shift that makes all of this click: a commit isn’t a backup. It’s a message.

When you write git commit -m "fix", you’re not just saving your work. You’re writing a letter to everyone who will ever read this codebase — including yourself at 2am six months from now, trying to understand why this line exists.

The developers who write good commits aren’t the ones who have more time. They’re the ones who’ve experienced the cost of bad commits firsthand and decided it was worth the thirty extra seconds.

It always is.


Quick Reference

# Stage specific file
git add path/to/file

# Stage specific lines interactively
git add -p path/to/file

# See what's staged vs unstaged
git diff           # unstaged changes
git diff --staged  # staged changes

# Commit with just a subject
git commit -m "feat(scope): description"

# Commit with subject + body (opens editor)
git commit

# Clean up last N commits before pushing
git rebase -i HEAD~N

# Amend the most recent commit (message or content)
git commit --amend

# Undo last commit but keep changes staged
git reset --soft HEAD~1

# Undo last commit and unstage changes (keep files)
git reset HEAD~1

Part 1: How Git Actually Thinks | Next: Part 3 — Branching Without Fear →

If this was useful, I turned the whole series into a 23-page PDF reference — checklists, hook templates, 80+ commands, reflog & bisect deep-dives, and a recovery playbook for 12 real emergencies.

Git Mastery Field Guide →

Committing with Intention: The Art of a Good Commit

Oh hi there 👋
It’s nice to meet you.

Sign up to receive awesome content in your inbox.

We don’t spam! Read our privacy policy for more info.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

3 Comments