繁体   English   中英

Git 工作流程和变基与合并问题

[英]Git workflow and rebase vs merge questions

我已经在与另一位开发人员的一个项目中使用 Git 几个月了。 我在SVN有几年的经验,所以我想我给这段关系带来了很多包袱。

我听说 Git 在分支和合并方面非常出色,到目前为止,我只是没有看到。 当然,分支非常简单,但是当我尝试合并时,一切都变得很糟糕。 现在,我已经习惯了 SVN,但在我看来,我只是将一个低于标准的版本控制系统换成了另一个。

我的搭档告诉我,我的问题源于我想不经意地合并,并且在许多情况下我应该使用 rebase 而不是合并。 例如,这是他制定的工作流程:

clone the remote repository
git checkout -b my_new_feature
..work and commit some stuff
git rebase master
..work and commit some stuff
git rebase master
..finish the feature
git checkout master
git merge my_new_feature

本质上,创建一个功能分支,始终从 master 到分支,然后从分支合并回 master。 需要注意的重要一点是分支始终保持在本地。

这是我开始的工作流程

clone remote repository
create my_new_feature branch on remote repository
git checkout -b --track my_new_feature origin/my_new_feature
..work, commit, push to origin/my_new_feature
git merge master (to get some changes that my partner added)
..work, commit, push to origin/my_new_feature
git merge master
..finish my_new_feature, push to origin/my_new_feature
git checkout master
git merge my_new_feature
delete remote branch
delete local branch

有两个本质区别(我认为):我总是使用合并而不是变基,并且我将我的功能分支(和我的功能分支提交)推送到远程存储库。

我对远程分支的推理是我希望在工作时备份我的工作。 我们的存储库会自动备份,如果出现问题可以恢复。 我的笔记本没有,还是没有那么彻底。 因此,我讨厌在我的笔记本电脑上有没有在其他地方镜像的代码。

我对合并而不是 rebase 的推理是合并似乎是标准的,而 rebase 似乎是一个高级功能。 我的直觉是我想要做的不是高级设置,所以 rebase 应该是不必要的。 我什至仔细阅读了关于 Git 的新的 Pragmatic Programming 书,它们广泛地涵盖了合并,几乎没有提到 rebase。

无论如何,我在最近的一个分支上遵循我的工作流程,当我试图将它合并回 master 时,一切都变得糟糕透了。 与本应无关紧要的事情发生了大量冲突。 这些冲突对我来说毫无意义。 我花了一天时间整理好一切,最终以强制推送到远程 master 达到高潮,因为我的本地 master 已经解决了所有冲突,但是远程 master 仍然不高兴。

像这样的“正确”工作流程是什么? Git 应该让分支和合并变得超级简单,我只是没有看到。

更新 2011-04-15

这似乎是一个非常受欢迎的问题,所以我想我会用我第一次问起两年来的经验来更新。

事实证明,原始工作流程是正确的,至少在我们的情况下是这样。 换句话说,这就是我们所做的并且有效:

clone the remote repository
git checkout -b my_new_feature
..work and commit some stuff
git rebase master
..work and commit some stuff
git rebase master
..finish the feature, commit
git rebase master
git checkout master
git merge my_new_feature

事实上,我们的工作流程有点不同,因为我们倾向于进行压缩合并而不是原始合并。 注意:这是有争议的,见下文。 )这允许我们将我们的整个功能分支变成对 master 的单个提交。 然后我们删除我们的功能分支。 这允许我们在 master 上逻辑地构建我们的提交,即使它们在我们的分支上有点混乱。 所以,这就是我们要做的:

clone the remote repository
git checkout -b my_new_feature
..work and commit some stuff
git rebase master
..work and commit some stuff
git rebase master
..finish the feature, commit
git rebase master
git checkout master
git merge --squash my_new_feature
git commit -m "added my_new_feature"
git branch -D my_new_feature

Squash Merge Controversy - 正如一些评论者所指出的, Squash 合并将丢弃您的功能分支上的所有历史记录。 顾名思义,它将所有提交压缩为一个。 对于小功能,这是有道理的,因为它将它压缩成一个包。 对于较大的功能,这可能不是一个好主意,特别是如果您的个人提交已经是原子的。 这真的归结为个人喜好。

