首页 > 解决方案 > 合并最新的

问题描述

我的本地机器上有 3 个分支,而远程机器上有更多分支。一些同事正在更新另一个分支,所以我需要让我的工作保持最新,以便让我们的代码运行。

假设我正在处理的分支称为branch-1,另一个由其他人更新的称为branch-2. 现在我试过git pull origin branch-2git checkout branch-1。它显示Everything is already up to date,这对我来说没有意义,因为当我比较我的 IDE 和 GitLab 上的代码之间的代码时。

为什么会发生,我应该如何解决?

标签: gitversion-controlgitlab

解决方案


如果,之后:

git checkout branch-1
git pull origin branch-2

你所有的工作都被丢弃了,你只有内容branch-2,你不会不高兴吗? 你不应该为你的工作被保留而感到高兴吗?

它显示Everything is already up to date...

您需要注意以下事项:

  1. git pull意味着运行一个git fetch命令,然后运行第二个 Git 命令。(第二个命令的选择是可配置的。)
  2. git fetch不会影响您的任何分支(正常使用时——不同分支上的 git pull的链接显示了不同的使用方式,这可能会影响您的分支)。
  3. 因此,第二个Git 命令是所有重要操作的所在。

在您的情况下,第二个命令是git merge,它git merge打印Everything up to date并且什么都不做。

我建议 Git 新手避免 git pull使用,因为它git fetch加上第二个命令的组合“太神奇了”,会干扰理解 Git 中发生的事情。将其拆分为两个单独的命令不会立即获得启发,但考虑到一对单独但又都困难的山路,在蒙眼的情况下步行或驾驶您的路线可能更明智。如果不出意外,当你死的时候你会知道你在哪条路上。此外,这些山路往往很漂亮。

现在您已经知道了首先要知道的几件事——pull = fetch +(在这种情况下)合并,而合并是你看到奇怪消息的地方——现在是时候回顾一下在使用 Git 之前应该知道的其他事情了:

  • Git 是关于提交的。这与文件无关,尽管提交确实保存了文件。这也与分支无关,尽管分支名称允许我们(和 Git)找到提交。提交是关键。

  • 每个提交都有编号。但是,这些数字是 (a)巨大的,目前大到 2 160 -1 或 1461501637330902918203684832716283019655932542975,以及 (b) 看似随机的。它们通常以十六进制表示,人类真的不使用它们:它们对我们来说只是一堆垃圾随机性。这就是我们使用分支名称的原因。但是 Git需要这些数字。

  • 每个提交存储两件事:

    • 提交具有每个文件的完整快照,永久保存(或只要提交本身持续)。这些快照中的文件以一种特殊的、仅限 Git 的格式存储,在这种格式中它们被压缩——有时非常压缩——而且,重要的是,去重。在大多数提交中,我们主要重用旧文件,这意味着这些新提交不会为重用文件占用空间。

    • 除了快照之外,每个提交都包含一些元数据,或有关此提交本身的信息。例如,这包括作者的姓名和电子邮件地址。它包括一些日期和时间戳。但它还包括——供 Git 自己使用——前一个提交或提交的原始哈希 ID。(实际上它是一个父哈希 ID 的列表,但大多数提交只存储一个,这就是我们将在这里看到的。)

  • 一旦完成,任何提交的任何部分无法更改,即使是 Git 本身也是如此。(如果提交有问题——如果它有问题——我们必须进行新的和改进的提交。新的提交得到一个新的编号;旧的提交,具有相同的旧编号,仍然存在。)

  • 一个分支名称存储一个提交号。

由于您有三个分支(branch-1branch-2,也许还有main?),因此您的 Git 存储了三个提交编号。这些分支名称可能都存储相同的数字,或者它们可能都是不同的数字。关于它们,我们要说的是它们指向它们存储其编号的提交,就像这样:

... <-F <-G <-H   <--branch-1

这里的名称branch-1包含提交编号——或者,更短的,指向——提交H。同时, commitH本身包含较早 commit 的提交编号G,作为H的元数据的一部分。CommitG包含一些更早的提交的提交编号,依此类推:整个过程只有在我们回到第一个提交时才结束,它不能向后指向父级,因此也不会。

当我们第一次创建新分支时,新名称指向构成旧分支的相同提交:

...--F--G--H   <-- main

变成:

...--F--G--H   <-- main, branch-1

现在所有的提交都在两个分支上。两个名称都指向或选择分支上的最后一次提交:当前是 commit H。但是,当然,我们现在将进行新的提交。我们需要在这张图上再添加一件事,它会告诉我们使用哪个名称来查找 commit H。为此,我们将使用特殊名称HEAD: 像这样全部大写,这个特殊名称是 Git 知道我们正在使用哪个分支名称的方式。Git“附加”HEAD到一个分支名称:

