改写历史的核心动作只有三个:commit --amend(改最近一次)、rebase -i(改任意一段)、filter-repo(批改全历史)。日常代码评审里"补一行到三次前的 commit",配合 --fixup / --autosquash 不必再手动开 rebase 菜单。
改写历史会重算 SHA,只在自己未推送、或确认没人基于该分支工作时做。已 push 到 protected branch 才发现要改的,先看末尾"安全网"一节再动手。
改最近一次提交
改提交说明
git commit --amend # 进编辑器
git commit --amend -m "msg" # 一行写完
git commit --amend --no-edit # 只追加文件,不改说明
追加 / 替换文件
git add fixed.cpp
git commit --amend --no-edit
从最近一次提交中移除某个文件
不要用 git reset HEAD~1 —— 那会把整个提交炸开,其余文件的暂存状态也丢了。正确做法是只把目标文件在 index 里还原到上一版,再 amend:
git restore --source=HEAD~ --staged -- path/to/file # index 回到上一版
git commit --amend --no-edit # 重新打包
如果还想丢掉工作区里的修改,再加 --worktree:
git restore --source=HEAD~ --staged --worktree -- path/to/file
改一段提交:交互式 rebase
git rebase -i HEAD~3 # 改最近 3 次
git rebase -i --root # 改到根提交(含第一次 commit)
打开的 todo 列表里,最早的提交在最上面(和 git log 相反),改第一列动词即可:
| 命令 | 作用 |
|---|---|
pick (p) | 保留 |
reword (r) | 保留 commit,只改说明 |
edit (e) | 停在这里,让你 amend 或拆分 |
squash (s) | 并入上一行,合并 message |
fixup (f) | 并入上一行,丢弃本条 message |
drop (d) | 删除(直接删行也行) |
exec (x) | 在该提交点执行 shell |
调整顺序:直接挪行。
拆分一个提交
todo 改为:
edit bb199a0 Update the version
rebase 暂停时执行:
git reset HEAD^ # 把这次提交的改动还原回工作区
git add file1 && git commit -m "Update version: bump"
git add file2 && git commit -m "Update version: changelog"
git rebase --continue
fixup + autosquash:无菜单 rebase
刚 review 完,发现 3 次前的 commit 漏写了校验逻辑。传统做法是打开 rebase 菜单挑出对应行手动 edit;--fixup 把这一步自动化:
git add validator.go
git commit --fixup=<old-sha> # 生成 "fixup! <old subject>"
git rebase -i --autosquash <old-sha>^ # 自动把 fixup! 行排到 old commit 后,并标 fixup
写进配置就不用每次加 --autosquash:
git config --global rebase.autosquash true
git config --global commit.verbose true # 顺带:让 commit 编辑器里看到 diff
改作者 / 邮箱
最新一笔:amend
漏配 user.name 就提交了?最新一笔用 amend 修:
git config --global user.name "Jiang Xin"
git commit --amend --allow-empty --reset-author
--allow-empty:允许空白提交(修补后无文件变化时需要)。--reset-author:同时重置 Author 与 AuthorDate;不加只会改 Committer。
Git 不做提交者身份认证,“谁都能写任意 name/email” 是分布式特性。权限控制交给 GitLab / GitHub 等服务端。
全历史:先 mailmap,必要时 filter-repo
amend 只管最新一笔。历史上一千个用旧邮箱提的 commit 它够不着 —— 两种正解,先试 mailmap。
用法 1:mailmap,不动历史只改显示
仓库根目录建 .mailmap,告诉 Git “看到旧身份就当新身份显示”:
# 前是新身份,后是旧身份,空格分隔
New Name <new@gmail.com> Old Name <old@qq.com> # 名 + 邮箱完整匹配
New Name <new@gmail.com> <old@qq.com> # 只按旧邮箱匹配,旧名任意
之后这些命令自动按映射显示:
git log --use-mailmap
git shortlog -sn # 默认就 honor mailmap
用法 2:filter-repo,真改写历史
需要 commit 字段本身也变(审计、合规、对外开源前清洗),上 git filter-repo:
git filter-repo --mailmap .mailmap
早年的
git filter-branch官方已弃用 —— 每 commit 起一个 shell 子进程,大仓库小时级别,且默认保留refs/original/导致旧对象"清理完又复活"。filter-repo 是 Python 单进程流式,快 10–100 倍。
两者对比
| 维度 | 用法 1:mailmap | 用法 2:filter-repo |
|---|---|---|
| 改的是 | 显示,原 commit 不动 | commit 字段本身 |
| commit SHA | 不变 | 全变 |
| 协作者成本 | 零(pull 即可) | 必须重 clone 或硬 reset |
| 撤销 | 删 .mailmap 即可 | 不可逆(除非备份) |
| 第三方工具识别 | GitHub 认;审计工具未必 | 永远认 |
.mailmap正好就是filter-repo --mailmap吃的输入 —— 先写 mailmap 跑用法 1,确认效果再决定要不要上用法 2。一份配置覆盖两种场景。
实操辅助
查看当前所有作者:
git log --format='%aN <%aE>' | sort -u
filter-repo 改完一定要 force push,并通知所有 collaborator 重新 clone —— 他们本地的旧 SHA 在远程已不存在,再 push 会把旧链子带回来:
git push --force-with-lease --tags # 比 -f 安全:远程被别人推过会拒绝
安全网
- 改坏了能找回:
git reflog列出 HEAD 最近 90 天的每次跳转,git reset --hard HEAD@{3}回到任意一步。reflog 是本地的,没 push 也照样能用。 - 强推首选
--force-with-lease:上游被别人 push 过时会拒绝覆盖,避免顺手抹掉同事的提交。 - 保护分支兜底:GitHub / GitLab 默认提供 protected branch;本地也可用
pre-pushhook 拒绝任何对main的非 fast-forward 推送。
References
– EOF –