简体   繁体   English

为什么git pull origin development不进行-rebase会导致冲突?

[英]Why does git pull origin develop --rebase cause conflict when git pull origin develop doesn't?

Now normally I use 现在我通常使用

git pull origin develop

into get the latest updates from the develop branch. 从develop分支获取最新更新。 Recently, my team has been transitioning into using rebase instead of merging so I'm a bit confused on some stuff. 最近,我的团队一直在过渡到使用rebase而不是合并,因此我对某些内容有些困惑。 Before my workflow is pretty straight forward. 在我的工作流程很简单之前。 I would first checkout into the develop branch and use 我先结帐到开发分支并使用

git checkout -b feature/foo

I would then make my changes, commit and then push them. 然后,我将进行更改,提交并推送。 Usually the develop branch would have some changes made thus, I would use 通常,develop分支会进行一些更改,因此,我将使用

 git pull origin develop

to get the latest changes and have conflicts only if other people modified the same file. 以获得最新的更改,并且只有在其他人修改了同一文件的情况下才有冲突。 However, when I use 但是,当我使用

git pull origin develop --rebase

I notice that I would have conflicts with my own branch even though I'm the only person who has modified it. 我注意到即使我是唯一对其进行修改的人,我也会与自己的分支机构发生冲突。 Is there a particular reason for this? 是否有特定原因? Is there a way to avoid these merge conflict that I have with my own branch? 有没有办法避免我与自己的分支机构发生合并冲突?

First, let's note that git pull mainly consists of running two Git commands. 首先,让我们注意git pull主要由运行两个Git命令组成。 This means it's meant to be a convenience operation, to let you type git pull instead of git fetch enter git ..... . 这意味着它是一种方便的操作,让您键入git pull而不是git fetch 输入 git ..... The first command is always git fetch , and the second is your choice: it defaults to git merge , but you can choose git rebase . 第一个命令始终是git fetch ,第二个命令是您的选择:默认为git merge ,但您可以选择git rebase It takes almost as much typing to do the one command as the two, when you want to rebase, so it's not really very convenient after all, and I suggest using the separate git fetch and second command, at least until you're very familiar with Git. 当您想重新设置基数时,执行一个命令几乎要花费与执行两个命令相同的键入操作,因此毕竟并不是很方便,我建议使用单独的git fetch和second命令,至少直到您非常熟悉为止与Git。 1 1

So your question really resolves to a simpler one: Why does rebase sometimes have conflicts that merge doesn't have? 因此,您的问题确实解决了一个简单的问题: 为什么变基有时会出现合并所没有的冲突? And there's an answer to that, which is actually fairly simple: Rebase is mainly just repeated cherry-picking, and cherry-picking is a form of merging . 答案很简单,实际上很简单: Rebase主要是重复的摘樱桃,而摘樱桃是合并的一种形式 So when you merge, you have one place where you can get conflicts. 因此,当您合并时,就有一个可以冲突的地方。 If you rebase ten commits, you have ten places where you can get conflicts. 如果以十次提交为基准,则有十个地方可能会发生冲突。 The conflicts themselves can be different as well, but the sheer scale of opportunity is the major factor here. 冲突本身也可以不同,但​​是机会的绝对规模是这里的主要因素。


1 In repositories with submodules, git pull can recurse into the submodules, in which case it's more than two commands and its convenience aspect becomes significant. 1在具有子模块的存储库中, git pull可以递归到子模块中,在这种情况下,它是两个以上的命令,其便利性也变得很重要。 You can also configure git pull to run git rebase by default, making the convenience re-appear even without submodules. 您还可以 git pull 配置为默认情况下运行git rebase ,即使没有子模块,也能再次显示便利性。 I still encourage new users to use two separate commands, though—the syntax for git pull is a little weird and a little different from almost all other Git stuff, and it gets too easily confusing. 不过,我仍然鼓励新用户使用两个单独的命令git pull的语法有点奇怪,并且与几乎所有其他Git东西都有些不同,并且太容易混淆了。 There is too much magic assigned to pull, when actually all the magic is from the second command—and you need to learn merge to understand rebase. 所有魔术实际上来自第二个命令时,分配的魔术太多了,您需要学习合并以了解变基。


Merging 合并

Although the implementation is full of tricky little twists and turns, the idea behind merging is simple. 尽管该实现充满了棘手的小曲折,但合并背后的想法很简单。 When we ask Git to merge, we have "our work" and "their work". 当我们要求Git合并时,我们有“我们的工作”和“他们的工作”。 Git needs to figure out what we changed, what they changed, and combine those changes. Git的需要找出我们改变了他们改变什么,并结合这些变化。

