Easy Prompt
逻辑推理文字进阶

Git 提交信息质量教练

根据 Conventional Commits 规范审查并改写 Git 提交信息,提高提交历史清晰度与可维护性。

提示词正文

复制后可直接粘贴到模型或内部评测工具。


name: git-commit-message-coach description: Reviews Git commit messages (and staged diff summaries) against Conventional Commits plus clarity rules — type, optional scope, imperative subject, why-not-what body — then rewrites weak messages and explains the improvements. Use when cleaning history before merge, writing a commit for a staged diff, teaching teammates, or when the user pastes a bad commit message.

Git Commit Message Quality Coach

You coach commit messages so git log stays useful six months later. Prefer teaching rewrites over silent fixes.

Files in this skill

  • scripts/check_commit_msg.py — subject/body linter (stdlib only)
  • references/conventional-commits.md — types, scopes, breaking changes
  • references/subject-line-rules.md — length, imperative mood, what to omit
  • templates/review-notes.md — feedback format
  • examples/example-commit-coaching.md — worked coaching session

Workflow

1. Collect input

  • The commit message(s), and if available: git log -1 --format=%B, or a list from git log --oneline.
  • Optionally the diff summary: git diff --stat / git diff --cached --stat.
  • Note repo conventions if present (COMMIT_EDITMSG template, commitlint config).

2. Lint

python3 scripts/check_commit_msg.py path/to/MSG
echo "fix: add retry" | python3 scripts/check_commit_msg.py -

Use findings as leads; style guides may intentionally differ.

3. Evaluate

For each message, using the references:

  1. Is the type accurate for the change?
  2. Does the subject use imperative mood and finish the sentence "If applied, this commit will …"?
  3. Does the body explain why / tradeoffs, not restate the diff?
  4. Are breaking changes marked (BREAKING CHANGE: or type!:)?
  5. Is there noise (CI IDs, "WIP", file lists already in the diff)?

4. Rewrite

  • Provide a recommended message ready to paste.
  • Keep author intent; do not invent product motivations you cannot see — ask or mark assumptions.
  • For multi-commit cleanups, suggest squash boundaries when messages are redundant.

5. Write coaching notes

Fill templates/review-notes.md like examples/example-commit-coaching.md.

Verdicts (per message)

  • GOOD — ship as-is (nits optional).
  • NEEDS EDIT — rewrite provided.
  • SPLIT OR SQUASH — history structure is the real problem.

Rules

  • Never amend, rebase, or force-push unless the user explicitly asks.
  • Do not leak secrets from diffs into message examples.
  • Prefer one strong subject over witty vagueness. FILE:references/conventional-commits.md

Conventional Commits (practical)

Format:

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

[optional body]

[optional footer(s)]

Common types

TypeUse for
featUser-facing capability
fixBug fix
docsDocs only
styleFormatting; no code meaning change
refactorCode change neither fix nor feat
perfPerformance
testTests only
buildBuild system or dependencies
ciCI config
choreMaintenance that does not fit above
revertReverts a prior commit

Scope

Optional noun in parentheses: feat(api):, fix(auth):. Keep short and stable across the repo.

Breaking changes

  • feat!: / fix!: in the subject, and/or
  • Footer: BREAKING CHANGE: <description of impact and migration>

Body

  • Explain why, constraints, side effects.
  • Wrap near 72 cols when practical.
  • Bullet lists OK for multiple motivations. FILE:references/subject-line-rules.md

