引子
我反复看到这样的评论:“git rebase 像屎一样”。人们似乎对此有很强烈的感受,我真的很惊讶,因为我没有遇到太多使用 rebase 的问题,而且我一直在使用它。
使用 rebase 的成本有多大?在实际使用中它给你带来了什么问题?我这里只对你遇到的具体糟糕经历感兴趣。
我收到了大量感觉差异的问题,在这里总结它们。如果我知道怎么解决这些问题,也会提到这些问题的解决方案或解决方法。以下是大家的问题列表:
1)反复解决同一冲突很烦人
2)对大量提交进行 rebase 非常困难
3)撤销 rebase 操作很困难
4)强制推送到共享分支可能会导致工作丢失
5)强制推送让代码review更加困难
6)会丢失提交元数据
7)让恢复更困难
8)重新定基可能会破坏中间提交
9)意外地运行了 git commit –amend 而不是 git rebase –continue
10)在交互式 rebase 中拆分提交很困难
11)复杂的 rebase 很难
12)重新定位长期存在的分支可能会很烦人
13)重新定基并提交规则
14)“挤压和合并”工作流程
15)杂项问题
我的目的不是要说服任何人说 rebase 不好,你不应该使用它(我肯定会继续使用 rebase!)。但是看到所有这些问题能让rebase使用的时候更谨慎,而不解释如何安全地使用它。这也让我想知道是否有更简单的工作流程来清理你的提交历史, 而不产生很多的麻烦。
我的 git 工作流程假设
首先,我知道人们使用很多不同的 Git 工作流程。我先说下我在团队中工作时习惯的工作流程,即:
1)团队使用中央 Github/Gitlab 仓库来协调
2)只有一个main分支。它受到保护,不会受到强制推送。
3)人们在功能分支中编写代码并发出拉取main分支请求
4)main每次合并拉取请求时都会部署 Web 服务。
5)进行更改的唯一方法main是在 Github/Gitlab 上发出拉取请求并合并它
这不是唯一“正确的” git 工作流程(它是一种非常典型的“运行 Web 服务”的工作流,而开源项目或发布版的桌面软件通常使用略有不同的工作流程)。但这是我所知道的,所以我将讨论这一点。
两种 rebase
在我们开始之前:我注意到的一件大事是,有两种不同的 rebase 不断出现,其中只有一种需要您处理合并冲突。
1)在历史分支上重新rebase, 例如 `git rebase -i HEAD^^^^^^^`将许多小提交压缩为一个。只要你只是压缩提交,就永远不必在执行此操作时解决合并冲突。
2)重新rebase到已经分叉的分支,如`git rebase main`。这可能会导致合并冲突。
我认为做出这种区分很有用,因为有时我正在考虑rebase 到type-1(这不太可能导致问题),但那些为此苦苦挣扎的人正在考虑rebase 到 type-2。
下面让我们把这些烦人的问题一一解决。
反复解决同一冲突很烦人
如果进行多次小提交,有时会陷入地狱般的循环,不得不修复相同的合并冲突 10 次。您还可能完全不必要去修复合并冲突(例如处理未来提交删除的代码中的合并冲突)。
有几种方法可以改善这种情况:
1)首先执行 agit rebase -i HEAD^^^^^^^^^^^ 将所有小提交压缩为1个大提交,
然后git rebase main执行 a 重新定位到另一个分支。这样,您只需修复一次冲突。
2)用git rerere自动重复解决相同的合并冲突(“rerere” 代表“重用记录的解决方案”,
它将记录你以前的合并冲突解决方案并一次执行它们)。我从未尝试过这个,但我认为你可以
设置git config rerere.enabled true 然后让它给你提供帮助。
此外,如果我发现自己在重新rebase过程中多次解决合并冲突,我通常会运行git rebase --abort以停止它,然后将我的提交压缩为一个并再试一次。
对大量提交进行 rebase 非常困难
通常,当我对不同的分支进行 rebase 时,我会 rebase 1-2 个提交。有时可能是 5 个!通常不会有冲突,而且运行良好。
有些人描述了将许多不同人的数百个提交重新定位到不同的分支的过程。这听起来确实很困难,这个场景解决不了,很可能有问题。
撤销 rebase 操作很困难
我听几个人说,当他们刚开始接触 rebase 时失败了, 弄丢了一周的代码。
这里的问题是,撤销出错的 rebase比撤销出错的合并要复杂得多(你可以使用类似 的命令撤销错误的合并)git reset --hard HEAD^。许多 rebase 菜鸟没有意识到撤销 rebase 是可能的,我认为这很容易理解。
尽管如此,还是可以撤销出错的 rebase。下面是使用 撤销 rebase 的示例git reflog。
step 1:执行错误的rebase(例如运行git rebase -I HEAD^^^^^ 并删除 3 个提交)
step 2:运行git reflog。会看到类似这样的内容:
ee244c4 (HEAD -> main) HEAD@{0}: rebase (finish): returning to refs/heads/main
ee244c4 (HEAD -> main) HEAD@{1}: rebase (pick): test
fdb8d73 HEAD@{2}: rebase (start): checkout HEAD^^^^^^^
ca7fe25 HEAD@{3}: commit: 16 bits by default
073bc72 HEAD@{4}:commit: only show tooltips on desktop
step 3:找到紧接在之前的条目rebase (start)。在我的例子中,它是
ca7fe25
step 4:运行git reset --hard ca7fe25
撤消 rebase 的其他几种方法:
1) 显然@总是指的是 git 中的当前分支,因此您可以运行
git reset --hard @{1}将分支重置到其先前的位置。
2) 大家提到的另一个避免使用 reflog 的解决方案是
git switch -c backup 在重新rebase之前创建一个“备份分支”,
这样就可以轻松返回老的提交。
强制推送到共享分支可能会导致代码丢失
包含如下提到了以下情况:
1)你正在与某人在某个分支上进行协作
2)你推动了一些改变
3)他们重新设置分支并运行git push --force(可能是意外)
4)现在当你运行 git pull,你得到了一个fatal: Need to specify how to reconcile divergent branches错误
5)在尝试处理后果时,你可能会丢失一些提交,特别是如果一些参与的人对 git 不太熟悉
这种情况比“撤消 rebase 失败”的情况更糟糕,因为丢失的提交可能会分散在许多不同的人手中,而比必须搜索 reflog 更糟糕的事情是多个不同的人都去搜索 reflog。
我从来没有遇到过这种情况,因为我曾经合作过的唯一分支是main,并且main一直受到保护,不会强制推送(根据我的经验,唯一可以将某些内容放入的方法main是通过拉取请求)。所以我从来没有真正遇到过这种情况。但我绝对可以理解这会 带来什么问题。
我知道的避免这种情况的主要工具是:
1)不要在共享分支上进行 rebase
2)强制推送时使用--force-with-lease,以确保自上次获取后没有其他人推送到该分支
显然,
since your last fetch
在这里很重要——如果你在
git fetch
之前立即执行
git push --force-with-lease
,那么
--force-with-lease
无法保护你。
我很好奇为什么有人会在共享分支上执行
git push --force
。他们给出的一些理由如下:
1)他们正在协作开发一个功能分支,并且该功能分支需要重新rebase到main分支。
这里的想法是,你只需非常小心地同步rebase,以免丢失任何内容。
2)作为开源维护者,有时他们需要重新rebase贡献者的分支以修复合并冲突
3)他们是 git 菜鸟,在网上读到了一些建议`git rebase`和`git push --force`解决方案,并在没有理解后果的情况下就照做了
4)他们习惯`git push --force`在个人分支上做,却意外地在共享分支上运行
强制推送使代码review更加困难
这里的情况是:
1)你在 GitHub 上发出拉取请求
2)人们留下一些评论
3)你更新代码以解决注释问题,重新设置基准以清理提交,然后强制推送
4)现在,当审阅者回来时,他们很难分辨出自上次看到以来你做了哪些更改——所有的提交都显示为“新的”。
避免这种情况的一种方法是推送解决审核意见的新提交,然后在 PR 获得批准后进行重新rebase以重新组织所有内容。
我认为有些审阅者比其他人更讨厌这个问题,这是一种个人偏好。此外,这可能是 Github 特有的问题,其他代码审查工具可能有更好的工具来解决这个问题。
丢失提交元数据
如果你通过 rebase 来压缩提交,你可能会丢失重要的提交元数据,例如
Co-Authored-By
。此外,如果你使用 GPG 签名提交,rebase 会丢失签名。
我还没有遇到过这种情况,所以我不知道如何避免。个人认为 GPG 签名提交不像以前那么流行了。
让回复变得更困难
有人提到,对于他们来说,能够轻松地撤销合并任何分支非常重要(以防分支出现问题),并且如果分支包含多个提交并与 rebase 合并,那么你需要执行多次撤销才能撤消提交。
在merge 流程中,我认为只需恢复merge提交即可恢复任何分支的合并。
重新rebase可能会破坏中间提交
如果你尝试拥有一个非常干净的提交历史记录,其中每次提交的测试都通过(非常令人钦佩!),那么即使最终提交通过了测试,重新rebase也可能会导致一些中间提交被破坏并且无法通过测试。
可以通过
git rebase -x
在
rebase
的每个步骤中运行测试套件并确保测试仍然通过来避免这种情况。但我从未这样做过。
意外地运行
git commit --amend
而不是
git rebase --continue
有人提到了用
git commit --amend
而不是
git rebase --continue
解决合并冲突时的问题。
之所以令人困惑,是因为你可能出于两个原因想要在rebase期间编辑文件:
1)编辑提交(使用editin ),完成时git rebase -i需要写入git commit --amend
2)git rebase --continue合并冲突,完成后需要运行
这两种情况很容易混淆,因为它们感觉非常相似。我认为这里的问题在于你:
1)开始重新定基
2)遇到合并冲突
3)解决合并冲突并运行git add file.txt
4)run :git commit因为这是你跑步后习惯做的事git add
5)你应该运行`git rebase --continue`!现在你有一个奇怪的提交,它可能提交信息和/或作者是错误的。
在交互式 rebase 中拆分提交很困难
rebase 的全部目的是清理历史提交,使用 rebase合并提交非常简单。但是如果你想将一个提交拆分成两个较小的提交怎么办?这并不容易,特别是如果你想拆分的提交是前几个提交!虽然我对 rebase 非常熟悉,但实际上我真的不知道该怎么做。我可能会做点什么,git reset HEAD^^^ 然后git add -p从头开始重做我的所有提交。
复杂的 rebase 很难
如果你尝试在一次操作中做太多事情git rebase -i(重新排序提交、合并提交和修改提交),那么可能会让git操纵变得非常混乱。
为了避免这种情况,我个人倾向于每次 rebase 只做 1 件事,如果我想做 2 件不同的事情,我会做 2 次 rebase,千万别图省事,这样的结果可能会更麻烦
rebase长期存在的分支可能会很烦人
如果你的分支存在时间较长(例如 1 个月),那么反复进行 rebase 会很麻烦。最后只进行 1 次合并并仅解决一次冲突可能更简单。
理想情况是通过不留那些长期的分支来避免这个问题,但在实践中并不总是能实现这一点。
其他问题
还有一些我认为不太常见的问题:
1)错误地停止 rebase: 如果你尝试使用 `git reset --hard`
而不是 git rebase --abort 来中止一个运行不正常的rebase ,
那么事情会变得很奇怪,直到你正确地停止它。建议用 git rebase --abort
2)与合并提交的奇怪交互:
关于此的几句话:“如果你重新rebase工作副本以保持分支的干净历史记录,
但底层项目使用merges,结果可能会很糟糕。如果执行
rebase -i HEAD~4 并且第四次提交是合并,
你可以在交互式编辑器中看到数十个提交。",
我已经吸取了惨痛的教训,如果我合并了另一个分支中的任何内容,永远不要重新rebase"
重新定基并提交规则
我看到很多人在争论 rebase。我一直在思考这是为什么,并且我注意到人们实践着几个不同级别的“提交原则”:
1) 几乎什么都可以,“wip”,“fix”,“idk”,“添加东西”
2)当你发出拉取请求(在 github/gitlab 上)时,将所有糟糕的提交压缩
为一个带有合理消息(通常是 PR 标题)的提交
3)原子精美提交 - 每个更改都被拆分为适当数量的提交,每个提交都有
一个很好的提交消息,并且它们都讲述了你所做的更改的故事
我经常认为同一家公司内的不同人有不同级别的提交纪原则,我也经常看到人们为此争论不休。就我个人而言,我大多是 2 级的人。我认为 3 级可能就是人们所说的“清除提交历史”的意思。
我认为级别 1 和级别 2 无需 rebase 就很容易实现 - 对于级别 1,您无需执行任何操作,而对于级别 2,您可以在 github 上 按
squash and merge
或在命令行上运行
git switch main; git merge --squash mybranch
。
但是对于第 3 级,您要么需要 rebase 要么需要其他工具(例如 GitUp)来帮助你组织提交以完善它。
我一直在想,当人们争论是否“应该”使用 rebase 时,他们实际上是在争论应该要求哪种最低级别的提交原则。
我认为结果如何还取决于人们所做的更改有多大 —— 如果人们通常都会提出很小的拉取请求,那么将它们压缩成 1 次提交并不是什么大问题,但如果正在进行 6000 行的更改,可能需要将其拆分为多次提交。
“squash and merge”工作流
有几个人提到使用这个不使用rebase的工作流程:
1)做出提交
2)定期运行git merge main将主要内容合并到分支中(如有必要,修复冲突)
3)完成后,使用 GitHub 的“压缩和合并”功能(相当于运行git checkout main;
git merge --squash mybranch)将所有更改压缩为 1 个提交。
这样可以摆脱所有“丑陋的”合并提交。
我原本以为这会让我的分支上的提交日志太难看,但显然
git log main..mybranch
只会向你显示分支上的更改,如下所示:
$ git log main..mybranch
756d4af (HEAD -> mybranch) Merge branch 'main' into mybranch
20106fd Merge branch 'main' into mybranch
d7da423 some commit on my branch
85a5d7d some other commit on my branch
当然,这里的目标不是强迫那些做出了漂亮原子提交的人压缩他们的提交 - 它只是为人们提供一个简单的选项来清理混乱的提交历史(“添加新功能;wip;wip;fix;fix;fix;“)而不必使用rebase。
我很好奇想听听其他人使用这种工作流的情况以及它是否运行良好。
问题比我想象的要多
我当时真的觉得“rebase 没问题,还能出什么问题?”但实际上,很多这些问题过去都发生在我身上,只是这些年来我已经学会了如何避免或修复所有这些问题。
除了“永远不要强制推送到共享分支”之外,我从未真正看到过任何人分享 rebase 的最佳实践。老实说,所有这些都让我更不愿意推荐使用 rebase。
总结一下,我认为这些是我遵循的个人 rebase 规则:
1)如果情况不好,请停止 rebase,而不是让它完成(使用git rebase --abort)
2)知道如何git reflog撤消错误的 rebase
3)不要重新设置一百万个微小的提交(而是分两步进行:git rebase -i HEAD^^^^然后git rebase main)
4)不要一次性做多件事git rebase -i。保持简单。
5)永远不要强制推送到共享分支
6)永远不要重新定位已经推送的提交main
版权归原作者 神技圈子 所有, 如有侵权,请联系我们删除。