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

Posted

技术标签:

【中文标题】我在 GIT 分支中只有一个提交。如何删除那个提交?【英文标题】:I have only one commit in a GIT branch. How to delete that one commit? 【发布时间】:2021-12-08 02:07:51 【问题描述】:

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

我已尝试使用删除该提交,

git reset --soft HEAD~1
git push origin +dev --force

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

【问题讨论】:

【参考方案1】:

首先,您应该结帐到您的分支git checkout yourbranch

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

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

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

【讨论】:

我试过了,但它显示“一切都是最新的”。 $ git log --oneline 18eff24 (HEAD -> dev, origin/dev) 在 AddUser 服务中添加了字段 $ git reset --hard 18eff24 HEAD 现在是 18eff24 在 AddUser 服务中添加了字段 $ git push origin +dev --force 一切都是最新的 你的git log --oneline 输出是什么?它是否包含您要删除的提交? $ git log --oneline 18eff24 (HEAD -> dev, origin/dev) 在 AddUser 服务中添加了字段 dev 分支只包含一个提交【参考方案2】:

使用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

并重新创建您的分支

【讨论】:

【参考方案3】:

[使用git reset,] 我能够删除所有提交。但无法删除最后一次提交。

您实际上无法删除 任何 个提交。你实际上并没有删除它们,你只是让它们很难找到。您不能为 last 提交执行此操作的原因很简单:分支名称 always 选择了 some 提交。在 Git 中没有空分支之类的东西。

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

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

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

(此数据库作为一个数据库实现。它是一个简单的key-value store,其中的键是hash IDsobject IDsgit log 打印为提交编号的那些看起来很丑的大东西。有不止一种类型的内部对象,但提交是您一直看到的那些。)

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

这意味着每个名称(在本例中为每个分支名称)仅包含 one 哈希 ID,用于 one 提交。

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

每次提交都会存储两件事:数据(每个文件的完整快照)和元数据。

在元数据中,Git 存储一些previous 提交(或commits,复数,用于合并提交,或者对于至少一种特殊情况,noprevious提交)。

这使得提交形成向后看的。也就是说,假设我们有一个很小的存储库,其中只有三个提交。这三个提交的哈希 ID 看起来很随机,但要绘制它们,我们将只使用单个大写字母:A 用于我们进行的第一个提交,B 用于第二个提交,@987654328 @ 第三个。

提交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)

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

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

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

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

再做一次会产生:

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

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

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

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

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


1Git 仅随机保证这种唯一性。这就是为什么哈希 ID 如此庞大和丑陋的原因:两个 Git 哈希 ID 之间只有 1-in-2160 发生意外冲突的可能性,它们“保证”是唯一的。 pigeonhole principle 告诉我们这种方法必须失败某天,但是散列的大小将那一天推向了足够远的未来,以避免需要关心它。或者至少,这是希望:但这完全是另一个话题。

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


创建根提交

让我们考虑一下新的、完全空的存储库的情况:

[no commits at all]

在根本没有提交的情况下,我们的初始分支名称(mainmaster 或其他任何名称)如何指向某个现有的提交?

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

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

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

A   <-- main (HEAD)

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

假设我们进行了三个提交,然后创建了一个dev 分支(它也指向了C),然后将dev 强制返回到A

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

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

git checkout --orphan newbranch

现在我们可以按照通常的方式工作并进行新的提交了。新的提交将是一个新的根提交。现有的三个提交继续存在:

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

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

D   <-- newbranch (HEAD)

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

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

让我们来看看我们的存储库:

  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)

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

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

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

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

结论

你不能从分支中“弹出”最后一个提交,因为分支名称——这是我们如何找到形成分支的提交(或“DAGlet”,取决于你在第一名)——必须指向某个提交。所以没有一个分支是真正的

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

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

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

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

那么main..dev 将按照 Git 通常的倒序选择这两个提交,JI

【讨论】:

以上是关于我在 GIT 分支中只有一个提交。如何删除那个提交?的主要内容,如果未能解决你的问题,请参考以下文章

git 如何删除远程提交方法总结

如何从 Git 历史记录中永久删除提交?

git:分支管理(分支的创建使用合并删除)

如何从 Git 主分支中间删除一些提交? [复制]

git删除所有提交历史记录

如何查看 git 中分支之间的提交差异?