Git 合并只是覆盖一个分支?

Git merge just overwrites a branch?

我是 git 的新手,我无法让它按照我想要的方式合并更改。所以这就是我想要完成的事情:

我需要更新我们的学习管理系统。 我正在处理 2 个分支:mergebranch 和 update。在 mergebranch 上,我有旧版本的系统,其中有一些自定义更改和文件不在更新中,但我也需要它们在更新版本中。然后在更新分支上,我有新版本的新文件。

我尝试了 git merge update,这导致 git 删除了 mergebranch 中的自定义文件,但不在更新中。此外,没有合并冲突,我觉得这很奇怪。我查看了第一行只有版本号的版本文本文件。我预计会发生冲突,但 git 只是用新版本号覆盖了旧版本号。似乎 git 只是将新版本复制到旧版本上,并没有合并发生。我还收到这些我不理解的“删除模式”和“创建模式”消息。

这就是我在 git 中所做的:

git checkout -b update

git add .
git commit -m "Update vanilla version"
git checkout master
git checkout -b mergebranch
git merge update

谁能帮帮我?我迷路了。

On mergebranch, I have the old version of the system that has some custom changes and files that are not in the update, but I need them to be in the updated version as well.

在 Git 中,您总是将一个分支合并到您当前签出的分支中。因此,如果您想将 mergedbranch 中的更改引入 updated,您必须将 mergebranch 合并到 update,方法是:

git checkout update
git merge mergebranch

从分支 branch1 获取一些代码到另一个 branch2 :

  • 结账到 branch2 : git checkout branch2

  • 通过运行拉命令从远程分支branch1获取代码:git pull origin branch1

更新

git fetch origin
git checkout origin/mergebranch

5-2) 合并这个分支:

git merge --no-ff update

解决冲突并纠正错误(如果存在)然后将此版本(如果完美)推送到您的分支 ex 中:mergebranch

这个新版本你可以把它推送到 master 或 mergebranch

git push origin mergebranch

如果您与显示的头部消息分离,则改为执行此操作:

git push origin HEAD:mergebranch

根据你的描述,我认为你有这段历史(时间从左到右流动):

          D--E--F   <-- update
         /
--A--B--C           <-- mergebranch

当您发出合并命令时,mergebranch 指向提交 C。这是 so-called fast-forward 的情况。不需要实际合并。 Git 只是将 mergebranch 重新指向提交 F,因此它等于分支 update

          D--E--F   <-- update, mergebranch
         /
--A--B--C

如果两个分支真的像这样分开,则需要进行实际的合并操作:

          D--E--F   <-- update
         /
--A--B--C
         \
          X--Y      <-- mergebranch

在这种情况下,将创建一个合并提交:

          D--E--F   <-- update
         /       \
--A--B--C         M <-- mergebranch
         \       /
          X-----Y

当提交 DEF 删除一些文件并添加其他文件时,这些删除和添加将反映在分支 [=15= 的最终状态中],无论操作是 fast-forward 还是真正的合并,因为使用命令

git merge update

你说:“我想要分支 updated 从当前分支 (C) 分叉到分支 updated 的尖端 (F ) 集成到当前分支中。"

在您理解 git merge 之前,您需要了解 Git 并不是关于 b运行ches 或文件的。 Git 是关于 提交 的。这意味着您需要准确地知道提交是什么以及做什么。我们不会在这里涵盖所有内容,但这些是现在的相关要点:

  • 每个提交都有编号。这些数字不是简单的计数——它们不是从提交 1 到提交 2 再到提交 3——而是丑陋的大哈希 ID。这些看起来 运行dom,但实际上不是:每个实际上都是提交内容的加密校验和。您将在 git log 输出中看到这些哈希 ID(或 运行 git log 现在可以看到它们)。

  • 由于这种加密校验和编号,任何提交的任何部分都不能更改。提交一旦完成,就完全 read-only,而且大部分是永久性的。 (可以摆脱提交,但我们不会在这里讨论。)

  • 一个提交有两部分,它的数据和它的元数据。数据部分包含 Git 在您(或任何人)提交时知道的每个文件的完整快照。元数据包含有关提交本身的信息,例如提交人、时间和原因:他们的姓名、电子邮件地址和日志消息。

  • 在元数据中,Git 添加了一些用于 Git 自身的信息:每个提交都包含其前一个提交的提交编号(哈希 ID)。 Git 将其称为提交的 parent。 (大多数提交恰好具有这些父 ID 之一,但正如我们将看到的,合并提交具有更多。)

