我将分支 develop 合并为 master 但在 master 分支上没有 develop 的变化

I merged branches develop into master but on master branch no changes from develop

git checkout develop
git pull origin develop
git checkout master
git merge develop
git push origin master

通过这种方法,我将 develop 合并到 master 中,但在合并后我是 运行 app in master 分支和应用程序在 branch 开发上没有得到更改。

除非我们可以访问您的特定存储库,否则我们无法对您的特定存储库说什么。不过,总的来说,这个语句:

doesn't get changes on branch develop

没有意义。原因是 Gitb运行ches 没有 changes。事实上,Git 甚至 store 都没有变化。相反,Git 存储 commits,并且提交保持 snapshots,而不是更改。要了解 git merge 的作用,您 必须 首先深入了解提交:提交是什么,对您有何作用,以及您如何获得提交的访问权限。

Git 存储提交;提交存储快照和元数据

任何 Git 存储库的核心是一个包含 提交 的大数据库(加上支持所有这些所需的其他内部 Git 对象) .这些提交(和其他对象)被编号,但数字不是简单的计数:它们不会提交 #1、#2、#3 等。相反,每个提交或其他内部 Git 对象有一个 哈希 ID: 一大串难看的字母和数字,在 hexadecimal 中代表一个非常大的数字。1 这些哈希 ID 看起来 运行dom,但不是:它们实际上是对象的 content 的加密校验和(提交对象的提交内容)。

这些数字——哈希 ID——是 Git 实际找到提交的方式。 hash ID是key-value database中的key;存储的数据必须散列回用于查找它的密钥,是存储的对象。这意味着一旦进行了提交或任何其他内部对象,就永远不会更改。如果您从数据库中取出一个存储的对象,对其进行更改,然后将其存储回去,它将进入 新的和不同的键 并且旧对象保留在数据库中,没有变化。所以字面上的提交不能被改变。

每次提交都包含两个有用的数据块。其中之一,我们称之为 metadata,因为它是关于提交本身的数据。这是 Git 保存 编写 提交的人的姓名和电子邮件地址等内容的地方。提交的另一个重要部分是主要数据,它是每个文件的 保存的快照 作为文件在您或任何人 运行 git commit.

请注意,这意味着提交 没有更改 。每个提交的数据都是 每个文件的完整副本 。每个文件的完整副本是 read-only(因为任何提交的任何部分都不能更改)。为了保存 space,commits 中的文件以特殊的 Git-only 格式存储——完全不是普通的计算机文件——压缩后 de-duplicated .因此,每次提交都可以 共享 之前提交的单个文件数据。由于这些东西都是 read-only,因此可以安全地共享:没有人,甚至 Git 本身,都不能更改保存的 file-content 数据(它使用相同的 key-value 数据库和散列ID 作为提交)。

还有一件重要的事情要知道。每个提交的元数据包含 Git 为该提交存储的 先前提交哈希 ID 的列表。大多数提交只有一个先前的提交。2 这对您来说意味着提交形成简单的 backward-looking 链:

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

在这里,H 代表链中 last 提交的实际丑陋的大哈希 ID。只要我们知道这个提交的哈希ID——例如,也许我们把它写在纸上,每次我们需要它的时候再输入它——我们就可以Git 提取 这个提交,包括它的数据(快照)和元数据(关于提交的信息)。在元数据中,我们——或者 Git——可以找到早期提交 G.

的实际哈希 ID

较早的提交 G,当然有数据和元数据。 G 的元数据包含 still-earlier 提交 F 的实际哈希 ID,因此 Git 可以从 G 中找到 FF 又指向另一个 even-earlier 提交,这一直持续到第一次提交,然后停止。

每次提交中的文件将永远存储(或者只要提交本身持续)。我们可以 Git 随时将它们从提交中复制到普通文件中。它们存储的形式只有Git可以使用,其他任何东西都不能改变[=482] =],但是一旦我们提取它们,我们就可以使用 并更改 它们。此提取过程称为 签出 提交。3


1可能数字的运行ge从1到2160-1,或1461501637330902918203684832716283019655932542975,在片刻。这比 10^48 多一点。以后运行ge会更大,达到2256-1—abut 1077 个可能的数字。

2通常的例外是第一次提交,它不能有任何先前的提交,因此没有,合并提交,它将之前的两个提交联系在一起,因此记住每个提交的哈希 ID。使用一个 previous-commit ID 的提交是 普通提交 none 的提交是 root 提交——大多数情况下只有一个,从第一次提交开始——并且有两个或更多的提交根据定义,合并提交.