Github 和 Bitbucket(其他?)拉取请求- 如果您想知道合并/rebase 与拉取请求的关系,我建议您按照上述所有步骤进行操作,直到您准备好合并回 master。 无需手动与 git 合并,您只需接受 PR。 请注意,这不会进行压缩合并(至少默认情况下不会),但非压缩、非快进是 Pull Request 社区中公认的合并约定(据我所知)。 具体来说,它是这样工作的:

clone the remote repository
git checkout -b my_new_feature
..work and commit some stuff
git rebase master
..work and commit some stuff
git rebase master
..finish the feature, commit
git rebase master
git push # May need to force push
...submit PR, wait for a review, make any changes requested for the PR
git rebase master
git push # Will probably need to force push (-f), due to previous rebases from master
...accept the PR, most likely also deleting the feature branch in the process
git checkout master
git branch -d my_new_feature
git remote prune origin

我已经爱上了 Git,再也不想回到 SVN。 如果你在挣扎,坚持下去,最终你会看到隧道尽头的曙光。

TL; 博士

git rebase 工作流并不能保护您免受不擅长解决冲突的人或习惯于 SVN 工作流的人的侵害,就像避免 Git 灾难:血腥的故事 中所建议的那样。 它只会让他们更乏味地解决冲突,并且更难从糟糕的冲突解决中恢复过来。 相反,使用 diff3 以便它首先不那么困难。


Rebase 工作流程并不适合解决冲突!

我非常赞成清理历史记录。 但是,如果我遇到冲突,我会立即中止变基并进行合并! 人们推荐 rebase 工作流作为解决冲突的合并工作流的更好替代方案,这真的让我感到震惊(这正是这个问题的内容)。

如果它在合并过程中“彻底下地狱”,那么在 rebase 期间它将“彻底下地狱”,并且可能还会有更多的地狱! 原因如下:

原因 #1:解决冲突一次,而不是每次提交都解决一次

当您变基而不是合并时,对于相同的冲突,您必须执行与变基提交的次数一样多的冲突解决!

真实场景

我从 master 分支来重构一个分支中的复杂方法。 我的重构工作总共包含 15 次提交,因为我正在努力重构它并获得代码审查。 我的部分重构涉及修复 master 中存在的混合制表符和空格。 这是必要的,但不幸的是,它会与随后在 master 中对该方法所做的任何更改发生冲突。 果然,当我在研究这个方法时,有人对 master 分支中的相同方法进行了简单、合法的更改,该更改应该与我的更改合并。

当需要将我的分支与 master 合并时,我有两个选择:

git merge:我遇到了冲突。 我看到他们对 master 所做的更改并将其与我的分支(的最终产品)合并。 完成。

git rebase:我与第一次提交发生冲突。 我解决了冲突并继续 rebase。 我与第二次提交发生冲突。 我解决了冲突并继续 rebase。 我与第三次提交发生冲突。 我解决了冲突并继续 rebase。 我与第四次提交发生冲突。 我解决了冲突并继续 rebase。 我与第五次提交发生冲突。 我解决了冲突并继续 rebase。 我与第六次提交发生冲突。 我解决了冲突并继续 rebase。 我与第七次提交发生冲突。 我解决了冲突并继续 rebase。 我与第八次提交发生冲突。 我解决了冲突并继续 rebase。 我与第九次提交发生冲突。 我解决了冲突并继续 rebase。 我与第十次提交发生冲突。 我解决了冲突并继续 rebase。 我与第十一次提交发生冲突。 我解决了冲突并继续 rebase。 我与第十二次提交发生冲突。 我解决了冲突并继续 rebase。 我与第十三次提交发生冲突。 我解决了冲突并继续 rebase。 我与第十四次提交发生冲突。 我解决了冲突并继续 rebase。 我与第十五次提交发生冲突。 我解决了冲突并继续 rebase。

如果是您的首选工作流程,您一定是在开玩笑。 所需要的只是一个与 master 上所做的更改冲突的空白修复,并且每次提交都会发生冲突并且必须解决。 这是一个只有空格冲突的简单场景。 天堂禁止您有涉及跨文件的主要代码更改的真正冲突,并且必须多次解决冲突。

