且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

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

更新时间:2023-11-07 23:16:04

首先,让我们注意git pull主要由运行两个Git命令组成.这意味着它是一种便捷的操作,让您键入git pull而不是git fetch enter git ......第一个命令始终为git fetch,第二个命令由您选择:默认为git merge,但是您可以选择git rebase.当您想重新设置基数时,执行一个命令与执行两个命令几乎一样多,因此毕竟并不是很方便,我建议使用单独的git fetch和第二个命令,至少要等到很久以后再使用熟悉Git. 1

First, let's note that git pull mainly consists of running two Git commands. This means it's meant to be a convenience operation, to let you type git pull instead of git fetchentergit ...... The first command is always git fetch, and the second is your choice: it defaults to git merge, but you can choose 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.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. 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 在具有子模块的存储库中,git pull可以递归到子模块中,在这种情况下,它有两个以上的命令,其便利性也变得很重要.您还可以默认配置配置 git pull以运行git rebase,即使没有子模块,也能再次显示便利性.不过,我仍然鼓励新用户使用两个单独的命令-git pull的语法有点奇怪,并且与几乎所有其他Git东西都有些不同,并且太容易混淆了.分配了太多的魔术,而实际上 all 魔术来自第二个命令-您需要学习合并才能了解变基.

1In 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. You can also configure git pull to run git rebase by default, making the convenience re-appear even without submodules. 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. 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.

尽管实现过程充满了棘手的小小的曲折,但合并背后的想法很简单.当我们要求Git合并时,我们有我们的工作"和他们的工作". Git需要弄清楚我们改变了什么,他们改变了什么,并结合这些改变.

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 needs to figure out what we changed, what they changed, and combine those changes.

为此,Git需要找到一个共同的起点.提交根本不是一组更改:它实际上是快照. Git可以将这些快照之一显示为与其直接前任的区别,即提取两个快照并查看有何不同.因此,如果我们从某个哈希ID为B的提交开始,而他们也从该相同提交开始:

In order to do that, Git needs to find a common starting point. 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, i.e., extract both snapshots and see what's different. So if we started from some commit with some hash ID B, and they also started from that same commit:

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

然后Git可以将B中的快照与最新的D以及最新的F进行比较. B -vs- D中的所有不同之处是我们更改了 的内容. B -vs- F中的所有不同之处是它们发生了变化.然后,Git合并更改,将 combined 更改应用于来自合并基础B的快照,并提交结果,将结果与一个或多个 挂钩.

then Git can compare the snapshot in B to our latest, D, and to their latest, F. Whatever's different in B-vs-D is stuff we changed. Whatever's different in B-vs-F is stuff they changed. 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:

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

要到达那里,Git必须运行:

To get there, Git has to run:

  • git diff --find-renames hash-of-B hash-of-D(我们所做的更改)
  • git diff --find-renames hash-of-B hash-of-F(更改之处)

当Git组合这两个差异时,我们和他们可能会在某些地方更改了相同文件相同行.如果我们没有对这些行进行相同的 change 更改,则Git将声明冲突并在中间停止合并,不进行commit G 并强制我们清理混乱并完成合并以创建G.

When Git goes to combine these two diffs, there can be places where we and they changed the same lines of the same file. 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.

cherry-pick背后的想法是复制一个提交.要复制提交,我们可以让Git将其变成一组更改:

The idea behind cherry-pick is to copy a commit. To copy a commit, we can have Git turn it into a set of changes:

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

然后我们可以进行这些更改并将其手动应用到其他地方,即进行其他一些提交.例如,如果我们有:

We can then take these changes and hand-apply them somewhere else, i.e., to some other commit. For instance, if we have:

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

,我们喜欢他们在F中所做的事情,但是我们还不希望E本身,我们可以将EF进行比较,以了解他们的所作所为.我们可以使用它来尝试对D中的快照进行相同的更改.然后,我们为自己做一个新的提交-将其称为F'表示F的副本:

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. We can use that to try to make the same change to our snapshot in D. Then we make ourselves a new commit—let's call it F' to mean copy of F:

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

但是,如果我们在C中进行了重大更改,或者在E中进行了重大更改,则可能很难使它们从E更改为F进行更改,以使其与其中的内容保持一致. D中的快照.为了让Git帮助我们并自动复制 ,Git想知道: ED有什么区别?也就是说,Git想要运行:

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. For Git to help us out, and do this copying automatically, Git would like to know: what's different between E and D? That is, Git wants to run:

  • git diff --find-renames hash-of-E hash-of-D(CE相比)
  • git diff --find-renames hash-of-E hash-of-F(它们在F中的更改)
  • 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-F (what they changed in F)

但是,等等,在git merge期间,我们刚刚在上面看到了相同的模式!实际上,这正是Git所做的:它使用与git merge相同的相同的代码,它只是强制合并基础(对于常规合并,该基础为B)要提交E,我们正在挑选的提交F的父级.现在,Git将我们的更改与更改进行组合,将组合的更改集应用于基础中的快照(在E中),并最终进行最终的F'提交,但这一次是常规提交.

But wait, we just saw this same pattern above, during 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 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.

新的提交也重用了来自提交F本身的提交 message ,因此新的提交F'(具有一些新的哈希ID,与F的不同) )非常类似于F:git show可能会为每个列表显示相同或非常相似的差异列表,当然还会显示相同的提交日志消息.

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.

git merge一样,此合并过程(我想称为动词合并)可能会出错.如果确实出错,Git会抱怨合并冲突,停止合并未完成,然后使您清理混乱并提交.当您提交时,Git知道您正在完成git cherry-pick并在此时为您复制提交消息,以制作F'.

