Imported from bansan85/bansAI-marketplace (
plugins/bansai/skills/git-commit-already-added/SKILL.md). Install upstream withnpx skills add bansan85/bansAI-marketplace --skill git-commit-already-added. Copyright stays with the author.
Git commit from already-staged changes
Write a commit message using only what is already in the git index
(git add has already been run). Never stage additional files, never
unstage anything, never use unstaged working-tree changes as input.
Exception: when amending, the content to summarize is what the amended commit will contain once the index is folded into it — see step 2.
1. Check the repository state
Run git status && git diff --cached --stat in one call.
- If
git statusreports an operation in progress — unmerged paths, "You are currently rebasing/cherry-picking/reverting", "you are still merging" — stop and tell the user which operation is in progress. Don't draft a message or commit: git already prepared its own message for these cases, and amending a commit being replayed would rewrite it. - If the diff stat is empty and the user is not amending: tell them
nothing is staged and stop — don't run
git addon their behalf. - If the diff stat is empty and the user is amending: continue, this is a pure reword of the previous commit's message.
2. Read the staged change as one block
Normal commit: run git diff --cached and read it as a single unit of
work.
Amend: an amend produces one tree, not two stacked patches — read the exact content the amended commit will hold, not two diffs.
- Refuse to amend a merge commit:
git rev-parse --verify -q HEAD^2. If it succeeds, HEAD is a merge commit — tell the user and stop. - Find the parent to diff against:
git rev-parse --verify -q HEAD^.- Succeeds →
git diff --cached -M HEAD^. - Fails → HEAD is the root commit (
HEAD^/HEAD~1would abort withfatal: ambiguous argument) — diff against the empty tree:git diff --cached -M 4b825dc642cb6eb9a060e54bf8d69288fbee4904.
- Succeeds →
- Read the previous message with
git log -1 --format=%B, to keep what still applies and deliberately replace what does not.
A concept is the minimal, coherent set of changes that answers one single change intention — what an atomic commit would contain on its own. Two different bug fixes landing in the same commit are two concepts. A config file edit and an unrelated one-line code change are two concepts. Refactoring a function and adapting all its callers is one concept. Group by change intention, never by file or by sentence: changes that serve the same intention form one concept even when they span several files.
Only split into several concepts if they are genuinely unrelated. With several concepts, give each its own paragraph and name all of them in the title if it still fits in at most 50 characters; otherwise pick the umbrella framing that covers them.
3. Infer the repository's commit style
Run git rev-parse --verify -q HEAD && git log -n 10 --no-merges --format="%B---END---"
in one call. Non-zero exit (not empty output — git log on an
empty repo aborts with fatal: your current branch ... does not have any commits yet, exit 128) means this is the initial commit: skip
inference, use Conventional Commits, write in the user's language.
Otherwise deduce from the log output — it prints each commit's full body, not just the subject (merge commits are excluded: their message is generated by git, not the user, so it carries no style signal): read just the first line of each block for the title-only checks below (prefix, casing, length); read the whole block for the trailer check, since a trailer lives in the body.
- Type prefix: only use a Conventional Commits type (
fix:,feat:,chore:, …) in the title if recent history already uses it consistently — don't introduce it otherwise. When in use, apply it in full: carry a(scope)if the history uses scopes, and mark a backward-incompatible change with!after the type/scope plus aBREAKING CHANGE:footer — see "Conventional Commits" in step 4. - Language: write in the same language as the commit history. No history (first commit) or unclear signal → use the language the user is speaking in the current conversation, not this skill file.
- Casing and punctuation: match whether titles start with a capital or lowercase letter, and whether they end with a period.
- Title length: at most 50 characters. Keep the title as short as it can be while still naming the change, even if past titles ran longer. Type prefix, scope and trailing period all count towards the 50.
- Trailer block: this only concerns trailers you'd add on your own
initiative, chiefly this session's required attribution footer —
add it only if that same trailer already appears in the history just
checked (e.g. a prior
Co-Authored-By:line); if the repository has never used it, leave it out. Exceptions:BREAKING CHANGE:is always mandatory for a backward-incompatible change regardless of history, and a trailer the user or the diff explicitly supplies (e.g.Closes #123) always goes in. Full formatting rules in "Trailer block (footer)" in step 4.
4. Draft the message
Produce the title and body — message text only: no preamble, no code fences, no explanation — by following every rule of this step. Do not draft from memory, from generic commit-message conventions, or from what the repository's history alone suggests: these rules (what counts as one concept, when a paragraph gets numbered, describing effect instead of the diff) are stricter and more specific than generic style knowledge, and are the only authority for content and structure.
The style detected in step 3 (type prefix, scope, language, casing, punctuation, title length) is applied on top of these rules and wins on those points only; it never supersedes them on content or structure.
Structure
- Reading level: understandable by a smart 16-year-old, but keep the technical precision of a software engineer with 10 years of experience. The reader is a competent engineer who does not know this codebase's architecture — don't lean on internal jargon without making it clear from context.
- 50/72 rule: title ≤ 50 characters (hard ceiling, prefix, scope and trailing period included — aim well below it), blank line, body lines wrapped at ≤ 72 characters, blank line between paragraphs.
- Action-verb subject: the title opens with a verb naming what the commit does, in the form the repository's history uses. In English that is the imperative ("Add", "Fix", "Remove", not "Added", "Fixes", "Adding"); in French it is normally the infinitive ("Ajouter", "Corriger", "Supprimer"). Match the history.
- RFC 2119: the title must comply with RFC 2119 — if it uses a normative keyword (MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, RECOMMENDED, MAY, OPTIONAL), that keyword carries exactly the meaning RFC 2119 gives it, never a casual synonym.
- What it may draw on: the concept paragraphs may draw on the
staged diff and nothing else. Only the context paragraph may also
draw on the current conversation. When reading other files of the
repository is unavoidable, read them from the index
(
git show :<path>) so unstaged edits cannot leak into the message. Reference anything outside the repository only if it is truly indispensable to understand the diff. - Context paragraph (optional): if useful, one leading paragraph stating the context — the problem, motivation, or reason the change was needed. Include it only if it adds information the concept paragraph(s) don't already convey; if it would just restate the concept, drop it and start directly with the concept paragraph(s).
- Concept paragraph(s): one paragraph per concept stating what the commit actually changes — the solution itself, not just the problem it addresses. Describe the finality, not the diff: state what goes wrong and why it matters, or what changed in effect — to the software's behaviour, or to what a caller or user can now expect — not how the code does it. The reader can already see the code; the body's job is to say what the code cannot say about itself. Stay factual and tied to what the diff actually does.
- Stay on the code: never mention how a bug was found (code review, fuzzing, a specific crash report, ...) or anything else about the process behind the commit, and never list alternatives that were rejected.
- Names: do not name functions, member variables or classes unless understanding the commit is impossible without that name. Prefer a plain description of the role or behaviour instead. Never name a private member variable: describe its role.
- Paragraph length: at most 5 lines per paragraph, and always the shortest wording that carries the information — 5 is the ceiling, not the budget to spend. If a paragraph runs past 5 lines, check whether it is really covering several concepts; if it is, split it into one paragraph per concept. Length alone is not proof of several concepts.
- Paragraph count: the optional context paragraph, plus one concept paragraph per concept — except bug fixes, whose concept part is the bug/fix pair described below.
- Numbering: only when there is more than one concept paragraph,
prefix each concept paragraph with
(X/N)at the very start of the paragraph (e.g.(1/2) Add …) so the reader can see how many independent concepts the commit bundles. The context paragraph, when present, is never numbered, and a single concept paragraph is never numbered either. - No filler body: if the title already says everything needed, don't add a body paragraph just to have one.
- Bug fixes: first paragraph describes the bug, second paragraph
describes the fix. Skip the second paragraph if it would just
restate the first one with the faulty condition inverted. This
applies at the clause level too: a fix paragraph must describe the
mechanism of the solution, not tack on a "so X no longer happens" /
"instead of Y" clause whose only content is the bug paragraph's
condition negated. If a clause states genuinely new information (a
concrete new behavior, not just the absence of the old one), keep
it; if removing it would lose nothing because the bug paragraph
already implies it by negation, cut it. When the defect has a
standard vulnerability-class name, use it (out-of-bounds read/write,
use-after-free, double free, integer overflow, type confusion,
divide by zero, race condition, etc.) — but only when it accurately
describes the flaw. Use it in the title as well as in the bug
paragraph, e.g.
fix: close use-after-free in NO_CACHE read buffer.
Conventional Commits
Only when step 3 established that the repository uses the convention:
<type>(<scope>)!: <subject>— include the(scope)when the history uses scopes, and pick the scope from the values the history already uses rather than inventing one.- Add
!after the type/scope when the change is backward-incompatible, and end the message with aBREAKING CHANGE: <what breaks and what callers must do>footer. The!alone is not enough. - Reproduce the issue-trailer form the history uses (
Closes #123,Refs: ABC-123,Fixes: …) when the user or the diff supplies the reference. Never invent an issue number and never guess one from the branch name.
Trailer block (footer)
Any trailer — BREAKING CHANGE:, Closes #123, Refs:,
Signed-off-by:, Co-Authored-By:, Change-Id:, Reviewed-by:,
Reviewed-on:, Tested-by:, and the session's attribution footer —
goes in a single block at the very end of the message, after one blank
line, one trailer per entry.
Whether to add a trailer on your own initiative (chiefly the session's
attribution footer) is decided in step 3. BREAKING CHANGE: is the
only exception: always mandatory for a backward-incompatible change,
regardless of history.
A trailer wraps only when its value is free prose. In practice that
means BREAKING CHANGE: and nothing else: its text varies in length, so
it obeys the ≤ 72-character limit like the rest of the message — wrap it
and indent each continuation line by two spaces, so the footer still
reads as a single entry.
Every other trailer holds one atomic value — an identity, a URL, a
hash, an issue id — with no natural break point, so it stays on one
line however long it gets. Splitting Reviewed-on: or Co-Authored-By:
across two lines destroys the grep or the parser that reads it.
Lines that git or a hook wrote itself are reproduced byte for byte:
Change-Id:, Gerrit's Reviewed-on: / Tested-by: / Reviewed-by:,
and (cherry picked from commit 765ae13). Never re-flow, re-indent,
re-order or drop them — when amending, carry the previous message's
trailer block over unchanged.
The trailer block is not a paragraph: it is never numbered, never
counted in N, and not subject to the paragraph-length rule.
Examples
Good — context paragraph plus a single concept paragraph
Fix stale cache after a config reload
Reloading the configuration rebuilt the resolver but left the
previous entries in the lookup cache, so requests kept being
routed with the old rules until the process restarted.
Invalidate the lookup cache as part of the reload, so a reload
takes effect on the next request instead of the next restart.
Co-Authored-By: Someone <someone@example.com>
Good — two unrelated concepts, numbered
feat(auth): refresh tokens and bump CI image
(1/2) Sessions no longer end abruptly after an hour: the client
now refreshes its token in the background before expiry, and
falls back to a normal re-login if the refresh is rejected.
(2/2) The CI image moves to Node 22, which the refresh timer
needs for its use of AbortSignal.timeout.
Good — bug fix, bug paragraph then fix paragraph
Fix cache never filled by const getters
The accessors are declared const and were meant to memoize their
result on first call, but the cache member is not mutable, so a
const member function cannot assign to it. Every accessor ended
up recomputing its value on each call.
Declare the cache member mutable, so the const accessors can
fill it on first use. Logical constness is preserved: the value
an accessor returns is unchanged, only the first call is slow.
Good — title only, no body
Fix a missing plural in a comment
Good — breaking change under Conventional Commits
fix(api)!: correct retreive_config spelling
The public entry point was exported misspelled as
retreive_config. It is now retrieve_config, with the same
signature and the same behaviour; the old name is gone rather
than kept as an alias.
BREAKING CHANGE: retreive_config() no longer exists. Call
retrieve_config() instead — the arguments and the return type
are unchanged, so migrating is a rename.
Bad — restates the diff as code, and pads the body for no reason
Update auth.ts
Changed the isValid function to also check expiresAt and added a
new refreshToken function that calls the /refresh endpoint and
sets this.token. Also removed an unused import.
Bad — fix paragraph tacks on a negated restatement of the bug
fix(verify): don't crash on unreadable object
The dialog runs git with throwOnErrorExit, so a single corrupted
dangling object broke the whole feature: batch-fetching commit
metadata aborted with no output, and previewing that object's
content threw an unhandled exception that crashed the dialog.
Fall back to querying each commit's metadata individually when
the batched git show fails, so one unreadable commit no longer
wipes out the metadata for the rest of the batch. Catch the same
failure when fetching an object's content for preview, and show
a placeholder message instead of letting the exception propagate.
Both clauses after the commas ("so one unreadable commit no longer wipes out the metadata for the rest of the batch", "instead of letting the exception propagate") add no information: they are the bug paragraph's conditions negated, not a description of the fix mechanism. Cut them:
fix(verify): don't crash on unreadable object
The dialog runs git with throwOnErrorExit, so a single corrupted
dangling object broke the whole feature: batch-fetching commit
metadata aborted with no output, and previewing that object's
content threw an unhandled exception that crashed the dialog.
Fall back to querying each commit's metadata individually when
the batched git show fails. Catch the same failure when fetching
an object's content for preview, and show a placeholder message.
Bad — invents information the diff does not contain
fix(cache): invalidate entries on reload
The problem was found during a code review. It is probably the
cause of the latency spikes reported last month, and lookups
should now be about twice as fast.
We first considered dropping the cache entirely, then adding a
TTL, before settling on explicit invalidation.
Closes #482
Not one of those statements comes from the diff: where the bug was found is process narration, the latency cause and the speed-up were never measured, the rejected alternatives are explicitly banned, and nobody supplied issue 482.
Review the draft
Before moving to step 5, re-read the draft against every rule of
Structure, Conventional Commits and Trailer block, one rule at a time,
and fix anything that fails. In particular, recount the concepts the
diff actually contains and check that (X/N) is present if and only
if more than one concept paragraph remains.
5. Create the commit
Commit exactly the staged content — do not run git add first, and do
not ask the user to approve the draft beforehand (the final message is
shown back in step 6).
When amending, first capture the current hash so it can be reported
later: git rev-parse --short HEAD.
Never pass the message with -m. Write it to a temporary UTF-8 file in
the session scratchpad with the Write tool and pass that file to git;
delete the file afterwards:
- new commit:
git commit -F <file> - amend:
git commit --amend -F <file>
This is the only form that behaves identically on Windows and Linux,
preserves multi-line formatting exactly, and cannot mangle accented
characters. If you pass the message through the shell instead, the
here-document delimiter must be quoted, otherwise the shell
expands $VAR and executes backticks found in the message before git
ever sees it:
- bash / Git Bash:
git commit -F - <<'MSG'… thenMSGat column 0 - PowerShell: a single-quoted here-string piped in —
@'… then'@at column 0, followed by| git commit -F -
Never use an unquoted <<EOF.
Append the attribution trailer only if step 3 found it already in the repository's history (see the Trailer block bullet in step 3).
6. Report back
Show the user the message exactly as git stored it (a commit-msg
hook may have rewritten it) together with the short hash, in one call:
git --no-pager log -1 --format="%h%n%B". When amending, also report
the pre-amend hash captured in step 5.