简体   繁体   English

为什么这个 GIT 合并不会导致冲突?

[英]Why does this GIT merge not result in conflicts?

We discovered a serious issue with GIT at work today and I would like to know whether this is a bug or by design, and also, how to resolve this issue.我们今天在工作中发现了 GIT 的一个严重问题,我想知道这是一个错误还是设计使然,以及如何解决这个问题。

Consider the following sequence of events:考虑以下事件序列:

  1. Create Branch "test1" on Master在 Master 上创建分支“test1”
  2. Switch to Master:切换到大师:
    1. Edit a file and commit the change as commit "X"编辑文件并将更改提交为提交“X”
  3. Switch to "test1":切换到“test1”:
    1. Cherry-pick commit "X" from Master樱桃选择从大师提交“X”
    2. Revert previous commit恢复之前的提交
    3. Merge Master into "test1"将 Master 合并到“test1”中

Result : No merge conflict is reported even though the file has been edited in both branches, and even worse, the revert in step 3.2 did not survive, even though it was the most recent commit.结果:即使文件已在两个分支中进行编辑,也不会报告合并冲突,更糟糕的是,即使是最近的提交,步骤 3.2 中的还原也没有保留。

This is a huge problem, as can be seen by the following recent example: A colleague of mine had commited similar changes to different branches, noticed that parts of these changes were malicious and manually reverted parts of them on one of the branches because of that.这是一个巨大的问题,从以下最近的示例中可以看出:我的一位同事对不同的分支进行了类似的更改,注意到这些更改的一部分是恶意的,因此在其中一个分支上手动还原了其中的一部分. After merging the branches, he was surprised to find out that his revert did not make it through the merge.合并分支后,他惊讶地发现他的恢复没有通过合并。

I uploaded a minimal example to google drive that demonstrates this issue.我上传了一个最小的例子到谷歌驱动器来演示这个问题。 You can merge Master into test1 or vice versa, to see for yourself.您可以将 Master 合并到 test1 中,反之亦然,以自己查看。

https://drive.google.com/drive/folders/19a-QPwOQKsn9PywUPd2DRnvUOml03nZ-?usp=sharing https://drive.google.com/drive/folders/19a-QPwOQKsn9PywUPd2DRnvUOml03nZ-?usp=sharing

If that's of any concern, I use TortoiseGIT 2.12.0.0 with Git for Windows 2.32.0.2.如果有任何问题,我将 TortoiseGIT 2.12.0.0 与 Git for Windows 2.32.0.2 一起使用。

You got that result because that is the correct result .你得到那个结果,因为那是正确的结果

Well, let's modify that statement: that is the correct results by the rules of Git merge .好吧,让我们修改该语句:这是 Git merge 规则的正确结果 (It's also correct by the rules of most other merge programs, as far as I know, but there are algorithms that would at least flag this for attention. Git does not use such an algorithm.) (据我所知,根据大多数其他合并程序的规则,这也是正确的,但有些算法至少会将此标记为注意。Git 不使用这样的算法。)

If Git's merge result is not the result you wanted, you have remedies: see below.如果 Git 的合并结果不是你想要的结果,你有补救措施:见下文。

