首页 > 解决方案 > git - 如何为 git prune 创建一个无法访问的提交?

问题描述

我正在为 git 编写教育材料,我需要演示如何git prune删除“分离的对象”。我想我可以通过将提交git reset与分支历史记录分离来将其置于分离状态。

这将触发git checkout将提交视为分离,但git prune不会关心它。

我当前的分离提交模拟设置如下:

~ $ mkdir git-prune-demo
~ $ cd git-prune-demo/
~/git-prune-demo $ git init .
Initialized empty Git repository in /Users/kev/Dropbox/git-prune-demo/.git/
~/git-prune-demo $ echo "hello git prune" > hello.txt
~/git-prune-demo $ git add hello.txt
~/git-prune-demo $ git commit -am "added hello.txt"
[master (root-commit) 994b122] added hello.txt
 1 file changed, 1 insertion(+)
 create mode 100644 hello.txt
~/git-prune-demo $ echo "this is second line txt" >> hello.txt
~/git-prune-demo $ git commit -am "added another line to hello.txt"
[master 5178bec] added another line to hello.txt
 1 file changed, 1 insertion(+)
~/git-prune-demo $ git reset --hard 994b122045cf4bf0b97139231b4dd52ea2643c7e
HEAD is now at 994b122 added hello.txt
~/git-prune-demo $ git prune -n
~/git-prune-demo $ nothing

是的,我理解git prune通常不用作独立命令,本质上是git gc.

标签: git

解决方案


TL;博士

您需要先运行git reflog expire --expire-unreachable=now,然后再运行git prune --expire now。即使这样,事情也可能会出错,尽管对于这个特别简单的例子来说,这可能就足够了。

我正在为 git 编写教育材料,我需要演示 git prune 删除分离的提交。

git prune 不过,这不是什么。它的所作所为可以产生这种效果,但仅限于特定条件。重要的是,分离的提交在 Git 中并不是一个定义明确的短语:Git 有一个分离的 HEAD的定义——我们稍后会回到这个——但提交本身要么是可访问的,要么是不可访问的。我认为您的意思是在这里谈论无法访问的提交。

重要的是,git prune处理比提交更通用的对象。Git 有四种类型的对象:提交、树、blob 和带注释的标签。如果满足其他几个条件,Gitgit prune可以删除任何无法访问的对象。不过,在我们到达那里之前,让我们再看一些项目。

纠正误解

我想我可以通过将提交git reset与分支历史记录分离来将其置于分离状态。

根据定义,如果有一些外部名称直接命名提交(或对象)本身,或者命名我们可以到达给定提交的其他对象,则提交(或任何其他 Git 对象)是可访问的。(有关更多信息,请参阅Think Like (a) Git。)使用git reset,我们可以使只能通过当前分支名称访问的提交变得不可访问。例如,如果提交a123456...只能通过当前分支名称访问 - 即,不能通过任何其他分支名称,也不能通过任何标记名称或其他非分支名称引用 - 然后使用git reset调整当​​前分支以使其排除a123456...使得提交无法访问。

这将触发git checkout将提交视为分离...

我认为您在这里谈论的是 Git 所谓的分离 HEAD。

分离的 HEAD 仅仅意味着 Git 的特殊 HEAD 引用,存储为名为 的文件.git/HEAD,包含提交的原始哈希 ID。相反的情况——我们可以称之为附加的 HEAD,因为这是分离的明显反义词——在.git/HEAD包含分支名称时发生。在这两种情况下,都HEAD指当前提交;当HEAD包含分支名称时,HEAD也指当前分支名称。Git 在内部处理这个问题的方式是它有不同的函数和程序来HEAD象征性地解决:

$ git symbolic-ref HEAD
refs/heads/master

或哈希 ID:

$ git rev-parse HEAD
c05048d43925ab8edcb36663752c2b4541911231

(对于分离的 HEAD 情况,git symbolic-ref会产生错误,因为没有分支名称。)

git checkout命令在以下情况下附加 HEAD(到某个指定的分支名称):

  • 你给它一个分支名称,或者
  • 您使用它来创建然后附加到新的分支名称。

它在以下情况下分离 HEAD:

  • 你给它一些可以解析为哈希 ID 的东西,而不是分支名称(例如,原始哈希 ID,或远程跟踪名称,如origin/master),或者
  • 您使用该--detach标志来强制分离 HEAD,即使它通常会附加 HEAD。

分离的 HEAD 模式并不意味着您正在处理无法访问的提交。事实上,将 HEAD 分离到一个原本无法访问的提交,会使该提交突然变得可访问,因为它现在是 HEAD 提交。换句话说,将 HEAD 分离到任何提交都会增加一种到达提交的方式,但就 prune 而言,有趣的问题不是有多少名称到达所讨论的对象,而只是该数字是否非零。一个名字,两个名字,十个名字,或数百万个名字:所有这些都与git prune. 当我在这里说名称时,我的意思不仅仅是参考名称加上可能的 detached HEAD,但在添加下一个复杂功能之前,我们将从这些名称开始。

Git 的对象模型和对对象的引用

