Git 表示分支已合并,但更改显然不存在

Git says branch is merged, but changes are apparently not present

我曾将自己置于一种对我来说毫无意义的境地。我会尽力描述它。

我有一个开发分支,我已经通过 git checkout develpment && git merge master 将 master 合并到其中。我在这里没有遇到任何合并冲突。

有一个我感兴趣的特定提交,假设它是 abcd123。当我运行git branch --contains abcd123时,它报告developmentmaster都包含abcd123。当我执行 git log 时,它会在 developmentmaster.

的提交列表中显示 abcd123

git show abcd123 显示包含对两个文件的更改。但我似乎无法找到这些变化。当我查看文件时,在 developmentmaster 上都没有看到这些更改。当我检查 git log -- path/to/file1 时,我没有看到 abcd123git log -- path/to/file2 也是如此。

这是怎么回事?怎么会有commit存在,而修改显然不存在?

可能 abcd123 最初是在合并到 master 的另一个分支(development 以外的分支)中引入的。我不知道这是否会有所作为。

顺便说一下,当我尝试 git checkout master && git merge development(如上所示将 master 合并到 development 之后)时,我遇到了很多合并冲突,包括 file1 和 file2。因此,这似乎表明 master 实际上并未合并到 development 中——如果 git merge master 已经执行,git merge development 不应该成功吗?为了引起更多混淆,git branch --merged developmentmaster 已合并到 development。我想这与 git merge master ....

一致

非常感谢此时的任何建议。

编辑:在这一点上,问题似乎是由于合并失败或以某种方式搞砸了。如果有人还在阅读,我认为 torek 的回答的方向似乎是最有成果的。

这个答案很长,因为这里发生了很多事情。不过,TL;DR 摘要可能是您想要 --full-history.

这里有多个单独的问题需要解决:

  • 短语 "show changes",或者您在 git log -pgit show 中看到的内容,通常会导致人们在解释 Git 存储的内容时走上错误的道路。
  • git log 命令有时会对您撒谎(尤其是在合并时),纯粹是为了避免让您被无用的信息淹没。
  • git merge 的作用可能有点棘手。原则上很简单,但大多数人不会马上明白。

普通提交

让我们先看看 Git 最常见的普通提交。 Git 中的提交是 快照 (file-contents 的文件名)。您进行一次提交,然后更改一些内容和 git add 一两个更改的文件并进行第二次提交,第二次提交与第一次提交 所有相同的文件提交,除了被 git add.

覆盖的那些

值得将这些作为 Git 的 提交图 的一部分进行绘制。请注意,每个提交都有自己唯一的哈希 ID(impossible-to-remember 字符串之一,如 93aefcbadf00dcafedad),加上 的 ID parent(或之前的)提交。父提交哈希让 Git 以向后的方式将这些东西串在一起:

... <-E <-F <-G ...

其中每个大写字母代表一个哈希 ID,箭头表示每个提交 "points back" 到其父节点的想法。通常我们不需要绘制内部箭头(最后它们不是很有趣)所以我将它们绘制为:

...--E--F--G   <-- master

然而,namemaster 仍然值得一个箭头,因为这个箭头指向的提交会随着时间而改变。

如果我们选择像 G 这样的提交并使用 git log -pgit show 查看它 而没有 ,我们将看到每个文件的完整内容,与存储在提交中的完全一样。事实上,这就是我们使用 git checkout 检查时发生的情况:我们将所有文件完整提取到 work-tree 中,以便我们可以查看和处理它们。但是当我们用 git log -pgit show 查看它时,Git 并没有向我们展示所有内容;它只向我们展示了发生了什么变化

为此,Git 提取提交 其父提交,然后 运行 在对上添加一个大的 git diff .父 F 和子 G 之间的任何不同,都是 改变的 ,所以这就是 git log -pgit show 向您展示的内容.

合并提交

这对于普通的 single-parent 提交来说很好,但它不适用于 merge 提交。合并提交只是具有两个(或更多,但我们不会担心这种情况)父提交的任何提交。你通过成功的 git merge 得到这些,我们可能会像这样画出来。我们从两个分支开始(从一些 starting-point 分支出来):

       H--I   <-- development (HEAD)
      /
...--E--F--G   <-- master

然后我们 运行 git merge master.1 Git 现在尝试合并这两个分支。如果成功,它会进行一个新的提交,该提交有 两个 个父项:

       H--I--J   <-- development (HEAD)
      /     /
