Git工作流程和rebase与合并问题

问题描述 投票:923回答:10

我和其他开发人员一起在一个项目上使用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

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

这是我开始的工作流程

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

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

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

我合并而不是rebase的原因是合并似乎是标准的,而rebase似乎是一个高级功能。我的直觉是,我正在尝试做的不是高级设置,所以不应该使用rebase。我甚至在Git上阅读了新的实用编程书,它们涵盖了广泛的合并,几乎没有提到rebase。

无论如何,我在最近的一个分支上关注我的工作流程,当我试图将它合并回主人时,一切都进入了地狱。对于应该不重要的事情存在大量冲突。冲突对我来说毫无意义。我花了一天的时间来解决所有事情,并最终被强制推向远程主人,因为我的当地主人解决了所有冲突,但是远程主人仍然不高兴。

这样的事情的“正确”工作流程是什么? 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上逻辑地构造我们的提交,即使它们在我们的分支上有点乱。所以,这就是我们所做的:

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

壁球合并争议 - 正如几位评论者所指出的那样,壁球合并将抛弃你的特征分支上的所有历史。顾名思义,它将所有提交压缩成一个提交。对于小功能,这有意义,因为它将其压缩到单个包中。对于更大的功能,它可能不是一个好主意,特别是如果您的个人提交已经是原子的。这真的取决于个人喜好。

Github和Bitbucket(其他人?)Pull Requests - 如果您想知道merge / rebase如何与Pull Requests相关,我建议您按照上述所有步骤进行操作,直到您准备好合并回master。您只需接受PR,而不是手动与git合并。请注意,这不会进行压缩合并(至少不会默认),但非压缩,非快进是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。如果你正在努力,只要坚持下去,最终你会看到隧道尽头的光线。

git version-control git-merge git-rebase
10个回答
356
投票

“冲突”是指“相同内容的平行演变”。因此,如果它在合并期间“完全地狱”,则意味着您在同一组文件上进行了大规模的演变。

因此,rebase比合并更好的原因是:

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

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

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


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

使用这种技术,你的工作总是在公共分支之上,就像一个与当前的HEAD最新的补丁。

(类似的技术exists for bazaar


2
投票

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


371
投票

TL;DR

git rebase工作流程不能保护您免受冲突解决能力差的人或习惯SVN工作流的人的攻击,如Avoiding Git Disasters: A Gory Story中建议的那样。它只会使冲突解决对他们来说更加乏味,并且使得从糟糕的冲突解决中恢复更加困难。相反,使用diff3,这样一开始就不那么困难了。


Rebase workflow is not better for conflict resolution!

为了清理历史,我非常专业。但是,如果我遇到冲突,我会立即中止rebase并进行合并!我真的让我感到害怕的是,人们推荐使用rebase工作流程作为解决冲突的合并工作流程的更好替代方案(这正是这个问题的内容)。

如果它在合并过程中“一切都变成地狱”,那么它将在变革期间“彻底地变成地狱”,而且可能还会更加地狱!原因如下:

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

当您进行rebase而不是merge时,对于同样的冲突,您必须执行冲突解决,直到您提交rebase的次数为止!

Real scenario

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

当我的分支与主人合并时,我有两个选择:

git merge:我遇到了冲突。我看到他们要掌握的变化并将其与我的分支(最终产品)合并。完成。

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

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

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

原因#2:使用rebase,没有撤消!

我想我们都同意解决冲突可能很困难,而且有些人也非常苛刻。它可能非常容易出错,这就是git让它易于撤消的原因!

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

当您重新分支一个分支时,如果冲突解决错误,可能会被搞砸。现在每个提交都包含错误的合并,你不能只重做rebase *。充其量,您必须返回并修改每个受影响的提交。不好玩。

在重组之后,无法确定最初提交的内容以及由于解决冲突而引入的内容。

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

Take the hell out of conflict resolution: use diff3

以此冲突为例:

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

看一下冲突,就不可能分辨出每个分支的变化或意图是什么。这是我认为解决冲突困难和困难的最大原因。

diff3来救援!

git config --global merge.conflictstyle diff3

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

<<<<<<< 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. 删除冲突代码的所有部分,而不是刚刚将更改合并到的部分

Alternate: Resolve by manually applying the branch's changes

最后,即使使用diff3,一些冲突也很难理解。特别是当diff找到非语义共同的共同行时(例如,两个分支碰巧在同一个地方都有一个空行!),就会发生这种情况。例如,一个分支更改类的主体的缩进或重新排序类似的方法。在这些情况下,更好的解决策略可以是从合并的任一侧检查更改并手动将diff应用于其他文件。

让我们看看如何在origin/feature1lib/message.rb冲突的情况下合并HEAD的情况下解决冲突。

  1. 决定我们目前检出的分支(--oursorigin/feature1)或我们合并的分支(--theirsgit diff a...b)是否适用更简单的变更。使用具有三点的差异(b)显示agit 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 的最后一次分歧以来发生的变化,或者换句话说,比较a和b的共同祖先与b。 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
  2. 查看更复杂的文件版本。这将删除所有冲突标记并使用您选择的一侧。 http://twitter.com/dysinger/status/1273652486
  3. 通过检查复杂的更改,拉出更简单更改的差异(参见步骤1)。将此差异中的每个更改应用于冲突文件。

30
投票

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

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

回想一下,merge实际上创建了一个有两个父节点的节点。现在考虑以下情况:我有两个独立的特征B和A,现在想要在特征分支C上开发东西,这取决于A和B,而A和B正在被审查。

我当时做的是以下内容:

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

现在分支C包括A和B的变化,我可以继续开发它。如果我对A做了任何更改,那么我将按以下方式重建分支图:

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

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


21
投票

不要使用git push origin --mirror几乎任何环境。

它不会询问您是否确定要执行此操作,并且最好确定,因为它将擦除本地盒子上没有的所有远程分支。

git checkout master git pull origin git checkout my_new_feature


14
投票

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

git pull origin

在功能分支中执行'git rebase / merge master'之前?

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


13
投票

在你的情况下,我认为你的伴侣是正确的。关于变基的好处是,对于局外人来说,你的变化看起来就像他们所有人都是以干净的顺序发生的。这意味着

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

为了备份,您仍然可以继续将私有开发分支推送到远程存储库,但是其他人不应将其视为“公共”分支,因为您将进行变基。顺便说一句,这样做的简单命令是Packaging software using Git

文章https://www.atlassian.com/git/tutorials/merging-vs-rebasing/做了一个相当不错的工作,解释了合并与变基的权衡。这是一个有点不同的背景,但主体是相同的 - 它主要归结为您的分支机构是公共的还是私有的,以及您计划如何将它们集成到主线中。


12
投票

无论如何,我在最近的一个分支上关注我的工作流程,当我试图将它合并回主人时,一切都进入了地狱。对于应该不重要的事情存在大量冲突。冲突对我来说毫无意义。我花了一天的时间来解决所有事情,并最终被强制推向远程主人,因为我的当地主人解决了所有冲突,但是远程主人仍然不高兴。

在您的合作伙伴和建议的工作流程中,您都不应该遇到没有意义的冲突。即使您有,如果您遵循建议的工作流程,那么在解决后不应该要求“强制”推送。它表明你实际上没有合并你所推动的分支,但是必须推动一个不是远程技巧后代的分支。

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

与许多其他版本控制系统相比,我发现使用Git可以减少对工具的影响,并允许您开始处理源流的基础问题。 Git不会执行魔法,因此冲突的更改会导致冲突,但它应该通过跟踪提交父级来轻松完成写入操作。


7
投票

从我观察到的情况来看,git merge趋向于在合并之后保持分支分离,而rebase然后merge将它组合成一个单独的分支。后者更清晰,而在前者中,即使在合并之后,也更容易找出哪些提交属于哪个分支。


6
投票

“即使你只是一个只有少数分支的开发人员,但是养成使用rebase并正确合并的习惯也是值得的。基本的工作模式如下:

  • 从现有分支A创建新分支B.
  • 在分支B上添加/提交更改
  • 来自分支A的重新更新
  • 将分支B的更改合并到分支A“

qazxswpoi

© www.soinside.com 2019 - 2024. All rights reserved.