3在现代 Git 中,我们实际上得到了几种不同的提取方式。在旧版本的 Git 中,从旧提交中提取特定文件存在问题,但现在 git restore 存在,我们可以正确地做到这一点。不过,这个答案不会涉及 Git 的 index 的更详细信息。


B运行ch 名称,git log 和您的屏幕截图如何工作

这个 backward-looking 链是允许 git log 工作的。给定某个起点,或者如果你愿意,可以给定多个起点,Git 可以找到这些 starting-point 提交。这些提交指向更早的提交,因此 Git 可以使用它们来查找更早的提交。

不过,要开始,Git 需要最后一次提交的哈希 ID。这些会从哪里来?上面,我建议我们可以在纸上写下提交 H 的实际哈希 ID。但如果我们这样做并重新输入,我们就会出错。我们不必那样做:我们有一台计算机计算机可以为我们记住这些哈希ID。

这就是名称——b运行ch 名称、标签名称和所有 Git 的其他名称——的用途。每个都进入第二个数据库——另一个 key-value store——由 name 而不是哈希 ID 索引。第二个数据库中的 values 是哈希 ID。然而,这个数据库是可写的:我们可以替换每个b[=702的哈希ID =]ch 名称,只要我们喜欢。对象数据库只允许添加 new 个对象,但是名称数据库允许我们随时覆盖任何名称。

我们需要小心使用这个功能,因为根据定义,存储在 b运行ch 名称中的任何哈希 ID 都是 last 提交我们想调用“b运行ch 的一部分”。在 Git 术语中,b运行ch 名称包含 tip commit 的哈希 ID。链的顶端是 b运行ch 中的最后一次提交,即使链继续运行:

...--G--H   <-- main
         \
          I--J   <-- feature

在这里,我们有两个 b运行ch 名称,mainfeature。名称 feature 定位提交 J(通过存储其哈希 ID)。提交 J 指向提交 I,后者指向提交 H。名称 main 定位到提交 H。提交 H 指向回提交 G,依此类推。

这意味着通过并包括 H 的提交都在 both b运行ches 上。 Git 在这里有点奇怪:许多版本控制系统不会做这样的事情,即一次在多个 b运行ch 上提交。但是 Git 确实如此,所以如果您要使用 Git,您必须接受它。

从快照获取更改

假设我们有这一系列快照:

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

last 快照,b运行ch master 的提示,是提交 H 中的快照。但是还有更多的快照。例如,提交 G.

中有一个

如果我们要求 Git 将提交 G(即它的快照)提取到一个位置,并将提交 H 提取到另一个位置,会发生什么情况?我们可以比较所有这些文件。其中一些可能完全相同。在这种情况下,Git 将具有 de-duplicated 文件的内容,因此 GH 实际上只是共享这些文件。其他的会有所不同:GH 具有这些文件的不同副本。

相同的文件不是很有趣。让我们把它们扔掉(也许通过从两个位置的 checked-out 副本中删除它们)。剩下的仍然是快照,但我们现在可以在 Spot the Difference 的游戏中比较它们 side-by-side。这些文件中 发生了什么变化,嗯,这就是提交 G 和提交 H.

之间发生的变化

如果我们要求 Git 或其他查看软件将提交 显示为 更改,这就是它的作用。它“提取”(在内存中)两个提交,使用存储提交的内部方式来非常快速地丢弃所有完全相同的重复文件——这很容易,因为早期 de-duplication。然后它比较不同的文件并提出一组指令。将这些指令应用于 earlier-commit 的文件会生成 later-commit 这些文件的副本。 这是一个 diff 这就是我们喜欢查看 com 的方式its. 但是 这不是它们的存储方式:Git 每次我们查看时都会产生 new 差异一个。

我们不必比较相邻的提交。我们可以比较提交 F 与提交 H,例如,看看从FH。使用 git diff 命令,我们可以比较 我们喜欢的任何两个提交 。 Git 将比较快照并为我们提供将一个快照转换为另一个快照的方法。如果愿意,我们可以向后进行比较,以获得将 H 变回 G 的方法(“反向差异”)。 所有 提交保存快照,因此您可以选择任何 两个提交并以这种方式比较它们。

合并是关于合并工作

到目前为止介绍的所有概念都不是很难。当您打开每个部件并查看内部时,它们有很多棘手的 部件 ,但是 概念 非常简单。您 理解 它们至关重要,至少在较高的层次上,这样我们才能在真正合并的情况下了解 git merge 的工作原理。

假设我们有以下一系列提交,以两个 b运行ch 提示提交结束:

          I--J   <-- br1
         /
...--G--H
         \
          K--L   <-- br2

