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 前缀相同,就是同步好了。

完整步骤

  1. 在自己 fork 的分支上开发,可以提多条 commit。
  2. 提交前:建备份分支,reset --soft master 压成一条,git diff --stat 核对范围。
  3. git push --force-with-lease 推到 fork 分支。
  4. 在 GitHub 开 PR,base 选 apache:master,compare 选自己 fork 的分支。
  5. CI 自动运行,没触发就在 PR 评论 @flinkbot run azure
  6. CI 失败就本地改,git commit --amend 并入那条,--force-with-lease 再推,CI 重跑。
  7. 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 是快照、分支是指针,这三条命令在做什么就清楚了。

posted @ 2022-11-28 09:39  lvlin241  阅读(144)  评论(0)    收藏  举报