Communitygithub.com

lguidolin/commit-history-rewrite

Use when an existing repository has messy commit history that needs to conform to conventional commits before adopting release-please, or when intermediate WIP/fixup/merge commits need to be cleaned up.

commit-history-rewrite란 무엇인가요?

commit-history-rewrite is a Claude Code agent skill that use when an existing repository has messy commit history that needs to conform to conventional commits before adopting release-please, or when intermediate WIP/fixup/merge commits need to be cleaned up.

지원 대상~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/lguidolin/agent-skills/tree/main/skills/commit-history-rewrite

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

Commit History Rewrite

Rewrite an existing repository's commit history to conform to conventional commits. Analyzes commits, classifies them by type, identifies squash candidates, and executes the rewrite with full safety gates.

Overview

History is documentation. Every commit message on main is a permanent record that future engineers (and agents) use to understand what changed and why. Messages like "fix", "update", "wip" destroy this documentation. Release-please parses commit messages to determine version bumps — non-conforming messages produce wrong or no releases.

Code is a liability. Hyrum's Law applies to git history too: with enough tooling depending on commit messages (changelogs, release automation, git bisect, git blame), the format of those messages becomes a contract. Cleaning up history before adopting automation prevents the old messages from polluting every downstream tool.

This skill rewrites the messages, not the code. The file tree must be identical before and after — only commit metadata changes.

When to Use

  • Adopting conventional commits in a repo with existing non-conforming history
  • Commit log is polluted with WIP, fixup, merge, or meaningless messages
  • Preparing a repo for release-please (which needs conventional commits to function)
  • Cleaning up before open-sourcing or transferring a repository

When NOT to Use

  • Repo has already been published with conventional commits (nothing to rewrite)
  • Shared branches with active collaborators who haven't been notified
  • You only need to fix the last few commits (use git rebase -i HEAD~N directly)
  • Repo is a fork where upstream controls the commit format

Process

Step 1: Assess the Repository

# Count total commits
git rev-list --count HEAD

# Show commit message summary
git log --oneline --no-merges

# Check for collaborators (authorship diversity)
git shortlog -sne --no-merges

# Identify merge commit density
git log --merges --oneline | wc -l

Decision point: If commit count < 50, use interactive rebase. If >= 50, use git-filter-repo.

Step 2: Classify Existing Commits

Analyze each commit using two signals: the message text and the files changed. When the message is ambiguous (e.g., "update stuff"), the diff is the source of truth.

Signal 1: Message Content

Signal in messageAssigned Type
"add", "new", "implement", "create", "introduce", "support"feat
"fix", "bug", "patch", "resolve", "correct", "handle error"fix
"refactor", "restructure", "reorganize", "simplify", "extract", "move"refactor
"update dep", "bump", "upgrade", "version", "config", "setup"chore
"test", "spec", "coverage", "assert"test
"doc", "readme", "comment", "typo in doc"docs
"ci", "workflow", "pipeline", "action", "deploy"ci

Signal 2: Changed Files (when message is ambiguous)

