TL;DR:
在How to undo last commit(s) in Git? 上查看各种答案,或者,跳到最后一部分,“最终示例”。
说明
您的问题中的错误是您没有正确描述您的情况。您不仅添加了各种文件,还提交了它们。这意味着它们现在永久存储在您的存储库中。
permanent 这个词在 Git 中有点奇怪。确实,无论您做什么,提交都是固定的、不变的,并且会在您的存储库中保留很长时间。默认情况下,它们会永远保留在那里,这是任何人每次提交的历史的一部分。 Git 从不删除任何提交:每个 new 提交都保留其先前提交的身份——称为它的 parent——而这个从新到旧提交的向后链 是 存储在您的存储库中的历史记录。
提交本身实际上是由哈希 ID 存储的,Git 向您展示的那些丑陋的东西(有时是缩写),例如 a9b307c 等等。这些哈希 ID 是每个提交的“真实姓名”。它们看起来是随机的,而且人们基本上不可能记住,所以我们要做的是让 Git 将一个 name 附加到某个特定的提交上,例如 master。我们将此提交称为分支的 tip。该提交本身具有 previous 分支提示的哈希 ID。
考虑这个只有三个提交的存储库图,都在master。我将为每个提交使用一个字母的名称,而不是哈希 ID:
A <--B <--C <--master
名称master 存储哈希ID badf00d 或其他任何东西,这是提交C 的实际哈希ID。我们说master 指向 C。同时C存储B的哈希ID;我们说C 指向B。提交B 存储A 的ID,因此B 指向A。
由于A 是第一次提交,它不能指向任何更早的提交。所以它只是没有。我们将 A 称为 root 提交。任何 Git 存储库都必须有第一次提交——嗯,任何有任何提交的 Git 存储库——所以通常总是有一个根提交。该根提交是 Git 可以停止向后遍历的方式。
要添加 new 提交,Git 只需写出提交及其所有文件——每个提交都存储与该提交相关的每个文件——并使其指向当前分支的提示:
A <--B <--C <--D
提交D 获得一个新的唯一哈希,它基于D 中的所有内容(包括C 的哈希ID,以及您的姓名和电子邮件地址以及当前时间)。但是——这是 Git 中分支名称的秘密——Git 现在将新提交的哈希 ID 写入分支名称,因此名称 master 现在指向提交 D:
A <--B <--C <--D <--master
这就是树枝的生长方式。提交完成后,名称指向它,并且提交本身永久存储在您的存储库中。
坏消息
这听起来像是个坏消息:你已经提交这些文件,所以你现在
和他们在一起。而且,这是个坏消息,但也不是致命坏消息。
好消息
但是,如果您努力工作,提交最终可能会被遗忘。此外,当您将提交转移到其他人的存储库(即,转移到 git push 您的新提交)时,Git 只会推送那些可从一个或多个分支访问的提交1 你在推。
因此,您需要在这里做的是“忘记”您的一些提交。你可以通过告诉 Git re-point 分支名称来做到这一点。例如,假设提交D 本身就是问题,我们只想摆脱它。假设我们可以告诉 Git:“嘿,让 master 再次指向 C”——就像这样:
A--B--C <-- master
\
D
提交D仍然存在(它是一种永久性的!)但它不再可从名称master 访问,因为 Git 从提交哈希开始master 命名的 ID,并向后工作。这意味着 Git 不会“看到”提交 D:它不再位于分支 master。
1您可以一次推送多个分支名称。这曾经是 git push 的默认操作,事实上,虽然这被证明容易出错,所以现在默认是只推送 当前 分支名称。
在 Git 中,仔细区分 名称 有时很重要,例如 master 和 develop,以及我称之为“DAGlets”的部分:提交图的部分,例如我们上面画的。提交图是 D 指向 A 循环 Graph 或 DAG。 git fetch 和 git push 都采用分支名称,或任何其他类型的名称。他们在互联网电话的另一端呼叫另一个 Git,并与它交谈:他们给彼此一些这些名称,然后将这些名称转换为适当的哈希 ID。然后,他们根据哈希 ID 和我之前提到的父链接来决定发送或接收哪些提交(和其他 Git 对象)。因为哈希 ID 仅基于每个提交的 内容,如果您的 Git 和他们的 Git 具有相同的 commit,那么这两个提交具有相同的哈希 ID。
关于git reset的知识
在 Git 中移动分支指针的主要方式是使用 git reset。不幸的是,git reset 是一个复杂的命令。
Git 有另外一对重要的特性:当你make 用git commit 提交时,你可以从 东西中做出它们。 “某物”本身实际上是 Git 的 index。索引主要是你构建下一次提交的地方。
当您运行 git add 时,Git 从您的工作树(您工作的地方,其中包含您可以实际使用的文件形式)获取文件并复制它们到索引。当您运行 git add --all 时,Git 会获取所有工作树文件2并将它们添加到索引中。
此时,它们在索引中,但未提交,因此它们不是永久性的。您可以git reset 他们退出索引out。这是git reset 的工作之一:重新设置索引。
但这些文件也在您的工作树中。工作树版本也未提交,因此它们不是永久性的。您也可以git reset 他们,这是git reset 的另一个工作。
当然,git reset 可以移动一个分支名称,这是我们在这种特殊情况下所需要的,因为您确实提交了文件,所以现在它们作为新提交的一部分永久存储。移动分支名称是git reset 的三个主要工作中的第三个。3 这三个工作按重要性排序:
-
git reset 总是移动(重新设置)分支名称;
-
git reset 有时会重置索引;和
-
git reset 偶尔会重置工作树。
您可以使用--soft(仅执行第 1 项任务)、--mixed(执行第 1 和第 2 项任务)和 --hard(执行所有三项任务)来控制这些。
当您执行移动分支的git reset 时,您必须选择是否保留索引和/或工作树。如果您使用git reset --hard,Git 将完成所有三件事。这没关系只要您准备好丢失索引和工作树中的临时内容。
commits 是永久性的,尽管一旦忘记了它们的哈希 ID,就很难找到它们。因此,可以将它们重置:您可以再次将它们取回。此外,您可以使用新名称保存哈希 ID,例如 new 分支名称,然后您可以很容易地将它们全部取回。让我们看最后一个例子。
2全部,即,除了 (a) 未已 在索引中的文件和 (b) 列在 .gitignore 或类似“don “不自动添加”文件。
3git reset 命令还可以做一些更具体的事情,在这种情况下它可以停止做一些主要的工作,但我们不会在这里提及。
最后一个例子
假设您在master 上进行了许多 次提交,而不仅仅是一个,其中包含太多文件。你想记住(保存)你的工作,但也扔掉索引和工作树,让 master 重新与你的“上游”存储库同步,你克隆的那个,你正在给你的 Git 打电话origin。
你的提交图有一些看起来像这样的东西:
...--o--o--o <-- origin/master
\
X--o--o--o--Y <-- master (HEAD)
你在你的 master 分支上,它上面有所有底行的提交,加上你和origin 的其他 Git 共享的所有顶行的提交。
你在第一次提交中犯了一个错误,标记为X。因此,您希望将master 一直重置为指向与origin/master 相同的提交,并丢弃您当前的索引和工作树,因为它们是“干净的”(已提交并匹配您的@ 987654394@,即提交Y)。您可以运行 git reset --hard 来执行此操作,但随后您将忘记从 X 到 Y 的所有哈希 ID。
所以,您可以简单地创建一个 new 分支,指向提交 Y:
git branch save-my-mistake
现在你的图片是这样的:
...--o--o--o <-- origin/master
\
X--o--o--o--Y <-- master (HEAD), save-my-mistake
现在是时候运行了:
git reset --hard origin/master
移动当前分支(仍然是master)指向与origin/master 相同的提交,并重新设置您的索引和工作树以匹配该提交:
...--o--o--o <-- master (HEAD), origin/master
\
X--o--o--o--Y <-- save-my-mistake
现在你可以随时从save-my-mistake 分支中获取任何你想要的东西,因为那个分支名称会为你记住你的提交。
最终,当你完成它时,你可以简单地删除那个分支。这些提交迟早会4被“垃圾收集”并真正从您的存储库中消失。在那之前,您将不再看到它们,因为您(和 Git)可以用来查找难以理解的哈希 ID 的名称已经不复存在。
4过期时间比较棘手。它部分取决于 reflog 条目, 对于“可达”提交,它们在 90 天后到期,对于“不可达”提交,它们在 30 天后到期,根据引用的当前值定义可达和不可达。一旦 reflog 条目本身过期,如果底层 Git 对象 全局 无法访问,即无法从 任何 名称访问,则它们将有资格进行此垃圾回收。从创建之日起,它们仍然有 14 天的宽限期,尽管在大多数情况下,如果 30 或 90 天的时间段已经到期,那么 14 天是很久以前的事了。但是,删除分支名称会删除其所有 reflog 条目,这可能会暴露较年轻的提交,然后会获得 14 天期限内剩余的任何内容。
无论如何,所有这些都是由git gc --auto 驱动的,当其他 Git 命令认为有充分的理由时,它们会运行这些命令。因此,在 那些 Git 命令之一运行 git gc --auto 之前,这些过期的对象可能会一直存在。这与 Git 的非正式名称“版本控制的 Borg”非常吻合:它将所有东西都粘在它的集合中,但它可能会摧毁你的世界。 :-)