As with git merge, this merging process—what I like to call merge as a verb—can go wrong. If does go wrong, Git complains about the merge conflict, stops with the merge unfinished, and makes you clean up the mess and commit. 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 rebase target,请执行Git:

To do a git rebase target, Git:

  • 列出您在分支机构中没有 reachable 的提交(技术术语:请参见 target 想到(a)Git
  • 在适当时修剪此列表-参见下文;
  • 将提交 target 检出为分离的HEAD";
  • 反复地,一次提交一次,使用git cherry-pick复制列表中的每个提交. 2
  • lists the commits you have on your branch that are not reachable (a technical term: see Think Like (a) Git from target;
  • trims this list if appropriate—see below;
  • checks out commit target as a "detached HEAD";
  • repeatedly, one commit at a time, uses git cherry-pick to copy each commit that's in the list.2

所有要复制的提交成功复制后,Git 将分支名称移动到复制列表的末尾.

Once all the to-be-copied commits have been copied successfully, Git moves the branch name to the end of the copied list.

假设我们从与之前类似的设置开始,尽管我将在此处列出更多提交:

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

我们运行git rebase their-branch,因此Git按此顺序列出要复制的提交:C-D-E-F.然后,Git将提交H检出为分离的HEAD":

We run git rebase their-branch, so Git lists out the commits to copy: C-D-E-F, in that order. Then Git checks out commit H as a "detached HEAD":

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

现在,Git将选择C进行复制.如果一切顺利:

Now Git will cherry-pick C to copy it. If that goes well:

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

Git对DEF重复.一旦完成DE,我们就处于这种状态:

Git repeats for D, E, and F. Once it's done D and E we're in this state:

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

Git完成将F复制到F'后,重新设置基准的最后一步是将名称our-branch移至指向最终复制的提交,然后将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:

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

每个cherry-pick进行一次三向合并,操作的合并基础是要复制的提交的父级,而我们的"提交是分离的HEAD上的提交.请注意,最初是他们的提交H,随着时间的推移,它变成他们的提交H加上我们的工作".每次,他们"的提交都是我们自己的提交.每个樱桃选择都可以具有所有通常的合并冲突,尽管在大多数情况下,大多数没有任何冲突.

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. 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.

特别有两种情况特别糟糕.其中一种可能是最常见的,例如,您自己的任何提交(例如,在C-D-E-F列表中)本身都是G-H链中某些内容的樱桃签(通常长于两个)提交),反之亦然,例如H本质上是D'.

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 C-D-E-F for instance, are themselves cherry-picks of something that was in the G-H chain (which is often rather longer than just two commits)—or vice versa, e.g., perhaps H is essentially D'.

如果您(或他们)能够轻松地较早地进行选择,而不会发生冲突,则您的副本可能看起来几乎与G-H链之一完全相同,甚至100%完全相同.如果是这样,Git可以识别出它这样的副本,并将其从要复制"列表中删除.在这里的示例中,如果H确实是D',并且Git可以看到,则Git将从要复制的列表中删除D,仅复制C-E-F.但是如果不是这样的话(例如,如果他们不得不更改一堆D的副本以制作H),那么Git 尝试复制D而且这些更改几乎可以肯定 与修改后的H冲突.

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 G-H chain. If that's the case, Git can recognize that it is such a copy, and remove it from the "to be copied" list. 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 C-E-F. 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.

如果合并而不是复制,则将比较BH(它们)和BF(您),并且可能减少冲突的可能性.即使存在冲突,它们也可能更明显且更容易解决.如果冲突是由于不必要的复制引起的,那么根据我的经验,它们往往看起来更棘手.

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. 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.

另一个常见的问题情况是,在您的C-D-E-F链中,您的最后几次提交是您为简化合并而专门进行的.也就是说,可能有人说过这样的话:我们更改了foo子系统,现在您需要第三个参数,并且在E中选择了更改后在F中添加了第三个参数.复制CD时会发生冲突.您可能会跳过复制E的原因,因为它樱桃采摘,然后在解决了DE中的冲突之后就不必复制F了,但这是两个副本需要修复的工具,一种是自动删除的工具,另一种是需要您自己手动删除的工具.

The other common problem case is when, in your C-D-E-F chain, your last few commits were something you did specifically in order to make merging easier. 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. You'll get conflicts when copying C and D. 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.

因此,最后,git merge进行一次合并,但git rebase进行许多樱桃小节,每个-在内部是一次合并,并且每一次可能导致合并冲突.重新部署更多的冲突也就不足为奇了!

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. It's not surprising that rebases get more conflicts!

2 从技术上讲,普通的(非交互式)git rebase通常使用git cherry-pick.实际上,它使用git format-patch ... | git am ....使用git rebase -i始终使用git cherry-pick,并且git rebase -m强制非交互式git rebase使用git cherry-pick.单纯的变基避免了这一事实,这主要只是从古老的Git(大概在2008年前左右)保留下来的,然后才让Cherry-pick进行正确的三向合并.

2Technically, a plain (non-interactive) git rebase often doesn't use git cherry-pick. Instead, it uses, in effect, 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. 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.

git am步骤使用-3,因此,如果补丁失败,Git将回退"到三路合并.结果通常是相同的,但是format-patch-pipe-to-am方法永远不会找到重命名的文件.这样会使格式补丁样式更快,但效果却不尽人意.

The git am step uses -3, so that if a patch fails, Git will "fall back" to a three-way merge. The result is usually the same, but the format-patch-pipe-to-am method never finds renamed files. This makes the format-patch style faster, but not as good.