git show --stat --format="" <hash>
File patternAssigned Type
Only *.md, docs/**docs
Only *test*, *spec*, __tests__/**test
Only .github/workflows/**, .gitlab-ci.yml, Jenkinsfileci
Only package.json, *.lock, requirements.txt, Cargo.tomlchore
Only config files (.eslintrc, tsconfig.json, .prettierrc)chore

Squash Candidates

SignalAction
Message is "WIP", "wip", "temp", "save", "checkpoint", single word, or emptysquash into nearest meaningful commit
Message is "fixup", "fix typo", "oops", "lint", "formatting"squash into previous
Merge commits (from non-squash merges)squash candidate — evaluate if content is already on main

For ambiguous commits, check the full diff (git show <hash>) to determine type from content.

Step 3: Write Quality Messages

Rewritten messages must follow commit message standards:

<type>[optional scope]: <short imperative description>

[optional body explaining WHY, not what]

Good rewrites:

"initial setup"           →  "chore: initial project setup"
"added login"             →  "feat: add user login"
"fix bug"                 →  "fix: resolve login redirect loop"
"Update README.md"        →  "docs: update README with setup instructions"
"updated deps"            →  "chore(deps): update dependencies"

Bad rewrites (don't do these):

"stuff"                   →  "chore: stuff"          ← still meaningless
"changes"                 →  "feat: changes"         ← no description
"fix"                     →  "fix: fix"              ← redundant

When the original message is meaningless, read the diff to write a proper description. If the diff is too large or ambiguous, ask the user what the commit was about.

Step 4: Present the Rewrite Plan

Before any destructive action, show the user a complete plan:

REWRITE PLAN
============
Total commits: 47
Commits to rewrite: 32
Commits to squash: 8
Commits already conforming: 7
Authors preserved: 3 (Alice <alice@co>, Bob <bob@co>, CI Bot <bot@co>)

PROPOSED CHANGES:
  abc1234  "initial setup"           →  "chore: initial project setup"
  def5678  "added login"             →  "feat: add user login"
  ghi9012  "wip"                     →  [SQUASH into def5678]
  jkl3456  "fix bug"                 →  "fix: resolve login redirect loop"
  mno7890  "Update README.md"        →  "docs: update README with setup instructions"
  pqr1234  "feat: add search"        →  [KEEP — already conforming]
  ...

SAFETY:
  ✓ Backup branch: backup/pre-rewrite-<timestamp>
  ⚠ Force push required after rewrite
  ⚠ All collaborators must re-clone or reset after push

UNCHANGED:
  - File tree (zero content changes)
  - Author name, email, and date on every commit
  - Commit order and parent relationships (except squashed commits)

Wait for explicit user approval before proceeding. Don't proceed on "sounds good" — require "yes" or equivalent.

Step 5: Create Backup

# Create a backup branch at current HEAD
git branch "backup/pre-rewrite-$(date +%Y%m%d-%H%M%S)"

# Verify backup exists
git branch -l 'backup/*'

# Optionally push backup to remote for extra safety
git push origin "backup/pre-rewrite-$(date +%Y%m%d-%H%M%S)"

Step 6: Execute the Rewrite

Option A: Interactive Rebase (< 50 commits)

# Rebase from root
git rebase -i --root

In the editor:

  • Mark squash candidates with squash or fixup
  • Mark message rewrites with reword
  • Preserve commit order

Authorship preservation: Interactive rebase preserves author name, email, and date by default. Do NOT use --reset-author. Do NOT use --committer-date-is-author-date unless specifically asked.

Option B: git-filter-repo (>= 50 commits)

Create a Python callback script (rewrite-messages.py) that maps old messages to new ones:

import re

REWRITES = {
    b'old hash prefix': b'new message',
    # ... generated from the plan
}

def rewrite(commit, metadata):
    # Look up by original message or hash
    if commit.original_id in REWRITES:
        commit.message = REWRITES[commit.original_id]

Apply with:

git filter-repo --commit-callback "$(cat rewrite-messages.py)" --force

Authorship preservation: git-filter-repo preserves authorship by default. Do NOT use --mailmap or author-rewriting flags unless the user explicitly requests it.

Squashing Commits

For squash candidates, combine into the nearest meaningful commit. The resulting commit:

  • Uses the conventional commit message from the meaningful commit
  • Preserves the original author of the meaningful commit (not the squash target)
  • Combines all changes from the squashed range

Step 7: Verify

Run all verification checks. Every check must pass before proceeding to push.

# 1. Review the rewritten log
git log --oneline --no-merges

# 2. Validate ALL messages match conventional commits
FAILURES=$(git log --format="%s" --no-merges | grep -cvE "^(feat|fix|docs|chore|refactor|test|ci)(\(.+\))?!?: .+")
if [[ "$FAILURES" -gt 0 ]]; then
  echo "FAIL: $FAILURES non-conforming commits found:"
  git log --format="%h %s" --no-merges | grep -vE "^[a-f0-9]+ (feat|fix|docs|chore|refactor|test|ci)(\(.+\))?!?: .+"
  exit 1
fi
echo "PASS: all commits conform"

# 3. Verify ZERO content changes (critical — this proves only messages changed)
DIFF=$(git diff "backup/pre-rewrite-"*..HEAD --stat)
if [[ -n "$DIFF" ]]; then
  echo "FAIL: content differences detected — rewrite changed code, not just messages"
  echo "$DIFF"
  exit 1
fi
echo "PASS: file tree identical"

# 4. Verify author preservation
echo "=== Authors before ==="
git -C . log backup/pre-rewrite-* --format="%an <%ae>" --no-merges | sort -u
echo "=== Authors after ==="
git log --format="%an <%ae>" --no-merges | sort -u
# These two lists must be identical

If any check fails, do NOT push. Reset to backup and investigate:

git reset --hard backup/pre-rewrite-*

Step 8: Push (with Confirmation)

This is destructive for remote collaborators. Confirm with the user:

⚠ FORCE PUSH WARNING
  Branch: main
  Remote: origin
  Action: git push --force-with-lease origin main

  After this push, all collaborators must run:
    git fetch origin
    git reset --hard origin/main

  Type "yes" to proceed:
git push --force-with-lease origin main

Use --force-with-lease (not --force) to prevent overwriting concurrent remote changes. If --force-with-lease is rejected, someone pushed to the branch since the rewrite — investigate before retrying.

Post-Rewrite Cleanup

After successful push and collaborator notification:

# Verify remote matches local
git log --oneline -5
git log --oneline -5 origin/main
# These should be identical

# Keep backup branch for at least 2 weeks, then delete
# git branch -D backup/pre-rewrite-*
# git push origin --delete backup/pre-rewrite-*

Rationalizations

RationalizationReality
"The old history doesn't matter"Release-please parses commit messages to determine versions. Non-conforming history produces wrong or no releases. git bisect, git blame, and git log --grep all depend on meaningful messages.
"I'll just start fresh from this commit"You lose blame, authorship, and context. An --orphan branch destroys the development narrative. Rewriting preserves everything except bad messages.
"It's too risky to rewrite"The backup branch makes this fully reversible. Verify the diff shows zero content changes. If anything goes wrong: git reset --hard backup/pre-rewrite-*.
"I'll rewrite later when it matters"History rewrites get harder over time as more people clone and branch from it. Do it now, before more downstream dependencies exist.
"Force pushing is dangerous"--force-with-lease protects against overwriting others' work. Combined with the backup branch, this is safe.
"The messages are close enough""Close enough" means release-please won't parse them. Conventional commits are a strict format — either a message conforms or it doesn't.
"I can just manually tag releases"Manual tags drift, get forgotten, and lack changelog integration. The whole point is automation — but automation needs parseable input.

Red Flags

  • Skipping the backup branch creation
  • Using --force instead of --force-with-lease
  • Rewriting a branch that others are actively working on without notification
  • Content differences showing up in the Step 7 diff (means the rewrite changed code, not just messages)
  • Author counts changing after rewrite (means authorship was not preserved)
  • Proceeding without user approval of the rewrite plan
  • Rewritten messages that are still meaningless (e.g., "chore: stuff")
  • Squashing commits across different authors without discussion
  • Not keeping the backup branch for at least 2 weeks after the rewrite

Individual skills in this repo

This repo contains 18 individual skills — each has its own dedicated page.

lguidolin/change-hygiene-and-code-craft

Use when writing or refactoring code, structuring a commit or PR, or deciding whether to abstract duplication. Symptoms — mixing reorg with logic changes, a PR doing several things at once, a file growing large, the second copy of similar code, or unsure whether to DRY something up.

lguidolin/cloud-delivery-aks

Use when deploying to Kubernetes or Azure Kubernetes Service (AKS), configuring cloud secrets, setting up progressive rollout/canary, per-PR ephemeral environments, or k8s health probes. Keywords — Kubernetes, AKS, Key Vault, Argo Rollouts, Flagger, canary, blue-green, liveness, readiness, PodDisruptionBudget, HPA, rollback, GHCR.

lguidolin/conventional-commits-and-releases

Use when committing, writing a commit message, opening a PR that will be squash-merged, or configuring automated versioning/changelogs. Keywords — conventional commits, release-please, semver, feat/fix/chore, breaking change, changelog.

lguidolin/defense-in-depth-security

Use when handling untrusted input, secrets, authentication/authorization, or dependencies — or threat-modeling a new surface. Keywords — STRIDE, threat model, least privilege, secrets management, supply chain, dependency scanning, input validation, audit log, defense in depth.

lguidolin/designing-before-building

Use when starting a feature, fixing a non-trivial bug, or about to write implementation code — before any code exists. Symptoms you need this: "this is simple, I'll just code it", reaching for the editor before a design is approved, or an idea that hasn't been turned into a spec and plan.

lguidolin/engineering-constitution

Use when starting work in a project that follows the engineering constitution, orienting to its rules, or deciding which engineering practice applies to a task — spec writing, commits, testing, security, deploys, database, or UI work.

lguidolin/graphql-contract-testing

Use when writing a GraphQL query/mutation that the UI and a test will share, or building route/schema contract or smoke tests. Symptoms — copying a query into a test, a test asserting on query text, schema change that didn't break the UI build, or RLS/permission drift. Keywords — graphql-codegen, typed document, contract test, route smoke test.

lguidolin/init-repo-CI

Use when setting up a new repository with conventional commits, release-please, and CI automation, or when retrofitting an existing repository that lacks automated versioning and PR validation workflows.

lguidolin/interface-craft-and-accessibility

Use when building or styling UI — components, layouts, forms, design tokens — or making accessibility decisions. Keywords — a11y, WCAG, keyboard navigation, focus state, contrast, design system, minimalist UI, component reuse, ARIA, semantic HTML.

lguidolin/merge-gates-and-automation

Use when setting up or changing CI, pre-push hooks, or a task runner, or deciding what must pass before merge. Symptoms — tempted to put authoritative checks only in a local hook, skip CI, bypass with --no-verify, or unsure what gates a merge vs. runs locally.

lguidolin/observability-and-slos

Use when adding logging, metrics, tracing, health checks, SLOs, or alerting — or when building a service surface that needs to be operable and debuggable. Keywords — structured logs, OpenTelemetry, correlation id, RED metrics, liveness, readiness, SLI, SLO, error budget, alerting.

lguidolin/performance-and-scale

Use when working on hot paths, list endpoints, pagination, data-access in loops, or public interfaces/schemas. Symptoms — unbounded queries, N+1 access, no latency budget, optimizing without measuring, or changing an interface many consumers depend on. Keywords — pagination, N+1, Hyrum's Law, performance budget, bundle size.

lguidolin/postgres-postgraphile-rls-and-sql

Use when writing PostgreSQL, PostGraphile config, Row-Level Security policies, SQL schema files, or working on the Browser→App→PostGraphile→Postgres data path. Keywords — RLS, SECURITY DEFINER, search_path, pgSettings, grants, roles, GraphQL depth limit, query cost, statement_timeout, SQL file organization.

lguidolin/recording-decisions

Use when a design or architecture decision has been made and needs to be captured — writing a decision record or ADR, updating a decision index, noting a deferred idea, or superseding a past decision. Keywords — ADR, decision record, rationale, rejected alternatives, dependency index.

lguidolin/resilience-and-deploy-safety

Use when planning a deploy, designing a rollback, or responding to an incident or writing a postmortem. Keywords — deploy safety, rollback, immutable artifact, progressive delivery, canary, blast radius, incident response, blameless postmortem, error budget.

lguidolin/ship-it

Use when the user wants to ship work — push, PR, archive decision records, merge, and clean up. Handles the full lifecycle from committing final changes through post-merge cleanup including converting specs/plans to compact decision records.

lguidolin/tests-as-a-control

Use when writing or modifying tests, when a test breaks during a refactor, or when testing permission/role rules. Symptoms — tempted to edit a test to make it pass, testing only the happy path, a deny-test that started passing, flaky tests, or unsure what to assert.

lguidolin/zero-downtime-migrations

Use when changing a database schema where data must survive the change — adding/removing/renaming columns, constraints, indexes, or backfilling. Symptoms — a destructive migration bundled with a code deploy, a NOT NULL column with a backfill, a table-locking UPDATE, or a rename. Keywords — expand/contract, parallel change, backfill, NOT VALID, CREATE INDEX CONCURRENTLY, graphile-migrate.

관련 스킬