我遇到过一些提交不属于Git存储库中的任何分支。例如,以下提交被标记为Apache Commons CSV的发布,但它不属于任何分支:
https://github.com/apache/commons-csv/commit/0fbd1af5e3bd70454d5e398493a5c983aead2b67
其父提交属于master。
https://github.com/apache/commons-csv/commit/7688fbc3f9f5acf73d3c5018dd83310f7580d02e
你有可能帮我理解这个吗?
答案 0 :(得分:6)
这种情况在Git中是正常的,它使用与大多数传统版本控制系统(VCS)截然不同的分支。事实上,这里隐藏着一个相当深刻的哲学问题:见What exactly do we mean by "branch"?
在大多数VCS中,分支的名称很重要,甚至可能是关于分支的最重要的事情。在Git中不是这样:在Git中,分支名称的价值非常小(无论如何都是Git本身)。对Git来说,重要的是提交。提交是永久性的,大部分是永久性的和不可变的:一旦制定,就不能改变提交。但是每个提交的真实名称是一个可怕的,不可思议的,不可发音的,不可能记住的数字和字母串,例如fe0a9eaf31dd0c349ae4308498c33a5c3794b293
。这些对人类不利,因此Git允许我们使用名称代替这些原始哈希ID。
每次提交的另一个重要事项是,任何一个提交都存储真实名称 - 另一个提交的哈希ID,我们称之为提交的父或前身。我们说这个子提交指向其父级。 1 如果我们采用一串不可发声的哈希ID并将它们放在&#34中;大多数祖父母 - "到了#34;大多数孩子"顺序,我们得到类似的东西:
... <-26e4... <-8b02... <-fe0a...
这些提交中最像子项的获取分支名称,然后名称指向 last 提交:
... <-26e4... <-8b02... <-fe0a... <--master
Git使用最后一个(或 tip )提交来查找其父级,然后使用父级在整个存储库中查找祖父级,依此类推。但是因为哈希ID看起来是随机的 - 并且故意几乎不可能预测 - 即使Git本身想要一个名称,它可以通过它找到链中的 last 提交。这个哈希ID特别重要,因为Git使用该提交来查找其余的提交。这给了我们这样的图片:
o--o <-- branch1
/
...--o--o
\
o--o--o <-- branch2
(我只是停止绘制箭头的内部向后方向,并用每个提交的圆点替换哈希ID)。
中间行的提交有点令人费解,但是他们在哪个分支? Git的答案是他们在两个分支上。 Git提交不属于属于首次提交的分支的提交,而是属于每个分支 - 井,每个分支名称 - 返回到它。
要向某个分支添加新提交,您git checkout
该分支会照常工作,git add
,并运行git commit
。这会写出一个新的提交,指向当前提交作为其父提交:
o (new!)
/
o--o <-- branch1 (HEAD)
/
...--o--o
\
o--o--o <-- branch2
然后,无论将提交的哈希ID分配给 new 提交,Git都会将哈希ID 写入分支名称。要知道要更新的哪个名称,Git会将您的HEAD
附加到其中一个分支名称。一旦安全地存储了新提交的哈希,我们就可以将更新的图片绘制为:
o--o--o <-- branch1 (HEAD)
/
...--o--o
\
o--o--o <-- branch2
这是分支增长的正常方式之一。
1 孩子记得父母,而不是相反。由于提交是不可变的,因此这是必要的。就像人类父母和孩子一样,父母在孩子被创建时存在,但是在父母被创建时孩子还不存在。由于提交只能记住过去,父母不能回忆起他们的孩子。
标记名称(如分支名称)只是直接指向提交。但是,与分支名称不同,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
然后删除名称 branch2
。如果没有名称branch2
,我们刚刚添加的最终提交将无法再找到:
o--o--o <-- branch1
/
...--o--o
\
o--o--o--o ???
^
|
tag:v1.2
标记名称v1.2
仍然存在,它使标记的提交可查找。那个标记的提交在 no 分支上(在这个图中,它的父或祖父都没有,尽管它的曾祖父母仍然在branch1
)。
我在上面提到过,提交主要是永久性的。最后一次提交,不再有名称,现在不受保护。 Git有一个名为垃圾收集器的设备,它可以作为一种死神来移除剩余的,不需要的东西。这个Grim Collector git gc
在整个Git数据库中搜索所有提交,同时还使用所有名称来查找所有提交。提交可以通过某个名称找到 - 任何名称,包括标签名称 - 都标记为保留。提交(和其他Git对象)不可以通过这种方式查找,无法访问来自命名提交,被收集和销毁。
这个过程让Git可以自由地生成对象,并且只决定在最后一分钟使用它们。它还允许您随时移动分支名称。只要提交受到名称的保护,它们就会存在。一旦有 no 名称,它们就可用于垃圾收集。这就是你(和Git)摆脱不必要的提交的方式。像git stash
这样的命令可以通过创建不在分支上的提交来工作,但受refs/stash
名称(或其 reflog )的保护,我不会在这里进入)。丢下一个藏匿的名字;最终git gc
将其删除。
标记保护标记的提交以及任何早期(父)提交,就像分支名称一样。如果删除标记,则现在未命名的提交将容易受到git gc
的攻击。但在那之前,即使它根本没有任何分支,也可以愉快地留下来。
答案 1 :(得分:1)