diff --git a/.github/workflows/test-tidy-commits.yml b/.github/workflows/test-tidy-commits.yml new file mode 100644 index 0000000..90b338c --- /dev/null +++ b/.github/workflows/test-tidy-commits.yml @@ -0,0 +1,17 @@ +name: Test tidy-commits + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + test-tidy-commits: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Run tidy-commits content checks + run: bash tests/tidy-commits-content.sh diff --git a/README.md b/README.md index c8e9a59..4389617 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,12 @@ npx skills add akunzai/agent-skills ## Skills +### [`tidy-commits`](skills/tidy-commits/SKILL.md) + +Clean up local git commit history before review or merge. Use it to turn WIP, +fixup, review-fix, format-only, poorly ordered, unsigned, or poorly messaged +commits into a clear, verified branch story. + ### [`agents-md`](skills/agents-md/SKILL.md) Audit, create, and improve `AGENTS.md` files to give AI assistants persistent project memory. @@ -33,4 +39,3 @@ to the commands below. - [`mem-clean`](skills/mem-clean/SKILL.md) — clean expired short-term logs; resolve cloud conflicts - [`mem-sync`](skills/mem-sync/SKILL.md) — sync daily logs across devices via a per-user branch - diff --git a/skills/tidy-commits/SKILL.md b/skills/tidy-commits/SKILL.md new file mode 100644 index 0000000..2bfc772 --- /dev/null +++ b/skills/tidy-commits/SKILL.md @@ -0,0 +1,104 @@ +--- +name: tidy-commits +description: Use when cleaning up local git commit history before review or merge, especially when a branch has WIP, fixup, review-fix, format-only, poorly ordered, unsigned, or poorly messaged commits. +--- + +# Tidy Commits + +Tidy an existing branch into a clear, reviewable commit story while preserving the intended final tree. + +## Quick start + +For local branch history cleanup only — for ordinary new commits use the repo's normal commit workflow. + +Inspect state → refuse unclear or unrelated working-tree changes → create a backup ref → plan `base..HEAD` (keep / squash / fixup / reword / reorder / split / drop) → show the plan and exact commands → rebase non-interactively → verify the final tree and tests before any push. + +## Preflight + +- Determine the base with repo evidence: PR base, `origin/HEAD`, or the user-provided base. +- Fetch first: `git fetch --prune --all`. +- Require a clean, understood state. If `git status` shows a rebase, merge, cherry-pick, or mixed unrelated changes, stop and ask. +- Record current state with `git log --oneline --decorate --stat ..HEAD`, `git diff --stat ...HEAD`, and nearby branch context. +- Create a backup ref, for example: + - `git branch backup/tidy-commits-$(date +%Y%m%d-%H%M%S) HEAD` + +## Cleanup plan + +Classify each commit before rewriting. + +| Commit type | Default action | +| --- | --- | +| Cohesive feature/fix/docs/test commit | Keep, maybe reword | +| `fix`, `fixup`, `review fix`, typo, format-only | Squash or fixup into the commit that introduced the need | +| Commit in the wrong layer/order | Reorder only when dependencies remain valid | +| Commit mixes unrelated concerns | Split if needed for reviewability | +| Debug, temporary, accidental, generated noise | Drop only when final behavior should not include it | + +Do not blend unrelated concerns just to reduce commit count. A good stack is a readable story, not necessarily one commit. + +## Rewrite + +**Collapsing the whole range into one commit?** Skip the todo: `git reset --soft && git commit`. This leaves the final tree staged and re-commits it as a single commit — nothing is replayed, so there are no conflicts (it signs automatically when `commit.gpgsign` is set). Use the rebase todo below only when you need selective fixup, reorder, or split. + +Otherwise prefer non-interactive rebase patterns (no editor prompts). Generate the todo oldest-first (the reverse of `git log`), edit the actions and order, then feed it back: + +```bash +git log --reverse --format='pick %h %s' ..HEAD > "${TMPDIR:-/tmp}/tidy-commits-todo" +# edit that file, then: +GIT_SEQUENCE_EDITOR="cp ${TMPDIR:-/tmp}/tidy-commits-todo" git rebase -i --update-refs +``` + +The first column is the action; lines run top (oldest) to bottom (newest): + +``` +pick a1b2c3d Add parser +fixup f4e5d6a fix typo in parser # folds into the pick above, discards its message +pick 7890abc Add CLI flag +edit def1234 Wire CLI to parser # stop to amend, then git rebase --continue +``` + +Avoid `reword` in todo files because it opens an editor; use `edit`, then `git commit --amend -m ...` and `git rebase --continue`. Use `GIT_SEQUENCE_EDITOR=:` with `--exec` for bulk mechanical amendments. + +Recovery: mid-rebase, `git rebase --abort` restores the pre-rebase state; after a bad finish, `git reset --hard `. + +When a branch contains merge commits, ask whether to preserve them with `--rebase-merges` or flatten them. Do not guess. + +## Stacked Branches + +Before rewriting, detect local branches that point inside the rewritten range. Use `--update-refs` by default so stacked branches follow rewritten commits instead of being orphaned. + +Flag branches checked out in another worktree: Git will not move those refs. Report them for manual verification before pushing. + +If the repo uses a stacked-PR tool such as `gh stack`, prefer that tool's sync/rebase workflow over hand-editing branch relationships. + +## Verification + +After rewriting: + +- Compare the final tree against the backup ref unless commits were intentionally dropped: `git diff --stat HEAD` and `git diff HEAD`. +- Show the new story: `git log --oneline --decorate ..HEAD`. +- Run relevant tests, type checks, linters, or focused reproductions. +- If branch protection requires verified signatures, check commit signatures with `git log --show-signature ..HEAD` or the repo's GitHub status. Re-sign rewritten commits before pushing when needed. + +## Push Safety + +Never use plain `git push --force`. + +If the branch was already pushed, list every ref that changed and show exact commands first. + +```bash +git push --force-with-lease origin HEAD: +git push --force-with-lease origin +``` + +Ask for confirmation before force-with-lease pushes. Report any local-only backup ref and do not delete it without explicit approval. + +## Stop Conditions + +Stop and ask when: + +- The intended base branch is ambiguous. +- A commit's purpose cannot be inferred from code, tests, or messages. +- Conflict resolution requires product judgment. +- Dropping a commit may change behavior. +- Another worktree or remote branch would be affected and cannot be verified. diff --git a/tests/tidy-commits-content.sh b/tests/tidy-commits-content.sh new file mode 100644 index 0000000..59b87e8 --- /dev/null +++ b/tests/tidy-commits-content.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +SKILL_DIR="$ROOT_DIR/skills/tidy-commits" + +fail() { + echo "tidy-commits content check failed: $*" >&2 + exit 1 +} + +[ -d "$SKILL_DIR" ] || fail "skills/tidy-commits directory is missing" + +grep -q '^name: tidy-commits$' "$SKILL_DIR/SKILL.md" \ + || fail "SKILL.md frontmatter must use name: tidy-commits" + +grep -q -E 'Use when .*commit history|Use when .*commit stack|Use when .*fixup|Use when .*squash' "$SKILL_DIR/SKILL.md" \ + || fail "description must trigger on commit history cleanup" + +grep -R -q -E 'git status|working tree' "$SKILL_DIR" \ + || fail "working tree preflight guidance is missing" + +grep -R -q -E 'backup (ref|branch|tag)|backup-before' "$SKILL_DIR" \ + || fail "backup ref guidance is missing" + +grep -R -q -E 'fixup|squash|reword|reorder|drop' "$SKILL_DIR" \ + || fail "commit tidy operations are missing" + +grep -R -q -E 'non-interactive|GIT_SEQUENCE_EDITOR' "$SKILL_DIR" \ + || fail "non-interactive rebase guidance is missing" + +grep -R -q -E 'reset --soft' "$SKILL_DIR" \ + || fail "collapse-all reset --soft shortcut is missing" + +grep -R -q -E 'diff .*backup|tree.*identical|final tree' "$SKILL_DIR" \ + || fail "tree-equivalence verification guidance is missing" + +grep -R -q -E -- '--update-refs|stacked branch|checked out in another worktree' "$SKILL_DIR" \ + || fail "stacked branch/update-refs guidance is missing" + +grep -R -q -E 'force-with-lease|never plain --force' "$SKILL_DIR" \ + || fail "safe force push guidance is missing" + +grep -R -q -E 'sign|signature|verified' "$SKILL_DIR" \ + || fail "commit signing guidance is missing" + +echo "tidy-commits content checks passed"