Git 可以通过 hash-ID 查找任何提交——或任何内部 Git 对象。因此,如果我们手头有一个哈希 ID,我们就说这个 指向 提交,Git 现在可以找到它。而且,因为每个提交都存储其父项的哈希 ID,所以这些提交指向向后。这意味着我们可以像这样绘制一个简单的普通提交线性链:

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

其中每个大写字母代表一个实际的哈希 ID。如果我们知道散列 ID H,我们可以 Git 找到实际提交,其中包含文件快照和较早提交的散列 ID G。这让 Git 找到 G,其中包含快照和哈希 ID F,让 Git 找到 F,等等。

这就是 Git 的工作方式,一般来说:向后,从 last 提交。请注意,例如,提交 G 无法更改:它可以指向 F,因为 F 在我们(或任何人)创建 G 时存在,但是 H 还不存在,在我们创建它之前我们不会知道 H 的 ID。所以 G 可以指向 F,但不能指向 HH 可以指向 G,但不能指向以后的内容。

但是我们还有一个问题。 Git 从哪里获取哈希 ID H?这就是 b运行ch names 进来的地方。

一个 b运行ch 名称持有一个提交哈希 ID

鉴于提交链:

...--F--G--H

Git只是简单地将hash ID H放入一些b运行ch name,比如master。这个名字然后指向提交 H,这样很容易找到:

...--G--H   <-- master

如果我们现在想添加一个 second b运行ch 名称,Git 需要我们选择一些 existing commit,并将新名称指向此提交。很多时候,我们选择我们现在使用的提交——例如,提交 H:

...--G--H   <-- develop, master

现在我们有两个 names——对于同一个提交,此刻——我们需要一种方法来知道我们实际上使用哪个名字。为了解决这个问题,Git 将特殊名称 HEAD 附加到一个 b运行ch 名称,因此我们应该更新绘图:

...--G--H   <-- develop, master (HEAD)

这表明虽然两个名称都选择提交 H,但我们使用的名称是 master。请注意,每个提交都在 b运行ches.

假设我们现在在 master 上进行了两次 new 提交(没有明显的原因,但也许我们忘记先切换到 develop)。当我们进行第一次新提交时,Git 将:

  • 保存Git知道的每个文件的快照;
  • 将我们的姓名和电子邮件地址添加为作者和提交者,并将时间戳设置为“现在”;
  • 添加我们的日志消息;
  • 使用 当前提交的哈希 ID H 作为我们新提交的父哈希;
  • 写出新的提交,从而获得自己的唯一编号,但我们将其称为I;和
  • 将新提交的哈希 ID 写入当前 b运行ch 名称.

结果是:

          I   <-- master (HEAD)
         /
...--G--H   <-- develop

如果我们在此状态下进行第二次新提交,我们将得到:

          I--J   <-- master (HEAD)
         /
...--G--H   <-- develop

此时,通过 H 的提交仍在两者上运行ches,但提交 IJ 仅在 master.

现在让我们 运行 git checkout develop(或者在 Git 2.23 和更高版本中,git switch develop 会做同样的事情)。这使得我们当前的 b运行ch name 变为 develop 并且我们当前的 commit 返回到 commit H:

          I--J   <-- master
         /
...--G--H   <-- develop (HEAD)

Git 将更新其内部 next-commit 文件(在 Git 的 index 又名 暂存区,我们这里没有涉及)和我们的工作树文件来匹配提交 H,这样我们就可以从永久保存在 H 中的相同文件开始。如果我们现在进行新的提交,我们会得到:

          I--J   <-- master
         /
...--G--H
         \
          K   <-- develop (HEAD)

请注意,每个 b运行ch 名称仅标识一个提交:J 代表 masterK 代表开发。 Git 将这些称为 b运行ches 的 tip commits

进行第二次新提交给我们:

          I--J   <-- master
         /
...--G--H
         \
          K--L   <-- develop (HEAD)

提交 KL 现在仅在 develop 上。我们现在处于 git merge 有意义的情况。

真正的合并

git merge的作用一句话就可以说清楚,但细节就比较复杂了。 合并就是合并更改。但是我们刚刚看到 Git 实际上并没有 store 更改。每个提交都有每个文件的完整快照。那么Git如何做到这一点呢?

Git对此的回答是回到我们正在制作的图纸上。这些绘图生成了一个 提交图 。通过从任意两个提交开始——通常是两个 b运行ch 名称找到的两个提交——然后 向后、Git 可以找到 最佳常见提交。在这种情况下,很容易看出 HG 以及更早的提交在两个 b运行 上。 H 最好的 这样的提交,因为它最接近两个 branch-tip 提交。