When Git does a merge, Git pays attention to three snapshots:当 Git 进行合并时,Git 会注意三个快照:

  • One snapshot is the current snapshot, ie, the commit whose hash ID you will get if you run git rev-parse HEAD .一个快照是当前快照,即,如果您运行git rev-parse HEAD您将获得其哈希 ID 的提交。 If HEAD is attached to a branch name (as it normally is), that's the tip commit of the given branch.如果HEAD附加到分支名称(通常是这样),那就是给定分支的提示提交。

  • One snapshot is the one you name on the command line: git merge foo looks up foo to get a commit hash ID.一个快照是您在命令行中命名的快照: git merge foo查找foo以获取提交哈希 ID。

  • The third, and in many ways the most important, snapshot is the merge base .第三个,在许多方面也是最重要的,快照是合并基础 (Git numbers this one "#1", with HEAD / --ours being #2 and the other / --theirs being #3, internally, even though we have to locate the other two inputs before we can locate this merge base input.) The commit hash ID of the merge base is located through the commit graph. (Git 将这个编号为“#1”, HEAD / --ours为 #2,另一个 / --theirs为 #3,在内部,即使我们必须先定位其他两个输入才能定位此合并基础输入.) 合并库的提交哈希 ID 通过提交图定位。 In your case, it's the commit just before the commit you are calling commit X.在您的情况下,它是您正在调用 commit X 的提交之前的提交。

Let's draw these commits like this, with newer commits towards the right and single uppercase letters standing in for each actual commit hash ID:让我们像这样绘制这些提交,将更新的提交放在右侧,单个大写字母代表每个实际提交哈希 ID:

          X   <-- master
         /
...--G--H   <-- here's where both branches start diverging
         \
          X'-X"  <-- test1

Here, commit X has the changes you made on master ;在这里, commit X包含您在master所做的更改; commit X' has the same changes, and X" undoes those changes, so that the snapshot in X" exactly matches the snapshot in H .提交X'具有相同的更改,而X"撤消这些更改,以便X"中的快照与H中的快照完全匹配。

Git's merge algorithm consists of doing the following, given that you're on master (so that commit X is the current / HEAD commit) and are merging test1 (commit X" ): Git 的合并算法包括执行以下操作,假设您在master (因此提交X是当前/ HEAD提交)并且正在合并test1 (提交X" ):

  • Extract commit H as "stage 1".将提交H提取为“阶段 1”。

  • Extract commit X as "stage 2".将提交X提取为“阶段 2”。

  • Extract commit X" as "stage 3". (Note that we have just established that the contents of X" match those of H .)将提交X"提取为“阶段 3”。(请注意,我们刚刚确定X"的内容与H的内容匹配。)

  • For each file in the index / stage, that exists in all three stage slots:对于索引/阶段中的每个文件,都存在于所有三个阶段插槽中:

    • Compare the copy in slot 1 vs the copies in slots 2 and 3.比较插槽 1 中的副本与插槽 2 和 3 中的副本。
    • For any file identical in all 3 slots, the result is any version of the file (all three match).对于所有 3 个插槽中相同的任何文件,结果是该文件的任何版本(三个都匹配)。
    • For any file identical in slot 1 and either slot 2 or slot 3, the result is the non-identical version: take the changed file.对于插槽 1 和插槽 2 或插槽 3 中相同的任何文件,结果是不相同的版本:获取更改后的文件。
    • For any file where all three slots differ, run a diff algorithm to find individual changes, and combine them.对于所有三个插槽都不同的任何文件,请运行差异算法以查找各个更改,并将它们组合起来。 This step can have merge conflicts, but if not, the result is the correct merge.这一步可能有合并冲突,但如果没有,结果就是正确的合并。
  • For files that don't exist in all three slots (eg, where renames or copies may have happened), things get more complicated.对于不存在于所有三个插槽中的文件(例如,可能发生了重命名或复制的位置),事情变得更加复杂。 These can result in high level aka tree conflicts, which are treated as conflicts but don't show up as conflicted working-tree copies.这些可能会导致高级别的冲突,这些冲突被视为冲突,但不会显示为冲突的工作树副本。 But this case doesn't apply here so we get to ignore it.但是这种情况在这里并不适用,所以我们可以忽略它。

The meat of git merge is done, but there are now cleanup steps: git merge已经完成,但现在有清理步骤:

  • Files that are correctly merged are dropped to slot 0 (and written out to the working tree if/as needed).正确合并的文件被放到槽 0(如果/根据需要写出到工作树)。

  • Files that have merge conflicts are left in all three slots;有合并冲突的文件留在所有三个插槽中; the working tree gets Git's best-effort at merging, including conflict markers.工作树在合并时得到 Git 的最大努力,包括冲突标记。

  • In the case of conflicts, the merge now stops in the middle;在发生冲突的情况下,合并现在在中间停止; the user must complete it.用户必须完成它。 The working tree and index copies of each file exist for the user's use here.每个文件的工作树和索引副本都存在于此处供用户使用。

  • Otherwise, unless told to stop without committing, git merge finishes the merge on its own, typically by creating a new merge commit .否则,除非被告知停止而不提交,否则git merge自行完成合并,通常是通过创建一个新的合并提交

