Community编程与开发github.com

roselletabuena/git-safe-history-rewrite

安全地重写 Git 历史,而不会丢失工作或覆盖他人的提交——支持 amend、reset、squash、reorder、drop 或编辑提交、更改提交信息、从分支中删除提交或强制推送。

git-safe-history-rewrite 是什么?

该技能提供了一个结构化的、安全优先的方法来重写 Git 历史,解决了协作开发中的一个关键痛点。它通过一系列步骤引导开发者:首先评估重写是否合适(检查提交是否已被他人拉取),然后创建命名的备份分支,选择最合适的命令(如 `git commit --amend` 或 `git rebase` 但避免交互式编辑器),通过 `git diff` 和 `range-diff` 验证变更,最后使用 `--force-with-lease` 和 `--force-if-includes` 等安全措施发布。该技能支持所有主流平台(Claude Code、Cursor、Codex 等),核心设计原则是:如果重写会干扰他人,则使用 `git revert` 而不是重写。它明确定义了哪些分支可以安全重写(例如个人 PR 分支)和哪些分支绝对不能重写(如 main 或 develop),并提供了处理合并提交回滚、冲突解决和签名提交等边界情况的实用指南。

兼容平台~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/roselletabuena/skills/tree/HEAD/plugins/git-workflows/skills/git-safe-history-rewrite

Installed? Explore more 编程与开发 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

安全地重写历史

每一次重写——commit --amend、reset、rebase、filter-repo——都会生成新的提交,并将分支指针从旧提交上移开。当没有其他人拥有旧提交时,这是无害的;但一旦有人拥有,它就会造成破坏:他们的克隆中仍然存在旧历史,下次他们推送或拉取时,历史会以重复或冲突的形式回来。因此,首要问题从来不是“如何做”,而是“谁还有这些提交?”

1. 决定:重写还是还原