Git 将此最佳公共/共享提交称为 合并基础 。要使用git merge,那么,我们做两件事:

  • 运行 git checkout 上两个 b运行ches 中的一个,这样 HEAD 附加到找到的 b运行ch其中一个提示提交;然后
  • 运行 git合并<em>otherb运行ch</em>,这样git merge 可以定位到其他tip commit.

Git然后自己找到合并基础。在我们的例子中,我们可能 运行:

git checkout master
git merge develop

这将使用 H 作为合并基础。

要查找 更改的内容 ,Git 将使用 git diff 命令的内部变体。 Commit H,合并基础,包含所有文件的快照。提交 J,在 master 的末尾,也包含所有文件的快照。使用git diff、Git可以比较这两个快照:

git diff --find-renames <hash-of-H> <hash-of-J>   # what we changed

然后,再次使用git diff,Git可以比较HL,看看他们在develop上有什么变化:

git diff --find-renames <hash-of-H> <hash-of-L>   # what they changed

Merge 现在的工作是合并这两组更改。这可能包括添加新文件和删除现有文件,但对于更典型的合并,我们可能只有一个或几个文件“被我们更改”和一个或几个文件“被他们更改”。

合并冲突发生在:

  • 我们和他们都对某个文件做了一些更改,
  • Git 不能合并 这两个变化本身。

因此,如果我们修改文件 F1 的第 42 行,并且他们修改任何其他文件的任何行而不触及文件 F1,Git 将简单地采用我们的 F1 版本,因为他们没有更改该文件。如果他们修改了文件 F2 而我们没有修改它,Git 将只采用他们的文件版本 F2。但是,如果我们都触摸了 F3,Git 将需要合并我们的更改——对我们更改的任何行——以及它们的更改。如果这些更改 重叠或接触 ,Git 将声明合并冲突。

Git 如果我们删除了他们修改过的文件(反之亦然),或者在其他各种情况下,现在还不是那么重要,

Git 也会声明合并冲突。请注意,删除文件是一个“整个文件”的更改,它会自动发生冲突,而不管另一方修改了该文件中的哪些行。但是如果我们删除了一个文件,而他们没有动它,Git 就可以了:我们的“删除这个文件”和他们的“对这个文件什么都不做”的组合就是删除文件。

如果Git能够组合我们的更改和他们的更改,Git将这些组合更改应用于合并库中的快照——在提交中H,在这种情况下——然后创建一个新的 merge commit,我们在这里称之为 M。新的合并提交有一个快照,就像任何提交一样。它有元数据,就像任何提交一样:你是作者和提交者,“现在》;用于两个时间戳。但是,与普通提交不同的是,新提交有 两个 个父项。一个是通常的:我们从 运行 git merge 开始的提交,在本例中是提交 J。另一个是我们在命令行中命名的提交:在本例中,提交 L。所以生成的合并提交看起来像这样:

          I--J
         /    \
...--G--H      M   <-- master (HEAD)
         \    /
          K--L   <-- develop

同样,提交 M 中的 快照 合并 H-vs- 的结果J随着H-vs-L的变化而变化。如果 Git 能够自行组合这些更改,Git 会这样做,然后将它们应用到 H 中的快照。这保留了我们的更改,但也添加了他们的更改。

请注意,从提交 M 返回,我们不仅会访问提交 J,还会访问提交 L。所以现在,所有 这些提交都在 master b运行ch 上。 b运行ch 一次获得了三个提交:新提交 M,还有提交 KL,之前只在 develop.

你的第一期

Also I get these "delete mode" and "create mode" messages that I don't understand.

这是 Git 的说法,您的更改和他们的更改的组合包括删除一些文件——Git 会告诉您 哪个 文件名正在被删除——并创建一些其他不同的文件(Git 会再次告诉您哪些文件)。 mode 部分是文件的模式:文件可执行 (mode 100755) 或不可执行 (mode 100644)。这是仅有的两种允许模式。1

您可以明白为什么 Git 认为这些文件是通过 运行 自己执行这两个 git diff --find-renames 命令中的一个或两个命令删除的。这里棘手的部分是找到合并基础提交的哈希 ID,但是 Git 有一个命令可以做到这一点:

git merge-base --all <name-or-hash-id> <name-or-hash-id>

会完成这项工作。例如,如果您在 master 上执行 git merge develop,而 master 确定了提交 J 并且 develop 确定了提交 L,您可以找到提交 JL 的哈希 ID,并将这两个用作 git merge-base --all 的参数。然后,您可以将此哈希 ID 与提交 J 的哈希 ID 进行比较,并再次与提交 L.

的哈希 ID 进行比较

(或者,如果您愿意冒一点点风险来拥有多个合并基础——git merge-base --all 命令将查明是否是这种情况,如果是,您需要一个稍微复杂一点,但通常情况并非如此——您可以使用 git diff 中内置的 three-dot 语法。由于 space 原因,我不会在这里详细介绍。)


