两个分支之间的 git diff 与合并期间的更改不匹配

mug*_*tsu 1 git git-merge

我有两个分支,master 和 feature。如果我做:

git diff --name-only master..feature
Run Code Online (Sandbox Code Playgroud)

我得到一长串文件,其中一些是源代码,因此不被 .gitignore 排除

但是,当我尝试将功能合并到主版本中时:

git checkout master
git merge feature
Run Code Online (Sandbox Code Playgroud)

在合并过程中,我只在 master 中更改了一个文件。

为什么会出现这种情况?

另一个有趣的事情是,如果我尝试相反的操作并将 master 合并到功能中,则在功能分支中创建的文件将被删除。

我该如何解决此问题并避免将来出现此问题?

tor*_*rek 8

这不是一个错误。

\n

考虑以下简单示例。假设有一个文件名为example.txt. 在分支 X 中,内容如下:

\n
This is\nquite\na file.\n
Run Code Online (Sandbox Code Playgroud)\n

在分支 Y 中,内容如下:

\n
This is\nnot\na file.\n
Run Code Online (Sandbox Code Playgroud)\n

合并分支 X 和 Y 的结果应该是什么?具体来说,您希望名为 的文件中出现哪些内容example.txt

\n

我没有向您提供哪些信息(如果有的话)?在回答这个问题之前,您还需要了解什么?

\n

(在继续阅读之前尝试找出答案。)

\n

Git 是关于提交,而不是文件

\n

在继续之前,让我们注意一下,在 Git 中,您处理的存储单元是提交而不是文件。提交确实包含文件,但这里的总体思想是它是一个包交易:提交具有所有文件的完整快照。如果我们进行一些开始提交:

\n
git checkout somebranch\n
Run Code Online (Sandbox Code Playgroud)\n

将一个大文件拆分bigfile.py为两个较小的文件,small1.py然后完全删除small2.py然后提交,新提交与旧提交相比缺少添加了两个较小的文件。当我们签出旧的提交时,我们只有三个文件之一\xe2\x80\x94(大的\xe2\x80\x94),而当我们签出新的提交时,我们只有三个文件中的两个。这是一揽子交易:您可以选择包含一个文件的提交,或包含两个文件的提交,但您永远不会同时获得大文件和其中一个小文件,或所有三个文件,或其他一些组合。 bigfile.py bigfile.py

\n

尽管如此,提交仍然包含文件,当我们稍后进行合并时,这将很重要。但是,除了包含文件\xe2\x80\x94之外,这是它们的主要数据:每个文件的快照(按照您进行提交时的显示方式)\xe2\x80\x94每个提交都包含一些元数据,或有关该文件的信息犯罪。这包括您在输出中看到的内容git log:例如,提交者的姓名和电子邮件地址,以及日期和时间戳。1

\n

在所有这些元数据中,Git 在每次提交中存储一些早期提交的原始哈希 ID 。大多数提交只存储一个较早的提交哈希 ID。这些哈希 ID 也是提交的“真实名称”:它们是 Git 实际查找每个提交的方式。提交存储在一个大的键值数据库中,提交的哈希ID是键,提交的内容是值。

\n

每个提交都存储一个提交的哈希 ID,我们最终得到一个很好的简单线性提交链。如果我们使用大写字母代表每个哈希 ID,我们会得到如下所示的图:

\n
... <-F <-G <-H\n
Run Code Online (Sandbox Code Playgroud)\n

其中是链中最后一次H提交的哈希 ID 。在提交内部,Git 存储了先前提交的实际哈希 ID 。在 commit 内部,Git 存储了更早提交的哈希 IDHGGF,等等。

\n

这些链允许 Git 向后工作,从最新的提交回到较早的提交。这些Git 存储库中的历史记录,因此这些链对于使用 Git 至关重要。而且,由于每个提交都存储完整的快照,因此我们必须让 Git比较两个提交以查看发生了什么变化。例如,如果我们让 Git 将中的快照G与 中的快照进行比较,它会告诉我们当我们从 中创建时更改了什么。H H G

\n

