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