...--F--G--H   <-- main (HEAD), branch-1

on branch main在这里,我们git status将说:我们H通过名称使用提交main。如果我们运行:

git switch branch-1

要更改分支,我们继续提交H,但现在我们通过名称到达那里branch-1

...--F--G--H   <-- main, branch-1 (HEAD)

一旦我们做出的提交,就会发生一些非常有趣的事情。git commit命令:

  • 收集元数据,包括您的姓名和电子邮件地址以及当前日期和时间,还包括当前提交(哈希 ID);
  • 打包所有文件的快照(去重,Git 的内部格式);
  • 将所有这些写成一个的提交,它会得到一个新的随机数字,但我们只是称之为I:新的提交指向现有的提交H;和
  • 最后——至关重要的是——将I的实际哈希 ID(无论是什么)写入当前分支名称,即branch-1.

结果如下所示:

...--F--G--H   <-- main
            \
             I   <-- branch-1 (HEAD)

名称 branch-1现在位于 commit I。所有提交,包括I,都在branch-1. CommitH是 branch 上的最后一次提交main。CommitH 保留在两个分支上

现在,假设您使用git clone从某个中央存储库复制所有提交(尽管没有分支),然后main在您的副本中创建一个名称。您的副本还将记住mainname 下的原始 Git origin/main,并且您的新克隆将创建您自己的main指向同一提交:

...--G--H   <-- main (HEAD), origin/main

(您的 Git 创建了您自己的main,以便它可以附加某个位置HEAD。该origin/main名称是您的 Git 用来记住另一个 Git 存储库的分支名称的远程跟踪名称,截至您上次运行或以其他方式从您的 Git 更新他们的。)git fetch

此时您可以在自己的分支上创建自己的提交:

          I   <-- branch-1 (HEAD)
         /
...--G--H   <-- main, origin/main

您的同事也克隆并开始工作;他们所做的提交获得唯一的哈希 ID,因此我们也将为他们的提交组成唯一的单字母名称。

最终他们将运行:

git push origin branch-2

或类似的。这会将他们的新提交发送到共享(集中)存储库副本,并在branch-2那里创建或更新名称,以便中央存储库现在具有:

...--G--H   <-- main
         \
          J   <-- branch2

如果您现在运行git fetch origin,您的 Git 将看到他们有一个新的提交J,并将从他们那里获取它。你的 Git 会看到他们有一个新名字branch2,并会创建你的名字origin/branch2来记住它。您的存储库中的结果如下所示:

          I   <-- branch-1 (HEAD)
         /
...--G--H   <-- main, origin/main
         \
          J   <-- origin/branch2

这可以持续到您和/或他们的多次提交。但最终,您可能希望他们的工作与您的工作合并。现在是时候了git merge

git merge工作原理

假设此时,您的存储库中有:

          I--K   <-- branch-1 (HEAD)
         /
...--G--H
         \
          J--L   <-- origin/branch2

由于不再需要它们,因此我已将名称main和从绘图中删除(尽管它们可能仍然存在):重要的部分是提交,直到and ,以及我们可以使用的名称这一事实找到这些提交(和分别)。所以我们现在可以运行:origin/mainKLbranch-1origin/branch2

git merge origin/branch-2

你的 Git 会找到两个提交:

  • 当前或HEAD提交,即 commit K;和
  • 由 找到的提交origin/branch2,即 commit L

您的 Git 现在将使用这些提交及其内部嵌入的向后箭头来找到最佳共享提交。在这种情况下,这是 commit H。Git 将此称为合并基础

因为你的两个分支提示提交都是从这个共同的起点下降的,所以 Git 现在很容易找出改变了什么,并找出它们改变了什么。为了找到您的更改,Git 运行git diff从合并基础提交到您的分支提示提交:

git diff --find-renames <hash-of-H> <hash-of-K>

这显示了哪些文件不同,并且对于每个不同的文件,提供了修改基本(提交H)版本以提出提示(提交K)版本的方法。

用他们的分支提示重复这个:

git diff --find-renames <hash-of-H> <hash-of-L>

查找他们更改了哪些文件,并为这些更改生成一个配方。

合并命令现在只需 (?) 需要组合这两组更改。如果这种组合一切顺利,Git 可以将组合更改应用到来自提交的文件H- 合并基础。这具有保留您的更改的效果,但也添加了他们的更改。

如果一切顺利,合并将在中间停止,让您负责修复 Git 留下的混乱。但我们只是假设它在这里进展顺利。

完成合并更改并将它们应用到合并库中的所有文件后,Git 现在进行新的提交。这个新的提交,就像每个提交一样,都有一个快照:快照是通过将组合更改应用到H. 像每个提交一样,这个合并提交也有元数据:你是作者和提交者,“现在”是什么时候,你可以包含比默认的“合并分支分支 2”更好的日志消息。