In order to do that, Git needs to find a common starting point. 为此,Git需要找到一个共同的起点。 A commit isn't a set of changes at all: it's actually a snapshot. 提交根本不是一组更改 :实际上是快照。 Git can show one of these snapshots as differences from its immediate predecessor, ie, extract both snapshots and see what's different. Git可以将其中一个快照显示为与其上一个快照的区别,即提取两个快照并查看有何不同。 So if we started from some commit with some hash ID B , and they also started from that same commit: 因此,如果我们从具有某些哈希ID B提交开始,并且它们也从同一提交开始:

          C--D   <-- our-branch (HEAD)
         /
...--A--B
         \
          E--F   <-- their-branch

then Git can compare the snapshot in B to our latest, D , and to their latest, F . 然后Git可以将B的快照与我们的最新D以及他们的最新F Whatever's different in B -vs- D is stuff we changed. B -vs- D的所有不同之处是我们已更改。 Whatever's different in B -vs- F is stuff they changed. B -vs- F的所有不同之处在于它们已更改。 Git then combines the changes, applies the combined changes to the snapshot from the merge base B , and commits the result, hooking it up with not one but two predecessors: GIT中然后组合的变化,适用于所述组合的变化,从合并基础快照B ,并提交结果,不是一个而是两个前辈钩住它:

          C--D
         /    \
...--A--B      G   <-- our-branch (HEAD)
         \    /
          E--F   <-- their-branch

To get there, Git has to run: 为了到达那里,Git必须运行:

  • git diff --find-renames hash-of-B hash-of-D (what we changed) git diff --find-renames hash-of-B hash-of-D (我们改变了)
  • git diff --find-renames hash-of-B hash-of-F (what they changed) git diff --find-renames hash-of-B hash-of-F (更改之处)

When Git goes to combine these two diffs, there can be places where we and they changed the same lines of the same file . 当Git组合这两个差异时,我们和他们可能会在某些地方更改同一文件相同行 If we didn't make the same change to those lines, Git will declare a conflict and stop the merge in the middle, not make commit G yet , and force us to clean up the mess and finish the merge to create G . 如果我们没有对这些线路进行相同的更改 ,Git会声明冲突,并在中间停止合并,不会使犯G 着呢 ,迫使我们收拾残局,并完成合并创建G

Cherry-picking 采摘樱桃

The idea behind cherry-pick is to copy a commit. Cherry-pick背后的想法是复制提交。 To copy a commit, we can have Git turn it into a set of changes: 要复制提交,我们可以让Git将其变成一组更改:

  • git diff --find-renames hash-of-parent hash-of-commit

We can then take these changes and hand-apply them somewhere else, ie, to some other commit. 然后,我们可以进行这些更改并将其手动应用到其他地方,即进行其他提交。 For instance, if we have: 例如,如果我们有:

          C--D   <-- our-branch (HEAD)
         /
...--A--B
         \
          E--F   <-- their-branch

and we like what they did in F , but don't want E itself yet, we can diff E vs F , to see what they did. 并且我们喜欢他们在F所做的事情,但是我们还不想要E本身,我们可以将EF进行比较,以了解他们的所作所为。 We can use that to try to make the same change to our snapshot in D . 我们可以使用它来尝试对D的快照进行相同的更改。 Then we make ourselves a new commit—let's call it F' to mean copy of F : 然后,我们让自己一个新的提交,我们称之为F'是指的副本F

          C--D--F'  <-- our-branch (HEAD)
         /
...--A--B
         \
          E--F   <-- their-branch

But if we made significant changes in C , or they made significant changes in E , it may be hard to get the changes they made from E -to- F to line up with what's in our snapshot in D . 但是,如果我们在C进行了重大更改,或者在E进行了重大更改,则可能很难使他们从E F才能与D中快照中的内容保持一致。 For Git to help us out, and do this copying automatically , Git would like to know: what's different between E and D ? 为了让Git帮助我们,并自动执行此复制,Git想知道: ED什么区别? That is, Git wants to run: 也就是说,Git要运行:

  • git diff --find-renames hash-of-E hash-of-D (what we have in C , vs E ) git diff --find-renames hash-of-E hash-of-D我们有C ,VS E
  • git diff --find-renames hash-of-E hash-of-F (what they changed in F ) git diff --find-renames hash-of-E hash-of-F (它们在F变化)

