为什么有些提交不属于任何分支?
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
你能帮我理解一下吗?
因为这个提交也是一个标签,你可以在这里看到:
可以有几个选项:
- 它在给定的分支上,该分支已被删除
- 它是在一个分支上提交的,
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
的攻击。但在那之前,它可以愉快地坚持下去,即使它根本不在任何分支上。
我遇到过一些提交不属于 Git 存储库中的任何分支。例如,以下提交被标记为 Apache Commons CSV 的发布,但它不属于任何分支:
https://github.com/apache/commons-csv/commit/0fbd1af5e3bd70454d5e398493a5c983aead2b67
它的父提交属于 master。
https://github.com/apache/commons-csv/commit/7688fbc3f9f5acf73d3c5018dd83310f7580d02e
你能帮我理解一下吗?
因为这个提交也是一个标签,你可以在这里看到:
- 它在给定的分支上,该分支已被删除
- 它是在一个分支上提交的,
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
的攻击。但在那之前,它可以愉快地坚持下去,即使它根本不在任何分支上。