有了你需要做的所有额外的冲突解决,它只会增加你犯错误的可能性。 但是 git 中的错误没有问题,因为您可以撤消,对吗? 当然除了...

原因#2:使用rebase,无法撤消!

我想我们都同意解决冲突可能很困难,而且有些人对此很不擅长。 它很容易出错,这就是为什么 git 可以轻松撤消如此出色的原因!

当您合并一个分支时,git 会创建一个合并提交,如果冲突解决效果不佳,可以丢弃或修改该提交。 即使您已经将错误的合并提交推送到公共/权威存储库,您也可以使用git revert撤消合并引入的更改,并在新的合并提交中正确地重做合并。

当你对一个分支进行rebase 时,如果冲突解决可能出错,你就完蛋了。 现在每个提交都包含错误的合并,您不能只重做 rebase*。 充其量,您必须返回并修改每个受影响的提交。 不好玩。

在 rebase 之后,无法确定提交的最初部分以及由于错误的冲突解决而引入的内容。

*如果您可以从 git 的内部日志中挖掘旧的 refs,或者如果您创建第三个分支指向 rebase 之前的最后一次提交,则可以撤消 rebase。

彻底解决冲突:使用 diff3

以这个冲突为例:

<<<<<<< HEAD
TextMessage.send(:include_timestamp => true)
=======
EmailMessage.send(:include_timestamp => false)
>>>>>>> feature-branch

纵观冲突,无法判断每个分支更改了什么或其意图是什么。 这是我认为解决冲突令人困惑和困难的最大原因。

diff3 来救援!

git config --global merge.conflictstyle diff3

当您使用 diff3 时,每个新冲突都会有一个第三部分,即合并的共同祖先。

<<<<<<< HEAD
TextMessage.send(:include_timestamp => true)
||||||| merged common ancestor
EmailMessage.send(:include_timestamp => true)
=======
EmailMessage.send(:include_timestamp => false)
>>>>>>> feature-branch

首先检查合并的共同祖先。 然后比较每一侧以确定每个分支的意图。 可以看到 HEAD 将 EmailMessage 更改为 TextMessage。 它的目的是改变用于 TextMessage 的类,传递相同的参数。 您还可以看到 feature-branch 的意图是为 :include_timestamp 选项传递 false 而不是 true。 要合并这些更改,请结合两者的意图:

TextMessage.send(:include_timestamp => false)

一般来说:

  1. 比较每个分支的共同祖先,并确定哪个分支的变化最简单
  2. 将这个简单的更改应用到另一个分支的代码版本,以便它包含更简单和更复杂的更改
  3. 删除所有冲突代码部分,而不是您刚刚将更改合并到一起的部分

替代:通过手动应用分支的更改来解决

最后,即使使用 diff3,一些冲突也很难理解。 这种情况尤其发生在 diff 发现语义上不常见的共同行时(例如,两个分支碰巧在同一个地方有一个空行!)。 例如,一个分支更改了类主体的缩进或重新排序类似的方法。 在这些情况下,更好的解决策略可以是检查合并任一侧的更改并手动将差异应用到另一个文件。

让我们看看在合并origin/feature1 lib/message.rb发生冲突的情况下,我们如何解决冲突。

  1. 确定我们当前签出的分支( HEAD--ours )还是我们正在合并的分支( origin/feature1--theirs )是一个更简单的应用更改。 使用带三点的 diff ( git diff a...b ) 显示自b上次与a发散以来发生的变化,或者换句话说,将 a 和 b 的共同祖先与 b 进行比较。

     git diff HEAD...origin/feature1 -- lib/message.rb # show the change in feature1 git diff origin/feature1...HEAD -- lib/message.rb # show the change in our branch
  2. 查看更复杂的文件版本。 这将删除所有冲突标记并使用您选择的一侧。

     git checkout --ours -- lib/message.rb # if our branch's change is more complicated git checkout --theirs -- lib/message.rb # if origin/feature1's change is more complicated
  3. 签出复杂的更改后,拉起较简单更改的差异(请参阅步骤 1)。 将此差异的每个更改应用到冲突文件。

