为什么 git 对合并分支的提交显示在我的主分支上?

Why are git commits on a merged branch showing on my main branch?

我目前有一个 git 提交树,看起来像这样,括号中有指针 (?):

*   305f Merge branch 'develop' (HEAD->master, origin/master, origin/HEAD)
|\
| * d97b Some other commit on dev branch (develop) 
| * df14 Some commit on dev branch
|/
* 7a761b6 Initial commit

我已经将 master 分支推送到远程(Gitlab,如果重要的话),当我在 Gitlab UI 中查看 master 分支上的提交时,所有 4 个提交都存在,我会在哪里预计只有 "Merge branch 'develop'" 和 "Initial commit" 提交会出现在主分支上。

我的理解是 master 指的是我刚刚列出的两个提交,而 develop 指的是 "Some other..." , "Some commit...",可能还有 "Initial commit"还有,因为它是一个祖先。

我哪里错了?

在某些版本控制系统中,当您在分支 B 上提交 C 时,提交 C 永远在分支 B 上。任何获得提交 C 的人都会获得分支 B。如果他们以前有他们自己的分支B,那么现在他们的分支B 中有一个新提交 C

Git 不会这样做。提交不会永久固定在分支上。但是,提交 大部分是永久性的,1 并永久固定在它们出现在 提交图 中的位置。为了使这项工作正常进行,提交并不是真正在 any 分支上进行的。相反,分支名称只是一个标签。多个标签都可以指向同一个提交,如:

*   305f Merge branch 'develop' (HEAD->master, origin/master, origin/HEAD)

此处masterorigin/master2都标识提交305f。如果您现在创建一个新分支 br2,该名称将 指向提交 305f.

提交 305f两个 parent:7a761b6(第一个 parent)和 d97b (它的第二个 parent)。 305f 的全名就是它的完整哈希 ID。这永远不会改变;并且该哈希 ID 永远保留给 that 提交,它将始终具有相同的两个 parent。该提交将永远冻结并且永远不会移动。

移动的Git中的东西是分支名称。目前,master 表示 305f。然而,刚才 master 意味着 7a761b6。提交将永远保留在原位,如其原始哈希 ID 所发现的那样。分支名称移动。

所有这一切的结果是,当我们创建和销毁分支名称时,包含一些提交的分支集会动态变化。此时,名称 master 可以让您找到所有四个提交。如果您允许 Git 以 Git 喜欢的方式移动名称 master,则通过从 305f 开始并查看其两个提交,这四个提交将继续可访问parents,然后查看 d97b 的 parent(然后从 d97b 返回到您已经看到的 7a761b6)。请注意,new 只会提交添加到图表。通常,每个新提交都会有一些现有的提交作为其 parent——如果它是典型提交,则只有 parent,如果是合并提交,则只有两个 parent 之一。3

如果我们把这些东西画在侧面,会容易一些。为了让它更简单,我们可以使用单个大写字母来代表 Git 使用的难以理解的哈希 ID:

        D--E   <-- br1
       /
A--B--C
       \
        F--G   <-- br2

在这里,名称br1允许我们找到提交E,它找到D,然后是C,然后是B,然后是A(并停止,因为 A 是第一个提交)。名称 br2 允许我们找到 G,然后是 F,然后是 C,然后是 B,然后是 A(然后停止)。所以前三个提交在两个分支上,而两个提交每个只有一个分支。

擦除 name br1 导致提交 DE 变得无法找到。最终 Git 会真的把它们扔出去。如果我们改为通过执行 git checkout br1 添加新提交,进行我们想要的任何更改,git addgit commit,我们将获得一个新的哈希 ID H 并推进名称 br1 包括它:

        D--E--H   <-- br1
       /
A--B--C
       \
        F--G   <-- br2

现在有六个提交可以从 br1 开始,从 H 开始并向后工作。如果我们创建一个新名称 br3 以记住 E 的位置,我们将需要重新绘制图形:

             H   <-- br2
            /
        D--E   <-- br3
       /
A--B--C
       \
        F--G   <-- br2

请注意 提交 的 none 实际上已经更改:我们只是将 H 向上推以为标签 br3 腾出空间。

如果我们稍后删除 name br3,那没关系:没有通过 exlusively 找到的提交 br3。提交 E 不会消失,因为 br1 找到 H 找到 E.


1大部分是通过可达性实现的。您 find 从某个分支或标记名称开始提交,它提供原始哈希 ID。然后,找到一个提交后,您可以使用它的 parent 哈希 ID 来查找它的前一个提交。然后你使用 those commits 来找到 their parents,等等。

通过从 每个 引用执行此过程——参见脚注 2——Git 找到所有 可达 提交。存储库中存在的任何提交,如果此过程无法访问,最终将 garbage-collected 并删除。

2名字master是分支名。它的全名真的是refs/heads/master,全名以refs/heads/开头的都是分支名。相比之下,origin/master 实际上是一个 remote-tracking 名称:它的全名以 refs/remotes/ 开头,然后继续说 origin/master。 Git 有时只从该名称中删除 refs/ 部分,以便您看到remotes/origin/master.

标签(如果有)位于 refs/tags/。这些东西统称为refsreferencesreflogs 中还保存了额外的隐藏引用。 reflog 只是一个日志文件,用于保存在您或 Git 更新之前存储在 ref 中的值(以前的哈希 ID)。这些 reflog 条目最终会过期,这就是 deliberately-abandoned 提交的方式——例如,那些你用具有新哈希 ID 的 new-and-improved 版本替换的条目——最终会被清除。

3合并提交的技术定义是它至少有两个parent。因此,您也可以创建包含 3 个或更多 parent 的合并提交——但很少有理由这样做。我还应该提到,可以使用 no parent 创建一个新的提交。除了第一次提交——我在横向图表中标记为 A 的那个——你在正常实践中也不会这样做。