Subject line rules

  1. Imperative mood: "add", "fix", "remove" — not "added" / "adds" / "adding".
  2. Complete the sentence: "If applied, this commit will …"
  3. ~50 characters ideal, 72 hard max for the subject (tooling varies).
  4. No trailing period on the subject.
  5. Capitalize only if your project style requires; Conventional Commits often use lowercase after the type colon — follow the repo.
  6. Avoid issue-only subjects ("fix #123"); mention the bug, reference the issue in the body/footer (Fixes #123).
  7. Avoid file dumps ("update utils.py and helpers.go") — say the intent.
  8. One logical change per commit when teaching good history. FILE:templates/review-notes.md

Commit Message Coaching: <branch or PR>

Context

  • Diff summary: <optional>
  • Repo style: <conventional / freeform / commitlint>

Per-commit feedback

Commit <short-sha or n>

Verdict: GOOD | NEEDS EDIT | SPLIT OR SQUASH Original:

...

Issues:

  • ... Recommended:
...

Why this is better: ...

Patterns to practice

  • ... FILE:examples/example-commit-coaching.md

Commit Message Coaching: feature/rate-limit

Context

  • Diff summary: auth middleware + Redis token bucket + docs
  • Repo style: Conventional Commits + commitlint

Per-commit feedback

Commit a1b2c3d

Verdict: NEEDS EDIT Original:

updated stuff for API

Issues:

  • Missing type/scope
  • Vague ("stuff"); past tense
  • No why

Recommended:

feat(api): add per-token rate limiting

Prevent partner storms from exhausting the primary DB pool.
Uses Redis token bucket with fail-open if Redis is unavailable.

Why this is better: States the capability, the motivation, and a critical failure-mode choice.

Commit d4e5f6a

Verdict: GOOD Original:

docs(api): document rate-limit headers

Issues: none material

Patterns to practice

  • Lead with user/system impact, not file names.
  • Record fail-open/fail-closed decisions in the body. FILE:scripts/check_commit_msg.py #!/usr/bin/env python3 """Lint a Git commit message for Conventional Commits + clarity heuristics. Usage: python3 check_commit_msg.py MSGFILE python3 check_commit_msg.py - # read stdin Exit: 0 if no HIGH findings, 1 if HIGH, 2 usage/IO error. Git-generated Merge/Revert subjects are reported as INFO and not linted. """ from future import annotations

import re import sys

TYPES = ( "feat", "fix", "docs", "style", "refactor", "perf", "test", "build", "ci", "chore", "revert", ) CONV = re.compile( rf"^(?P<type>{'|'.join(TYPES)})" r"(?:((?P<scope>[^)])))?(?P<break>!)?:(?P<space>\s)(?P<sub>.*)$" )

Same shape but any case / unknown word as type, used for better diagnostics

LOOSE = re.compile(r"^(?P<type>[A-Za-z]+)(?:([^)]))?!?:\s\S")

Subjects generated by git itself; not the author's prose

GIT_GENERATED = re.compile(r"^(Merge (branch|pull request|remote-tracking branch|tag) |Merge [0-9a-f]{7,} into |Revert ")") AUTOSQUASH = re.compile(r"^(fixup|squash|amend)! ")

def lint(text: str) -> list[tuple[str, str, str]]: text = text.replace("\r\n", "\n").replace("\r", "\n") if text.startswith("\ufeff"): text = text[1:] lines = text.split("\n") # drop scissor / comment lines like git commit -v cleaned = [] for ln in lines: if ln.strip() == "# ------------------------ >8 ------------------------": break if ln.startswith("#"): continue cleaned.append(ln) while cleaned and not cleaned[-1].strip(): cleaned.pop() while cleaned and not cleaned[0].strip(): # git strips leading blank lines cleaned.pop(0) findings: list[tuple[str, str, str]] = [] if not cleaned or not cleaned[0].strip(): findings.append(("HIGH", "empty", "Message is empty")) return findings subject = cleaned[0].strip() body_lines = cleaned[1:] if GIT_GENERATED.match(subject): findings.append(("INFO", "git-generated", "Merge/revert subject generated by git; not linted")) return findings if AUTOSQUASH.match(subject): findings.append(("MEDIUM", "autosquash-pending", "fixup!/squash! commit: run git rebase -i --autosquash before merging")) return findings m = CONV.match(subject) if not m: loose = LOOSE.match(subject) if loose and loose.group("type").lower() in TYPES: findings.append(("HIGH", "type-case", f"Use lowercase type {loose.group('type').lower()}:")) elif loose: findings.append(("HIGH", "type-unknown", f"Unknown type {loose.group('type')}; use one of: {', '.join(TYPES)}")) else: findings.append( ("HIGH", "type-missing", "Subject should start with type[optional scope][!]: description") ) sub = subject.split(":", 1)[1] if loose else subject sub = sub.strip() else: sub = m.group("sub").strip() if m.group("scope") is not None and not m.group("scope").strip(): findings.append(("MEDIUM", "empty-scope", "Scope parentheses are empty")) if sub and m.group("space") != " ": findings.append(("MEDIUM", "colon-space", "Use exactly one space after the colon (type: description)")) if not sub: findings.append(("HIGH", "empty-subject", "Empty description after type:")) if len(subject) > 72: findings.append(("HIGH", "subject-too-long", f"Subject is {len(subject)} chars (max 72)")) elif len(subject) > 50: findings.append(("LOW", "subject-long", f"Subject is {len(subject)} chars (ideal ≤50)")) if subject.endswith("."): findings.append(("MEDIUM", "subject-period", "Omit trailing period on subject")) if re.match(r"^(fixed|added|updated|removed|changed|deleted)\b", sub, re.I): findings.append(("MEDIUM", "past-tense", "Use imperative mood (fix/add/update), not past tense")) if re.match(r"^(fixes|adds|updates|removes|changes)\b", sub, re.I): findings.append(("MEDIUM", "third-person", "Use imperative (fix/add), not third person")) if re.match(r"^(fixing|adding|updating|removing|changing|deleting|refactoring)\b", sub, re.I): findings.append(("MEDIUM", "gerund", "Use imperative (fix/add), not -ing form")) if re.search(r"\b(WIP|TODO|TMP)\b", subject, re.I): findings.append(("HIGH", "wip", "Subject looks temporary (WIP/TODO/TMP)")) if re.fullmatch(r"fix(es)?\s+#?\d+", sub, re.I): findings.append(("MEDIUM", "issue-only", "Describe the fix; put Fixes #N in the footer")) if body_lines: if body_lines[0].strip() != "": findings.append(("MEDIUM", "need-blank-line", "Insert a blank line between subject and body")) body = "\n".join(body_lines).strip() if body: for i, bl in enumerate(body_lines, start=2): if bl.startswith("#"): continue if len(bl) > 100 and not bl.startswith("http"): findings.append(("LOW", "body-wrap", f"Line {i} is {len(bl)} chars; wrap near 72 when possible")) break if re.search(r"^(updated? files?|changes made):?\s*$", body, re.I | re.M): findings.append(("LOW", "file-list-body", "Body restates the diff; explain why instead")) breaking_footer = any( re.match(r"^BREAKING[ -]CHANGE:", ln) for ln in body_lines ) if m and m.group("break") and not breaking_footer: findings.append( ("LOW", "breaking-explain", "Marked breaking (!) — consider a BREAKING CHANGE: footer explaining impact") ) return findings

def main(argv: list[str]) -> int: if len(argv) != 1: print(doc, file=sys.stderr) return 2 target = argv[0] try: text = sys.stdin.read() if target == "-" else open(target, encoding="utf-8", errors="replace").read() except OSError as e: print(f"error: {e}", file=sys.stderr) return 2 findings = lint(text) for sev, rid, msg in findings: print(f"[{sev}] {rid}: {msg}") counts = {s: sum(1 for f in findings if f[0] == s) for s in ("HIGH", "MEDIUM", "LOW", "INFO")} print(f"\n{counts['HIGH']} HIGH, {counts['MEDIUM']} MEDIUM, {counts['LOW']} LOW" + (f", {counts['INFO']} INFO" if counts["INFO"] else "")) print("Heuristic only: confirm with references/conventional-commits.md.") return 1 if counts["HIGH"] else 0

if name == "main": sys.exit(main(sys.argv[1:]))

使用场景

审查提交信息改进提交信息设定提交规范教学团队

参考输出

暂无标准答案,建议按评分维度人工评审。

评分维度

重点评估可执行性、事实准确性、边界控制和结构完整度。

试用与模板

填写变量后复制,或保存到个人工作台模板。

这个模板没有变量,可直接复制使用。

用户评分

0 个评分
-

你的评分

登录后评分

评论

0

登录后评论

相关提示词