今天临时兴起想修改仓库首次提交commit信息(也就是根提交),记录一下这个过程并复盘
为什么改根提交难?因为 Git 里改任何一个 commit,都会连锁重写所有它之后的 commit 的哈希(因为每个 commit 哈希里都包含父提交哈希)。改根提交 = 整条历史全部重写。
实施过程的 5 个坑
坑 1:本地和远端历史已分叉
第一次 git status 看到:
bash
[origin/main: ahead 1, behind 3]
意味着:
- 本地有 1 个远端没有的提交(我刚做的 Makefile)
- 远端有 3 个本地没有的提交
问题 :如果直接重写本地历史 + 强推,会抹掉远端那 3 个提交。
解法 :先 git pull --rebase 把远端拉下来对齐,再统一重写。
坑 2:工作区有脏文件,pull 被拒
vbnet
error: cannot pull with rebase: You have unstaged changes.
backend/data/business_demo.db 被后端运行时改写了,rebase 会触碰这个文件,所以 git 拒绝。
解法 :git stash 把脏文件临时藏起来,完事再 git stash pop 还原。
坑 3:最隐蔽的坑------遗留了一个未完成的 rebase
sql
fatal: It seems that there is already a rebase-merge directory,
and I wonder if you are in the middle of another rebase.
.git/rebase-merge/ 目录还在,说明你之前某次 rebase 没跑完就退出了(可能是关了终端、电脑重启)。
我看了一眼 .git/rebase-merge/git-rebase-todo:
arduino
pick daa1097 # docs: 排坑记扩写为面试讲述版
reword 69e568d # docs: 移除文档中所有'面试'字样
这正是你之前重写那几个 commit message 时留下的残骸。
解法 :git rebase --abort 干净中止它。
坑 4:abort 把我的提交弄丢了
git rebase --abort 之后状态变成:
csharp
behind 'origin/main' by 3 commits ← 之前是 ahead 1 behind 3
我那个 5a434d5 feat(backend): 新增 backend/Makefile 不见了。
为什么? 因为 abort 是回到那个未完成 rebase 开始的位置 (f33db07),而我的 Makefile 提交是在那之后才做的,被认定为 rebase 过程中产生的"中间态",一并被丢弃了。
怎么救回来? Git 有 reflog(引用日志),记录 HEAD 移动过的所有位置:
scss
5a434d5 HEAD@{3}: commit: feat(backend): 新增 backend/Makefile...
哈希还在,只是没有任何分支指向它。这种状态叫**"悬空提交"(dangling commit)**,Git 默认保留 30 天才回收。
解法:
bash
git pull --ff-only origin main # 先把远端 3 个提交拉下来
git cherry-pick 5a434d5 # 把悬空提交"拣"回到 HEAD
💡 核心知识点 :reflog 是 Git 的"后悔药"。只要你做过 commit,即使分支被删、被覆盖,30 天内都能通过
git reflog找回来。
坑 5:非交互式环境怎么 reword 根提交?
git rebase -i --root 默认会:
- 弹一个编辑器让你编辑 todo 列表(把
pick改成reword) - 然后弹另一个编辑器让你输入新的 commit message
但我在自动化环境里没法手动编辑。Git 提供两个环境变量来劫持这两步:
| 环境变量 | 控制什么 |
|---|---|
GIT_SEQUENCE_EDITOR |
编辑 rebase todo 列表的编辑器 |
GIT_EDITOR |
编辑 commit message 的编辑器 |
最终命令:
bash
GIT_SEQUENCE_EDITOR="sed -i '' '1s/^pick/reword/'" \
GIT_EDITOR='sh -c "echo 初始化智能助手项目 > \"$1\"" --' \
git rebase -i --root
逐段拆解:
-
sed -i '' '1s/^pick/reword/'把 todo 文件第 1 行开头的pick替换成reword。这就告诉 rebase:"第一个 commit(根提交)我要改 message"。-i ''是 macOS 上sed的"原地修改"语法(Linux 上是-i不带空字符串)。 -
sh -c "echo 初始化智能助手项目 > \"$1\"" --当 git 调用$GIT_EDITOR <commit-msg-file>时,实际执行的是sh -c '...' -- <commit-msg-file>。这个脚本把新 message 写入$1(也就是那个 commit message 临时文件),覆盖原内容。 -
--root告诉 rebase:"从根提交开始 rebase",这是改第一个 commit 的唯一办法(普通HEAD~N数不到根)。
坑 6(避免了的坑):强推风险
最后 git push 时我用的是:
bash
git push --force-with-lease origin main
不是 --force。
区别:
--force:无脑覆盖,即使别人在你 pull 之后又 push 了新 commit,也会被你抹掉。--force-with-lease:推之前先检查"远端 HEAD 是否还是你上次拉到的那个",如果别人趁你不注意推了新东西,会拒绝推送,保护协作者。
💡 铁律 :强推一律用
--force-with-lease,永远不要用--force。
整体流程图
scss
[起点] ahead 1, behind 3, 脏文件,残留 rebase
│
├─ git stash push ← 解决坑 2
│
├─ git rebase --abort ← 解决坑 3
│ ⚠️ Makefile 提交丢失
│
├─ git pull --ff-only ← 解决坑 1(同步远端)
│
├─ git cherry-pick 5a434d5 ← 解决坑 4(从 reflog 救回)
│
├─ GIT_SEQUENCE_EDITOR=... \
│ GIT_EDITOR=... \
│ git rebase -i --root ← 解决坑 5(非交互式 reword)
│
├─ git push --force-with-lease ← 解决坑 6(安全强推)
│
└─ git stash pop ← 还原坑 2 藏起来的脏文件
[终点] 历史干净,远端同步,工作区还原
这次任务教会我的 4 个 Git 知识点
- 改根提交必须用
git rebase -i --root,不能用HEAD~N。 - reflog 是后悔药,30 天内可以救回任何"丢失"的 commit。
- 非交互式控制 rebase :
GIT_SEQUENCE_EDITOR+GIT_EDITOR两个环境变量分别接管 todo 列表和 commit message。 - 强推必须用
--force-with-lease,这是协作安全的底线。
如果下次自己想改根提交
最简单的交互式版本(在终端里):
bash
git stash # 如果有脏文件
git pull --rebase # 先同步远端
git rebase -i --root # 编辑器打开后,把第一行 pick 改成 reword,保存退出
# 然后编辑器再次打开,改 commit message,保存退出
git push --force-with-lease # 强推
git stash pop # 还原脏文件
5 行命令搞定。我刚才走的弯路是因为踩到了那个残留的 rebase-merge 目录,正常情况下不会遇到。