“冲突”意味着“相同内容的平行演变”。 因此,如果它在合并过程中“一败涂地”,则意味着您对同一组文件进行了大量演变。

变基比合并更好的原因是:

  • 你用主人之一重写你的本地提交历史(然后重新应用你的工作,然后解决任何冲突)
  • 最终的合并肯定是“快进”的,因为它将拥有 master 的所有提交历史,以及您要重新应用的更改。

我确认在这种情况下正确的工作流程(公共文件集的演变)首先rebase ,然后是 merge

但是,这意味着,如果您推送本地分支(出于备份原因),则其他任何人都不应该拉取(或至少使用)该分支(因为提交历史将被连续的 rebase 重写)。


关于这个话题(rebase 然后合并工作流程), barraponto在评论中提到了两个有趣的帖子,都来自randyfay.com

使用这种技术,您的工作始终在公共分支之上,就像与当前HEAD的补丁一样。

(类似的技术存在于 bazaar

在我的工作流程中,我尽可能多地变基(并且我尝试经常这样做。不让差异累积会大大减少分支之间冲突的数量和严重程度)。

然而,即使在大部分基于 rebase 的工作流中,也有合并的地方。

回想一下,merge 实际上创建了一个有两个父节点的节点。 现在考虑以下情况:我有两个独立的功能分支 A 和 B,现在想在依赖于 A 和 B 的功能分支 C 上开发东西,而 A 和 B 正在接受审查。

然后我要做的是:

  1. 在 A 之上创建(并结帐)分支 C。
  2. 与 B 合并

现在分支 C 包含来自 A 和 B 的更改,我可以继续开发它。 如果我对 A 进行任何更改,那么我会按以下方式重建分支图:

  1. 在 A 的新顶部创建分支 T
  2. 将 T 与 B 合并
  3. 将 C 变基到 T
  4. 删除分支 T

这样我实际上可以维护任意的分支图,但是做一些比上面描述的情况更复杂的事情已经太复杂了,因为没有自动工具来在父更改时进行重新定位。

不要在几乎任何情况下使用 git push origin --mirror 。

它不会询问您是否确定要执行此操作,您最好确定一下,因为它会删除所有不在本地机器上的远程分支。

http://twitter.com/dysinger/status/1273652486

在你的情况下,我认为你的伴侣是正确的。 变基的好处在于,对于局外人来说,您的更改看起来就像它们全部以干净的顺序发生。 这意味着

  • 您的更改很容易查看
  • 你可以继续做出漂亮的小提交,但你可以一次性公开这些提交的集合(通过合并到 master 中)
  • 当您查看公共主分支时,您会看到不同开发人员针对不同功能的不同系列提交,但它们不会全部混合

为了备份,您仍然可以继续将您的私有开发分支推送到远程存储库,但其他人不应将其视为“公共”分支,因为您将进行变基。 顺便说一句,执行此操作的一个简单命令是git push --mirror origin

使用 Git 打包软件一文很好地解释了合并与变基的权衡。 这是一个稍微不同的上下文,但主体是相同的——它基本上归结为您的分支是公共的还是私有的,以及您计划如何将它们集成到主线中。

看了你的解释后,我有一个问题:难道你从来没有做过

git checkout master
git pull origin
git checkout my_new_feature

在您的功能分支中执行“git rebase/merge master”之前?

因为您的主分支不会从您朋友的存储库自动更新。 你必须用git pull origin来做到这一点。 也就是说,也许你总是从一个永不改变的本地 master 分支变基? 然后是推送时间,您正在推送一个存储库,该存储库具有您从未见过的(本地)提交,因此推送失败。

无论如何,我在最近的一个分支上遵循我的工作流程,当我试图将它合并回 master 时,一切都变得糟糕透了。 与本应无关紧要的事情发生了大量冲突。 这些冲突对我来说毫无意义。 我花了一天时间整理好一切,最终以强制推送到远程 master 达到高潮,因为我的本地 master 已经解决了所有冲突,但是远程 master 仍然不高兴。

在您的合作伙伴和您建议的工作流程中,您都不应该遇到没有意义的冲突。 即使您有,如果您遵循建议的工作流程,那么在解决问题后,不应需要“强制”推送。 这表明您实际上并没有合并您要推送的分支,而是不得不推送一个不是远程提示的后代的分支。

我认为你需要仔细看看发生了什么。 其他人是否可以(有意或无意)在您创建本地分支和您尝试将其合并回本地分支之间重绕远程主分支?

与许多其他版本控制系统相比,我发现使用 Git 涉及的工具较少,并允许您开始处理对源流至关重要的问题。 Git 不会执行魔术,因此冲突的更改会导致冲突,但它应该通过跟踪提交的出身来使写入操作变得容易。

“即使你是一个只有几个分支的单一开发者,养成正确使用 rebase 和 merge 的习惯也是值得的。基本的工作模式将如下所示:

  • 从现有分支 A 创建新分支 B

  • 在分支 B 上添加/提交更改

  • 来自分支 A 的 Rebase 更新

  • 将分支 B 的更改合并到分支 A"

https://www.atlassian.com/git/tutorials/merging-vs-rebasing/

根据我的观察, git merge 即使在合并后也倾向于保持分支分开,而 rebase then merge 将其合并为一个分支。 后者更简洁,而在前者中,即使在合并后也更容易找出哪些提交属于哪个分支。

Git 没有“正确”的工作流程。 使用任何漂浮你的船的东西。 但是,如果您在合并分支时经常遇到冲突,也许您应该与其他开发人员更好地协调您的工作? 听起来你们两个一直在编辑相同的文件。 另外,注意空格和颠覆关键字(即“$Id$”等)。

我只使用 rebase 工作流,因为它在视觉上更清晰(不仅在 GitKraken 中,而且在 Intellij 和gitk ,但我最推荐第一个):你有一个分支,它起源于 master,它回到主人。 当图表干净漂亮时,你就会知道没有什么是地狱,永远

在此处输入图片说明

我的工作流程几乎与你相同,但只有一个小的差异:我squash之前提交到一个在我的本地分支rebase我的分支上的最新变化master ,这是因为:

rebase基于每次提交工作

这意味着,如果您有 15 次提交更改了与master相同的行,那么如果您不压缩,则必须检查 15 次,但重要的是最终结果,对吗?

所以,整个工作流程是:

  1. Checkout to master和 pull 以确保您拥有最新版本

  2. 从那里,创建一个新分支

  3. 在那里做你的工作,你可以自由提交几次,然后推送到远程,不用担心,因为它是你的分支。

  4. 如果有人告诉你,“嘿,我的 PR/MR 被批准了,现在它合并到 master 了”,你可以 fetch 他们/pull 他们。 您可以随时执行此操作,也可以在步骤 6 中执行此操作。

  5. 完成所有工作后,提交它们,如果您有多次提交,则压缩它们(它们都是您的工作,您更改一行代码多少次无关紧要;唯一重要的是最终版本)。 推不推,无所谓。

  6. 结帐到master ,再次pull以确保您在本地拥有最新的master 您的图表应该类似于:

在此处输入图片说明

如您所见,您在本地分支上,这源于master上的过时状态,而master (本地和远程)随着您同事的变化而前进。

  1. 结帐回到您的分支,并重新设置为 master。 您现在将只有一次提交,因此您只需解决一次冲突。(在 GitKraken 中,您只需将您的分支拖到master并选择“Rebase”;我喜欢它的另一个原因。)在那之后,您将就像:

在此处输入图片说明

  1. 所以现在,您拥有最新master上的所有更改,以及您的分支上的更改。 您现在可以推送到您的遥控器,如果您之前推送过,则必须强制推送 Git 会告诉你,你不能简单地快进。 这是正常的,因为 rebase,你已经改变了你的分支的起点。 但你不应该害怕:明智地使用力量,但不要害怕 最后,远程也是你的分支,所以即使你做错了,你也不会影响master

  2. 创建 PR/MR 并等待它被批准,这样master就会有你的贡献。 恭喜! 因此,您现在可以结帐到master ,拉取更改并删除本地分支以清理图表 远程分支也应该被删除,如果你在将它合并到 master 时没有这样做。

最后的图表又干净又清晰:

在此处输入图片说明

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM