首页 文章

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

提问于
浏览
913

我和其他开发人员一起在一个项目上使用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应该让分支和合并超级简单,我只是没有看到它 .

Update 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

事实上,我们的工作流程略有不同,因为我们倾向于进行压缩合并而不是原始合并 . ( Note: This is controversial, see below. )这允许我们将整个功能分支转换为主服务器上的单个提交 . 然后我们删除我们的功能分支 . 这允许我们在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 - 正如几位评论者所指出的那样,壁球合并将丢弃你的特征分支上的所有历史记录 . 顾名思义,它将所有提交压缩成一个提交 . 对于小功能,这有意义,因为它将其压缩到单个包中 . 对于更大的功能,它可能不是一个好主意,特别是如果您的个人提交已经是原子的 . 这真的取决于个人喜好 .

Github and Bitbucket (others?) Pull Requests - 如果你're wondering how merge/rebase relates to Pull Requests, I recommend following all the above steps up until you'准备合并回主人 . 您只需接受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 . 如果你正在努力,只要坚持下去,最终你会看到隧道尽头的光线 .

10 回答

  • 347

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

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

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

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

  • 6

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

    • 您的更改很容易查看

    • 你可以继续做出很好的小提交但你可以一次性公开这些提交(通过合并到master中)

    • 当你看公共主分支时,你'll see different series of commits for different features by different developers but they won' t都是混合的

    为了备份,您仍然可以继续将私有开发分支推送到远程存储库,但是其他人不应将其视为"public"分支,因为您将进行变基 . BTW,执行此操作的简单命令是 git push --mirror origin .

    文章Packaging software using Git做了一个相当不错的工作,解释了合并与变基的权衡 . 这是一个有点不同的背景,但主体是相同的 - 它主要归结为您的分支机构是公共的还是私有的,以及您计划如何将它们集成到主线中 .

  • 14

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

  • 2

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

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

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

    我当时做的是以下内容:

    • 在A之上创建(和结帐)分支C.

    • 与B合并

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

    • 在A的新顶部创建分支T.

    • 与B合并T.

    • 将C转换为T

    • 删除分支T.

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

  • 12

    DO NOT use git push origin --mirror UNDER ALMOST ANY CIRCUMSTANCE.

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

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

  • 13

    “冲突”是指“相同内容的并行演变” . 因此,如果它在合并过程中“全部变成地狱”,则意味着您在同一组文件上进行了大规模的演变 .

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

    • 用一个master重写你的本地提交历史记录(然后重新申请你的工作,然后解决任何冲突)

    • 最终合并肯定会是一个"fast forward",因为它将拥有主的所有提交历史记录,加上只有你重新申请的更改 .

    我确认在这种情况下正确的工作流程(普通文件集的演变)是 rebase first, then merge .

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


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

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

    (类似技术exists for bazaar

  • 367

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

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

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

    • 来自分支A的Rebase更新

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

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

  • 21

    TL; DR

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


    Rebase工作流程并不能更好地解决冲突!

    为了清理历史,我非常专业 . 但是如果 I ever hit a conflict, I immediately abort the rebase and do a merge instead! 真的让我失望的是,人们建议将rebase工作流作为解决冲突的合并工作流程的更好替代方案(这正是这个问题的内容) .

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

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

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

    真实场景

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

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

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

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

    如果这是您首选的工作流程,您必须开玩笑吧 . 所需要的只是一个空白修复,与master上的一个更改冲突,每个提交都会发生冲突,必须解决 . 这是一个只有空白冲突的简单场景 . Heaven forbid you have a real conflict involving major code changes across files and have to resolve that multiple times.

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

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

    我认为我们都同意解决冲突可能很困难,而且有些人对此非常不满意 . 它很容易出错,这就是为什么git让它很容易撤消的原因!

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

    When you rebase 一个分支,在可能发生冲突解决错误的情况下,你只需要重做rebase * . 充其量,您必须返回并修改每个受影响的提交 . 不好玩 .

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

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

    摆脱冲突解决方案:使用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)
    

    一般来说:

    • 将公共祖先与每个分支进行比较,并确定哪个分支具有最简单的更改

    • 将该简单更改应用于其他分支的代码版本,以便它包含更简单和更复杂的更改

    • 删除冲突代码的所有部分,而不是刚刚将更改合并到的部分

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

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

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

    • 确定我们当前签出的分支( HEAD ,或 --ours )或我们正在合并的分支( origin/feature1 ,或 --theirs )是否是一个更简单的变更申请 . 使用具有三点的差异( git diff a...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
    
    • 查看更复杂的文件版本 . 这将删除所有冲突标记并使用您选择的一侧 .
    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
    
    • 检查出复杂的更改后,拉出更简单更改的差异(参见步骤1) . 将此差异中的每个更改应用于冲突文件 .
  • 7

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

    git checkout master
    git pull origin
    git checkout my_new_feature
    

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

    因为 your master分支赢得了't update automatically from your friend'的存储库 . 你必须使用 git pull origin 这样做 . 即也许你会永远改变从不改变的本地主分支?然后来推送时间,你正在推进一个存储库,它具有你从未见过的(本地)提交,因此推送失败 .

  • 30

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

相关问题