Overview
Git is the project’s permanent record. A scrambled history hides the bug that shipped on Tuesday and confuses every agent that reads git log for context. This page covers the rules that keep the record legible. Read general-principles first.
One commit equals one logical change
Every commit is a coherent unit a reviewer can revert in isolation. A commit that renames a file, fixes a bug, and bumps a dependency is three commits stapled together; split it with git add -p. If the diff cannot be summarized in one sentence, it is too big. Refactors land before behavior changes, never mixed in. A clean stack of small commits also gives git bisect something to work with.
A commit message is the smallest unit of documentation
The subject line is the API contract for future readers and for git log --oneline. The body explains the why that the diff cannot show.
fix(parser): reject invoices with negative line items
The upstream supplier started emitting refunds as negative
amounts on the same invoice as positive line items. Treat the
whole invoice as a credit note instead, per the 2026-05 spec.
Closes #482.
Rules: 50-character subject, blank line, 72-character wrapped body. No git commit -m "fix". No WIP or updates in main’s history.
Conventional Commits for the subject line
Use the Conventional Commits prefix so changelogs, release tooling, and humans can scan the log. The set worth using:
feat(scope):a user-visible feature.fix(scope):a bug fix.refactor(scope):behavior unchanged, structure changed.docs(scope):documentation only.chore(scope):build, deps, or tooling changes that ship nothing.test(scope):test-only changes.
The scope is the directory or subsystem (parser, auth, coding). Drop the scope only for repo-wide changes.
Rebase short-lived branches, merge long-lived ones
A feature branch alive for under a week should rebase onto main before opening a PR. The result is a linear history a reviewer can read top to bottom.
git fetch origin
git rebase origin/mainA long-lived integration branch (a release train, a multi-month migration) merges instead, because rebasing rewrites shared history. The rule: rebase what is yours alone, merge what other people have already pulled.
Never rebase published commits
Once a commit is pushed and someone else may have pulled it, it is immutable. Rebasing or --amend-ing a published commit forces every collaborator to reconcile a divergent history. git push --force-with-lease is the safe form for force-pushing your own feature branch; git push --force on main is a fireable offense. The same rule applies to git commit --amend: only amend before the push.
Pre-commit hooks run formatters and linters
Hooks are the cheapest place to catch the bugs that block CI. Wire prettier, ruff, eslint, shellcheck, and a secrets scanner into a pre-commit hook so they run before the commit lands. Use pre-commit (the framework) to keep the config portable. Do not skip hooks on a whim. git commit --no-verify exists for the rare emergency, not for everyday speed. A team that habitually skips hooks has hooks that need fixing.
.gitignore is not a security boundary
.gitignore keeps build output and editor noise out of the diff. It does not protect secrets. A .env file committed once stays in the history forever; the file is reachable in every clone after the “fix” commit. Treat any accidentally committed secret as compromised and rotate it. Run gitleaks or trufflehog in CI to fail the build on a key pattern. See dotfiles and config-files for the public-vs-private split that keeps secrets out of the repo in the first place.
Squash-merge as the PR default
Squash-merge collapses a feature branch into one commit on main. Combined with Conventional Commits on the squashed subject, main’s history becomes one line per shipped change. Set this as the default in github repository settings. Use merge commits only when the individual commits on the branch are themselves meaningful.
Aliases worth setting
Three aliases save real time. Add to ~/.gitconfig:
[alias]
lg = log --graph --oneline --decorate --all
st = status -sb
amend = commit --amend --no-editCombine with shell for scripted Git workflows and claude-code-workflow for agent-driven commits.