关于 recomposing-commits
recomposing-commits 是用于在提交 Pull Request 前整理功能分支提交的技能。它在隔离的 worktree 中重建提交,并要求先审阅和明确批准,才会应用到原分支。
主要功能
- 01拒绝受保护分支或脏工作区
- 02分析变更并划分提交组
- 03在隔离 worktree 中重建历史
- 04应用变更前必须审阅
- 05应用前验证内容差异
适用场景
- 01在 Pull Request 前整理 WIP 提交
- 02将无关变更拆分为不同提交
- 03改进含糊的提交说明
- 04调整或拆分功能分支提交
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ nur-zaman/ git-recompose-skill --skill recomposing-commits安装后在 Claude Code 中输入/recomposing-commits确认已加载。
详细介绍
重新组织提交
概述
分析杂乱的提交 → 在工作树中隔离 → 让用户审核 → 应用回去。
核心原则: 在用户明确批准新历史记录之前,绝不修改原分支。工作树隔离和审核关卡不可妥协——即使用户说“直接做”或“我信任你,跳过审核”也不例外。
开始时声明:“我正在使用 recomposing-commits 技能来重新组织你的分支历史记录。”
安全检查 — 首先执行
git rev-parse --abbrev-ref HEAD
如果当前分支是以下任一分支,立即拒绝:
main、master、dev、develop、development、staging、production、release,或匹配 release/* 或 hotfix/*。
⛔ Recomposing commits on '<branch>' is not allowed.
This skill only works on feature branches.
Please switch to your feature branch first.
如果 HEAD 处于分离状态,也要拒绝(上述命令会输出 HEAD 而不是分支名):
⛔ You're in a detached HEAD state, not on a branch.
This skill needs a real branch to reset back onto at the end. Check out a branch first.
在进行任何其他操作之前,还要检查工作树是否有未提交的更改:
git status --porcelain
如果有任何输出,停止:
⛔ You have uncommitted changes. These won't be included in the recompose,
and they'll be at risk when the original branch is reset at the end.
Please commit or stash them first.
不要创建临时分支。不要在上述任何情况下继续操作。拒绝并停止。
适用场景
- 提交处于 WIP 状态或杂乱,而即将发起 PR
- 多个无关更改混在同一批提交中
- 提交说明不清晰(“fix”、“WIP”、“stuff”)
- 需要拆分、合并或重新排序提交
请勿在以下情况使用:
- 历史记录已经清晰
- 当前处于受保护的共享分支上(应拒绝)
- 只有一个提交需要重组
- 范围内包含合并提交(参见步骤 3 — 不支持,仅适用于线性历史)
工作流(步骤 1–8)
步骤 1:显示当前历史记录
git log --oneline
向用户显示输出。
步骤 2:询问起始 SHA
Which commit should be the start of the recompose?
Provide the SHA of the first commit you want to reorganize.
(All commits from that SHA through HEAD will be recomposed.)
Enter SHA:
继续之前先验证:
# Must actually be an ancestor of HEAD
git merge-base --is-ancestor <user-sha> HEAD || echo "NOT AN ANCESTOR"
如果输出 NOT AN ANCESTOR,告知用户该 SHA 不在当前分支的历史记录中,并再次询问。
**根提交特殊情况:**如果 <user-sha> 是仓库的第一个提交,它没有父提交:
git rev-parse <user-sha>^ 2>&1 # will error: "unknown revision" if it's the root commit
如果是这种情况,停止并告知用户此情况不会自动处理:从仓库的根提交开始重新组织,需要进行本技能无法安全处理的手动操作。请用户选择根提交之后的起点,或手动处理根提交的历史记录。
否则,按常规计算基点:
BASE=$(git rev-parse <user-sha>^)
步骤 3:分析要重新组织的提交
git log --oneline $BASE..HEAD
git diff $BASE HEAD
阅读所有差异。识别逻辑分组。标记出现在多个逻辑分组中的文件(文件重叠 — 参见步骤 6)。
如果范围内包含合并提交,则拒绝:
git log --merges --oneline $BASE..HEAD
如果有任何输出:
⛔ This range contains a merge commit. This skill only handles linear history —
recomposing across a merge isn't supported. Pick a start point after the merge,
or restructure the branches involved separately.
检查是否有多个作者(重新组织会将作者信息和提交日期重置为执行此操作的人):
git log --format='%ae' $BASE..HEAD | sort -u
如果出现多个地址,继续操作之前告知用户:
⚠️ Commits in this range have multiple authors. Recomposing will reset every
new commit's author and date to yours. Continue anyway?
步骤 4:创建隔离的工作树
始终使用绝对路径。 像 .git/recompose/feat/HISBA-mastra-ai 这样的相对路径,在分支名包含斜杠时,或 Shell 状态无法在工具调用之间保留时,都会悄然失效。
BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD)
REPO_ROOT=$(git rev-parse --show-toplevel)
WORKTREE_PATH="$REPO_ROOT/.git/recompose/$BRANCH_NAME"
WORKTREE_BRANCH="recompose/$BRANCH_NAME"
先清理上次运行或中断运行留下的残留内容——否则,如果路径或分支已存在,worktree add/-b 会失败:
if git worktree list | grep -q "$WORKTREE_PATH"; then
git worktree remove "$WORKTREE_PATH" --force
fi
if git rev-parse --verify "$WORKTREE_BRANCH" >/dev/null 2>&1; then
git branch -D "$WORKTREE_BRANCH"
fi
git worktree add "$WORKTREE_PATH" -b "$WORKTREE_BRANCH"
之后的所有 Git 操作都在 $WORKTREE_PATH 中进行。 原分支不会被修改。
步骤 5:将提交合并为未暂存更改
使用带绝对路径的 git -C — 不要依赖 cd。每次 Bash 工具调用都会启动一个新的 Shell;之前调用中的 cd 不会保留。
git -C "$WORKTREE_PATH" reset --mixed $BASE
现在,所有更改都已在工作树中处于未暂存状态,可以按逻辑重新暂存了。
步骤 6:暂存并提交每个逻辑分组
每条命令都使用 git -C "$WORKTREE_PATH"。绝不要假设当前 shell 已 cd 到工作树中。
对于明确只属于一个逻辑提交的文件:
git -C "$WORKTREE_PATH" add <file> <file2>
git -C "$WORKTREE_PATH" commit --no-verify -m "type: description"
对于跨越多个逻辑提交的重叠文件:
# git add -p requires a TTY — run it inside a single shell block
cd "$WORKTREE_PATH" && git add -p <file> # stage hunk by hunk: y/n/s/e
git -C "$WORKTREE_PATH" commit --no-verify -m "type: first concern"
cd "$WORKTREE_PATH" && git add -p <file>
git -C "$WORKTREE_PATH" commit --no-verify -m "type: second concern"
对于跨越多个提交的新建/未跟踪文件,单独使用 git add -p 不会提供代码块选择——它只适用于已跟踪文件。请先将其标记为意向添加:
git -C "$WORKTREE_PATH" add -N <new-file> # tracks it for diff purposes, stays unstaged
cd "$WORKTREE_PATH" && git add -p <new-file>
绝不要使用 git add -i 或 git rebase -i——它们需要交互式 TTY 输入,在此情境下会挂起或失败。
步骤 7:审查关卡——必须执行,不可协商
在工作树中创建所有提交后,务必向用户展示以下内容:
Recomposition complete in isolated worktree.
To review:
cd .git/recompose/<branch-name>
git log --oneline
Original branch is UNCHANGED. Nothing will be applied until you approve.
Note: commits were created with --no-verify (hooks skipped) — run your
linter/formatter after applying. Authorship and commit dates on the new
commits reflect whoever ran this, not the original commits.
Does the new history look good?
[yes] → Apply to original branch
[no/edit] → Describe what to change, I'll update the worktree
[cancel] → Discard worktree, original branch stays as-is
等待用户明确回复。 在收到回复前,不要继续执行步骤 8。
如果用户之前说过“不要审查,直接做”: 仍必须展示此审查关卡。请说明:“审查步骤是此技能安全保障的一部分。我已隔离这些更改——审查只需 30 秒,并能确保原始分支安全。以下是新历史记录的样子:”
步骤 8:处理用户回复
[yes] — 应用回原分支:
在操作原始分支之前,先验证差异完全相同(见下方的验证部分)——如果结果显示“DIFFERENCES FOUND”,不要继续。
先检查是否有远程上游:
git -C "$REPO_ROOT" remote -v
git -C "$REPO_ROOT" branch -vv
如果分支跟踪了远程分支:
⚠️ This branch has a remote upstream. Applying recomposed commits will
require a force-push. Do you want to proceed?
等待确认,然后:
# Safety net: tag where the original branch was, in case anything needs to be undone
git -C "$REPO_ROOT" tag "backup/$BRANCH_NAME-$(date +%s)" "$BRANCH_NAME"
# Confirm we're actually resetting the branch we think we are
CURRENT=$(git -C "$REPO_ROOT" rev-parse --abbrev-ref HEAD)
if [ "$CURRENT" != "$BRANCH_NAME" ]; then
echo "⛔ Expected to be on $BRANCH_NAME but found $CURRENT — aborting."
exit 1
fi
# DO NOT run `git checkout $BRANCH_NAME` — you are already on it in the main worktree.
# git checkout will fail with "already used by worktree". Skip it entirely.
git -C "$REPO_ROOT" reset --hard "recompose/$BRANCH_NAME"
# Verify it worked
git -C "$REPO_ROOT" log --oneline -5
# Clean up — worktree FIRST, then branch
git -C "$REPO_ROOT" worktree remove "$WORKTREE_PATH"
git -C "$REPO_ROOT" branch -D "recompose/$BRANCH_NAME"
如果上面确认了远程上游,则推送:
git -C "$REPO_ROOT" push --force-with-lease
优先使用 --force-with-lease,而不是 --force——如果期间有其他人推送到远程分支,它会安全失败,而不是悄悄覆盖对方的工作。
告知用户备份标签(backup/$BRANCH_NAME-<timestamp>),让他们知道如有需要,如何恢复重组前的状态;并说明满意结果后即可安全删除该标签。
[no/edit] — 迭代:
若要在不触碰 rebase -i 的情况下调整工作树中已有的提交,请将历史展开回该提交之前,然后从那里重新执行步骤 6:
# Find the SHA of the commit right before the one you need to change
git -C "$WORKTREE_PATH" log --oneline
# Uncollapse everything from that point forward back to unstaged
git -C "$WORKTREE_PATH" reset --mixed <sha-before-the-commit-to-fix>
然后按照步骤 6 的方法重新暂存并提交,再次展示审查关卡(步骤 7)。
[cancel] — 丢弃:
git worktree remove "$WORKTREE_PATH"
git branch -D "recompose/$BRANCH_NAME"
原始分支未受影响。
重叠文件策略
| 情况 | 命令 |
|---|---|
| 文件更改属于不同部分 | git add -p <file> → 对每个代码块选择 y/n |
| 代码块混合了两个关注点 | 使用 s 拆分,然后选择 y/n |
| 不同关注点确实交错在各行中 | 使用 e 手动编辑代码块差异 |
| 文件完全属于一个提交 | git add <file>(无需使用 -p) |
| 新建/未跟踪文件跨越多个提交 | 先执行 git add -N <file>,再执行 git add -p <file> |
验证(宣告完成之前)
# 1. Confirm new clean history
git -C "$WORKTREE_PATH" log --oneline $BASE..HEAD
# 2. Full content diff — stats alone are NOT enough
# Stats can match while content differs. Always do a byte-for-byte comparison.
diff \
<(git -C "$REPO_ROOT" diff $BASE HEAD) \
<(git -C "$WORKTREE_PATH" diff $BASE HEAD) \
&& echo "IDENTICAL — diffs match exactly" || echo "DIFFERENCES FOUND — investigate before proceeding"
统计信息不足以证明内容一致。 即使 66 files changed, 8393 insertions 相符,也无法证明内容一致——只有完整的差异比较才能做到。务必使用 diff <(...) <(...) 格式,并在展示审查关卡前确认结果为“IDENTICAL”。
如果结果显示“DIFFERENCES FOUND”:请停止。 不要展示审查关卡,也不要应用回原分支。返回步骤 5/6,查明遗漏或分配错误的内容(代码块暂存到了错误的提交中,或有文件未纳入),然后重新运行此检查,直到结果显示“IDENTICAL”。
快速参考
| 步骤 | 操作 |
|---|---|
| 0 | 检查分支、分离头指针状态和工作树是否干净——任一检查失败则拒绝 |
| 1 | git log --oneline → 请求起始 SHA |
| 2 | 验证 SHA 是祖先提交;处理根提交的边缘情况 |
| 3 | git diff $BASE HEAD — 分析;遇到合并提交则拒绝;标记多位作者 |
| 4 | 清理过期的工作树/分支,然后执行 git worktree add |
| 5 | git reset --mixed $BASE — 合并提交 |
| 6 | 按逻辑分组暂存并提交(注意未跟踪文件) |
| 7 | 提供审查关卡 — 必须执行,注明钩子/作者身份重置 |
| 8 | 验证差异完全相同 → 创建备份标签 → 应用回去 → 强制推送(--force-with-lease)→ 清理 |
常见错误 / 合理化借口对照表
| 借口 | 事实 |
|---|---|
"git rebase -i 是处理这个问题的标准工具" | git rebase -i 需要交互式 TTY——在这种情况下会卡住。改用 git reset --mixed + 选择性暂存。 |
| "用户说不需要审查,我会尊重他们的偏好" | 审查关卡不可妥协。它只需 30 秒,还能保护原始分支。无论如何都要提交审查。 |
| "我直接修改原始分支就好,这样更快" | 不可妥协:所有提交都只能在工作树中进行。获得批准前,不得改动原始分支。 |
| "清理分支的变通方案也能达到相同效果" | 受保护分支检查不允许任何变通方案。如果当前位于 main/master/dev 分支上,请拒绝并停止。 |
"git add -i 和 git add -p 差不多" | git add -i 需要交互式 TTY。只能使用 git add -p。 |
| "可以等用户审查完再清理工作树" | 在步骤 8 中完成清理——留下工作树会污染仓库。 |
| "我可以使用相对路径 WORKTREE_PATH,路径很明显" | 分支名称包含斜杠时,相对路径会失效。始终使用 REPO_ROOT=$(git rev-parse --show-toplevel) 并指定绝对路径。 |
| "我会进入工作树目录,然后在那里运行 git 命令" | Bash 工具的多次调用之间不会保留 shell 状态。每条命令都使用 git -C "$WORKTREE_PATH"。 |
"重置前我需要先执行 git checkout $BRANCH_NAME" | 该 checkout 会失败——该分支已被主工作树占用。跳过这一步,直接运行 git -C "$REPO_ROOT" reset --hard。 |
| "diff 统计数据一致,所以内容也完全相同" | 即使内容不同,统计数据(66 个文件、8393 次插入)也可能一致。始终使用 diff <(git diff ...) <(git -C worktree diff ...) 进行验证。 |
| "工作树里有几处未提交的更改,没关系" | 未提交的更改不会包含在重新组合中,而且最终执行 reset --hard 时有丢失风险。先检查 git status --porcelain,如果工作区不干净就停止。 |
"git add -p 会像处理其他文件一样处理这个新文件" | 对未跟踪文件,git add -p 不会提供分块选择。先运行 git add -N <file>。 |
| "我跳过备份标签就好,工作树里已经有所有内容了" | 清理时会删除工作树。原始分支重置前 SHA 对应的备份标签,是之后唯一的撤销途径——务必创建一个。 |
| "既然默认要强制推送,我就不用真的执行了" | 如果用户确认且该分支有上游,就执行推送(--force-with-lease)——不要只说明操作然后停止。 |
| "范围内只有一个合并提交,我可以变通处理" | 不支持处理范围内的合并提交——应拒绝操作,并请用户改选其他起点,而不是自行变通。 |
危险信号 — 停止
如果你发现自己在想以下任何事情,请停下来并遵循此技能:
- “我会使用
git rebase -i” - “用户说跳过审查,那我就跳过”
- “直接在原始分支上操作更快”
- “我会创建备份分支,而不是工作树”
- “受保护分支的事在这里无关紧要”
- “我可以对有冲突的文件执行
git add <file>,这样更简洁” - “我会
cd到工作树目录,这比git -C简单” - “统计数据一致,所以差异没问题,我会跳过内容检查”
- “我需要先检出分支,才能重置到该分支”
- “工作树有点脏,继续操作应该没问题”
- “范围内有一个合并提交,我就直接重置过去”
- “我不需要备份标签,不会出问题”
- “我提到了需要强制推送,这就等同于已经执行了”