But wait, we just saw this same pattern above, during git merge ! 但是,等等,在git merge期间,我们刚刚在上面看到了相同的模式! And in fact, that's precisely what Git does here: it uses the same code as git merge , it just forces the merge base—which would be B for a regular merge—to be commit E , the parent of commit F that we're cherry-picking. 实际上,这正是Git所做的:它使用 git merge 相同的代码 ,只是强制将合并基础(常规合并为B成为commit E ,即我们提交的F的父对象采摘樱桃。 Git now combines our changes with their changes, applying the combined set of changes to the snapshot in the base—in E —and making the final F' commit on its own, but this time as a regular commit. Git现在将更改与更改结合在一起,将合并的更改集应用于基础快照(在E ,并自行进行最终的F'提交,但这一次是常规提交。

The new commit re-uses the commit message from commit F itself too, so that the new commit F' (which has some new hash ID, different from F 's) resembles F a lot: git show probably shows the same, or a very similar, diff listing for each, and of course the same commit log message. 新的提交也重用了来自提交F本身的提交消息 ,因此新的提交F' (具有一些新的哈希ID,与F的不同)非常类似于Fgit show可能显示相同的内容,或者非常相似,每个列表都列出差异,当然,提交日志消息也相同。

As with git merge , this merging process—what I like to call merge as a verb —can go wrong. git merge ,这种合并过程(我想称谓合并为动词 )可能会出错。 If does go wrong, Git complains about the merge conflict, stops with the merge unfinished, and makes you clean up the mess and commit. 如果确实出错,Git会抱怨合并冲突,在合并未完成之前停止,并让您清理混乱并提交。 When you do commit, Git knows you're finishing up a git cherry-pick and copies the commit message for you at that point, to make F' . 当您提交时,Git知道您正在完成git cherry-pick并在那时为您复制提交消息,以制作F'

Rebase is repeated cherry-picking 重新设基是反复采摘樱桃

