如果一个分支只有一个提交,我该如何删除第一个提交?

3
我在第一次提交时搞错了作者信息。
然而,似乎大多数变基或树修改操作都依赖于其他某个提交已经存在。
即使我运行Git的交互式变基命令,我在我的提交列表中看到的只有一行“noop”。 :/(通过使用git rebase -i --root对Git树的root运行我的变基来解决此问题,但是删除我进行的第一个提交的行并没有实际从树中删除它。)
root或第一个提交之上进行变基是不起作用的。
[vagrant@localhost vagrant]$ git rebase -i HEAD~1
fatal: Needed a single revision
invalid upstream HEAD~1
[vagrant@localhost vagrant]$ git rebase -i HEAD
Unknown command: rnoop
Please fix this using 'git rebase --edit-todo'.
[vagrant@localhost vagrant]$ git rebase --edit-todo
[vagrant@localhost vagrant]$ git rebase --abort

以下内容看起来像是一个相关的可能的替代答案,但我认为对于这种特殊情况,需要更多或至少更具体的回答: 从Git分支中删除提交 另一个答案(如何撤消最后一次提交?)导致了以下结果:
[vagrant@localhost vagrant]$ git reset --soft HEAD~
fatal: ambiguous argument 'HEAD~': unknown revision or path not in the working tree.
Use '--' to separate paths from revisions, like this:
'git <command> [<revision>...] -- [<file>...]'
[vagrant@localhost vagrant]$ git reset --soft HEAD

对于那些仍在阅读的人,这里是我之前提到的带有noop的vim缓冲区:

noop

# Rebase 3d1c632..3d1c632 onto 3d1c632 (1 command(s))
#
# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash", but discard this commit's log message
# x, exec = run command (the rest of the line) using shell
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out

对于那些想知道为什么我不直接编辑文件并重新添加到提交中的人,我有错误的作者信息,我在运行“git commit”之后才纠正了它,并且当我打开提交消息进行编辑时更改了作者信息。
也许我可以尝试删除整个.git文件夹之类的东西,但我更喜欢一种更优雅的方式。
此外,如果我不删除整个.git文件夹,我就可以保留我的挂钩和其他东西,我有一种感觉,这种问题的答案将涉及到Git中的基本设计原则,我可能还没有理解。

1
如果你在单一提交中出现了错误,只需在你的仓库中提交即可。你为什么不使用 git commit --amend 呢?我很可能误解了你的问题所在。 - castis
2个回答

3
您可以在--root的基础上进行变基:
git rebase -i --root

要在最后一次提交中简单更改作者,请使用以下命令:

git commit --amend --no-edit --author="The Name <email@domain>"

// , git commit --amend --no-edit --author="...", 在项目的顶层目录中输入以下内容:$ git commit --amend --no-edit --author="Nathan Bassanese <nathan-gitcommit@basanese.com>"。然而,git rebase -i --root 就不一样了。在交互式 rebase 中删除提交后,它仍然存在。 - Nathan Basanese
// 在交互式变基中尝试删除提交行后,我尝试应用“fixup”,以查看是否可以从日志条目中删除它:[vagrant@localhost vagrant]$ git rebase -i --root Cannot 'fixup' without a previous commit ...但是失败了。无论如何,感谢您周到的回答,但在成功测试之前,我不认为我能够问心无愧地将其标记为已接受。 - Nathan Basanese
// 特别需要注意的是,带有 --amend--author 标志的 git commit 命令在一个分支中更改了提交记录,但在另一个分支中未更改。我必须在每个分支上都运行它。 - Nathan Basanese
// , 此外,运行 git rebase -i --root 而不进行提交会导致有趣的结果。 :) - Nathan Basanese
// ,AHA,我想我已经将master分支rebase到了root上,根据$ git lg命令的第一行输出,没有跟踪的文件:*6b64313(HEAD -> master)。然而,现在我似乎成为了root的作者。至少,当我运行$ git log时,我的联系信息显示出来了。 - Nathan Basanese

2
我看到你已经解决了修复一个特定问题的机制。
不过,我会尝试回答你更一般性的问题:
“...我感觉对这种问题的回答将涉及到我可能还不理解的Git基本设计原则。”
以下是涉及到这个特定过程的所有棘手部分。
  1. 分支名称(如master)是可移动的指针,指向一个特定的提交。
  2. 每个提交都指向其父提交。根提交按定义没有父提交。(这里不相关但有用的是:合并提交简单地定义为具有至少两个父提交的任何提交。)
  3. 要创建任何提交,我们需要在分支上,1但要创建一个没有父提交的提交,我们需要使分支没有提交——但请参见第1点:分支名称指向一个提交。
  4. 变基通过在从--onto目标增长的匿名分支上执行重复的git cherry-pick操作来工作。(一旦重复的樱桃拣选完成,新的匿名分支尖端提交将替换原始分支尖端提交,即重新将分支指向新的尖端。)
  5. 单个git cherry-pick通过将提交与其父提交进行比较来工作。例如,如果父提交有三个文件,而被拣选的提交有四个文件,则该提交添加了一个新文件。如果其中一个三个继承的文件也有所不同,则在那个文件中发生的任何更改构成该提交的其余部分(好吧,提交的作者和其他元数据也计算在内:它们也被带入樱桃拣选中)。Git将原始提交转换为一组更改,然后将相同的更改应用于某个其他(已存在的)提交,并从中创建一个新的提交。新提交的父提交是另一个提交。