所以,这就是它的作用git log:它从最新的提交(例如H)开始,打印出哈希 ID 和元数据,如果我们用来-p获取补丁,则提取GH(到临时内存区域)并比较两个提交的快照来找出发生了什么变化,并向我们展示。然后,显示 commit 后H,Git 向后移动一步以 commit G:它打印出哈希 ID 和元数据,如果我们使用-p,则比较F-vs- G。打印出来后Ggit log再后退一步至F,依此类推。

\n

(换句话说,Git 是向后工作的工作的。我不会在这里再强调这一点,但一旦你意识到这一点,它就会解释很多关于 Git 的内容。)

\n
\n

1如果您使用git log --pretty=fuller,您将看到每个提交实际上有两个:作者提交者。每个都由三元组组成:姓名、电子邮件、时间戳。通常,现在两者都是相同的,除了樱桃挑选的提交,原始提交的作者被保留,提交者是进行樱桃挑选的人,提交者时间戳是樱桃的时间 -选择动作。

\n
\n

分支名称只是帮助我们找到提交

\n

为了使上述工作正常进行,我们必须以某种方式知道\xe2\x80\x94\xe2\x80\x94链中最后一次提交的哈希ID。我们需要将该哈希 ID 提供给 Git,因为 Git最终只能通过哈希 ID找到提交。我们可以写下这些哈希 ID,将它们记在纸上、白板上或其他东西上。但它们确实又大又丑,而且很难正确输入。另外,我们还有一台电脑。为什么不让计算机帮我们记住哈希 ID?我们可以向 Git 存储库添加第二个数据库:它将保存名称,例如masterdevelopfeature,并使用这些名称记住最后一次最近的、最有用的,等等)提交的哈希 ID。

\n

这就是分支名称:它是名称数据库中的一个条目。实际名称稍作扩展:masteris realrefs/heads/masterfeatureis real refs/heads/feature。这为其他类型的名称留下了空间,例如标签名称:v2.1is real refs/tags/v2.1。但特别是对于分支名称,它们都保存提交哈希 ID \xe2\x80\x94,每个 \xe2\x80\x94 并且该哈希 ID 是我们将考虑的“在分支上”的最后一次提交的 ID ”。

\n

如果我们只有一个分支,一切都很简单:

\n
...--F--G--H   <-- master\n
Run Code Online (Sandbox Code Playgroud)\n

在这里,分支名称master是唯一的名称,它保存了我们最近提交的哈希 ID commit H。所以名称 master 指向链末尾的提交。这让我们(和 Git)可以访问 commit H。CommitH指向向后的 commit G,这让我们(和 Git)可以访问它;再次向后提交G点,依此类推。

\n

如果我们现在创建一个新的分支名称,例如feature,我们可以选择任何现有的提交来指向这个新名称。不过,大多数情况下,我们会选择我们正在使用的提交:H, via master。所以我们会得到:

\n
...--F--G--H   <-- feature, master\n
Run Code Online (Sandbox Code Playgroud)\n

现在我们有一个问题。我们使用哪个分支名称?请记住,我们将添加一个特殊名称 ,HEAD并将其附加到这两个分支名称之一。如果需要的话,让我们通过运行\xe2\x80\x94来附加HEAD到\xe2\x80\x94并绘制它:featuregit checkout feature

\n
...--F--G--H   <-- feature (HEAD), master\n
Run Code Online (Sandbox Code Playgroud)\n

我们仍在使用 commit H,但现在我们因为名称而使用它feature

\n

现在让我们以通常的方式创建一个新的提交:修改一些文件,甚至可能创建新文件和/或删除现有文件,并根据需要使用git add和/或git rm来更新它们以及git commit结果。无需过多担心所有细节,Git 会保存新快照,添加一些元数据,并将集合写为新提交。新的提交会获得一个新的、唯一的哈希 ID\xe2\x80\x94,它看起来是随机的且不可预测,因为它取决于我们进行提交的确切时间\xe2\x80\x94,但我们将其称为 commit I。新的提交将向后指向现有的提交H

\n
             I\n            /\n...--F--G--H\n
Run Code Online (Sandbox Code Playgroud)\n

