Git rebase的`--update-refs`选项使得堆叠式分支更加轻松
2022年10月, Git发布了2.38版本. 其中rebase功能新增了--update-refs
选项. 而近期我们的项目中也开始推行了堆叠式分支/PR. 在这里, 我将根据自己的理解和使用体验讨论一下如何使用Git rebase的--update-refs
功能. 这会让处理"堆叠式"分支变得更容易.
备注: Stacked分支/PR, 即堆叠式分支/PR, 也叫栈式分支/PR, 在我这篇经验分享中统一使用前者.
现在要堆叠啥玩意儿?
我很喜欢小而多的Git提交. 尤其是在创建大的功能时. 我喜欢用我的提交创建一个"故事", 通过一个又一个的提交添加一个大功能. 这样做的目的是让其他人在review时尽可能地简单, 一次只看一个提交.
在此基础上, 我还经常为功能中的每几个提交创建单独的PR. 这也是为了方便他人review. GitHub的PR的review页面确实无法很好地处理大型PR, 即使你有"增量"提交. 为每个功能单元创建独立的分支和PR, 可以让人们更容易地了解和跟踪提交的"故事".
这种方法被称为堆栈式分支/PR, 即在一个分支/PR的基础上构建大量独立的分支/PR. 如果把分支的git图谱想象成: 每个分支都"堆叠"在前一个分支之上, 这就说得通了.
例如, 在下面的repo中, 我有6次提交, 作为一个特性feature-xyz
的一部分, 并将其分解为3个逻辑单元, 每个单元都有一个分支. 然后, 我可以为每个分支创建一个PR:
对于分支andrew/feature-xyz/part-1
的第一个PR, 我会创建一个PR, 请求合并到dev
(在这个例子中). 对于分支andrew/feature-xyz/part-2
的第二个PR, 我会创建一个PR, 请求合并到andrew/feature-xyz/part-1
, 而对于part-3
分支, 该PR会请求合并到part-2
中:
每个PR只包含该分支的特定提交, 这将带来更好的review体验(在我看来).
不要以为我在创建功能时自然地且完美地分割了这些提交. 在创建PR之前, 我对这些提交进行了大量地rebase和编辑.
这一切都很顺利, 直到有人真正review了part-1
并请求修改. 这时, 我们就会遇到堆叠式分支棘手的一面.
rebase…太多的rebase了
假如有人评论说, 我在part-1
分支中遗漏了一些重要内容. 好极了. 我可以查看该分支, 进行修改, 提交并推送. 现在的git日志看起来就像下面这样:
Shit, Git图谱看起来有点混乱. 它不再是一个"堆栈"了. 如果我们想让part-2
和part-3
分支包含PR Feedback
提交(我们几乎肯定会这样做), 那么我们就必须在part-1
分支的基础上重新建立分支.
我总是
rebase
, 基本上从不merge
. 我觉得不断交叉合并的分支实在难以理清楚. 不过这就是战火纷飞的地区, 我就要一头扎进去, 就先不多说了!
要重置part-2
和part-3
分支, 我们必须像这样运行点命令:
# Rebase commit 4 and commit 5 on top of the part-1 branch
git checkout andrew/feature-xyz/part-2
git rebase HEAD~2 --onto andrew/feature-xyz/part-1
# Rebase commit 6 on top of the (now rebased) part-2 branch
git checkout andrew/feature-xyz/part-3
git rebase HEAD~ --onto andrew/feature-xyz/part-2
运行这些命令后, 我们将回到一个漂亮的堆叠式列表:
不过, 这仍然相当困难. 在那里要运行很多rebase
命令, 你必须正确地获取不同的"base"并且--onto
到对应的引用(每个分支的引用都不同), 因此很难说服人们这是一项值得付出的努力(这是可以理解的).
这就是--update-refs
的用武之地.
使用--update-refs
来rebase堆叠式分支
Git 2.38为rebase
命令引入了一个新选项: --update-refs
. 根据文档, 该选项在设置后将:
自动强制更新任何指向正在被rebase的提交的分支. 任何在工作树中被checkout的分支都不会以这种方式更新.
这是什么意思? 下面我将介绍几种场景, 以及--update-refs
如何在每种情况下提供帮助.
Rebase分支栈
我们的PR看起来不错, 但随后又有一个提交到了dev
分支, 我需要在最新提交的基础上重新rebase, 将其纳入我的所有分支:
如果没有--update-refs
, 这就很麻烦. 如果使用前面的方法的话, 我需要checkoutpart-1
, 想办法将它正确地rebase到dev
上, 然后重复栈中的每个分支.
另一种方法是将栈"顶部"的part-3
rebase到dev
之上. 然后, 我们可以将每个分支reset
到新基于的分支中的"正确地"提交, 类似于这样:
# Rebase the tip of the stack first
git checkout andrew/feature-xyz/part-3
git rebase dev
# Set part-2 branch to the new location
git checkout andrew/feature-xyz/part-2
git reset 782b4db --hard # <-- Need to grab the correct commit for this
# Set part-1 branch to the new location
git checkout andrew/feature-xyz/part-1
git reset 0d976a1 --hard # <-- Need to grab the correct commit for this
这基本上就是--update-refs
的作用, 但它让事情变得非常简单; 它会rebase一个分支,"记住"所有现有(本地)分支的指向, 然后将它们reset
到之后正确的位置. 我们可以通过运行以下命令来"修复"上述示例, 而不必手动rebase每个分支的位置:
git checkout andrew/feature-xyz/part-3
git rebase dev --update-refs
这会打印出:
Switched to branch 'andrew/feature-xyz/part-3'
Successfully rebased and updated refs/heads/andrew/feature-xyz/part-3.
Updated the following refs with --update-refs:
refs/heads/andrew/feature-xyz/part-1
refs/heads/andrew/feature-xyz/part-2
正如你所看到的, git在rebasepart-3
分支时, 自动更新了andrew/feature-xyz/part-1
和andrew/feature-xyz/part-2
分支:
这对于让多个分支与main分支保持同步非常方便.
在已变化分支上rebase堆叠式分支
让我们回到最初的场景--基于part-1
的第一个PR有了变化, 我们需要在此基础上rebasepart-2
和part-3
.
好消息是, 无论堆叠了多少分支, 我们都只需运行两条命令: checkout尖端分支然后rebase:
# Checkout the "top" branch in the stack
git checkout andrew/feature-xyz/part-3
# Rebase the tip and all intermediate branches
git rebase andrew/feature-xyz/part-1 --update-refs
这样做有多个好处:
- 我们只需要做一次rebase
- 我们不需要使用
--onto
, 也不需要选取每个中间分支的特定提交, 或执行任何reset --hard
.
运行此命令后, 分支看起来和预想的一样:
你仍然需要签出中间分支并强制推送它们, 但至少大部分工作已经完成.
使用--update-refs
进行交互式rebase
在这个最后的场景中, 我们有了分支栈:
在处理part-3
时, 我们注意到一个错字需要修正. 我们首先在part-3
分支中提交, 如下所示:
不幸的是, 我们需要将该提交包含在part-1
分支中. 如果没有--update-refs
, 我们就必须进行多次checkout, cherry-pick和rebase, 但有了--update-refs
, 我们就可以使用交互式rebase::
git rebase dev -i --update-refs
这会弹出编辑器, 让你选择如何rebase. 正如你在下面的例子中看到的, 除了pick
和squash
等选项外, 还有一个额外的选项: update-ref
:
pick d323fff Commit 1
pick 45768bc Commit 2
pick 9b97cc6 Commit 3
update-ref refs/heads/andrew/feature-xyz/part-1
pick 31ab2ab Commit 4
pick 48cdb40 Commit 5
update-ref refs/heads/andrew/feature-xyz/part-2
pick 2338145 Commit 6
pick 9d698f5 Fix typo # <-- need to move this
update-ref
定义了分支将在rebase完成后更新到的位置. 因此我们可以将Fix typo
分支移到Commit 3
之后, 但将其包含在part-1
分支中:
pick d323fff Commit 1
pick 45768bc Commit 2
pick 9b97cc6 Commit 3
pick 9d698f5 Fix typo # <-- Moved to here
update-ref refs/heads/andrew/feature-xyz/part-1 # <-- Above this, so will be included in this branch
pick 31ab2ab Commit 4
pick 48cdb40 Commit 5
update-ref refs/heads/andrew/feature-xyz/part-2
pick 2338145 Commit 6
运行了rebase之后, 分支看起来如下:
同样, 你需要把所有分支都推送上去, 但这比你不这样做要简单得多.
默认启用--update-refs
说到这里, 你可能会想, 是否有什么时候不想使用--update-refs
呢? 虽然这并非总是必要的(例如, 如果没有中间分支), 但我想不出有什么时候不想这么做. 所以好消息是, 你可以在默认情况下启用--update-refs
!
通过运行以下命令, 你可以在所有版本库中默认启用--update-refs
功能:
git config --global --add --bool rebase.updateRefs true
或者, 您也可以在.gitconfig
文件中添加以下内容:
[rebase]
updateRefs = true
如果你不想在特定的rebase中使用--update-refs
, 你可以使用git rebase --no-update-refs
来禁用它.
这个简单的改进让我非常兴奋. 有各种工具和方法可以改善堆叠式PR体验, 但内置
的体验也真的是无与伦比!
总结
我在这篇文章中介绍了Git 2.38中rebase命令的新选项--update-refs
. 我还介绍了堆叠式分支和堆叠式PR的概念, 以及为什么我喜欢它们用于功能开发. 有了--update-refs
, 命令就大大简化了, 正如我在各种场景中展示的那样. 你甚至可以默认启用--update-refs
, 这样你的所有rebase
都会使用它!
转载自:https://juejin.cn/post/7267567645493674021