我在 GIT 分支中只有一个提交。如何删除那个提交?

I have only one commit in a GIT branch. How to delete that one commit?

我想从只包含一个提交的分支中删除一个提交。

我尝试使用

删除那个提交
git reset --soft HEAD~1
git push origin +dev --force

我能够删除所有提交。但是无法删除最后一次提交。

首先,您应该到您的分行结账 git checkout yourbranch

其次,查看命令 git log --oneline 的输出,它以 [HASH] commit message 格式显示提交列表。您应该在要删除的那个之前(在列表下方)复制提交的提交哈希。

第三,执行 git reset --hard [copied hash],您的分支将重置为之前的提交。

现在,如果需要,您可以强制将分支推送到服务器。

使用 git log

获取您提交的哈希值

然后去另一个分支最有可能的主要git checkout main

将你的提交暂时放在那里

git cherry-pick [commit-hash]

现在您可以简单地删除您的分支

// delete your branch locally
git branch -d [branch-name]

// delete your  branch remotely
git push origin --delete [branch-name]

现在您可以使用

git reset --soft HEAD~1

并重新创建您的分支

[Using git reset,] I was able to delete all the commits. But unable to delete the last commit.

您实际上无法删除 任何 提交。你实际上并没有删除它们,你只是让它们很难找到。您不能为 last 提交执行此操作的原因很简单:分支名称 always selects 一些 提交。在 Git.

中没有空分支这样的东西

事实上,在 Git 中, 分支名称 并不重要。它们使我们(人类和 Git 两者)有可能 find 提交,但重要的是 commits。要正确理解这一点,我们必须看看 Git 如何“看到”提交。

Git 存储库的核心是数据库的集合:

  • 有一个数据库,通常是迄今为止最大的数据库,用于存储提交和其他支持 object。这就是 git clone 副本:克隆是大数据库的副本。

    (这个数据库是作为数据库实现的。它是一个简单的key-value store,其中的键是散列IDobject IDgit log 打印为提交编号的那些又大又丑的 random-looking 东西。有不止一种类型的内部 object ,但提交是您一直看到的内容。)

  • 有一些较小的包含 names 等。这些目前是一种俗气、蹩脚的实现,不是合适的数据库,但它们大多像简单的 key-value 存储一样工作,其中名称是键,值是那些丑陋的大哈希 ID 之一。

这意味着每个名称——在这种情况下,每个分支名称——只包含 one 哈希 ID,对于 one commit .

commits 构成了实际...好吧,这里常用的词是 branches,但我们正在努力定义 branch,所以让我们避免这个词,只说“我们关心的东西”。我们知道,基于以上内容,每个提交都是 编号 和哈希 ID。这些哈希 ID 对于提交是唯一的:没有两个提交可以具有相同的哈希 ID。1 我们需要知道的其余部分是:

  • 每个提交存储两件事:数据——每个文件的完整快照——和元数据。

  • 在元数据中,Git存储了一些previous commit(或commits,复数,for merge commits,or for at至少一个特例,没有 之前的提交)。

这使得提交形式 backwards-looking chains。也就是说,假设我们有一个只有三个提交的小型存储库。这三个提交具有 random-looking 哈希 ID,但要 绘制 它们,我们将只使用单个大写字母:A 表示我们所做的第一个提交,B 第二个,C 第三个。

提交 C,在此 Git 存储库中,存储提交 B实际 哈希 ID(无论它是什么),它在我们制作 C 时存在。所以我们说C指向B:

    B <-C

但是提交 B 在其内部存储了早期提交 A 的实际哈希 ID。所以 B 指向 A:

A <-B <-C

提交 A,作为有史以来的第一个提交,不存储 任何 更早的哈希 ID。 (Git 称其为 root 提交。)它只是独立存在。

要使用这三个提交,我们需要能够找到它们的哈希 ID。所以 Git 为我们创建了一个 分支名称 就像 maindev:

A--B--C   <-- dev (HEAD)

在这里我懒得在提交之间绘制内部箭头:不过没关系,因为每个提交的所有部分都是 read-only,一直冻结,包括向后指向的箭头。由于它们不能改变,我们知道它们指向后方。某些未来提交的哈希 ID 是未知且不可预测的,2 而过去提交的哈希 ID 是一成不变的,因此可以将 [=202= 的哈希 ID 刻在石头上]旧提交。

如果我们强制名称 dev 向后退一步,会发生以下情况:

     C
    /
A--B   <-- dev (HEAD)

请注意,提交 C 删除。只是我们 find 通过 Git 将名称(例如 dev)转换为哈希 ID 来提交。 names 中存储的哈希ID可以更改,现在dev找到B,而不是C . B 向后指向 A,所以我们只 看到 提交 BC.

再这样做一次会产生:

  B--C
 /
A   <-- dev (HEAD)

同样,没有任何提交,但现在我们只看到 提交A

我们无法完全停止看到 A:如果我们强制 dev 返回提交 B,它会重新出现,但 A 是仍然存在,如果我们强制 dev 返回提交C,所有三个提交都回来了,A 仍然存在。或者,我们可以使用现有三个中的任何一个作为其 parent 进行一些新的提交 D。使用 A 作为它的 parent 给我们:

  B--C
 /
