git 我如何删除不属于任何分支的提交?

pbpqsu0x  于 2023-02-02  发布在  Git
关注(0)|答案(1)|浏览(224)

如果我把分支A合并到分支B中,然后删除A,那么来自分支A的提交(现在已删除)属于哪个分支?当我得到这些提交的链接时,我发现“这个提交不属于这个仓库上的任何分支,可能属于仓库外的一个分支。”

我尝试了此问题的所有答案,但未解决mu问题Listing and deleting Git commits that are under no branch (dangling?)
解决办法是什么?

bnlyeluc

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"。这些形式的提交形成链,带有向后指向的箭头。简单的情况下,每个提交只有一个向后的箭头,指向它的前任:

A <-B <-C ... <-F <-G <-H

这里,在我们的简单仓库R中,我们正好有8个提交。我们没有使用Git的实际提交哈希ID,而是给它们一个大写字母。(这个方案在真实的仓库中是不切实际的:如果存在多于26个提交,我们会怎么做?但是这对于思考这里的问题是有用的。)我们所做的 * 最后 * 提交H在其自身内存储倒数第二个提交G的散列ID。我们说H * 指向 * GG存储F的散列ID,所以我们说G指向F,这个过程会向后,沿着整个提交链向下,直到我们点击提交A,因为这是第一次提交,所以它不能向后指向,而且它没有:其父散列ID列表为空。
[1]这个特殊的定义稍微向后,因为Git本身是向后工作的。在正常的DAG中,可达性意味着后继,而不是先行。但在Git中,所有的箭头都指向后,而不是前。
2大多数refs拼写为refs/*,但也存在 * pseudo-refs *,如HEADCHERRY_PICK_HEAD。伪refs是特殊情况,会给那些致力于为Git创建合适的ref数据库的人带来麻烦。请注意,伪refs是per-work-tree,但其他一些refs,如对分refs,也是per-work-tree。

可从分支名称访问意味着提交位于分支上

我们从简单的八次提交仓库开始,结尾是:

...--G--H   <-- main (HEAD)

我们已经添加了 * 分支名称 * main,并在main中存储了提交H的真实哈希ID,所以我们说main指向H,与H指向G的方式相同。(为了在Stack Overflow上显示文本/ASCII艺术效果,我未能将从HG的箭头画成 * 箭头:我们只需要记住,commits只会向后链接,没有从GH的链接,反之亦然。
这个设置意味着名称main允许我们访问仓库中的8个提交中的任何一个,现在让我们再添加两个分支名称br1br2,它们都指向提交H

...--G--H   <-- br1, br2, main (HEAD)

所有三个 * name * 都指向提交H。所以所有八个提交都是从 * all names**可达的。这意味着所有提交都在所有分支上。
这里HEAD连接到main意味着我们 * 使用 * 的分支名称是HEAD,因此当前提交是H.让我们现在运行git checkoutgit switch,来改变HEAD连接到哪个 * name *:

git switch br1

这导致:

...--G--H   <-- br1 (HEAD), br2, main

此时唯一 * 改变 * 的是HEAD现在附加到br1,所有八个提交仍然存在;我们仍然使用提交H;但是现在我们使用H * 通过 * 名称br1

现在我们用通常的方式做一个新的提交,这个新的提交有一个新的唯一的哈希ID,但是我们称之为“commit I“,为了保持我们的理智,我们需要画一个从I指向H的箭头,并使名称br1指向I。因为Git内部就是这样处理的

I   <-- br1 (HEAD)
         /
...--G--H   <-- br2, main

我们现在使用提交I,通过名称br1
如果我们添加另一个新的提交J,我们得到:

I--J   <-- br1 (HEAD)
         /
...--G--H   <-- br2, main

我们现在使用提交J通过名称br1.现在我们切换到br2

git switch br2

          I--J   <-- br1
         /
...--G--H   <-- br2 (HEAD), main

现在我们再次使用commit H,通过名称br2,如果我们再提交两次,我们得到:

I--J   <-- br1
         /
...--G--H   <-- main
         \
          K--L   <-- br2 (HEAD)
  • 瞧 *,我们有了“分支”!提交I-J可以说是“属于”分支br1,提交K-L可以说是“属于”分支br2,但是从HH的提交呢?有些人会说这些提交“属于”main,但是Git没有这样的区分:当我们第一次给br*分支命名时,所有的提交都是在所有的三个分支上,而且这些提交仍然在所有的三个分支上,只是现在 new commits I-J * 只 * 在br1上,而 new commits K-L * 只 * 在br2上。

当我们使用git merge时,我们并不是在真正的“合并分支”,而是在真正的“合并提交”。

git switch br1
git merge br2

git switch通过将HEAD附加到br1,使提交J成为 * 当前提交 *。git merge让Git定位不是一个,不是两个,而是 * 三个 * 提交:

  • 当前的/HEAD提交,J;
  • 我们在命令行中命名的提交:br2指向L;以及
  • 合并基础提交。

合并基是通过最低公共祖先算法定义的,其输入是提交JL,并且该算法计算出提交H的散列ID。
合并本身是通过比较HJL中存储的快照来实现的,这使得Git能够计算出“我们在H-I-J链上做了什么”,以及“他们在H-K-L链上做了什么”。(注意,提交IK只用于它们的 * 链接 *,而不是它们的快照:两者都链接回提交H,这使得提交H成为合并基。
如果一切顺利,Git会自己创建新的合并提交。这个新的合并提交M有两个父提交,两个向后指向的箭头链接到JL,如下所示:

I--J
         /    \
...--G--H      M
         \    /
          K--L

我暂时从绘图中删除了所有的 * 分支名称 *,因为我们不需要它们:提交独立于任何 * 分支名称 * 而存在,但在Git中创建一个新的提交总是做同样的事情:

  • 当我们提交I时,Git将新提交的哈希ID写入当前分支名称br1;
  • 当我们提交J时,Git将新提交的哈希ID写入当前分支名称br1;以及
  • 当我们提交KL时,Git将新提交的哈希ID写入当前分支名称br2;
  • 现在我们创建了M,Git将M的哈希ID写入当前分支名称br1
I--J
          /    \
 ...--G--H      M   <-- br1 (HEAD)
          \    /
           K--L

名称mainbr2仍然存在,并且仍然指向HL。在这个ASCII艺术中,没有空间在main中绘制,并且现在没有 * 需要 * 在br2中绘制。我们可以问:* 哪些提交可以从名称br1到达?* 答案是:* 所有人 *
提交K-L * 以前 * 只在br2上,但是现在,由于合并提交M,提交K-L在 * 两个 * 分支上。所以这就给了我们你最初问题的答案,只要我们稍微修改一下:在一个真正的合并之后,删除一个分支名称是“安全的”,因为提交仍然是可以通过合并提交找到的。它们现在“在”两个分支上,并且去掉一个名称--我们现在不使用的名称,在这个例子中是br2--仍然留下至少一个它们“在”的其他名称。

警告:不是所有的合并都是真合并

虽然git merge命令有时会使合并提交M

I--J   <-- br1
         /
...--G--H
         \
          K--L   <-- br2

我们也可以想出其他的情况

...--P--Q   <-- br3 (HEAD)
         \
          R   <-- br4

这里,git merge br4将执行 * 快进 * 操作而不是合并,生成:

...--P--Q--R   <-- br3 (HEAD), br4

在快进的情况下,删除br4仍然是安全的:过去仅“在”br4上的提交R现在也“在”br3上。
但是我们也可以运行git merge --squash,这个特定的选项会指示git merge进行一个 non-merge“squash”提交:

I--J   <-- br1 (HEAD)
         /
...--G--H
         \
          K--L   <-- br2

[we now run:
    git merge --squash br2
and a second Git command that we're forced to run, to get:]

          I--J--S   <-- br1 (HEAD)
         /
...--G--H
         \
          K--L   <-- br2

新提交Sgit 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

I--J--S   <-- br1 (HEAD)
         /
...--G--H
         \
          K--L   <-- 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说:
此️提交不属于此仓库上的任何分支,可能属于仓库外的分支。
这是什么意思?

  • direct* 的意思是 * 这个提交不能从这个仓库中的一个分支名称到达 *。我刚才引用的短语和我更精确的替换短语都有两个this形容词,它们都起着限定词的作用:一个特定的提交--可能是您在浏览器中显示的提交--以及一个特定的仓库,Git通过该仓库 * 找到 * 了提交。

我们刚刚说过,我们通常使用名称来查找提交。但实际上,我们使用哈希ID在仓库的对象数据库中查找底层提交 * 对象 。哈希ID * 是 * 提交的“真实名称”。我们使用名称找到的是 * 哈希ID,而不是提交对象本身。如果我们手头有哈希ID,这就是我们所需要的--当我们使用浏览器查看GitHub仓库提交时,我们提供提交哈希ID。例如,URL https://github.com/git/git/commit/5a73c6bdc717127c2da99f57bc630c4efd8aed025a73c6bdc7...结尾。这是一个提交哈希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,其他人可以拥有不同存储库Rsese代表其他人),他们从你的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,但这是间接的。

相关问题