---
title: "Git"
slug: "git"
category: "coding"
tags: ["coding", "git", "workflow", "commits"]
status: "stable"
last_updated: 2026-05-14
summary: "Commit hygiene, rebase vs merge, Conventional Commits, hooks, and the rule that a commit message is the smallest unit of documentation."
related: ["[[coding/general-principles]]", "[[coding/shell]]", "[[tooling/github]]", "[[tooling/claude-code-workflow]]", "[[ops/github-pages]]"]
---

## 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 [[coding/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 [[glossary/conventional-commits|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.

```bash
git fetch origin
git rebase origin/main
```

A 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. For the full set of merge strategies and their options, see [[cheatsheets/git-merge-strategies]].

## 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 [[file-organization/dotfiles]] and [[file-organization/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 [[glossary/conventional-commits|Conventional Commits]] on the squashed subject, `main`'s history becomes one line per shipped change. Set this as the default in [[tooling/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. For day-to-day commands, see [[cheatsheets/git-commands]]. Add to `~/.gitconfig`:

```ini
[alias]
  lg = log --graph --oneline --decorate --all
  st = status -sb
  amend = commit --amend --no-edit
```

Combine with [[coding/shell]] for scripted Git workflows and [[tooling/claude-code-workflow]] for agent-driven commits.

## Related

- [[coding/general-principles]]
- [[coding/shell]]
- [[tooling/github]]
- [[tooling/claude-code-workflow]]
- [[ops/github-pages]]
- [[cheatsheets/git-commands]]