A
 \
  D   <-- dev (HEAD)

换句话说,根提交似乎很特别。事实上它确实如此,但它并不 那么 我们 不能 对此做任何事情。有一个标志,--orphan,我们可以给 git checkoutgit switch,使我们进入特殊模式。


1Git 只能随机保证这种唯一性。这就是哈希 ID 如此大和丑陋的原因:两个 Git 哈希 ID 之间只有 1-in-2160 意外碰撞的可能性,它们是“保证”是独一无二的。 pigeonhole principle 告诉我们这种方法 必须 会失败 总有一天 ,但是哈希的大小将那一天放在足够远的未来避免需要关心它。或者至少,这就是这里的希望:但这完全是另一个话题。

2实际哈希 ID 是加密哈希函数 运行 在所有(元)数据 in[=287= 上的输出] 提交。这包括不可预测的输入,例如 date-and-time-stamp 将在 下一次提交。由于哈希本身对每个输入位都很敏感,而我们不知道输入位是什么,我们也不知道未来的哈希 ID 是什么。 (这也是 为什么 我们不能更改提交中的任何数据:这样做会破坏散列。Git 验证散列 ID 我们 用于检索数据在对检索到的数据进行哈希处理时与获得的哈希ID相匹配,从而自动检测任何错误。)


创建根提交

让我们考虑一下新的 totally-empty 存储库的情况:

[no commits at all]

完全没有提交,我们的初始分支名称——mainmaster 或其他可能的名称——如何指向某个现有的提交?

答案是:不能。规则是:分支名称 必须 指向某个提交。所以分支名称不能满足它的规则。

Git对这个问题的解决方法很简单:不要创建分支名称。这意味着我们在某个分支 mainmaster 上, 不存在 。 Git 将其称为 孤儿分支 未出生的分支

当我们处于这种状态时,运行ning git commit 将——如果成功的话——写出一个 root 提交和 创建 分支:

A   <-- main (HEAD)

现在我们在分支 main 上,它现在已经存在,现在我们有了根提交 A

假设我们做了三个提交,然后做了一个 dev 分支(它也指向 C)然后强制 dev 一直回到 A:

  B--C   <-- main
 /
A   <-- dev (HEAD)

如果我们现在想创建一个新的 根提交,我们需要在一个尚不存在的分支上创建相同的 状态。我们需要一个未出生的或 孤儿,分支:

git checkout --orphan newbranch

现在我们可以正常工作并进行新的提交。新提交将是 新根提交。现有的三个提交继续存在:

  B--C   <-- main
 /
A   <-- dev

但是我们有另一个新提交,D,它在我们的新分支上:

D   <-- newbranch (HEAD)

并且newbranch(仍然)是我们当前的分支。

您不能删除提交,但可以放弃它们

让我们来 repository-so-far:

  B--C   <-- main
 /
A   <-- dev

D   <-- newbranch (HEAD)

强制名称devmain指向提交D,像这样:

git branch -f dev HEAD
git branch -f main HEAD

现在我们有:

A--B--C

D   <-- dev, main, newbranch (HEAD)

All 名称查找提交 D。我们现在可以切换到 devmain 并删除名称 newbranch,如果我们愿意:它不再需要任何东西,因为其他两个名称找到提交 D .

那三个 A-B-C 提交呢?该问题的答案是:它们仍在存储库中,但除非您知道或可以找到它们的哈希 ID,否则您甚至 看不到它们。他们被遗弃了。

Git 将——最终,有一天,也许——垃圾收集 (git gc) 放弃提交。这里的细节取决于很多因素。一些托管站点,例如 GitHub,在清除废弃的提交方面非常糟糕;其他人可能更擅长。在您自己的笔记本电脑上,您可以强制 Git 加速通常的垃圾收集,但默认情况下,放弃的提交将至少保留 30 天,以防您想要取回它们。

挂在“已删除”提交上的机制称为 refloggit reflog 将向您显示已保存的哈希 ID。 (这是存储库中的另一个数据库或一系列数据库。您不应该过分依赖这些 name-to-ID 数据库中任何一个的确切实现,因为 Git 核心组现在正在研究处理它们的新方法。旧的方法在很长一段时间内都运作良好——大约二十年了——但压力在某些地方显现出来。)

结论

您不能从分支中“弹出”最后一次提交,因为分支名称——这是我们找到构成分支的提交的方式(或“DAGlet”,具体取决于分支一词的含义)首先)——必须指向某个提交。所以没有一个分支是真正的 empty.

通常当我们查看一个分支时,我们希望查看该分支的一些selected子集:我们可以找到的提交,从一个通过分支名称找到的,然后向后工作直到......某个时候。通过仔细选择截止点,我们可以假装我们有一个空分支:

...--G--H   <-- main, dev

如果我们列出“在”maindev 上的提交,它是相同的列表。例如,如果我们要求 dev 上提交,但不在 main 上提交——我们通过 git log main..dev 获得;注意这里的两个点——然后我们会看到一个空列表。一旦我们 git checkout dev 并添加 new 提交:

...--G--H   <-- main
         \
          I--J   <-- dev (HEAD)

然后 main..dev 将 select 只是那两个提交,JI,在 Git通常的倒序。