git-workflow

Git commit messages with Conventional Commits, branch names, pull requests and merging.

npx skills add haythamasalama/skills --skill git-workflow

Git workflow: commits, branches and pull requests

Read when: about to commit, reword, split work, open or update a pull request, answer a review, or merge one. Owns: the commit format, types, scopes, bodies, footers, breaking changes, splitting, rewording; PR titles, bodies, review replies and merge messages.

Conventional Commits (v1.0.0, checked 2026-11-03) for any repository. Commit, push, open a PR or merge only when the user asks; many owners review and commit themselves.

Where each topic lives

Topic File
Format, types, scope, body, footers, splitting, rewording this file
The 16 rules of the spec, SemVer mapping, reverts, edge cases references/spec.md
Branches, PR title and body, linking issues, review replies, updating, merging references/pull-requests.md

TL;DR

  • type(scope): description: imperative, lowercase start, no trailing period, under ~72 chars.
  • The body explains why, not what. The diff already shows the what.
  • One commit per reason to change. Test: can it be reverted alone without taking something unrelated with it?
  • Breaking change: ! before the colon and/or a BREAKING CHANGE: footer.
  • The scope names the area the change is about, never a code layer. Learn the repo's scopes from its history before inventing one.
  • The build (with its type check) passes before the commit, not after.
  • Reword only commits that are not pushed, unless the owner says otherwise.
  • A PR title is a commit subject; the PR rules are in references/pull-requests.md.

Before you write

git status
git diff --staged            # what is actually going in
git log --oneline -20        # the house style
git log --format=%s | grep -oE '^[a-z]+(\([^)]+\))?!?:' | sort | uniq -c | sort -rn | head -20

The last line lists the types and scopes the repo already uses. Check that the history really follows the convention: commits from before it was adopted are not a model.

Format

<type>[optional scope][optional !]: <description>

[optional body]

[optional footer(s)]

The colon and the space after it are required. The description starts right after them.

Types

feat and fix are the only two the spec defines. The rest are convention.

Type Use for
feat new behaviour a user can observe
fix a defect. Say what was broken, not only what changed
docs documentation, READMEs, agent instructions and skills
refactor behaviour identical, structure different
perf a measured improvement. Quote the before and after
style formatting only, zero behaviour change
test adding or changing tests only. Skip it in a repo with no tests
build build tooling, containers, web server config, dependencies
ci CI workflows
chore housekeeping that fits nothing above. Reach for it last
revert reverting an earlier commit; Refs: footer with its SHA (how)

Pick by what the change does for a user of the code, not by the files touched. A bug fix in a build script is fix(build) if it was broken, build if it only changes how the build works. A dependency bump that fixes a security advisory is fix(deps); a routine bump is chore(deps) or build(deps), whichever the history uses.

Scope

A noun naming the area the change is about, so git log reads by area: a business area (invoices, orders, auth), an app or panel (admin, api), or a tool (deps, ci, docker). Optional, but in a repo with clear seams it usually writes itself.

  • Never a code layer (controllers, views, migrations, domain-actions). Why: the diff already shows which files changed; the scope should say what part of the product moved.
  • One spelling per area. Business areas are plural kebab-case nouns (orders, payment-requests); areas with no plural keep their name (billing, tracking). Use the spellings the history already has, and never two for one area (order and orders).
  • Pick the narrowest true area. Tooling first (deps, ci), then an app or panel when every changed file belongs to it, otherwise the business area.
  • A change that spans unrelated areas is two commits. If it cannot be split, scope it to the area it is mainly about.

Description

Imperative mood, lowercase start, no trailing period, under ~72 characters. It completes the sentence "If applied, this commit will …".

feat(invoices): add a due-date reminder email
fix(orders): count each line's price once in the order total
refactor(payments): move refund totals into one place

Not:

Added the CSV export.        (past tense, full stop)
fix: bug                     (says nothing)
feat(blocks): update files   (says nothing)

Follow the repo if it uses a ticket prefix or a different case; consistency with the log beats this file.

Body

Optional. One blank line after the description, then free-form paragraphs wrapped at ~72.

Write one when the change is not self-evident from the diff. Explain the why:

  • the failure mode being prevented;
  • the constraint that forced this approach;
  • the alternative you rejected, and the reason.

When a commit touches a convention the codebase depends on but nothing enforces (a naming rule another file greps for, a glob, a module-level singleton, a "null means hidden" rule), say so in the body. Those are the ones that break silently.

Don't narrate the session ("first I tried…"), list files, or restate the diff.

Footers

One blank line after the body. A token, then : or #, then the value. Tokens use - instead of spaces (Reviewed-by, Refs), which is what separates a footer from another body paragraph. BREAKING CHANGE is the only token allowed to contain a space.

Use the attribution line (Co-Authored-By: …) that the environment or the owner specifies, and only that one. Don't invent one.

Breaking changes

Either a ! before the colon, or a BREAKING CHANGE: footer, or both. BREAKING-CHANGE is a synonym. The token must be uppercase; everything else in the format is case-insensitive.

With !, the BREAKING CHANGE: footer may be left out, and the description itself must then say what breaks. Prefer both: the ! for people scanning the log, the footer for the migration note.

A change is breaking when it forces work outside the commit: a public URL people were given changes, an API or event contract changes, the shape of stored data or content changes, a config key or environment variable is renamed, a supported version is dropped. A change that only affects the app's own rendering is not breaking, however visible it is.

refactor(api)!: rename the customer endpoints to plural nouns

BREAKING CHANGE: /api/customer/* moves to /api/customers/*. The old paths
answer 301 for one release, then 404. Clients must update their base URL.

Splitting work

  • One commit per reason to change. Could this commit be reverted on its own without undoing something unrelated?
  • Content and code go in separate commits. A content update (feat(content): add the pricing FAQ) should revert without undoing a component fix that landed the same afternoon.
  • A refactor and the feature built on it are two commits: the refactor first, behaviour unchanged.
  • Formatting churn gets its own style commit, or stays out of the diff.
  • Stage by hunk when one file holds two reasons: git add -p <file>.

Rewording

Situation Do
last commit, not pushed git commit --amend (message only: --amend -m "…", or --amend with no staged changes)
an older commit, not pushed git rebase -i <base> and mark it reword. Without an interactive terminal, script both editors (GIT_SEQUENCE_EDITOR turns pick <sha> into reword <sha>, GIT_EDITOR writes the new message), or ask the owner to do it
already pushed to a shared branch don't rewrite. Ask the owner; a force-push rewrites everyone's history

Amending changes the commit's SHA. Never amend to "fix" a commit that a hook rejected: the commit did not happen, so amending would change the previous one. Fix the problem and commit again.

Before committing

  • The build passes. If the build runs a type check first (for example vue-tsc before vite build), template errors surface only there. A commit that does not build is not finished.
  • Lint and format the files you touched.
  • For anything visual, check it in a real browser before committing, not after.
  • Stage files by name, not git add -A or git add ., which sweep in untracked secrets and large files. Never stage .env / .env.*, credential or key files, logs, dumps, or generated output that is not meant to be tracked. A secret that was committed must be rotated: deleting it later leaves it in history.
  • Never bypass hooks (--no-verify). When a hook fails, fix what it reports and commit again.
  • Read git diff --staged once more. Nothing temporary, no stray debug output.

Pull requests

A PR title is a commit subject, and with squash merge it becomes one. Branch names, the body, review replies, updating and merging: references/pull-requests.md.