Think Like (a) Git很好地描述了引用如何使提交变得可访问。但是,它没有提到,一般来说,引用可以指定任何对象的哈希 ID,而不仅仅是提交。这是因为它关注的是分支,而不仅仅是任何旧对象,并且分支名称 ( refs/heads/*) 和远程跟踪名称 ( refs/remotes/*) 都被限制为仅指向提交。它也没有详细介绍提交中的内容,即 Git 如何存储文件和文件名。这就是树和 blob 对象的用武之地。

每个提交都包含单个树对象的哈希 ID。树对象包含一系列三值项:模式、名称和哈希 ID。该模式指定此树条目是用于文件、子树还是用于更奇特的项目之一(符号链接和 gitlinks)。name 给出了被表示的实体的名称,例如README.txtorsubdirfile.ext。哈希 ID 通常是一个 blob 对象或另一个树对象的 ID:如果条目是用于类似 的文件README.txt,则为 blob 哈希,如果用于类似 的子树subdir,则为子树的哈希 ID。

如果我们为一次提交绘制所有这些内容,从最上面一行右侧的分支名称开始,我们会得到如下内容:

... <-  commit a1234...   <-- branchname
               |
               v
        tree 07f39...: (100644, README.txt, 531c2...); (040000, subdir, ...)
                                               |                         |
                                               v                         |
                                blob 531c2...: data for README.txt       |
                                                                         |
                                                                         v
                                                               tree ...: ...

允许带注释的标记对象指向任何其他对象(包括其他带注释的标记对象),尽管大多数情况下,它们只是指向提交对象。因此,在这张图片中添加带注释的标签,我们通常只会看到一个标签引用,比如refs/tags/v1.0指向带有一些哈希 ID 的带注释的标签对象,然后带注释的标签对象继续指向,比如说,提交a1234...。这将为该提交提供另一个参考。如果我们没有创建任何标签,我们不需要担心这些,但它们对于完整的画面很重要。

与提交一样,如果存在从某个外部名称(或者对于 blob,存储在 Git索引中的内部引用)指向这些对象的路径,则任何对象都会被引用。索引只能引用 blob,所以当我们只对提交感兴趣时,我们可以忽略索引的引用,但就像标签一样,它们对全貌很重要。

无论如何,在上图中,我们可以看到该名称branchname使提交a1234...可达。提交a1234...使树07f39...可访问,这使 blob 和另一个子树可访问,依此类推。由于这些都是可达的,因此git prune绝对不会修剪它们。

重要的是,每个引用名称加上特殊HEAD名称都有一个可选的reflog,用于存储该引用的先前值。这些保存的值会在一段时间内保持有效,直到它们过期。Git 用来使过时的保存值过期的命令是git reflog expire,使用两个不同的命令行选项,和.--expire=when--expire-unreachable=when

如果要显示git prune删除对象,则需要确保该对象完全未被引用。这意味着您将需要删除任何直接(提交)或间接(树和 blob)记住其哈希 ID 的 reflog 条目。这样做的简单(尽管相当具有破坏性)方法是使用:

git reflog expire --expire-unreachable=now --all

(我们可以添加--expire=now,但我们可以假设引用的当前值未达到 reflog 值,因此该--expire-unreachable设置将适用。)

这设置了一个必要条件;现在是时候回归git prune自我了。

抛开所有这些,让我们回到git prune

git prune命令处理所有四种类型的对象。它的工作是删除未引用的对象。从上面我们知道,我们必须确保提交未被引用,通过使任何可能记住它的 reflog 条目过期,在使用类似git branch -forgit branch -D的命令之后git reset确保没有分支名称记住它。

但是现在我们需要了解关于 Git 对象的另外两件事:

  • 它们可以是松散的,也可以是包装的,并且
  • 他们有一个年龄,就像 reflog 条目一样。

松散的对象存储在文件系统中的单独文件中。这使得 Git 很容易操作它,但意味着它被最小化压缩。Git 将根据命令(或自动通过git gc)将许多单独的对象打包到一个单独的文件中。此时,文件系统中的一个文件包含许多对象:可能是数十个,可能是数百万个,或者介于两者之间。

prune命令永远不会修剪打包的对象,因为这太难了。打包对象可能是其打包文件中增量压缩链的一部分。因此,git prune只会查看松散的对象。一个单独的程序——<code>git repack——将重新打包对象,并且可以将未引用的打包对象变回松散的对象(或完全丢弃它们)。

通常,对象不会立即打包,因此最近创建的对象很可能是松散的。但是,如果对象被打包,并且现在未被引用,则您将需要运行git repack

同时,作为对竞争 Git 进程的保护,git prune 检查松散对象的时间戳。此时间戳必须足够老以允许git prune删除对象。这样做的原因是,当 Git 创建对象(包括新提交)时,它会一次将一个(或几个)这些对象写入存储库数据库。Git 必须使用它们的 blob 散列编写最深的子树,然后使用子树及其散列以及这些树中的任何 blob 散列编写下一层树。一旦 Git 写出所有树并获得顶层树哈希以进入新的提交,只有这样 Git 才能写入提交对象。在此之前,所有这些树都未被引用。即使写了提交,那也是也未引用,直到当前分支名称(或 detached HEAD)更新为指向新创建的提交。

这个过程需要时间。默认情况下,Git 给自己 14 天的时间来完成该过程。如果 agit commit需要 14 天以上才能完成,agit prune可能会删除它的一些对象,但 14 天应该足够了。

如果你知道你没有运行任何其他 Git 命令,你可以手动覆盖默认值:

git prune --expire now

意味着任何未引用的、松散的对象都应该被删除,无论它们有多新。因此,您需要做的就是确保您的提交未被引用,然后使用“现在”到期时间进行修剪。


推荐阅读