一旦新提交存在,甚至在我们恢复能够运行更多命令之前,Git 现在会执行最后一个特殊技巧:它将新提交的哈希 ID 写入当前分支名称,即HEAD附加的分支名称 -到。既然这样feature,我们就得到:

\n
             I   <-- feature (HEAD)\n            /\n...--F--G--H   <-- master\n
Run Code Online (Sandbox Code Playgroud)\n

CommitH就在 commit 之前I,但它仍然是分支上的最后一次提交master。提交I是上的最后一次提交feature,但之前的提交也都Hfeature

\n

现在让我们继续进行另一项承诺feature

\n
             I--J   <-- feature (HEAD)\n            /\n...--F--G--H   <-- master\n
Run Code Online (Sandbox Code Playgroud)\n

然后运行git checkout master。这将使我们HEAD远离feature并附加它master。它还将更新我们的工作区,以便我们使用 commit 的内容H,而不是 commit 的内容J:现在我们所有的文件都匹配H,而不是不匹配J。我们所做的任何更新和快照都I安全J地存储在I和中,但它们现在已经从我们的视野J中消失了,因为我们已经提交了:H

\n
             I--J   <-- feature\n            /\n...--F--G--H   <-- master (HEAD)\n
Run Code Online (Sandbox Code Playgroud)\n

我们现在可以创建另一个新的分支名称,例如feature2,并附加到HEAD该名称上:

\n
             I--J   <-- feature\n            /\n...--F--G--H   <-- feature2 (HEAD), master\n
Run Code Online (Sandbox Code Playgroud)\n

然后进行两个新的提交feature2

\n
             I--J   <-- feature\n            /\n...--F--G--H   <-- master\n            \\\n             K--L   <-- feature2 (HEAD)\n
Run Code Online (Sandbox Code Playgroud)\n

或者,我们可以直接在 上进行这些提交master

\n
             I--J   <-- feature\n            /\n...--F--G--H\n            \\\n             K--L   <-- master (HEAD)\n
Run Code Online (Sandbox Code Playgroud)\n

图表本身而言\xe2\x80\x94提交集之间有向后指向的箭头(此处绘制为线条,因为文本中可用的箭头图形很差)\xe2\x80\x94这并不重要:我们无法更改任何现有的提交(永远),但我们始终可以添加新的提交,无论哪种方式,我们最终都会得到这组提交。这只是哪些名称 找到这些提交的问题。但 Git 允许我们随时创建、销毁或移动分支名称。提交不会改变;只是我们用来查找它们的名称可能不同。

\n

合并

\n

是时候回答上面的问题了:缺少什么?

\n

当我们在 Git 中合并一些提交时,这就是合并工作。这个想法是,某人在某些系列的提交中(I-J也许)做了一些工作,而某人\xe2\x80\x94可能是其他人\xe2\x80\x94在其他一些系列的提交中(K-L)做了一些工作。这给了我们这个:

\n
          I--J   <-- br1\n         /\n...--G--H\n         \\\n          K--L   <-- br2\n
Run Code Online (Sandbox Code Playgroud)\n

由于提交\xe2\x80\x94的性质,它们永远不会改变\xe2\x80\x94,从这张图中我们可以看出,这两行工作是从一个共同的起点开始的,即提交。从视觉上很容易看出, 中的所有内容都源自,对于 也是如此。它们也源自,但“更好”,因为它“更接近”端点提交。HJHLGH

\n

现在,我们已经知道 Git 可以比较两个快照,例如GH、或IJ。如果 Git 可以轻松地H直接与 进行比较怎么办J?嗯,可以;如果我们让 Git 这样做,我们就会发现与to的不同之处。这就是有人在一线所做的工作。这些就是.​HJbr1

\n

H类似地,如果我们让 Git 比较 in和 中的内容L,我们就会发现某人在底线上做了什么工作。无论文件有何不同,无论我们使用什么规则将文件内容更改H为 中的文件内容L,这都是有人在 上所做的br2

\n

