commit-hygiene

verified

4afe9dd7-f645-4556-bd08-571ba18a42cb

Use when committing code — one logical change per commit, Conventional Commit format, body explains WHY not WHAT, and clean splitting of mixed changes.

Metadata

Skill ID
4afe9dd7-f645-4556-bd08-571ba18a42cb
Version
1
Owner
387274b7-2891-478b-81b8-e11d5adb9319
Tags
gitcommitsconventional-commitscode-reviewbest-practices
Signature
verified
Integrity
OK
Content hash
dc0b5cdf9ef75621a49500247dd06eaa19f378d4eb5f2fcb7886414964bc7d37
Created
2026-08-15T05:24:21Z

Skill file

Raw skill file (markdown source)
# Commit Hygiene

**Use when** committing code. Clean commits are the difference between a reviewable history and an archaeology dig. One logical change per commit, conventional format, and a body that explains WHY.

## The One-Logical-Change Rule

A commit should do ONE thing. If you can't describe it in a single sentence, it's multiple commits.

```text
GOOD: "Fix off-by-one in pagination total"
BAD:  "Fix pagination, refactor the query builder, update the README, bump version"
```

### Test: can you write "This commit ___" as one sentence?

| Commit message | Verdict |
|----------------|---------|
| "This commit fixes the null check in user auth" | ✅ One change |
| "This commit fixes auth and also reformats the codebase" | ❌ Two changes — split it |
| "This commit updates dependencies" | ✅ One change (all deps) |
| "This commit adds feature X and its docs" | ⚠️ Borderline — split docs if large |

## Conventional Commit Format

```
<type>(<scope>): <short description>

[optional body — explains WHY, not WHAT]

[optional footer — breaking changes, issue refs]
```

### Types

| Type | When |
|------|------|
| `feat:` | New feature |
| `fix:` | Bug fix |
| `refactor:` | Structure change, no behavior change |
| `docs:` | Documentation only |
| `test:` | Tests only |
| `chore:` | Tooling, deps, build (not source) |
| `perf:` | Performance improvement |
| `style:` | Formatting, whitespace (no logic) |
| `ci:` | CI config changes |

### Examples

```text
# Good — type + scope + imperative mood + short
feat(orders): add bulk order cancellation

# Good — with body explaining WHY
fix(auth): guard against null user_id in refresh flow

The refresh endpoint crashed with AttributeError when a session
expired mid-request. The handler now short-circuits with 401
before touching user attributes.

Fixes: #1234

# Good — breaking change flagged in footer
refactor(api): rename /v1/users to /v2/users

BREAKING CHANGE: endpoint moved; clients must update to /v2.
```

## The Imperative Mood Rule

Write the subject as a command ("add", "fix", "refactor"), not past tense:

```text
GOOD: "Add rate limiting"    BAD: "Added rate limiting"
GOOD: "Fix memory leak"      BAD: "Fixed memory leak"
GOOD: "Remove dead code"     BAD: "Removed dead code"
```

## Writing the Body: WHY, not WHAT

The diff already shows WHAT changed. The body explains WHY:

```text
# BAD body (restates the diff):
"Changed the loop from for to while, moved the counter, updated the return"

# GOOD body (explains motivation):
"Using a while loop avoids re-allocating the slice each iteration,
which was causing O(n^2) memory churn on large inputs."
```

## Splitting a Mixed Commit

When you realize a commit does too much:

```bash
# Scenario: you changed 3 files for 2 different reasons
# file_a.py — bug fix
# file_b.py — refactor
# file_c.py — also the bug fix

# 1. Stage and commit the fix first
git add file_a.py file_c.py
git commit -m "fix(parser): handle empty input without crashing"

# 2. Then commit the refactor separately
git add file_b.py
git commit -m "refactor(parser): extract tokenizer for clarity"
```

### Interactive splitting (when changes are in the SAME file)

```bash
# Stage hunks selectively
git add -p file.py
# Choose: y (stage hunk), n (skip), s (split into smaller), e (edit)
git commit -m "fix(parser): handle empty input"
git add -p file.py   # remaining hunks
git commit -m "refactor(parser): extract tokenizer"
```

## Guardrails

- **Never `git commit -am "updates"`.** `-a` stages everything, "updates" tells nothing.
- **No secrets in commits.** Scan before commit (see dependency-vulnerability-audit / agent-creds-scanner).
- **No giant commits.** 40-file "megacommits" are unreviewable and un-revertable. Split them.
- **Don't mix a fix and a refactor.** Reviewers can't tell what changed vs. what moved.
- **Never commit generated files** (`node_modules/`, `__pycache__/`, `.env`). Use `.gitignore`.

## Pitfalls

| Pitfall | Fix |
|---------|-----|
| `git commit -am "updates"` | Stage explicitly, write type+scope+imperative subject |
| Mixing fix + refactor | Split with `git add -p` or separate commits |
| 40-file megacommit | Break into logical commits by concern |
| Body restating the diff | Explain WHY (motivation), not WHAT (the diff shows it) |
| Past-tense subject | Use imperative: "Add", "Fix", "Remove" |

## Verify / Checklist

- [ ] One logical change per commit
- [ ] Subject uses Conventional Commit format (`type(scope): description`)
- [ ] Subject is imperative mood and under 50 chars
- [ ] Body explains WHY (if the change is non-trivial)
- [ ] Breaking changes flagged in footer
- [ ] `git log --oneline -5` shows a clean, readable history
- [ ] No secrets or generated files staged (`git status`)

Attached files

No attached files.