From 16e0a563d41d796e375d16b72727015540753ada Mon Sep 17 00:00:00 2001 From: Henry Chen Date: Mon, 8 Jun 2026 15:51:06 -0400 Subject: [PATCH] .git: Add git commit message guidelines Co-authored-by: Claude Opus 4.8 --- .githooks/commit-msg | 8 ++++ .githooks/lib/check-message | 45 ++++++++++++++++++ .github/workflows/ci.yml | 20 ++++++++ .gitmessage | 15 ++++++ AGENTS.md | 26 ++++++++++ CONTRIBUTING.md | 95 +++++++++++++++++++++++++++++++++++++ Justfile | 3 +- 7 files changed, 211 insertions(+), 1 deletion(-) create mode 100755 .githooks/commit-msg create mode 100755 .githooks/lib/check-message create mode 100644 .gitmessage create mode 100644 CONTRIBUTING.md diff --git a/.githooks/commit-msg b/.githooks/commit-msg new file mode 100755 index 0000000..8934aa9 --- /dev/null +++ b/.githooks/commit-msg @@ -0,0 +1,8 @@ +#!/usr/bin/env sh +# commit-msg: warn (never block) about messages that break the project convention. +# Delegates to the shared checker; advisory only. CI runs the same check on PRs. +# See CONTRIBUTING.md. + +hook_dir=$(dirname "$0") +sh "$hook_dir/lib/check-message" < "$1" || true +exit 0 diff --git a/.githooks/lib/check-message b/.githooks/lib/check-message new file mode 100755 index 0000000..70b40cc --- /dev/null +++ b/.githooks/lib/check-message @@ -0,0 +1,45 @@ +#!/usr/bin/env sh +# check-message: warn about commit messages that stray from the project convention. +# +# Reads the full commit message on stdin; prints any problems to stderr. +# Exit 0 = clean, 1 = has warnings. Nothing here ever blocks a commit: the +# commit-msg hook and CI both treat a non-zero exit as advisory only. +# See CONTRIBUTING.md for the convention this checks. + +# Normalize exactly as git does on commit (drop comment lines, trim blank runs). +msg=$(git stripspace --strip-comments) +[ -z "$msg" ] && exit 0 + +subject=$(printf '%s\n' "$msg" | sed -n '1p') +second=$(printf '%s\n' "$msg" | sed -n '2p') +maxlen=72 +rc=0 + +# Exempt merge, revert, and autosquash (fixup!/squash!) subjects. +case "$subject" in + "Merge "*|"Revert "*|"fixup! "*|"squash! "*) exit 0 ;; +esac + +warn() { + echo "⚠ $1" >&2 + rc=1 +} + +# Subject shape: : +printf '%s' "$subject" | grep -qE '^\S+: [A-Z].+' \ + || warn "Subject should match ': ' (e.g. 'JS: Add password reset form'). Got: $subject" + +# Subject length. +[ "${#subject}" -le "$maxlen" ] \ + || warn "Subject is ${#subject} chars (max $maxlen): $subject" + +# No trailing period on the subject. +case "$subject" in + *.) warn "Subject should not end with a period: $subject" ;; +esac + +# A body must be separated from the subject by a blank line. +[ -z "$second" ] \ + || warn "Leave a blank line between the subject and the body." + +exit $rc diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3cbcd76..49f729d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -68,3 +68,23 @@ jobs: - name: Compile check run: pnpm run build + + commit-lint: + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Check commit messages (advisory — never fails the build) + run: | + for sha in $(git rev-list \ + "${{ github.event.pull_request.base.sha }}".."${{ github.event.pull_request.head.sha }}"); do + subject=$(git log -1 --format=%s "$sha") + if ! git log -1 --format=%B "$sha" | sh .githooks/lib/check-message; then + echo "::warning::Commit ${sha} ('${subject}') doesn't follow the convention — see log above." + fi + done + exit 0 diff --git a/.gitmessage b/.gitmessage new file mode 100644 index 0000000..e5c9354 --- /dev/null +++ b/.gitmessage @@ -0,0 +1,15 @@ +# Format: : +# Example: JS: Add password reset form +# Domains: prefer capitalized (JS, Java, DB, CI, Docs, Infra, Hooks...) +# symbols ok when natural (.git, v2, JS/CSS, API/Auth) +# Rules: imperative mood, capitalize description, ≤72 chars, no trailing period +: + +# Body (optional): explain WHY, not what. Separate from the subject with a blank line. +# (leave blank to omit) + + +# Footers (optional git trailers, "Key: value" — add any you need): +# Test: none|manual|compile|auto +# Refs: #12 or a ticket id, e.g. PROJ-123 +# Co-authored-by: Name diff --git a/AGENTS.md b/AGENTS.md index 013a0d6..d10de74 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -78,6 +78,32 @@ See `Justfile` at the repo root for all available commands. --- +## Commit Message Guidelines + +Use the **domain prefix** format: + +``` +: +``` + +- Domain: short label for the area changed, prefer capitalized (`JS`, `Java`, `DB`, `CI`, `Docs`, `Infra`). Symbols are fine when natural (`JS/CSS`, `API/Auth`, `.git`). +- Description: capitalized, imperative mood (`Add login page`, not `Added login page`). +- Subject line ≤ 72 chars, no trailing period. + +Examples: + +``` +JS: Add password reset form +Java: Validate email uniqueness on signup +DB: Add users table migration +CI: Cache pnpm dependencies in workflow +``` + +AI Agents should add a Co-Authored by with their model when writing a commit message. +See `CONTRIBUTING.md` for the full spec. + +--- + ## Secret Management Secrets are encrypted with [SOPS](https://github.com/getsops/sops) and committed as `secrets-ro.yaml` (read-only) and `secrets-rw.yaml` (read-write). Never commit plaintext secrets. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..fe09c42 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,95 @@ +# Contributing to PatChats + +## Commit Message Guidelines + +Commit messages use a **domain prefix** format that communicates *where* a change lives at a glance. + +### Format + +``` +: + +[optional body] + +[optional footers] +``` + +### Rules + +- **Domain** — a short label for the area changed. Prefer starting with a capital letter. Symbols are fine when natural (e.g. `.git`, `v2`, `JS/CSS`). +- **Description** — capitalized, imperative mood. Write what the commit *does*, not what you *did*. + - ✅ `JS: Add password reset form` + - ❌ `JS: added password reset form` +- **Subject line** — 72 characters max, no trailing period. +- **Body** (optional) — explain *why*, not *what*. Separate it from the subject with a blank line. +- **Footers** (optional) — structured [git trailers](#footers); see below. + +No fixed domain taxonomy — pick whatever makes the change's location obvious to someone scanning the git log. + +### Examples + +``` +JS: Add password reset form +Java: Validate email uniqueness on signup +DB: Add users table migration +CI: Cache pnpm dependencies in workflow +Docs: Document commit message convention +Hooks: Add commit-msg validation hook +Infra: Pin Java version to 25 in pom.xml +JS/CSS: Fix button hover contrast ratio +API/Auth: Restrict admin endpoints to ROLE_ADMIN +``` + +### Footers + +Footers are optional [git trailers](https://git-scm.com/docs/git-interpret-trailers) — `Key: value` lines at the end of the message, after a blank line. Sticking to the trailer format means `git interpret-trailers`, GitHub, and changelog tools can parse them. + +| Trailer | Use | +|---|---| +| `Test:` | How the change was verified — `none`, `manual`, `compile`, or `auto` | +| `Refs:` | Link an issue or ticket — `#12`, or a ticket id like `PROJ-123` | +| `Co-authored-by:` | Credit a pair partner or AI assistant — `Name ` | +| `BREAKING CHANGE:` | Describe an incompatible change and how to migrate | + +Example with body and footers: + +``` +Java: Validate email uniqueness on signup + +Prevents duplicate accounts when two OAuth providers report the same email. + +Refs: #42 +Test: auto +Co-authored-by: Ada Lovelace +``` + +### Merge strategy & commit hygiene + +We **rebase-merge** PRs, so every commit you make lands on `main` as-is. A little hygiene keeps the history readable: + +- Keep commits **atomic** — one logical change each. +- Tidy up `wip` / `fix typo` / `address review` commits before merge (`git rebase -i main`, or `git commit --fixup=` then `git rebase -i --autosquash`). +- Ideally each commit subject follows the convention above — not just the PR title. + +### Conventions are advisory + +Nothing here is blocking — this repo is a learning playground, and we optimize for development speed. + +- The local `commit-msg` hook *warns* at commit time (install via `just install-pre-scripts`). +- CI adds a *non-blocking* check that annotates any PR commits that stray from the convention. + +Treat both as friendly nudges: clean history helps everyone learning from it, but a warning will never stop your work. + +### Setup + +Run: + +```sh +just install-pre-scripts +``` + +This registers the commit message template (so your editor pre-fills it on `git commit`) and points Git at the shared `.githooks` directory. To register just the template manually: + +```sh +git config commit.template .gitmessage +``` diff --git a/Justfile b/Justfile index b29dda3..fa761de 100644 --- a/Justfile +++ b/Justfile @@ -95,6 +95,7 @@ encrypt file *args: edit file *args: just install-pre-scripts && sops edit {{ file }} {{ args }} -# Configure Git to use the .githooks directory (idempotent) +# Configure Git to use the .githooks directory and commit message template (idempotent) install-pre-scripts: [ "$(git config core.hooksPath)" = ".githooks" ] || git config core.hooksPath .githooks + [ "$(git config commit.template)" = ".gitmessage" ] || git config commit.template .gitmessage