这也告诉我们缺少什么。为了合并example.txt,我们不仅需要两个端点文件\xe2\x80\x94 quite,例如,一个在第 2 行,另一个not在第 2 行\xe2\x80\x94,而且还需要文件的基本副本。的基本副本example.txt是 commit 中文件的副本H。提交H是两个提示提交的合并基础,每个文件的副本是我们找出更改内容的方式。

\n

如果基础副本说:

\n
This is\nquite\na file.\n
Run Code Online (Sandbox Code Playgroud)\n

那么我们就知道,仍然写着的那一行没有任何quite改变,而写着的那行中有一行发生了变化not

\n

如果基础副本说:

\n
This is\nnot\na file.\n
Run Code Online (Sandbox Code Playgroud)\n

那么我们就知道,仍然写着的那一行没有任何not改变,而写着的那行中有一行发生了变化quite

\n

如果基本副本没有第 2\xe2\x80\x94 行(如果完整读取):

\n
This is\na file.\n
Run Code Online (Sandbox Code Playgroud)\n

那么我们就有了合并冲突,因为两个人都做了更改:都添加了 line-2,但他们添加了不同的line-2-s。

\n

这对您的案例意味着什么

\n

如果两个分支提示 commits\xe2\x80\x94(通过名称找到的一个)master和通过名称feature\xe2\x80\x94 找到的一个不同,这只是告诉我们它们是不同的。Git 提出的配方将更改一个提交以使其与另一个提交匹配,只是告诉我们如何将一个提示提交更改为另一个提示提交。

\n

如果这两个分支提示提交之间的合并基础提交是第三次提交,2我们需要知道第三次提交中的内容,因为这样才能git merge弄清楚 中发生了什么变化master以及 中发生了什么变化feature。然后,合并命令将尝试组合两组更改,将组合的更改应用于合并基础中的任何内容。

\n

正如phd 评论的那样,您可以在命令中使用三点表示法git diff

\n
git diff master...feature\n
Run Code Online (Sandbox Code Playgroud)\n

例如。这有 Git:

\n
    \n
  • 找到两个提示提交之间的合并基础(我们称之为$B);然后
  • \n
  • 运行相当于git diff $B feature
  • \n
\n

feature它告诉您关于此合并基础的更改。如果您随后运行相同的命令并交换两个名称:

\n
git diff feature...master\n
Run Code Online (Sandbox Code Playgroud)\n

Git 会找到相同的两个提示提交的合并基础,3然后是 diff $Bvs master:这显示了 上发生了什么变化master

\n

同样,git merge这些情况的作用是:4

\n
    \n
  • 运行两个差异,将输出保存在临时区域中;
  • \n
  • 获取每个文件的合并基础版本;
  • \n
  • 如果可能的话,合并差异;和
  • \n
  • 组合的差异应用于文件的合并基础版本。
  • \n
\n

如果一切顺利,git merge将从结果中进行合并提交。合并提交与常规非合并提交没有太大区别:它仍然具有由上面的组合过程构建的所有文件 xe2x80x94 的快照和一些元数据。合并提交的特殊之处在于,它将两个分支尖端提交都列为其父级,以便 Git 可以沿着两个分支返回(它们现在通过合并提交合并为一个“分支”:这暴露了“分支”一词;请参阅“分支”到底是什么意思?)。

\n
\n

2退化的情况。特别是,如果合并基础是两个分支提示提交之一,我们要么有一个简单的“可快进”情况,要么没有什么可以合并。不过,根据您发布的内容,您一定不会遇到这些情况之一。

\n

3如果只有一个合并基础提交\xe2\x80\x94(通常是这种情况\xe2\x80\x94),则两个分支提示提交的列出顺序并不重要。对于某些复杂的提交图,但是,可能有两个或多个合并基础提交。在这里,情况变得相当模糊。直到最近,该git diff命令并没有很好地处理这个问题;git merge处理得更好,但仍然很棘手。

\n

4此描述对如何进行合并、图形的形状等做出了很多假设,并且与git merge内部实际执行的操作相比,在其他方面进行了极大的简化。这个想法是为了实现总体目标,而不涉及一些棘手的机制。例如,这忽略了合并如何处理重命名文件的情况。

\n