实际上,这个新的合并提交只有一个特别之处,那就是它不仅仅是一个父提交,就像我们之前看到的提交一样,它有两个:新提交指向当前提交K to- be-merged(现在实际合并) commit L,像这样:

          I--K
         /    \
...--G--H      M   <-- branch-1 (HEAD)
         \    /
          J--L   <-- origin/branch2

当您进行更多提交时,它们只是构建在此结构上:

          I--K
         /    \
...--G--H      M--N   <-- branch-1 (HEAD)
         \    /
          J--L   <-- origin/branch2

您的分支名称branch-1现在指向 commit NN指向,M指向 ,同时指向KL. 这两个分别指向IJ,这两个指向H,历史重新加入的地方。

有时git merge无事可做

如果你现在 make new a new commit O,那也只是添加:

          I--K
         /    \
...--G--H      M--N--O   <-- branch-1 (HEAD)
         \    /
          J--L   <-- origin/branch2

假设此时您要运行git merge origin/branch2. 会发生什么?

的规则git merge从查找两个分支提示提交开始。那些是现在OL大多数合并1的下一步是找到这两个分支提示提交的合并基础。合并基础被定义为最佳共享提交(尽管从技术上讲,它是DAG 中两个提交的最低共同祖先)。这意味着我们需要找到一个好的提交,可以通过以下方式找到:

  • 从开始O并向后工作
  • 从开始L并向后工作。

因此,让我们坐L一会儿,我们从ON向后工作M下一次提交,又 向后退了一步,是K and L提交L在两个分支上! 因此,提交L最好的提交,因此它是合并基础。

现在,真正合并的下一部分将是运行两个git diff命令,将基础的快照与每个分支尖端的快照进行比较。但是基础另一个提示提交,所以这个差异是空的。

由于这次合并尝试的合并基础是另一个提交,Git 将什么都不做。它会说:Already up to date.

请注意,这并不意味着OL中的快照相同。事实上,合并基础L是另一个重要的提交。从字面上看,没有什么可以合并的。命令这样git merge说并报告成功:一切都完成了。


1git merge -s ours是这里的例外:不需要计算合并基础来运行其余的合并策略。该命令是否无论如何都会这样做,以便检测退化情况,我没有测试过。


快进合并

这里值得一提的是另一种特殊情况,即快进操作。假设,而不是这种退化的情况:

          o--O   <-- ours (HEAD)
         /
...--o--B   <-- theirs

其中git mergeup to date,我们有:

          o--T   <-- theirs
         /
...--o--B   <-- ours (HEAD)

我们什么时候跑git merge theirs?现在,就像上次一样,合并基础是 commit B。一个 diff from Bto B,为了弄清楚我们改变了什么,将是空的。B但是 from to (他们的提交)的差异T将提供一个更改配方,它将从 commitT中的快照生成 commit 中的快照B

因此,Git 可以在这里进行真正的合并,如果你运行git merge --no-ff,Git会这样做

          o--T   <-- theirs
         /    \
...--o--B------M   <-- ours (HEAD)

但是,默认情况下,2 git merge意识到任何合并提交M都会自动具有与提交(他们的)相同的快照T,因此它只是将当前分支名称移动到指向 commit T

          o--T   <-- ours (HEAD), theirs
         /
...--o--B

(不再有任何理由为图纸中的扭结而烦恼。我把它留在里面是为了更清楚地说明名字ours移动了。)

(从技术上讲,快速转发是发生在name上的事情。但是,当使用git mergeorgit merge --ff-only使其发生在当前分支上时,我们会得到一个“快进合并”,这实际上只是一个git checkoutorgit switch到另一个拖拽的提交带有它git push的分支名称git fetch。and 命令能够以快进方式移动某些名称。)


2还有另一种特殊情况也可以强制进行真正的合并,涉及带注释的标签,但这种情况很少见。它记录在案:搜索merging an annotated.


底线

如果你一路走到这里,(a)恭喜!和(b)这一切告诉你的是你git pull的工作很好:只是你已经在你的分支中有他们的提交pull运行 a fetch,在他们的分支上没有发现新提交,因此没有添加任何这样的提交。pullran git merge,它发现没有什么可合并的:您的分支已经通过任何父链找到它们的提交,将它们作为祖先提交。

反过来,这意味着无论拥有他们没有的任何东西 - 无论git diff您从他们的分支提示提交运行到您的分支提示提交中出现的任何内容 –git merge如果您要合并的分支提示 - 将会被合并的内容-致力于他们的。如果以另一种方式运行差异,您看到的差异是“您必须删除的东西,才能恢复到他们的东西”。


推荐阅读