...--E--F--G   <-- master

名称 development 现在指向新的合并提交 J。这里括号(HEAD)表示这是我们的当前分支。这告诉我们哪个 name 被移动了:我们做了一个新的提交——包括任何新的合并提交——而 development 是更改为指向新的 branch-name提交。

如果我们不担心合并提交的内容(各种提交的文件)是如何确定的,这一切都非常简单。合并提交就像任何其他提交一样:它有一个完整的快照,一堆包含内容的文件。我们检查合并提交,这些内容像往常一样进入我们的 work-tree。

但是当我们去 查看 合并提交时......好吧,Git 通常会根据其父级区分提交。合并有 两个 个父项,每个分支一个。 Git 应该比较哪一个,以向您展示变化?

在这里,git loggit show 采用不同的方法。当您使用 git log 查看提交时,它默认显示 什么都没有 。不会选I-vs-J,也不会选G-vs-J!对于 git log -p.

,它什么也没显示

1在某些 Git 工作流中,不鼓励将 from master 合并到任何其他分支。不过它可以工作,既然你这样做了,让我们运行使用它。


查看合并提交

git show 命令做了一些不同且更好的事情。它运行s 两个 git diffs,一个用于I-vs-J,一个用于G-vs -J。然后它会尝试 合并 这两个差异,只向您显示 both 中发生的变化。也就是说,J 不同于I 但不是特别 有趣 的方式,Git 抑制了差异。 J 不同于 G 但不是特别 有趣的 方式,Git 也抑制了这种差异。这可能是最有用的模式,所以它就是 git show 显示的内容。它仍然很不完美,但是您在这里所做的一切都不是完美的。

您可以通过将 --cc 添加到 git log -p 选项来让 git log 做同样的事情。或者,您可以使用 -m (请注意 -m 的一个破折号,--cc 的两个破折号。

-m 选项告诉 Git 为了查看目的,它应该 拆分 合并。现在 Git 将 IJ 进行比较,以向您展示通过合并 G 带来的所有内容。然后 Git 将 GJ 的 split-off 额外版本进行比较,以向您展示通过合并 I 带来的所有内容。产生的差异通常非常大,但(或因为)它向您展示了 一切

有更多方法可以尝试查找某个文件发生了什么,但我们需要稍等片刻才能找到您的:

git log -- path/to/file1

命令。正如我们看到 git log skipping 合并一样,它可能会在这里跳过更多的东西(但有一些方法可以阻止 Git 这样做)。

实际进行合并:Git 如何构建合并的内容

让我们再看看那个 pre-merge 图:

       H--I   <-- development (HEAD)
      /
...--E--F--G   <-- master

此处,分支 development 上有两个不在分支 master 上的提交,还有两个 master 上不在 development 上的提交。提交 E(连同 all 之前的提交)在 both 分支上。不过,提交 E 是特殊的:它是两个分支上的 最近的 2 提交。提交 E 是 Git 调用的 合并基础

要执行合并,Git 有效地 运行 两个 git diff 命令:

git diff E I
git diff E G

首先产生一组对各种文件的修改,即"what we did on branch development"。实际上,如果将它们视为补丁,则它是 HI 的总和。第二个产生了一组——可能不同的——对各种文件的更改,"what we did on master",和以前一样,它实际上是 FG 作为补丁的总和。

Git 然后尝试合并这两个差异。无论它们之间是完全独立的,它都需要 both 组更改,将它们应用于提交 E 的内容,并将其用作结果。每当两个 change-sets 触及 same 文件中的同一行时,Git 会尝试查看它是否可以只复制该更改的一个副本。如果两者都修复了文件 README 第 33 行的一个单词的拼写,Git 可以仅获取 一份 的拼写修复。但是只要两个 change-sets 触及同一个文件的同一行,但进行 不同的 更改, Git 声明一个 "merge conflict",抛出其隐喻举起手来,让解决由此产生的混乱。

如果您(或进行合并的任何人)愿意,他们可以阻止 Git 提交合并结果,即使 Git 认为一切顺利:git merge --no-commit master 使 Git 合并所有内容后停止。此时,您可以在编辑器中打开 work-tree 个文件,更改 它们,写回,git add 更改的文件,以及 git commit merge 将不是来自三个输入(基础和两个 branch-tips)中的 any 的东西放入合并中。

无论如何,理解所有这些的关键是 merge base 提交的概念。如果您坐下来绘制提交图,合并基础通常非常明显,除非图失控(实际上这种情况经常发生)。您还可以让 Git 为您找到合并基础——或者合并基础,复数,在某些情况下:

git merge-base --all master development

这会打印出一个散列 ID。在我们这里的假设情况下,这将是提交 E 的哈希 ID。一旦你有了它,你可以手动 运行 git diff 来查看每个文件发生了什么。或者你可以 运行 一个 individual-file git diff:

git diff E development -- path/to/file1
git diff E master -- path/to/file1

请注意,如果您将名称 masterdevelopment 替换为 当前 之前的提交的哈希 ID =341=] 你做了 git merge,这甚至在 合并后也有效。这将告诉您 Git 认为它应该为 path/to/file1 组合什么。反过来,这会告诉您 Git 是否没有看到更改,或者是否进行了合并 overrode Git,或者是否错误地处理了冲突的合并。

