likes
comments
collection
share

Git rebase的`--update-refs`选项使得堆叠式分支更加轻松

作者站长头像
站长
· 阅读数 62

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:

Git rebase的`--update-refs`选项使得堆叠式分支更加轻松

对于分支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中:

Git rebase的`--update-refs`选项使得堆叠式分支更加轻松

每个PR只包含该分支的特定提交, 这将带来更好的review体验(在我看来).

不要以为我在创建功能时自然地且完美地分割了这些提交. 在创建PR之前, 我对这些提交进行了大量地rebase和编辑.

这一切都很顺利, 直到有人真正review了part-1并请求修改. 这时, 我们就会遇到堆叠式分支棘手的一面.

rebase…太多的rebase了

假如有人评论说, 我在part-1分支中遗漏了一些重要内容. 好极了. 我可以查看该分支, 进行修改, 提交并推送. 现在的git日志看起来就像下面这样:

Git rebase的`--update-refs`选项使得堆叠式分支更加轻松

Shit, Git图谱看起来有点混乱. 它不再是一个"堆栈"了. 如果我们想让part-2part-3分支包含PR Feedback提交(我们几乎肯定会这样做), 那么我们就必须在part-1分支的基础上重新建立分支.

我总是rebase, 基本上从不merge. 我觉得不断交叉合并的分支实在难以理清楚. 不过这就是战火纷飞的地区, 我就要一头扎进去, 就先不多说了!

要重置part-2part-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

运行这些命令后, 我们将回到一个漂亮的堆叠式列表:

Git rebase的`--update-refs`选项使得堆叠式分支更加轻松

不过, 这仍然相当困难. 在那里要运行很多rebase命令, 你必须正确地获取不同的"base"并且--onto到对应的引用(每个分支的引用都不同), 因此很难说服人们这是一项值得付出的努力(这是可以理解的).

这就是--update-refs的用武之地.

使用--update-refs来rebase堆叠式分支

Git 2.38为rebase命令引入了一个新选项: --update-refs. 根据文档, 该选项在设置后将:

自动强制更新任何指向正在被rebase的提交的分支. 任何在工作树中被checkout的分支都不会以这种方式更新.

这是什么意思? 下面我将介绍几种场景, 以及--update-refs如何在每种情况下提供帮助.

Rebase分支栈

我们的PR看起来不错, 但随后又有一个提交到了dev分支, 我需要在最新提交的基础上重新rebase, 将其纳入我的所有分支:

Git rebase的`--update-refs`选项使得堆叠式分支更加轻松

如果没有--update-refs, 这就很麻烦. 如果使用前面的方法的话, 我需要checkoutpart-1, 想办法将它正确地rebase到dev上, 然后重复栈中的每个分支.

另一种方法是将栈"顶部"的part-3rebase到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-1andrew/feature-xyz/part-2分支:

Git rebase的`--update-refs`选项使得堆叠式分支更加轻松

这对于让多个分支与main分支保持同步非常方便.

在已变化分支上rebase堆叠式分支

让我们回到最初的场景--基于part-1的第一个PR有了变化, 我们需要在此基础上rebasepart-2part-3.

Git rebase的`--update-refs`选项使得堆叠式分支更加轻松

好消息是, 无论堆叠了多少分支, 我们都只需运行两条命令: 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.

运行此命令后, 分支看起来和预想的一样:

Git rebase的`--update-refs`选项使得堆叠式分支更加轻松

你仍然需要签出中间分支并强制推送它们, 但至少大部分工作已经完成.

使用--update-refs进行交互式rebase

在这个最后的场景中, 我们有了分支栈:

Git rebase的`--update-refs`选项使得堆叠式分支更加轻松

在处理part-3时, 我们注意到一个错字需要修正. 我们首先在part-3分支中提交, 如下所示:

Git rebase的`--update-refs`选项使得堆叠式分支更加轻松

不幸的是, 我们需要将该提交包含在part-1分支中. 如果没有--update-refs, 我们就必须进行多次checkout, cherry-pick和rebase, 但有了--update-refs, 我们就可以使用交互式rebase::

git rebase dev -i --update-refs

这会弹出编辑器, 让你选择如何rebase. 正如你在下面的例子中看到的, 除了picksquash等选项外, 还有一个额外的选项: 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之后, 分支看起来如下:

Git rebase的`--update-refs`选项使得堆叠式分支更加轻松

同样, 你需要把所有分支都推送上去, 但这比你不这样做要简单得多.

默认启用--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
评论
请登录