Git :squash、amend、force-with-lease 的用法
给 Apache Flink 这类项目提 PR,维护者一般要求一个 PR 只留一条 commit。他们维护的是十几年、几十万条提交的主干,git log 里每一条都要是一次完整的改动。开发时那些"改 A""补 B""spotless 又报错了"的中间提交,对他们没有意义。
但开发过程本身就是一堆中间提交。所以需要在提交 PR 前,把这些中间提交合并、整理成一条。下面先讲清楚 Git 的模型,再讲具体怎么做。
Git 的两个基本概念
- commit 是一次快照,记录当时所有文件的状态,并指向它的父提交。生成之后,它的内容和 hash 就固定了。
- 分支是一个指针,指向某一条 commit,可以移动。
整理历史,做的不是修改已有的 commit(改不了),而是生成新的 commit,再把分支指针移到新的 commit 上。旧的 commit 还在仓库里,只是没有分支指着它,过段时间被 Git 回收。
以压缩(squash)为例,前后是这样:
压之前: … ── A ── c1 ── c2 ── c3 ← 分支指针在 c3
压之后: … ── A ── C ← 分支指针移到新提交 C(C = c1+c2+c3 的合并)
└── c1 ── c2 ── c3 旧提交还在,但没有指针指着,稍后回收
squash、amend、force push 都是这一个操作的不同形式:
- squash:用一条新 commit 代替原来的几条。
- amend:用一条新 commit 代替上一条。
- force push:把远端的分支指针也移到新 commit 上。
整体流程
从本地开发到合并,一个 PR 的过程如下。带 CI 失败时的返工回路:
本地(你的机器) GitHub
─────────────── ──────────────────────────
开发:c1 → c2 → c3
│
│ git reset --soft master && git commit (压成一条)
▼
一条 commit:C
│
│ git push --force-with-lease
└──────────────────────────────► 你的 fork:分支
│
│ 开 PR(base = apache:master)
▼
apache/flink 的 PR
│
▼
CI(Azure)
/ \
失败 通过
│ │
本地改 + git commit --amend ◄──────┘ ▼
git push --force-with-lease ──────►(回到 CI 重跑) review → 合并
下面按图里用到的命令逐个说。
把多条提交压成一条:reset --soft
git reset --soft master
git commit -m "xxxx"
git reset --soft master 把当前分支的指针移回 master。--soft 表示只移指针,工作区和暂存区的内容不动。原来几条提交的改动都还在暂存区,再 commit 一次就成了一条。
--soft 和另外两个选项的区别要分清,用错会丢改动:
--soft:只移指针,改动留在暂存区。压提交用这个。--mixed(默认):改动留在工作区,取消暂存。--hard:直接丢弃改动。
改写历史前,可以先建一个备份分支:
git branch backup-before-squash
它只是在当前位置记一个点,不切过去。如果整理坏了,git checkout backup-before-squash 回到之前的状态。PR 合并后再删掉。
修 CI 报的小错:amend 并入上一条
PR 提交后 CI 失败,本地改一个文件修好。这时不要再提一条新的 commit,用 amend 并进上一条:
git add flink-formats/flink-avro/src/main/java/.../AvroFormatFactory.java
git commit --amend --no-edit
--amend 是拿暂存区的改动和上一条 commit 的内容,生成一条新的 commit 顶替它。--no-edit 表示提交说明不变,去掉它会打开编辑器让你改说明。
改写历史后推送:force-with-lease
reset 和 amend 都生成了新 commit,本地分支指针变了,和远端对不上。这时普通 git push 会被拒绝,需要强推:
git push --force-with-lease origin FLINK-xxx-master
用 --force-with-lease 而不是 --force:前者会先确认远端分支还是你上次看到的状态,如果期间有别的提交,就拒绝执行,避免覆盖掉别人的提交。
只在自己 fork 的 PR 分支上改写历史。master 或别人也在用的分支不能强推,否则会破坏别人本地的历史。
把提交挪到另一个分支:cherry-pick
如果改动提到了错误的分支,要挪到当前分支:
git show ccccccc --stat # 先确认这条改了哪些文件,别挪错
git cherry-pick ccccccc # 把它的改动应用到当前分支
cherry-pick 是把某条 commit 的改动,在当前分支上重新应用一次。有冲突就改文件、git add,再 git cherry-pick --continue;放弃用 --abort。
一个功能如果在原分支是多条 commit(主改动加后续修复),要逐条挪,只挪一条会缺东西。
推送前核对范围
整理历史时容易把无关文件带进提交,推送前先看一眼:
git diff --stat master...HEAD
master...HEAD(三个点)表示从两者的共同祖先到 HEAD 的差异,也就是这个 PR 相对基线加了什么。输出应该正好是预期的那几个文件:
.../avro/AvroDeserializationSchema.java | 72 ++++...
.../avro/AvroFormatFactory.java | 16 +++-
.../avro/AvroRowDataDeSerializationSchemaTest.java | 92 ++++...
3 files changed
多出别的文件,说明压提交时带错了,处理掉再推。
再确认本地和远端一致:
git log --oneline -1 # 本地 HEAD
git ls-remote origin FLINK-xxx-master # 远端该分支
两个 hash 前缀相同,就是同步好了。
完整步骤
- 在自己 fork 的分支上开发,可以提多条 commit。
- 提交前:建备份分支,
reset --soft master压成一条,git diff --stat核对范围。 git push --force-with-lease推到 fork 分支。- 在 GitHub 开 PR,base 选
apache:master,compare 选自己 fork 的分支。 - CI 自动运行,没触发就在 PR 评论
@flinkbot run azure。 - CI 失败就本地改,
git commit --amend并入那条,--force-with-lease再推,CI 重跑。 - review 和 CI 都通过后合并,合并后删掉备份分支。
几个常见问题
- amend、squash 会改变 commit 的 hash。改写后原来的 hash 就失效了,在 IDE 或 GitHub 上要按新 hash 找。
- IDE 的 Git Log 看不到新提交,通常是没刷新、过滤器设错、或者 IDE 丢了 Git 根目录。对应处理:刷新、过滤器改成 All、在设置里重新登记 Git 根。
- 强推只适用于自己独占的分支。协作分支只能用 merge 或新提交,不能改写历史。
整理历史用到的就是 reset --soft、amend、force-with-lease 三条命令。理解了 commit 是快照、分支是指针,这三条命令在做什么就清楚了。

浙公网安备 33010602011771号