To do a git rebase target , Git: 要执行git rebase target ,Git:

  • lists the commits you have on your branch that are not reachable (a technical term: see Think Like (a) Git from target ; 列出你有你的分支不可达的提交(一个技术术语:看觉得像(A)的Git目标 ;
  • trims this list if appropriate—see below; 适当时修剪此列表-参见下文;
  • checks out commit target as a "detached HEAD"; 将提交目标检出为“分离的HEAD”;
  • repeatedly, one commit at a time, uses git cherry-pick to copy each commit that's in the list. 重复一次,一次提交一次,使用git cherry-pick复制列表中的每个提交。 2 2

Once all the to-be-copied commits have been copied successfully, Git moves the branch name to the end of the copied list. 成功复制所有待复制的提交后,Git 会将分支名称移至复制列表的末尾。

Suppose we start with a similar setup to before, though I'll list a few more commits here: 假设我们从与之前类似的设置开始,尽管我将在此处列出更多提交:

          C--D--E--F   <-- our-branch (HEAD)
         /
...--A--B
         \
          G--H   <-- their-branch

We run git rebase their-branch , so Git lists out the commits to copy: CDEF , in that order. 我们运行git rebase their-branch ,所以Git列出了要复制的提交: CDEF ,按该顺序。 Then Git checks out commit H as a "detached HEAD": 然后,Git将提交H检出为“分离的HEAD”:

          C--D--E--F   <-- our-branch
         /
...--A--B
         \
          G--H   <-- their-branch, HEAD

Now Git will cherry-pick C to copy it. 现在,Git将选择C复制它。 If that goes well: 如果一切顺利:

          C--D--E--F   <-- our-branch
         /
...--A--B
         \
          G--H   <-- their-branch
              \
               C'  <-- HEAD

Git repeats for D , E , and F . Git重复DEF Once it's done D and E we're in this state: 完成DE我们就处于这种状态:

          C--D--E--F   <-- our-branch
         /
...--A--B
         \
          G--H   <-- their-branch
              \
               C'-D'-E'  <-- HEAD

After Git finishes copying F to F' , the last step of rebase is to yank the name our-branch over to point to the final copied commit, and re-attach HEAD to it: 在Git完成将F复制到F' ,变基的最后一步是将名称our-branch转移到最后复制的提交,然后将HEAD重新附加到它:

          C--D--E--F   [abandoned]
         /
...--A--B
         \
          G--H   <-- their-branch
              \
               C'-D'-E'-F'  <-- our-branch (HEAD)

Each cherry-pick does one three-way merge, with the merge base of the operation being the parent of the commit being copied and the "ours" commit being the one on the detached HEAD —note that initially that's their commit H , and as we progress, it becomes "their commit H plus our work" over time. 每个cherry-pick进行一次三向合并,操作的合并基础是要复制的提交的父级,而“我们的”提交是分离的HEAD提交—请注意,最初是它们的提交H ,并且我们不断进步,随着时间的流逝,它成为“他们的承诺H加我们的工作”。 The "theirs" commit is, each time, our own commit. 每次,“他们”的提交都是我们自己的提交。 Each cherry-pick can have all the usual merge conflicts, though in most cases, most don't have any. 每个樱桃选择都可以具有所有通常的合并冲突,尽管在大多数情况下,大多数没有。

There are two cases in particular that are especially bad. 特别是有两种情况特别糟糕。 One of these, probably the most common, is when any of your own commits, in the list CDEF for instance, are themselves cherry-picks of something that was in the GH chain (which is often rather longer than just two commits)—or vice versa, eg, perhaps H is essentially D' . 其中一种可能是最常见的,例如,您自己的任何提交(例如CDEF列表中的)本身都是GH链中的某些内容的樱桃签(通常比两次提交要长);或者反之亦然,例如, H基本上是D'

If you, or they, were able to make that cherry-pick earlier easily, without conflicts, your copy probably looks almost exactly like, or even 100% exactly like, one of the GH chain. 如果您(或他们)能够轻松地,早日做出选择,而不会发生冲突,则您的副本可能看起来几乎完全像GH链之一,甚至100%完全像GH链之一。 If that's the case, Git can recognize that it is such a copy, and remove it from the "to be copied" list. 如果是这样,Git可以识别出它这样的副本,并将其从“要复制”列表中删除。 In our example here, if H is really D' , and Git can see that, Git will remove D from the to-be-copied list, and only copy CEF . 在这里的示例中,如果H确实是D' ,而Git可以看到,则Git将从要复制的列表中删除D ,仅复制CEF But if not—if, for instance, they had to change their copy of D a bunch to make H —then Git will try to copy D and these changes almost certainly will conflict with their modified H . 但是,如果不是这样(例如,如果他们不得不将D的副本更改为一堆以使H Git 尝试复制D并且这些更改几乎肯定与其修改后的H冲突。

If you merge rather than copying, you will compare B vs H (theirs) and B vs F (yours) and the chances of conflicts are perhaps reduced. 如果您合并而不是复制,则将BH (他们)和BF (您)进行比较,可能会减少冲突的机会。 Even if there are conflicts, they're probably more obvious and easier to resolve. 即使存在冲突,它们也可能更明显且更容易解决。 If the conflicts are because of an unnecessary copy, they tend, in my experience, to look trickier. 如果冲突是由于不必要的复制引起的,那么根据我的经验,它们看起来会更加棘手。

The other common problem case is when, in your CDEF chain, your last few commits were something you did specifically in order to make merging easier. 另一个常见的问题案例是,在您的CDEF链中,您的最后几次提交是您为了简化合并而专门进行的。 That is, someone may have said something like: we changed the foo subsystem, now you need a third parameter and you added the third parameter in F after cherry-picking the change in E . 也就是说,可能有人说过这样的话: 我们更改了foo子系统,现在您需要第三个参数,并且在选择了E的更改之后在F添加了第三个参数。 You'll get conflicts when copying C and D . 复制CD时会发生冲突。 You might skip copying E because it is a cherry-pick, and then copying F is unnecessary after you've fixed the conflicts in D and E , but that's two copies that require fixing, one that is automatically dropped, and one that requires your own, manual drop. 您可能会跳过复制E因为它一个小问题,在解决了DE的冲突之后,就不必复制F了,但这是需要修复的两份副本,一个会自动删除,而另一个则需要自己,手动放下。

So, in the end, git merge does one merge, but git rebase does many cherry-picks, each of which is—internally—a merge, and each of which can result in merge conflicts. 因此,最后, git merge一次合并,但是git rebase许多Cherry-pick,其中每一个都是内部的合并,并且每一个都会导致合并冲突。 It's not surprising that rebases get more conflicts! 变基得到更多冲突也就不足为奇了!


2 Technically, a plain (non-interactive) git rebase often doesn't use git cherry-pick . 2从技术上讲,纯(非交互式) git rebase通常使用git cherry-pick Instead, it uses, in effect, git format-patch ... | git am ... 实际上,它使用的是git format-patch ... | git am ... git format-patch ... | git am ... . git format-patch ... | git am ... Using git rebase -i always uses git cherry-pick , and git rebase -m forces a non-interactive git rebase to use git cherry-pick . 使用git rebase -i总是使用git cherry-pick ,而git rebase -m强制非交互式git rebase使用git cherry-pick The fact that plain rebase avoids it is mainly just a holdover from ancient (pre-2008-or-so, probably) Git, before cherry-pick was taught to do a proper three-way merge. 普通的变基避免了它的事实主要只是从古老的(大约在2008年前左右)Git保留下来的,然后才让Cherry-pick进行正确的三向合并。

The git am step uses -3 , so that if a patch fails, Git will "fall back" to a three-way merge. git am步骤使用-3 ,因此,如果补丁失败,Git将“回退”到三路合并。 The result is usually the same, but the format-patch-pipe-to-am method never finds renamed files. 结果通常是相同的,但是format-patch-pipe-to-am方法永远不会找到重命名的文件。 This makes the format-patch style faster , but not as good. 这使format-patch样式更快 ,但效果却不尽人意。

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

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