1在 2005 年创建的 Git 存储库中,允许的文件模式更多。这被发现是一个坏主意,现代 Git 只生成这两种模式,但是 git fsck 仍然允许模式 100664,例如,以容纳这些古老的存储库。请记住,任何提交都不能更改,因此无法修复这些包含 mode 100664 文件的提交。


Fast-forwards

有时候,如果你 运行:

git checkout master
git merge develop

Git 会告诉您它执行了 fast-forward,而不是合并。如果我们再次绘制提交图,这意味着什么就更清楚了。假设我们开始于:

...--G--H   <-- master, develop (HEAD)

然后以通常的方式向 develop 添加一些提交:

...--G--H   <-- master
         \
          I--J   <-- develop (HEAD)

如果我们现在检查master并合并develop,Git将以通常的方式找到合并基础:从两个b运行ch开始tip 提交 HJ,并根据需要向后工作以找到最佳 shared 提交。但是这一次,在从 J 退回两步后,Git 到达提交 H,这是另一个提交。因此 Git 可以从 H 后退 步,因此使用 H 作为此合并的合并基础。

此合并将:

  • diff H 与自身进行对比以查看 we 发生了什么变化(当然什么也没有发生!);和
  • HJ 进行比较,看看 他们 发生了什么变化;然后
  • 什么都不结合。

这个组合的结果,当应用于提交 H 时,显然会匹配与提交 J 关联的快照。

因此,在这种情况下,Git 将默认采用 short-cut。它根本不会merge。相反,它将简单地检查另一个提交——在本例中为提交 J——同时将 current b运行ch name 向前拖动,这样我们就结束了与:

...--G--H--I--J   <-- master (HEAD), develop

您可以强制 Git 进行真正的合并,使用 git merge --no-ff,这会禁用 fast-forward short-cut。这次 Git 真的会比较 H 和自己,比较 HJ,并结合两组变化:

...--G--H------K   <-- master (HEAD)
         \    /
          I--J   <-- develop

(什么时候有用甚至是否有用在某种程度上是品味问题,而不是正确性。)

我认为这就是您所看到的情况。另请参阅 ,这是我接近尾声时出现的

“已经是最新的”

还有一个 somewhat-interesting 合并案例。假设你在一些 b运行ch 上,比如 master,你 运行 git merge develop 并收到消息 Already up to date. 这意味着你有一个看起来像这样的情况:

...--G--H   <-- develop
         \
          I--J   <-- master (HEAD)

Git 照常计算合并基数,但这次合并基数 H 落后于 当前 b[=581= 的尖端]ch master。它实际上是另一个 b运行ch 的尖端。当两个名称定位到同一个提交时也会发生这种情况(例如,如果两个名称都指向 H,或都指向 J)。

结论

要查看 git merge 会做什么:

  • 绘制图表(或让Git为您绘制;参见git log --graph,通常与--all --decorate --oneline一起使用)。
  • 找到 merge base 提交和两个 tip 提交。
  • 查看合并基础是一个还是两个提示提交。如果是这样,则合并是微不足道的(fast-forward-able)或已经完成。
  • 否则需要真正的合并。如果需要,使用 git diff 查看将合并的两组更改。
  • 如果更改集不是您想要的,请检查从合并基础到任何 b运行ch 提示的提交,其中存在一些问题:
    • 出了什么问题?
    • 您将来如何防止(或至少阻止)这种情况?
    • 如果合适,向一个或两个 b运行 分支添加新提交以解决问题。

如有必要,请考虑使用 git merge --no-commit 让 Git 开始 合并但不 完成 合并。然后您可以更正合并,但请注意,这会产生一些人所说的 evil merge。如果你有 Git repeat 这个合并,2 你将不得不做同样的手动修正。或者,让 Git 进行合并,然后添加修复提交。这样做的好处是,如果让 Git 重复合并,它会得到相同的(坏的)结果,但是你可以让 Git 重复修复。


2旧的,now-deprecated git rebase -p 和新的 git rebase -r 命令将“复制”合并,就像任何 rebase 的方式一样复制普通提交,但与普通提交不同,git cherry-pick 不能 复制合并提交。所以这些通过 重复 合并来工作。此重复不包括您在 运行 和 git merge 时指定的任何标志,也不包括您所做的任何手动修正。

感谢大家的意见,问题已解决。

所以我缺少的是合并基础。我从头开始使用系统的原始版本,并从那里创建了两个新分支,一个用于自定义旧版本,一个用于更新。从那里开始,合并过程如我所料。