If this merge result is not what you wanted, your remedies include, but are not limited to, the following:如果此合并结果不是您想要的,您的补救措施包括但不限于以下内容:

  • Change the inputs (eg, by adding more commits to one or both branches).更改输入(例如,通过向一个或两个分支添加更多提交)。

  • Use git merge -n so that git merge stops before committing the merge result.使用git merge -n以便git merge在提交合并结果之前停止。 Use the index and working tree files—all staged for commit now, with just one version of each file in index slot #0—to produce the result you'd like instead.使用索引和工作树文件——现在都暂存提交,索引槽 #0 中每个文件只有一个版本——来产生你想要的结果。 Then, commit the result.然后,提交结果。 Note that this is known as an evil merge .请注意,这称为邪恶合并 There's nothing really wrong with it, but if you ever have Git repeat the merge—eg, using the fancy new git rebase --rebase-merges code—Git won't know to make the new merge an evil merge, so it's wise to mark this clearly with the commit message or something.它没有什么问题,但是如果你让 Git 重复合并——例如,使用花哨的新git rebase --rebase-merges代码git rebase --rebase-merges不会知道让合并成为一个邪恶的合并,所以明智的做法是用提交消息或其他东西清楚地标记这一点。

  • Do the merge, let it be "wrong", commit it (and maybe mark it, especially for later skip-during-bisect), then add a commit to fix things.进行合并,让它成为“错误的”,提交它(并且可能标记它,特别是对于稍后的skip-during-bisect),然后添加一个提交来修复问题。

In the situation you describe : this is the expected result of the git merge operation -- and torek's answer gives a detailed description of why.在您描述的情况下:这是git merge操作的预期结果——torek 的回答详细说明了原因。

On the git side of things :在 git 方面:

One issue with the workflow you mention is that git cherry-pick doesn't register a link to the original commit.您提到的工作流程的一个问题是git cherry-pick没有注册原始提交的链接。 You have to know that somehow, managing a cherry-pick is a bit like managing a "copy / paste" in code : if a bug is fixed on one side it should be manually ported to the other side.您必须知道,不知何故,管理cherry-pick有点像管理代码中的“复制/粘贴”:如果在一侧修复了错误,则应手动将其移植到另一侧。

The same goes for git rebase by the way : if you apply a rebase action which somehow keeps the original branch and also creates a new branch with some copied commits, that's also copy/pasting commits.顺便说一下, git rebase也是如此:如果您应用了一个 rebase 操作,该操作以某种方式保留了原始分支,并且还创建了一个带有一些复制提交的新分支,那么这也是复制/粘贴提交。

A more generic principle is :一个更通用的原则是:

" git merge succeeds without conflicts" doesn't mean "the resulting code is free of bugs". git merge没有冲突的情况下成功”并不意味着“生成的代码没有错误”。

You should always validate that the resulting code matches your expectations :您应该始终验证生成的代码是否符合您的期望:

  • the resulting code should be reviewed,应审查结果代码,
  • you should validate your code with other steps, like compilation/build scripts, unit tests and QA,您应该使用其他步骤验证您的代码,例如编译/构建脚本、单元测试和 QA,
  • in your example : if that issue is critical enough, you may for example add a linter (or any script that inspects the code) which checks that the incriminated code isn't present在您的示例中:如果该问题足够严重,您可以例如添加一个 linter(或任何检查代码的脚本)来检查被控代码不存在

This is especially true for your release branch ;对于您的发布分支尤其如此; on occasions, such as the one you describe (the developer merged towards a side branch), it's probably more about your team being aware of the potential traps -- and check the diff.有时,例如您描述的那个(开发人员合并到一个分支),这可能更多是关于您的团队意识到潜在的陷阱——并检查差异。

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

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