我在 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 IDs或object IDs ,git 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 为我们创建了一个 分支名称,例如 main
或 dev
:
A--B--C <-- dev (HEAD)
在这里,我懒得在提交之间绘制内部箭头:不过,这没关系,因为每个提交的所有部分都是只读的,一直冻结,包括向后指向的箭头。由于它们不能改变,我们知道它们指向后退。某些未来提交的哈希 ID 是未知且不可预测的,2 而过去提交的哈希 ID 是一成不变的,因此可以将 旧 的哈希 ID 刻在石头上> 提交。
如果我们将名称 dev
强制倒退一步,会发生以下情况:
C
/
A--B <-- dev (HEAD)
请注意,提交 C
没有被删除。只是我们通过让 Git 将名称(例如 dev
)转换为哈希 ID 来查找提交。 names 中存储的哈希 ID 可以更改,现在 dev
找到 B
,而不是 C
。 B
向后指向A
,所以我们只看到提交B
和C
。
再做一次会产生:
B--C
/
A <-- dev (HEAD)
同样,没有提交到任何地方,但现在我们只看到提交A
。
我们不能让它完全停止看到A
:如果我们强制dev
返回提交B
,它会重新出现,但A
仍然存在,如果我们强制dev
返回提交C
,所有三个提交都回来了,A
仍然存在。或者,我们可以使用现有的三个中的任何一个作为其父级,进行一些新的提交 D
。使用 A
作为它的父级给了我们:
B--C
/
A
\
D <-- dev (HEAD)
换句话说,根提交似乎很特别。事实上确实如此,但我们不能对此做任何事情并不是如此特别。有一个标志 --orphan
,我们可以将其提供给 git checkout
或 git switch
,这使我们处于特殊模式。
1Git 仅随机保证这种唯一性。这就是为什么哈希 ID 如此庞大和丑陋的原因:两个 Git 哈希 ID 之间只有 1-in-2160 发生意外冲突的可能性,它们“保证”是唯一的。 pigeonhole principle 告诉我们这种方法必须失败某天,但是散列的大小将那一天推向了足够远的未来,以避免需要关心它。或者至少,这是希望:但这完全是另一个话题。
2实际的散列 ID 是一个加密散列函数的输出,该函数对提交中的所有(元)数据 in 运行。这包括不可预知的输入,例如将在下一次提交 on 的日期和时间戳。由于散列本身对每个输入位都很敏感,而且我们不知道输入位是什么,所以我们也不知道未来的散列 ID 是什么。 (这也是为什么我们不能更改提交中的任何数据:这样做会破坏哈希。Git 会验证我们用于检索数据的哈希 ID 匹配在对检索到的数据进行哈希处理时获得的哈希 ID,从而自动检测任何错误。)
创建根提交
让我们考虑一下新的、完全空的存储库的情况:
[no commits at all]
在根本没有提交的情况下,我们的初始分支名称(main
或 master
或其他任何名称)如何指向某个现有的提交?
答案是:不能。规则是:分支名称必须指向某个提交。所以分支名称不能满足它的规则。
Git 解决这个问题的方法很简单:不要创建分支名称。这意味着我们在某个分支上,main
或 master
,不存在。 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)
并且强制名称dev
和main
指向提交D
,像这样:
git branch -f dev HEAD
git branch -f main HEAD
现在我们有了:
A--B--C
D <-- dev, main, newbranch (HEAD)
所有名称找到提交D
。我们现在可以切换到dev
或main
并删除名称newbranch
,如果我们愿意:它不再需要任何东西,因为其他两个名称找到提交D
。
那三个A-B-C
提交呢?该问题的答案是:它们仍在存储库中,但除非您知道或可以找到它们的哈希 ID,否则您甚至无法看到它们。他们被遗弃了。
Git 最终会——最终,也许有一天——垃圾收集 (git gc
) 放弃提交。这里的细节取决于很多因素。一些托管网站,例如 GitHub,在擦除废弃提交方面非常糟糕;其他人可能更擅长。在您自己的笔记本电脑上,您可以强制 Git 加快通常的垃圾收集速度,但默认情况下,放弃的提交将保留至少 30 天,以防您想找回它们。
挂在“已删除”提交上的机制称为 reflog,git reflog
将向您显示保存的哈希 ID。 (这是存储库中的另一个数据库或一系列数据库。您不应过分依赖任何这些名称到 ID 数据库的确切实现,因为 Git 核心小组正在研究新的方法来处理他们现在。旧方法在很长一段时间内运行良好 - 现在大约二十年 - 但压力正在某些地方表现出来。)
结论
你不能从分支中“弹出”最后一个提交,因为分支名称——这是我们如何找到形成分支的提交(或“DAGlet”,取决于你在第一名)——必须指向某个提交。所以没有一个分支是真正的空。
通常,当我们查看一个分支时,我们希望查看该分支的一些选定子集:我们可以找到的提交,从通过分支名称找到的提交开始,然后向后工作直到……某个点。通过仔细选择截止点,我们可以假装我们有一个空分支:
...--G--H <-- main, dev
如果我们列出“开启”main
或 dev
的提交,它是相同的列表。如果我们要求,例如,提交在 dev
,但不在 main
——我们通过 git log main..dev
得到;注意这里的两个点——然后我们会看到一个空列表。一旦我们 git checkout dev
并添加 new 提交:
...--G--H <-- main
\
I--J <-- dev (HEAD)
那么main..dev
将按照 Git 通常的倒序选择这两个提交,J
和 I
。
【讨论】:
以上是关于我在 GIT 分支中只有一个提交。如何删除那个提交?的主要内容,如果未能解决你的问题,请参考以下文章