我们想 运行 git checkout br1,然后 git merge br2,这里 想法 目标这个操作——就是合并两个b运行ches所做的功。此外,我们希望 Git 尽可能多地进行这种组合。

现在,提交 J 只有一个 快照 ,加上通常的元数据。这同样适用于提交 L。我们可以使用 git diff 比较 J 中的快照和 L 中的快照,但这不会很好地工作。假设某些文件在 JL 中不同:

This is
quite
a file.

对比:

This is
not
a file.

比较此文件的 JL 副本只会告诉我们它们 不同 。我们对 b运行 中的任何 完成的工作 一无所知。

使用提交 IK 怎么样?好吧,如果我们将 I 中的快照与 J 中的快照进行比较,那可以告诉我们有关 br1 中所做的事情。所以这看起来至少好一点。但是,在提交 I 中所做的使 that 快照与提交 H 不同的事情呢?这也是 br1 中的“工作完成”。所以我们最好一直回到 H

同时,相同的参数适用于提交 LKH。在我们开始看到“在 b运行ch br2 中完成的工作”之前,我们需要一直回到 H提交 H 出现在这两个中。 commit H? 有什么特别之处 想一想。再看这张图:

          I--J   <-- br1
         /
...--G--H
         \
          K--L   <-- br2

如果你说特别之处在于提交 Hboth b运行ches 上,你是对的。当然,提交 G 以及 G 之前的任何提交也是如此。我们实际上可以利用这些,但是 G-vs-H 已经 on “both b运行ches”,通过中的快照H。所以没有理由再往回走。 H 是停止的正确位置:它是 b运行ches.[=203 上的 best 共享提交=]

Git 将此 best-shared-commit 称为 合并基础 。只有一个合并基础是非常常见的,如本例所示,尽管也有包含两个或更多合并基础的复杂情况。我们不会在这里担心这些情况,但如果您想探索所有这些背后的理论,请查看 Lowest Common Ancestor of a Directed Acyclic Graph。如果您正在使用 Git 并希望 找到 LCA,例如 运行 git merge-base --all br1 br2,以查看这些提交哈希 ID。在这种特殊情况下,您只会获得提交 H 的一个提交哈希 ID,因此这是此合并的合并基础。

现在,Git 可以比较HI,然后IJ。但实际上,不需要这样做。4所以它直接比较HJ , 就好像 运行ning:

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

然后它对 H-vs-L 和另一个 git diff --find-renames 和两个哈希 ID 做同样的事情:

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

这会产生两个配方,用于将 H 中的快照分别更改为 JL 中的快照。合并过程现在很简单:我们查看 每个配方 中需要完成的内容,合并任何单独的更改。如果 H-vs-J 说对 nothing

This is
not
a file.

文件,但是H-vs-L说把中间一行改成quite,那么组合就是进行更改。

Git 将这些组合指令应用于在提交 H 中找到的快照。这样,我们就保留了在 br1 上所做的所有工作——记住,我们从 git checkout br1 开始——并且 add 所有 t他们的工作——无论 他们 是谁——在 br2.

上完成

4Git 处理文件重命名的方式很棘手,实际上可能受益于从合并基到 b运行通道提示。 Git 目前还没有这样做。


进行合并提交

合并——例如由 运行ning git merge br2 执行——是一个相当漫长和复杂的过程。我们首先选择我们想要“打开”的 b运行ch,git checkout br1:

          I--J   <-- br1 (HEAD)
         /
...--G--H
         \
          K--L   <-- br2

此处括号中的 HEAD 向我们显示了我们“打开”的 b运行ch。我们所做的任何新提交都将“在”同一个 b运行ch 上,并将导致 Git 将 new 提交的哈希 ID 写入 b运行通道名称。

git merge 命令然后进行一系列分析。这包括查找合并基础提交或提交——在本例中为提交 H——并决定是否实际执行计算合并结果的工作。 git merge 命令的一些选项可以帮助控制这一点。在我们的特殊情况下,git merge 无论如何都必须进行真正的合并,因此我们不需要任何选项来让 Git 完成工作。5

这意味着 Git 开始合并过程,或者我喜欢将 合并称为动词 。它找到了提交 H 并确定需要真正的合并来合并 H-vs-JH-vs-L。它在内部生成两个差异,以确定如何进行这种组合。然后它查看每个 to-be-merged 文件以实际 执行 合并。

如果这里出现问题,Git 将以 合并冲突 停止,让我们收拾残局。这样的合并仍在进行中,但是 Git 命令不再 运行ning:它们已将所有内容记录在文件中并退出。您现在必须使用单独的修复命令6 来修复每个冲突;这些记录你所说的是正确的结果。全部录制完成后,您 运行 git merge --continue 获取合并以读取录音并完成合并。

