为什么有些提交不属于任何分支?

Why do some commits belong to no branch?

我遇到过一些提交不属于 Git 存储库中的任何分支。例如,以下提交被标记为 Apache Commons CSV 的发布,但它不属于任何分支:

https://github.com/apache/commons-csv/commit/0fbd1af5e3bd70454d5e398493a5c983aead2b67

它的父提交属于 master。

https://github.com/apache/commons-csv/commit/7688fbc3f9f5acf73d3c5018dd83310f7580d02e

你能帮我理解一下吗?

因为这个提交也是一个标签,你可以在这里看到:

可以有几个选项:

  1. 它在给定的分支上,该分支已被删除
  2. 它是在一个分支上提交的,reset 是在该分支上完成的

1 最有可能发生
内容在一个特性分支中开发,最后一次提交被赋予了标签并删除了该分支

这是一个示例场景:

  • 我创建了一个功能分支
  • 向其中提交内容
  • 标记此提交
  • 删除了分支但没有将其合并回任何其他分支
  • 列出标签并在列表中

这种情况在 Git 中很常见,它以与大多数传统版本控制系统 (VCS) 截然不同的方式使用分支。其实这里隐藏着一个相当深刻的哲学问题:见What exactly do we mean by "branch"?

分支名称标识提示提交

在大多数 VCS 中,分支的 名称 很重要,甚至可能是最重要的关于 分支的事情。这在 Git 中是不正确的:分支 names,在 Git 中几乎没有价值(无论如何对 Git 本身)。对于Git,重要的是提交。提交是永久性的——好吧,大多数情况下是永久性的——而且是不可变的:一旦提交,任何提交都不能更改。但是每次提交的真实名称都是一串可怕的、笨拙的、无法发音的、无法记住的数字和字母,例如fe0a9eaf31dd0c349ae4308498c33a5c3794b293。这些对人类不利,因此 Git 让我们使用名称来代表这些原始哈希 ID。

关于每个提交的另一个重要的事情是,任何一个提交都会存储另一个提交的真实名称——哈希 ID,我们称之为提交的 parent 前任 。我们说这个child提交指向它的parent.1如果我们将一串不可发音的哈希 ID 放入 "most grandparent-y" 到 "most child-y" 的顺序中,我们会得到如下内容:

... <-26e4... <-8b02... <-fe0a...

这些提交的most-child-like获取分支名称,然后该名称指向最后一个提交:

... <-26e4... <-8b02... <-fe0a...   <--master

Git 使用最后一个(或 tip)提交来找到它的 parent,然后使用 parent 找到 grand parent,依此类推,遍及整个存储库。但是因为散列 ID 看起来是随机的——并且故意几乎不可能预测——甚至 Git 本身也希望有一个 name 来找到 last 在链中提交。该哈希 ID 特别重要,因为 Git 使用该提交来查找其余提交。这给了我们这样一张图片:

          o--o   <-- branch1
         /
...--o--o
         \
          o--o--o   <-- branch2

(我只是停止绘制箭头的内部向后方向,并为每次提交用圆点替换哈希 ID)。

虽然中间一行的提交有点令人费解:它们在哪个分支上? Git 的回答是它们在 两个 分支上。 Git 提交属于 每个 分支,而不是属于首次提交的分支的提交——好吧,每个分支 name—这又回到了它。

要向某个分支添加新提交,您 git checkout 分支照常工作,git add 视情况而定,运行 git commit。这会写出一个新的提交,该提交指向当前提交作为其 parent:

               o   (new!)
              /
          o--o   <-- branch1 (HEAD)
         /
...--o--o
         \
          o--o--o   <-- branch2

然后,无论分配给 new 提交的提交哈希 ID 是什么,Git 都会将该哈希 ID 写入分支名称.要知道 要更新哪个 名称,Git 将您的 HEAD 附加到其中一个分支名称。安全存储新提交的哈希后,我们可以将更新后的图片绘制为:

          o--o--o   <-- branch1 (HEAD)
         /
...--o--o
         \
          o--o--o   <-- branch2

这是树枝生长的正常方式之一。


1child 记住了 parent,而不是相反。由于提交是不可变的,因此这是必要的。就像人类 parents 和 children 一样,当 child 创建时 parent 存在,但当 parent 被创建。由于 commit 只能记住过去,parents 无法回忆起他们的 children.


标签还标识提交

标签名和分支名一样,直接指向一个提交。但是,与分支名称不同,Git 不会自动 更改 标记名称以使其指向任何其他提交。事实上,一般来说,你也不应该这样做——并不是说它会破坏你自己的 Git,而是它可能会破坏其他人对你的 的 期望 Git 存储库。一旦他们有了 tag-name-to-hash-ID 映射,他们可能会认为从那时起他们就拥有了正确的哈希 ID,因为标签不 打算 像分支名称一样移动。因此,如果我们标记一些提交:

          o--o--o   <-- branch1
         /
...--o--o
         \
          o--o--o   <-- branch2 (HEAD)
                ^
                |
             tag:v1.2

然后添加另一个提交:

          o--o--o   <-- branch1
         /
...--o--o
         \
          o--o--o--o   <-- branch2 (HEAD)
                ^
                |
             tag:v1.2

标签保留在原位。

名字可以随时删除

如果我们认为 branch2 不是一个好主意,我们可以 git checkout branch1 然后删除 name branch2。没有名称 branch2,我们刚刚添加的最终提交不再是 find-able:

          o--o--o   <-- branch1
         /
...--o--o
         \
          o--o--o--o   ???
                ^
                |
             tag:v1.2

但是,标签 名称v1.2 仍然存在,它使标记提交 find-able。标记的提交在 no 分支上(在这张图中,它的 parent 或 grandparent 都不在,尽管它的 great-grand-parent 仍然在 branch1).

名称保护提交

我在上面提到,提交大部分是永久的。最后一次提交,hich 不再有名字,现在 不受保护。 Git 有一个名为 垃圾收集器 的设备,它充当一种死神来清除剩余的、不需要的东西。这个 Grim Collector git gc 在整个 Git 数据库中搜索所有提交,同时还使用所有 names 来查找所有提交。可以通过某个名称(任何名称,包括标签名称)找到的提交都被标记为保留。提交(和其他 Git objects)not 可以通过这种方式找到,从命名提交中 unreachable,获取收集并销毁。

这个过程让 Git 自由地生成 objects,并且只有在最后一刻才决定真正使用它们。它还允许您随时移动分支名称。只要提交受名称保护,它们就会一直存在。一旦 没有 名称,它们就可以用于 garbage-collection。这就是您(和 Git)摆脱不需要的提交的方式。 git stash 之类的命令通过创建不在分支上的提交来工作,但受 refs/stash 名称(或其 reflog 保护,我不会深入讨论这里)。删除一个 stash 会删除它的名字;最终 git gc 将其真正删除。

标签保护标记的提交,以及任何更早的(parent)提交,就像分支名称一样。如果删除标记,now-unnamed 提交将变得容易受到 git gc 的攻击。但在那之前,它可以愉快地坚持下去,即使它根本不在任何分支上。