如果我把分支A合并到分支B中,然后删除A,那么来自分支A的提交(现在已删除)属于哪个分支?当我得到这些提交的链接时,我发现“这个提交不属于这个仓库上的任何分支,可能属于仓库外的一个分支。”
我尝试了此问题的所有答案,但未解决mu问题Listing and deleting Git commits that are under no branch (dangling?)
解决办法是什么?
如果我把分支A合并到分支B中,然后删除A,那么来自分支A的提交(现在已删除)属于哪个分支?当我得到这些提交的链接时,我发现“这个提交不属于这个仓库上的任何分支,可能属于仓库外的一个分支。”
我尝试了此问题的所有答案,但未解决mu问题Listing and deleting Git commits that are under no branch (dangling?)
解决办法是什么?
1条答案
按热度按时间bnlyeluc1#
TL; DR
你不能"删除"这个提交。* 你 * 没有 * 这个提交摆在首位,即使你有,你仍然不能真正地 * 删除 * 它。
长
如果我将分支A合并到分支B中,然后删除A,那么来自分支A的提交(现在已删除)属于哪个分支?
你可能想要的答案是"分支B"--这不是错的,但也不是对的。不幸的是,这个问题有一个根本性的错误。我相信这个错误本身就来自于GitHub关于提交不"属于这个仓库的任何分支"的误导性声明:
此提交不属于此仓库上的任何分支,可能属于仓库外的分支。️ This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
这个问题本身的错误--也是上面的文字误导的原因--在于提交的存在并不归功于分支名称的存在。在Git中,你可以有任意多的提交,而且 * 根本没有分支名称 。提交从一开始就不"属于" 任何 * 分支。
相反,我们在Git中使用的一个关键概念是reachability,如果某个提交 * Ci * 可以从 * Git仓库R中的 * 其他提交 * Cj,* 到达,这意味着 * Ci * 是 * Cj * 的 * 祖先 (或者等价地, Ci**Ci *,其中""--一种弯曲的小于号--读作"precedes"):这定义了提交图上的偏序,即Directed Acyclic Graph或DAG.1
然后我们在Git中定义 * branch *--或者至少是 * branch name --为 * 一个引用(或ref),其名称以
refs/heads/
开头,其哈希ID被限制为某个提交的哈希ID, ref * 本身被定义为 * 一个包含哈希ID的名称。2因此,像refs/heads/branch
这样的名称是一个 * branch * 名称,存储在这个分支名称中的哈希ID必须是某个 * commit * 的哈希ID。一个提交会"到达"它的所有祖先。每个提交保存一个列表--通常只有一个条目长--包含"以前的提交哈希ID"。这些形式的提交形成链,带有向后指向的箭头。简单的情况下,每个提交只有一个向后的箭头,指向它的前任:
这里,在我们的简单仓库R中,我们正好有8个提交。我们没有使用Git的实际提交哈希ID,而是给它们一个大写字母。(这个方案在真实的仓库中是不切实际的:如果存在多于26个提交,我们会怎么做?但是这对于思考这里的问题是有用的。)我们所做的 * 最后 * 提交
H
在其自身内存储倒数第二个提交G
的散列ID。我们说H
* 指向 *G
。G
存储F
的散列ID,所以我们说G
指向F
,这个过程会向后,沿着整个提交链向下,直到我们点击提交A
,因为这是第一次提交,所以它不能向后指向,而且它没有:其父散列ID列表为空。[1]这个特殊的定义稍微向后,因为Git本身是向后工作的。在正常的DAG中,可达性意味着后继,而不是先行。但在Git中,所有的箭头都指向后,而不是前。
2大多数refs拼写为
refs/*
,但也存在 * pseudo-refs *,如HEAD
和CHERRY_PICK_HEAD
。伪refs是特殊情况,会给那些致力于为Git创建合适的ref数据库的人带来麻烦。请注意,伪refs是per-work-tree,但其他一些refs,如对分refs,也是per-work-tree。可从分支名称访问意味着提交位于分支上
我们从简单的八次提交仓库开始,结尾是:
我们已经添加了 * 分支名称 *
main
,并在main
中存储了提交H
的真实哈希ID,所以我们说main
指向H
,与H
指向G
的方式相同。(为了在Stack Overflow上显示文本/ASCII艺术效果,我未能将从H
到G
的箭头画成 * 箭头:我们只需要记住,commits只会向后链接,没有从G
到H
的链接,反之亦然。这个设置意味着名称
main
允许我们访问仓库中的8个提交中的任何一个,现在让我们再添加两个分支名称br1
和br2
,它们都指向提交H
:所有三个 * name * 都指向提交
H
。所以所有八个提交都是从 * all names**可达的。这意味着所有提交都在所有分支上。这里
HEAD
连接到main
意味着我们 * 使用 * 的分支名称是HEAD
,因此当前提交是H
.让我们现在运行git checkout
或git switch
,来改变HEAD
连接到哪个 * name *:这导致:
此时唯一 * 改变 * 的是
HEAD
现在附加到br1
,所有八个提交仍然存在;我们仍然使用提交H
;但是现在我们使用H
* 通过 * 名称br1
。现在我们用通常的方式做一个新的提交,这个新的提交有一个新的唯一的哈希ID,但是我们称之为“commit
I
“,为了保持我们的理智,我们需要画一个从I
指向H
的箭头,并使名称br1
指向I
。因为Git内部就是这样处理的我们现在使用提交
I
,通过名称br1
。如果我们添加另一个新的提交
J
,我们得到:我们现在使用提交
J
通过名称br1
.现在我们切换到br2
:现在我们再次使用commit
H
,通过名称br2
,如果我们再提交两次,我们得到:I-J
可以说是“属于”分支br1
,提交K-L
可以说是“属于”分支br2
,但是从H
到H
的提交呢?有些人会说这些提交“属于”main
,但是Git没有这样的区分:当我们第一次给br*
分支命名时,所有的提交都是在所有的三个分支上,而且这些提交仍然在所有的三个分支上,只是现在 new commitsI-J
* 只 * 在br1
上,而 new commitsK-L
* 只 * 在br2
上。当我们使用
git merge
时,我们并不是在真正的“合并分支”,而是在真正的“合并提交”。git switch
通过将HEAD
附加到br1
,使提交J
成为 * 当前提交 *。git merge
让Git定位不是一个,不是两个,而是 * 三个 * 提交:HEAD
提交,J
;br2
指向L
;以及合并基是通过最低公共祖先算法定义的,其输入是提交
J
和L
,并且该算法计算出提交H
的散列ID。合并本身是通过比较
H
、J
和L
中存储的快照来实现的,这使得Git能够计算出“我们在H-I-J
链上做了什么”,以及“他们在H-K-L
链上做了什么”。(注意,提交I
和K
只用于它们的 * 链接 *,而不是它们的快照:两者都链接回提交H
,这使得提交H
成为合并基。如果一切顺利,Git会自己创建新的合并提交。这个新的合并提交
M
有两个父提交,两个向后指向的箭头链接到J
和L
,如下所示:我暂时从绘图中删除了所有的 * 分支名称 *,因为我们不需要它们:提交独立于任何 * 分支名称 * 而存在,但在Git中创建一个新的提交总是做同样的事情:
I
时,Git将新提交的哈希ID写入当前分支名称br1
;J
时,Git将新提交的哈希ID写入当前分支名称br1
;以及K
和L
时,Git将新提交的哈希ID写入当前分支名称br2
;M
,Git将M
的哈希ID写入当前分支名称br1
:名称
main
和br2
仍然存在,并且仍然指向H
和L
。在这个ASCII艺术中,没有空间在main
中绘制,并且现在没有 * 需要 * 在br2
中绘制。我们可以问:* 哪些提交可以从名称br1
到达?* 答案是:* 所有人 *提交
K-L
* 以前 * 只在br2
上,但是现在,由于合并提交M
,提交K-L
在 * 两个 * 分支上。所以这就给了我们你最初问题的答案,只要我们稍微修改一下:在一个真正的合并之后,删除一个分支名称是“安全的”,因为提交仍然是可以通过合并提交找到的。它们现在“在”两个分支上,并且去掉一个名称--我们现在不使用的名称,在这个例子中是br2
--仍然留下至少一个它们“在”的其他名称。警告:不是所有的合并都是真合并
虽然
git merge
命令有时会使合并提交M
:我们也可以想出其他的情况
这里,
git merge br4
将执行 * 快进 * 操作而不是合并,生成:在快进的情况下,删除
br4
仍然是安全的:过去仅“在”br4
上的提交R
现在也“在”br3
上。但是我们也可以运行
git merge --squash
,这个特定的选项会指示git merge
进行一个 non-merge“squash”提交:新提交
S
在git merge --squash
之后,有相同的 * 快照 *,如果我们让git merge
做一个真正的合并。也就是说,Git仍然经历了所有正常的“找到合并基,运行两个比较,合并工作”的步骤,它会做一个真正的合并。但后来git merge
停止,让我们运行git commit
,当我们这样做的时候,git commit
会进行一个普通的非合并提交,我在上面画成S
。3这样做没有什么好的理由。如果我们 * 想要 * 这个操作,我们可以运行
git merge --squash --no-commit
。这种组合是允许的!它与今天的git merge --squash
执行完全相同的操作。但在遥远的过去,--squash
选项被作为--no-commit
的特例处理,因此它同时执行这两个操作。这意味着它现在必须以向后兼容的名义继续做这两件事。无法找到的提交和垃圾回收
一般来说,在Git中,我们--甚至是Git本身--使用名称 * 查找 * 提交。**它们不必是分支名称,**但它们通常是 * 分支名称,或者在克隆中是远程跟踪名称(例如
origin/*
)。不管名称分支名称、标签名称、远程跟踪名称、内部二分引用的 * 种类 * 如何,或者不管它是什么--名字持有 * 一个 * 哈希ID。如果它是一个 commit 哈希ID,那么通过图可达性算法,它足以找到所有的 predecessor 提交。但有时我们可能会有 * 只能 * 被 one ref找到的提交,比如一个分支名称
br2
:如果我们 * 删除 * 这个ref
br2
,我们如何找到提交K-L
?答案之一是:* 我们不做 (另一个--但只是暂时的--答案是使用Git的 reflogs,它会半秘密地保留一段时间来提交哈希ID。不过最终reflog条目会过期,然后我们又回到“我们不做”的答案。)
如果我们和Git * 找不到 * 提交,该提交将被纳入
git gc
的“垃圾收集”。4 Git将自动运行git gc
,在不规则的Git决定的时间运行。这个git gc
将-缓慢而痛苦地,通过爬行整个仓库R-找到任何提交和其他Git对象, 是 * 不可访问的,并且,如果满足其他几个条件,5实际上会从存储库对象数据库中“删除"对象。这个gc系统非常聪明,它允许Git程序自由地生成内部对象,只要它们有用,当它们不再有用的时候就丢弃它们。垃圾收集器/清洁服务稍后会沿着并清理它们。
4
git gc
是Git常规维护和内务管理的一部分,目前正在开发一个git maintenance
命令,该命令将以更通用、更可预测、更易于使用的方式来处理这一问题。git maintenance
可能最终对普通用户和Git管理员都有用,但首先还有很多工作要做。5最重要的一点是对象本身要足够老,因为
git gc
可以在任何时候“在后台”运行,所以它不能删除一个已经存在的对象,因为某个命令--比如git commit
--刚刚“创建”了这个对象。但是还没有找到时间将它连接起来使其可见。如果git gc
在 *git commit
可以将其哈希ID写入分支名称之前垃圾收集了一个新提交 *,那将是很糟糕的。因此,默认情况下,两周的窗口来完成它正在做的事情。两周 * 可能 * 足够git commit
完成一个新的提交。😀(玩笑归玩笑,Git的操作比我们过去使用的版本控制系统要快得多。我最好就此打住,以免这变成Monty Python Four Yorkshiremen sketch。)
那么GitHub的声明是关于什么的呢?
当GitHub说:
此️提交不属于此仓库上的任何分支,可能属于仓库外的分支。
这是什么意思?
this
形容词,它们都起着限定词的作用:一个特定的提交--可能是您在浏览器中显示的提交--以及一个特定的仓库,Git通过该仓库 * 找到 * 了提交。我们刚刚说过,我们通常使用名称来查找提交。但实际上,我们使用哈希ID在仓库的对象数据库中查找底层提交 * 对象 。哈希ID * 是 * 提交的“真实名称”。我们使用名称找到的是 * 哈希ID,而不是提交对象本身。如果我们手头有哈希ID,这就是我们所需要的--当我们使用浏览器查看GitHub仓库提交时,我们提供提交哈希ID。例如,URL https://github.com/git/git/commit/5a73c6bdc717127c2da99f57bc630c4efd8aed02以
5a73c6bdc7...
结尾。这是一个提交哈希ID。因此GitHub可以访问提交,* 而无需 * 使用 * 分支名称 *。现在,这个特殊的提交-
5a73c6bdc7...
-是最近的master
提交,在我写这篇文章的时候,所以如果GitHub查看这个仓库中的 * 分支名称 *,他们会立即看到5a73c6bdc7...
是master
的 * 提示提交 。如果,当你读到这篇文章的时候,GitHub的refs/heads/master
名称定位到了某个 * 其他 * 提交,GitHub软件很容易判断5a73c6bdc7...
是否是master
的tip提交的祖先,如果是,5a73c6bdc7...
仍然可以从master
访问,因此仍然在master
分支上。但是,如果我们在其他仓库中选择了其他提交,那么可能 * 该 * 提交 * 无法 * 从任何分支名称访问,如果是这样,则满足引号中子句的第一部分:
此提交不属于此资源库上的任何分支️ This commit does not belong to any branch on this repository
我们可以到此为止,或者推测
git gc
最终会移除这个提交(如果可以通过其他名称找到提交,比如标记名,git gc
* 不会 * 移除提交。你可以只通过标记名找到提交,而不是任何分支名。GitHub是否会对这样的提交发出这样的警告取决于GitHub)。但他们接着补充道:
并且可以属于储存库外部的分支。
这是GitHub特有的。fork不是Git的一部分:它们是GitHub的附加组件(这个附加组件也可以在其他托管网站上找到,但据我所知,GitHub是最早出现的。Bitbucket和GitLab似乎是以GitHub的分支为模型的)。
GitHub上的fork是服务器端的一个克隆,它增加了一些特性,包括能够发出Pull Request(这是GitHub的另一个附加功能)。要使这些Pull Request工作,GitHub内部使用了一些Git已经实现了几十年的技巧(至少从2005年的Git v1.0.0开始)其中一个技巧是Git可以在 * 其他仓库的对象数据库 * 中查找Git对象。这意味着如果你在GitHub上有某个仓库Ryou,其他人可以拥有不同存储库Rse(
se
代表其他人),他们从你的Ryou派生而来。他们可以自己提交并发送到Rse...然后,在以后可能适用的任何条件下,你可以使用一个URL将 * 他们的 * 提交哈希ID嵌入到 * 你的 * 仓库名称下,由于这类alternates技巧,看到 * 他们的 * 提交,即使它在 * 他们的fork * 中。7所有这些的结果是,你可以查看 * 他们 * 仓库中的提交,他们已经向 * 你 * 提出了拉取请求,就好像它在 * 你的仓库 * 中一样.当你这样做时,你肯定会触发同样的"不属于这个仓库中的任何分支"条件,这会产生你在这里看到的警告:
此提交不属于此仓库上的任何分支,可能属于仓库外的分支。️ This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
在这个特殊的例子中,提交确实不在GitHub上你的仓库里,所以没有办法从Ryou里删除它。它不在Ryou里,它在Rse里。你可以从Ryou里看到它。
从警告中你无法判断是哪一个潜在条件触发了警告。你所知道的是你现在查看的提交无法从Ryou中的任何 * 分支名称**到达。这可能是因为它 * 是 * 可到达的,但无法从分支名称到达;这可能是因为它 * 是 * 可达的,并等待被GC-ed;也可能是因为它在别人的仓库里。
在这三种情况下, 你 * 都不能直接删除提交本身。在一种情况下,
git gc
可能会自己删除它,但你不能让GitHub运行git gc
。8在一种情况下--例如,如果你有一个提交的标签--你可以做一些事情,然后 * 使 *git gc
能够自己删除它。它不是你可以删除的,即使你可以让git gc
来做。7同样的规则也适用于你的提交:如果他们知道哈希ID,他们就可以在 * 他们的fork * 中看到这些提交。这显然有安全隐患,我不知道GitHub可能对此做了什么。GitHub有很多非常有能力的程序员,他们可能已经把这一切做得非常安全,所以你只能在他们向你提交PR时才能看到他们的提交。而且他们只能看到你公开的提交。我只是指出,在底层,不小心使用"alternates"会带来各种安全问题,所以如果你使用它,一定要小心。
8 * GitHub支持 * 可以为你 * 运行
git gc
*,但你必须联系他们才能启动进程。从这个意义上说,你 * 可以 * 让他们运行git gc
,但这是间接的。