假设没有任何问题,但是,Git 将自行完成所有 work-combining,并自行完成最终的合并提交,没有 git merge --continue。结果 一次提交。像每个提交一样,它有一个快照和元数据。唯一特别的是元数据列出了两个 以前的提交。这就是使它成为 merge commit(作为形容词合并)或 a merge 的原因,使用 merge ] 作为名词。让我们画出结果:

          I--J
         /    \
...--G--H      M   <-- br1 (HEAD)
         \    /
          K--L   <-- br2

请注意,名称 br1 现在像往常一样指向新的提交。这个新合并的唯一特别之处——作为名词合并——它不仅指向后向提交 J 像任何其他提交一样,而且还指向提交 L,这就是使它成为合并提交的原因。 merge 作为动词 process 将此 merge 作为 noun/adjective 提交,这让我们知道 br2 现在合并到 br1ML 作为父级,并且 br2 指向 L,所以名称br2 不再需要,如果需要可以立即删除。


5如果合并基础是当前提交并且另一个b运行ch提示提交是“提前" 此提交 git merge 可以并且默认情况下将执行 fast-forward 操作而不是合并。您可以强制 Git 在这里与 git merge --no-ff 进行真正的合并。如果合并基础是 other 提交,并且与此提交相同,或者“在”此提交的“后面”,则无法合并,并且 git merge 会说 Already up to date 并退出。如果两个提交不相关——以至于根本没有合并基础——git merge 会抱怨历史不相关,然后退出; --allow-unrelated-histories 选项使它无论如何都进行合并,使用 empty tree 作为假合并基础。 -n / --no-commit-s / --squash 选项阻止 Git 进行新提交,-s 选项使 Git 在停止时“忘记”合并。不过,我们不会在这里详细介绍这些选项。

6这可以包括 运行 在 work-tree 文件上编辑您的编辑器并使用 git add,或者您可以使用 git mergetool 如果你喜欢它(我不喜欢),或者你喜欢的任何其他程序。 Git 不会强制您使用任何特定的方法,但它 确实 信任 您得到的合并结果是正确的。它不会检查,它只是假定您所做的一切都是正确的!


合并可以意味着你已经完成了,但并不必须意味着

现在我们有:

          I--J
         /    \
...--G--H      M   <-- br1 (HEAD)
         \    /
          K--L   <-- br2

我们可以简单地删除名字br2A b运行ch name 的目的是找到一些提交(提示提交b运行ch) 如果 L 不需要特别查找,我们可以从 M 开始并向后查找。但是,如果我们愿意,我们可以继续在 br2 上进行 new 提交,方法是:

git checkout br2

然后做工作和运行宁git commit。结果可能如下所示:

          I--J
         /    \
...--G--H      M   <-- br1
         \    /
          K--L--N--O--P   <-- br2 (HEAD)

此时,br2 上的提交可以只能P 开始找到——通过名称 br2 — 并且向后工作,所以现在我们 不能 删除名称 br2

在这一点上,我们也可以 运行 git checkout br1 然后 git merge br2。合并将再次找到 最佳共享提交 。这次,实际上是提交 L.

merge-as-a-verb 过程将从比较 L-vs-M 开始,看看“我们”在 br1 上做了什么,然后比较 L-vs-P,看看“他们”在br2上做了什么。 Git 将尝试合并这些更改,将它们应用到 L 中的快照。这将保留 M 上的“我们的”更改——我们通过 进行的合并带来的更改 M——并在 [=171 上添加“他们的”更改=],如果可行,我们将得到一个新的合并提交 M2Q 或任何我们想称呼它的东西:

          I--J
         /    \
...--G--H      M--------M2   <-- br1 (HEAD)
         \    /        /
          K--L--N--O--P   <-- br2

此时,我们可以删除 名称 br2 安全地(再次),或保留它(再次),如我们所愿。

结论

Merge 是关于合并 工作——变化——但Git 不会存储 变化。找到更改的唯一方法是比较一些特定的提交。这意味着如果你想知道合并会做什么,或者为什么合并会这样做,你必须比较各种提交:

  • 找到合并基,使用git merge-base --all
  • 使用 git diff --find-renames.
  • 将基础与 b运行ch 技巧进行比较

您所看到的这两组差异是合并的输入。为合并提供的任何 选项 ,例如 -s ours-X ours,都会影响更改如何 合并 。请注意 -s ours 表示 完全忽略他们的更改 。不幸的是,git merge 没有在最终的合并提交中记录用于 产生 合并的任何选项。 (我认为这是一个错误:它确实应该在合并提交的元数据中。)