这些原则形成了一个戈尔迪亚结,--root切入其中。值得注意的是,--root在Git 1.6.2版本中是新的,在1.7.12版本中进行了大量修改(我模糊地记得在那之前使用rebase时遇到了一些问题)。
Git总是必须解决#1和#3之间的紧张关系(在某个时候,git checkout增加了--orphan选项,从而改进并正式化了解决方案)。这里的诀窍在于,当处于所谓的“未出生分支”上时,Git像往常一样记录当前分支的名称在HEAD中,但没有创建分支名称本身。

(了解这一点有所帮助——尽管这是一个不需要担心的实现细节——分支指针当前存储在两个或其中一个地方。Git将这些名称到ID映射存储在.git/refs/中的小型独立文件和/或单个平面文本文件.git/packed-refs中。如果一个分支名称既不在这两个地方,但是在HEAD中,则Git认为这是一个“未诞生的分支”。)

当创建一个新提交时,如果当前状态是“处于未诞生的分支上”,Git会创建一个根提交。新的提交ID成为新创建分支的ID,我们现在不再处于同时在一个分支上的自相矛盾的状态下,而没有提交。

我们仍然有一个问题,那就是git cherry-pick通过比较提交和其父提交的区别来工作。这就是为什么,如果您想要挑选一个合并提交,必须指定哪个父提交:合并是具有两个或多个父提交的提交,因此不清楚与哪个进行比较。但是对于一个具有零计数父提交的根提交呢?

Git通过将挑选的根提交与空树进行比较解决了这个特定的问题。由于空树为空,每个文件都是新添加的,这是挑选的正确结果。
因此,要使用--root进行变基,我们需要创建一个孤立分支,然后像往常一样挑选根提交和其余所有提交。但是,git rebase使用的是匿名分支。创建孤立分支的唯一正常方法是按名称,但是变基使用“分离的HEAD”模式。此外,包括第一个在内的每个挑选都需要应用更改到现有提交,但是为了创建一个新的根提交,我们必须避免使用某些现有的父提交。
这个复杂的内部操作(间接地)可以在VonC所提供的答案中找到,其中链接到了Git代码中的此次提交。rebase脚本创建了一个真正的空根提交(使用空树和git commit-tree内部的“plumbing”命令来创建具有用户指定父ID的提交对象),然后生成一个内部的“squash”操作来挑选现有的根提交(通过与同一空树进行差异比较的cherry-pick)并将其与虚拟的空根提交组合起来。
在rebase期间,“压缩”操作意味着“修改先前的提交”,就像git commit --amend一样。 --amend操作告诉git commit创建一个新的提交——现有的提交不能被更改——但是使新提交的父级与现有提交的父级匹配。 这会将旧提交推到一边:分支现在指向新提交,新提交指向“修改后”的提交的父级(们)。 修改后的提交仍然在存储库中,但是除非有其他方法找到它,否则它不再是可见的
这个两步骤的技巧意味着作者信息被特殊处理。
为了此要求的目的,至少在分离 HEAD 模式下(例如,在执行“git checkout ”或“git checkout --detach ”后),被视为处于特殊的匿名分支上,其顶端是当前提交的哈希值。

// 我会在睡眠充足后再仔细阅读这段内容,但我的第一印象是,我难以删除“第一个”提交的困难来自于上述第1点和第3点之间的矛盾。感谢您提醒我“空树”的存在,并给出了周到的答复。 - Nathan Basanese
2
Git 通过不让分支指针移动,直到有替换提交,解决了这个问题,也就是说,从未删除那个首次提交。(事实上,除了 git gc 和其管道工具,Git 永远不会删除 任何 提交或其他对象。它围绕着添加新内容的构建,不会对任何现有对象进行任何更改。这来自哈希的性质,它们是内容敏感的。)当计划从现有提交 E 开始并添加新的挑选提交时,而你的第一个选择结果应该没有父级时,你就有问题了。 :-) - torek
// 我猜能否删除第一个提交的最终答案应该是坚定的“不行”? - Nathan Basanese
Git 的一个特点是你永远不会“删除”任何东西,只是让它变得“不可达”。从某些外部引用(如分支标签)开始可以到达的图中节点会被显示出来,而不能被到达的节点则不会被显示。使用 --amend 不会改变或丢弃任何内容,只是添加新内容。Rebase 不会改变任何内容,只是添加新的东西。即使 git filter-branch 也只是添加新的东西:如果过滤整个存储库,则会复制整个存储库。最终,git gc 会移除未被引用的对象。(并且,克隆只会复制引用的对象。) - torek
创建任何提交,我们需要在分支上。这是什么意思?git checkout <sha> && touch foo.txt && git add . && git commit -m "foo"可以正常工作。 - Joseph Silber
@JosephSilber:在这种分离 HEAD 的情况下,新提交是创建在(单个,特殊的)匿名分支上的。虽然在 git status 的措辞中它并不是“在分支上”,称其为“在特殊的匿名分支上”有点奇怪,但请考虑一下你刚才说的话:git checkout <sha>:我们必须有一个现有的提交,而新提交将以 <sha> 作为其父提交,这意味着新提交不会成为根提交。(但至少我会把这个作为脚注添加进去。) - torek

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接