改错一个 commit message,顺利时只要一条命令;不顺利时,Vim、interactive rebase、detached HEAD 和一串报错会一起冒出来。
这篇不打算把 Git 命令逐条背一遍。先把最重要的判断说清楚:
🧭
写错的是最新一条 commit,用 git commit --amend;写错的是更早的 commit,用 git rebase -i HEAD~N。
动手前,我们先看它排第几
先查最近五次提交:
lua
git log --oneline -5
假设结果是:
sql
fa73d03 feat: add get user by id endpoint
09e5d28 feat: add user creation enpoint
b17a1c5 feat: register repository and service
第二条里的 enpoint 拼错了。它并非最新提交,所以不能直接用一次 amend 解决。
| 要改什么 | 用什么 |
|---|---|
| 最新一条 commit | git commit --amend |
| 更早的一条 commit | git rebase -i HEAD~N,把目标行改成 reword |
| 最近几条都要改 | 一次 rebase,在多行使用 reword |
这里的 N 要覆盖目标 commit。比如目标是倒数第二条,就处理最近两条:
css
git rebase -i HEAD~2
拿不准时,我们可以先数 git log 里的位置,不要凭感觉写数字。
最新一条写错:直接 amend
如果要改的就是 HEAD,可以直接指定新消息:
sql
git commit --amend -m "feat: add user creation endpoint"
也可以不写 -m,让 Git 打开编辑器:
sql
git commit --amend
amend 容易被误解成"改一下标签",其实它会重新创建最新 commit。代码可能一个字符都没动,commit hash 仍然会变。
原因很简单:commit message 本身就是 commit 对象的一部分。
更早的写错:interactive rebase
执行:
css
git rebase -i HEAD~2
编辑器里通常会出现:
sql
pick 09e5d28 feat: add user creation enpoint
pick fa73d03 feat: add get user by id endpoint
把需要改名的那一行从 pick 改成 reword:
sql
reword 09e5d28 feat: add user creation enpoint
pick fa73d03 feat: add get user by id endpoint
保存退出后,Git 会在目标 commit 处暂停,再打开一次编辑器。第二次才是真正修改 commit message。
改为:
sql
feat: add user creation endpoint
再次保存退出。如果后面还有步骤,运行:
kotlin
git rebase --continue
为什么 rebase 列表和 git log 顺序相反
git log 通常从新到旧显示;rebase todo 则按旧到新排列,因为 Git 要按时间顺序重新播放这些提交。
假设历史是:
css
A ← B ← C
HEAD
git rebase -i HEAD~2 会先回到 A,然后重新创建 B 和 C。所以 B 在 todo 里排在 C 前面。
这也解释了两个容易吓人的现象:
- rebase 期间看到
detached HEAD,通常是正常的临时状态; - 改了较早的 commit 后,后续 commit 的 hash 也会跟着变,因为它们的父 commit 已经换了。
Vim 有光标却无法输入,让我们进入输入模式
Git 打开的编辑器经常是 Vim。刚进去时一般处于 Normal 模式,这时字母代表命令,不代表文字。直接敲键盘,会感觉"什么也没输入",甚至莫名删掉内容。
改 commit message,只需要记住这几步:
- 按
i,进入 Insert 模式。 - 修改文字。
- 按
Esc,回到 Normal 模式。 - 输入
:wq,按 Enter,保存并退出。
底部出现 -- INSERT --,才说明可以正常打字。
如果 Esc 被终端或 IDE 截走,可以按:
Ctrl + [
它在 Vim 里基本等价于 Esc。
想把第一行整行换掉,可以用更快的操作:
gg
cc
gg 跳到第一行,cc 清空当前行并直接进入 Insert 模式。输入新 message 后,再按 Esc、输入 :wq、回车。
为什么输入 wq 没反应
因为正确命令是 :wq,前面的冒号不能丢,而且必须在 Normal 模式执行。
:w:保存:q:退出:wq:保存并退出ZZ:Normal 模式下也可保存退出
rebase 卡住时,我们先看报错属于哪一种
不要一出错就重新运行 git rebase -i ...。rebase 已经开始后,应当处理当前状态。
报错:empty commit message
sql
Aborting commit due to empty commit message.
commit message 编辑页里会有很多以 # 开头的说明:
bash
# Please enter the commit message...
# interactive rebase in progress...
这些全是注释,Git 不会把它们当作 message。如果第一行真正的提交说明被删掉,只剩注释,Git 看到的就是空消息。
重新打开当前 commit:
sql
git commit --amend
写入至少一行不以 # 开头的内容,保存退出,再继续:
kotlin
git rebase --continue
报错:could not parse 或 invalid line
例如:
go
error: could not parse 'e09e5d28'
error: invalid line 1
这通常是 rebase todo 里的命令或 hash 被敲错了。真实 hash 是 09e5d28,却多写了一个 e,Git 自然找不到。
编辑当前 todo:
css
git rebase --edit-todo
修正并保存,然后:
kotlin
git rebase --continue
实在不想继续
c
git rebase --abort
它会放弃这次 rebase,尽量恢复到开始前的状态。hash 写错或 message 为空通常不必 abort;如果已经分不清自己改了什么,退出重来反而省时间。
工作区还有没提交的代码怎么办
整理历史前,最好让工作区保持干净。正在写的内容可以先临时收起来:
perl
git stash push -u -m "wip before rebase"
其中 -u 会连未跟踪的新文件一起保存。rebase 完成后取回:
perl
git stash pop
stash 是临时存档,commit 是正式历史。两者用途不同。
怎么确认已经改好
rebase 成功时会看到类似:
bash
Successfully rebased and updated refs/heads/main.
接着检查:
lua
git log --oneline -5
git status
确认 message 正确,并且工作区没有意外修改。理想状态是:
vbnet
On branch main
nothing to commit, working tree clean
如果这些提交已经 push 了
没 push 过的本地历史,改起来最省心。
已经 push 到远端后,本地的 commit hash 变了,普通 git push 往往会被拒绝。确定远端历史确实应该被覆盖时,可以用:
csharp
git push --force-with-lease
它会检查远端分支是否出现了你本地不知道的新提交,比裸 --force 多一道保护。
多人共用的分支不要随手改历史。别人已经基于旧 commit 开始工作时,强推会让所有人的分支都变麻烦。
一张故障处理清单
| 当前情况 | 下一步 |
|---|---|
| 最新 message 写错 | git commit --amend |
| 更早的 message 写错 | git rebase -i HEAD~N,使用 reword |
| todo 的命令或 hash 写错 | git rebase --edit-todo |
| 当前 commit message 为空 | git commit --amend |
| 当前步骤处理完了 | git rebase --continue |
| 彻底放弃这次 rebase | git rebase --abort |
几个结论
HEAD~2不是"只选择倒数第二条",而是选定一个基点,让 Git 处理它后面的两条 commit。reword只改 message;如果要停下来修改 commit 内容,才会用到edit。- rebase 不是在原 commit 上涂改,而是在重新创建一段历史。
- 后续 commit 会记录父 commit,因此前面的 hash 一变,后面的 hash 往往也要重算。
- Vim 的 Normal 模式是命令模式。无法输入文字时,先看底部有没有
-- INSERT --。 - rebase 中途报错,先修当前步骤。能用
--edit-todo或--continue解决时,不要再开一轮 rebase。 --force-with-lease只是降低误覆盖风险,不代表重写共享分支就变得没有代价。
🧠
最后记住一句:最新一条用 amend,更早的用 interactive rebase;Vim 里先确认模式,rebase 卡住就从 continue、edit-todo、abort 里选。