合并后,后续合并将找到不同的合并基础:

       H--I--J----K   <-- development
      /     /
...--E--F--G--L--M   <-- master

我们现在查看两个分支提示并通过历史(向左方向)向后工作,遵循像 J 这样的合并的两个分支,以找到我们可以从 both 分支提示中获得第一个提交。从 K 开始,我们回到 J,然后回到 IG。从 M 开始,我们回到 L,然后到 G。我们发现 G 在两个分支上,所以提交 G 是新的合并基础。 Git 将 运行:

git diff G K
git diff G M

让两个 change-sets 应用于 merge-base 提交 G


2"Most recent"这里指的是commit graph顺序,而不是时间戳,虽然可能也是commit with两个分支上的最新时间戳。


Git 再次记录和简化谎言

我们已经看到 git log -p 直接跳过了合并提交。您根本看不到任何差异,就好像合并完全是魔术一样。但是当你 运行:

git log -- path/to/file1

还有其他更阴险的事情发生了。在 the (long) git log documentation under the section titled History Simplification.

中对此进行了描述,尽管相当不透明

在我们上面的例子中,假设 git logK 向后走。它找到 J,这是一个合并。然后它检查 IG,将每个文件与 J 进行比较,在排除所有文件后,除了您正在查看的一个文件。 也就是说,它是只是比较三个提交中的 path/to/file1

如果合并的一侧 没有 显示对 path/to/file1 的任何更改,这意味着合并结果 J 与输入没有区别(IG)。 Git 称之为 "TREESAME"。如果合并 J,在被剥离到这个文件之后,匹配 IG 类似 stripped-down,那么 J 是 TREESAME 到 IG(或两者)。在这种情况下,Git 选择 TREESAME parent(s) 或其中任何一个,并在该路径上 only 查找。假设它选择 I(沿着顶行)而不是 G(沿着底部)。

在我们的例子中,这意味着如果有人在合并过程中失误,丢失了本应从 F 进入 J 的更改,git log 永远不会显示它。 log命令查看K,然后查看J,然后查看但丢弃G,只查看I,然后查看H,然后是 E,然后是任何更早的提交。它从不查看提交 F !所以我们没有看到从 F.

path/to/file1 的变化

这里的逻辑很简单;我将直接引用 git log 文档,但添加一些重点:

[Default mode] Simplifies the history to the simplest history explaining the final state of the tree. Simplest because it prunes some side branches if the end result is the same ...

由于 F 中的更改已删除,Git 声明它们无关紧要。你不需要看到他们!我们将完全忽略合并的那一面!

你可以用--full-history彻底打败它。这告诉 Git not p运行e 合并的任一侧:它应该查看两个历史记录。这将为您找到提交 F。添加 -m -p 还应该找到 where 更改被删除,因为它会找到 all 涉及文件的提交:

git log -m -p --full-history -- path/to/file1

如果更改存在(在我们的示例中提交 F)并且不再存在,则只有两种方式丢失它们:

  • 它们在普通提交中被还原(使用 git revert,或手动)。即使没有 -m,您也会将其视为在历史记录中触及 path/to/file1 的提交; -p 会告诉你差异。

  • 或者,它们在合并过程中丢失了。即使没有 -m,您也会看到合并,但不确定进行合并的人是否在此处丢球;但是 -m -p 将显示两个父差异,包括 应该(但没有)进行更改的差异。