git fetch --prune
git status --short --branch                  # 查看 ahead/behind 和脏状态
git branch -r --contains <commit>            # 检查哪些远程分支已经包含此提交
git log --oneline @{u}..HEAD                 # 仅存在于本地的提交
提交所在位置操作
仅本地,从未推送自由重写。
推送到只有当前用户操作的分支(例如其 PR 分支)重写,然后使用 lease 推送(步骤 5)。
在共享或受保护的分支上:main、master、develop、release/* 或任何其他人在其上叠加了提交的分支不要重写。 使用 git revert。
已发布或部署git revert。重写无法撤销部署。

当用户说“从 main 中删除那个提交”时,他们通常希望其效果消失,而不是其 SHA。git revert <sha> 可以做到这一点,保持历史诚实,并且只需要一次普通推送。对于合并提交,使用 git revert -m 1 <merge-sha>。说明你为什么选择还原,如果用户坚持从共享分支中擦除它,请说明这会影响到谁,并先获得明确的同意。

当后续提交编辑了相同行时,还原会产生冲突——这正是提交建立在糟糕提交之上的情况。像处理任何冲突一样解决它(请参阅 git-conflict-resolution):删除糟糕的更改,保留后续工作,然后执行 git revert --continue。

一个还原陷阱:在还原合并之后,再次合并同一个分支不会带回任何东西,因为 Git 认为这些提交已经合并过。要重新引入这些工作,请先还原之前的还原。

2. 在处理任何内容之前,先设置一个恢复点

git rev-parse HEAD                           # 记下来
git branch backup/<branch>-pre-rewrite       # 一个真正的引用,而不仅仅是 reflog

Reflog 也会记录分支当时的位置,但它仅本地存在于这个克隆中,会过期(可达条目 90 天,不可达条目默认 30 天),并且 reflog expire 后的 git gc 会结束它。一个命名的备份分支不花什么成本。在用户确认结果之前,保留它。

3. 选择破坏性最小的命令

目标命令备注
修复最后一次提交的信息或添加遗漏的文件git commit --amend-m "..." 或 --no-edit 避免打开编辑器。
取消提交但保留已暂存的更改git reset --soft HEAD~1
取消提交并取消暂存,保留文件git reset --mixed HEAD~1(默认)
将分支向后移动,拒绝丢失本地编辑git reset --keep <commit>如果会覆盖未提交的更改则中止。优先于 --hard。
同时也丢弃未提交的工作git reset --hard未提交的工作不在 reflog 中且无法恢复。 仅在确认那里没有任何想要的东西后执行;如果不确定,先执行 git stash -u。
丢弃对一个文件的更改git restore <file>
将修复压缩或折叠到早期的提交中git commit --fixup=<sha> 然后 autosquash见下文。
删除或重新排序提交rebase -i 并提供自定义的 todo见下文。

失败的预提交钩子意味着没有提交被创建。 运行 git commit --amend 来“重试”会修改前一个提交,将新更改错误地合并到其中。钩子失败后,修复问题,重新暂存,并创建一个正常的普通新提交。

无需交互式编辑器的交互式变基

git rebase -i 会打开一个编辑器,这在非交互式 shell 中会挂起。不要依赖它。两种可行的方法:

# 将修复提交折叠到它们的目标中——接受 Git 生成的 todo:
git commit --fixup=<target-sha>
GIT_SEQUENCE_EDITOR=true git rebase -i --autosquash <base>

# 删除或重新排序:自己编写 todo。
GIT_SEQUENCE_EDITOR="sed -i '/^pick <short-sha> /d'" git rebase -i <base>

<base> 是你想要更改的第一个提交之前的那个提交。如果变基在冲突处停止,解决它,执行 git add,然后执行 GIT_EDITOR=true git rebase --continue。如果出错了,git rebase --abort 会返回到开始的地方。

4. 验证重写是否按预期更改了内容

信息编辑、重新排序或压缩应该使最终的树相同:

git diff backup/<branch>-pre-rewrite HEAD --stat      # 空 = 内容相同
git range-diff backup/<branch>-pre-rewrite...HEAD     # 逐提交比较

如果你打算删除内容,diff 应该恰好显示这些内容,没有别的。任何意外都意味着从备份恢复并重做。

5. 发布——仅在用户同意后进行

推送重写是面向外部、难以逆转的步骤。除非用户已经要求推送,否则先确认。

git push --force-with-lease=<branch>:<expected-remote-sha> --force-if-includes origin <branch>
  • 永远不要使用裸的 --force。 它会覆盖远程上的任何内容,包括你未看到的队友的推送。
  • 指定预期的 SHA。 单纯的 --force-with-lease 会与你的远程跟踪 ref 进行比较,而 IDE 的后台 git fetch 会悄悄更新该 ref,因此 lease 实际上保护不了什么。使用你在步骤 1 中看到的 SHA(git rev-parse origin/<branch>)。
  • --force-if-includes(Git 2.30+)额外拒绝在远程移动到你的本地分支从未集成过的内容时执行推送。
  • 永远不要强制推送 main/master/develop/release/*。 如果托管平台的规则阻止了它,那这条规则就是正确的;不要绕过它。

推送后,告诉用户任何签出了旧分支的人都必须重置到新分支,而不是拉取,并且打开的 PR 的审查评论可能会失去其锚点。

安全护栏

  • 签名提交在重写后会丢失签名; 使用 -S 重新签名或告知用户。
  • 不要重写他人创作的提交,即使是在功能分支上,未经询问也不行。
  • 被重写分支上方的堆叠分支现在已过时。 使用 git-stacked-pr-rebase 重新堆叠它们,而不是手动逐个变基。
  • 已提交的机密无法通过重写修复。 首先轮换凭据;假设它已被泄露。
  • 如实报告: 旧的 tip 是什么,新的 tip 是什么,备份 ref 在哪里,以及是否有任何未推送的内容。如果还原是正确的选择,说明你已经这样做了。

Individual skills in this repo

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

相关技能