Git:去了旧提交,现在我不能回来了



我使用以下内容移动到一个旧的提交,其中包含一些我已删除的代码:

git checkout xxx .

但是现在我想回到我的工作中,我尝试了以下内容:我没有隐藏或提交我的最新代码

git checkout xxx/xxxx .

xxx/xxx 是我一直在工作的分支的名称。

但是我的文件没有改变,它们仍然包含所有旧代码,没有包含我的新代码!

任何帮助将不胜感激。

恐怕par的答案可能是正确的。 但是,如果您git add编辑了文件(即使没有git commit结果),您可能很幸运。 如果这都是"TL;DR",您可以跳到下面的最后一部分。

为了清楚起见,让我们首先注意基本上有三种或四种形式的git checkout1

  • git checkoutbranchname
  • git checkoutcommit-ish
  • git checkoutcommit-ish--path[path... ]
  • git checkout --path[path... ]

第一种形式,你给git checkout一个分支名称,没有路径参数,试图把你放在分支上。 例如,git checkout master会让你处于分支master(或者失败并且什么都不做)。

在第二种形式中,你git checkout提供解析为特定提交 ID 的东西,但由于第一种形式,它不是普通的本地分支名称,签出指定的提交并给你一个"分离的 HEAD"。 (或者,同样,它可能会失败并且什么都不做。 这在内部的工作方式是,你离开你之前使用的任何分支,现在在一个新的、临时的、匿名的(未命名的)分支上,该分支在你刚刚签出的提交处结束。

第二种形式是查看旧提交、重建它们、签出标签并构建该标签或类似内容的常用方法。 例如,您可以git checkout c0ffee1(哈希 ID)或git checkout v2.7.2(标签)。 但这不是你所做的。

第三种和第四种形式在行动上非常不同。 如果我负责 Git 世界,它们根本不会被拼写git checkout,因为它们不会将您带到另一个提交。 它们对HEAD没有影响。 如果你在一个分支上,你留在那个分支上。 如果您处于具有特定提交的分离 HEAD 模式,则在相同的特定提交中保持分离的 HEAD 模式。 但是现在我们进入第一个复杂情况,因为现在我们必须讨论索引和工作树。

前两种形式的git checkout相对安全,因为它们确保您不会丢失索引和工作树中的文件。 您可以使用-f/--force标志请求它们覆盖文件。 但是,第三和第四种形式的git checkout并不安全。 Git 将每个path参数视为覆盖给定文件或目录的请求。

commit-ish参数是 Git 可以用来查找提交的任何内容。 这可以是原始哈希 ID,如c0ffee1或标签,如v2.7.2,甚至是分支名称,如master。 请注意,我们将分支名称拆分为git checkout的特殊(第一个)形式,但这仅适用于没有path参数的情况。 当你给出一些path参数时,像master这样的分支名称就不再特别了。

顺便说一下,path之前的--是可选的。 它存在的原因是允许您使用类似于选项的path。 例如,如果你有一个名为-f--force的文件,你需要能够告诉 Git 引用文件-f--force,而不是使用-f--force选项。 如果你有一个名为master的文件,你需要能够告诉 Git 你引用的是名为master的文件,而不是名为master的分支。 如果你省略了--,Git 会最好地猜测你指的是分支名称、提交 ID、选项还是文件名。 在您的情况下,您使用了.,这绝对是一个文件名,因此git checkout xxx .成为第三种形式。 当然,.的意思是"整个当前目录,包括其所有文件和子目录"。


1表格的确切数量取决于您决定如何计算这些表格。 例如,您可以将前两个折叠成一个表单,将后两个折叠成另一个形式,或者将前两个分开并组合第三个和第四个。 还有git checkout -mgit checkout -pgit checkout --oursgit checkout --theirs,它们都与这四个主要有点不同。 但是在开始使用它们之前不要担心它们。


提交、索引和工作树

提交是 Git 存在的理由。 它们记录了你曾经做过的一切——或者至少是承诺的一切——永远,这样你就可以把它们找回来。 每个提交都保存一组文件的完整快照,这些文件排列在树中(充满文件和子目录等的目录)。 它还保存以前(父)提交的 ID,以及一些元数据,例如您的姓名和电子邮件地址、时间戳以及提交日志消息。 差不多就是这样:提交是一个保存的树加上一些元数据。

工作树也很明显:它是 Git 让你工作的地方。 Git 内部的提交采用只有 Git 本身可以使用的格式,但您需要使用计算机来执行实际工作,处理普通文件。 因此,Git 可以从提交中填充工作树——尽管实际上它必须使用它的索引,正如我们稍后会看到的那样——然后你就有了常规程序、Web 服务器或任何都可以使用的常规文件。 出于许多原因,工作树还可以保存您不会提交以及您从不打算提交的文件。 你会把这些文件作为"未跟踪"的文件保留(Git 会对它们发牢骚,你会想要关闭它)。

Git 的索引,也称为暂存区(如git diff --staged)或有时称为缓存(如git diff --cached:与--staged完全相同),具有多个角色,但有趣的是,它是您构建下一次提交的地方。

当您从某个现有存储库的克隆开始并签出某个分支名称时,Git 会从该分支上的最新提交附带的快照中填充索引。 (此最新提交称为提示提交。 所以现在索引与提交匹配。 因此,如果您现在要进行新的提交,2您的新提交将与当前提交具有相同的树,因为您尚未更改索引。

当您运行git add来更新现有文件时,Git 只是将索引副本替换为工作树中的版本。 现在,您所做的下一次提交将具有新版本。 当你运行git add添加一个全新的文件时,Git 会将该文件从工作树复制到索引中,现在下一次提交将拥有新文件。 如果在文件名上使用git rm,则会从工作树和索引中删除该文件,现在下一次提交将没有该文件。

(顺便说一下,这使我们能够准确地说明工作树文件"未跟踪"的含义。 当且仅当文件不在索引中时,才会取消跟踪该文件。 就是这样——这就是它的全部! 现在,当 Git 对一个未跟踪的文件发牢骚时,您可以将文件名添加到名为.gitignore的文件中,这将使 Git 对此闭嘴。 它实际上不会使文件取消跟踪:这是由文件不在索引中决定的。 它主要只是让 Git 闭嘴,并且当您使用 Git 的"一次添加多个文件"快捷方式之一时也不会自动添加它。


2Git 会尽量阻止您进行与HEAD提交完全匹配的新提交。 但是,您可以使用--allow-empty强制它允许提交。 "空"是一种有趣的拼写方式,因为新提交根本不空的,它只是相同的,至少在保存的工作树方面是相同的。 (即使它们与HEAD匹配,也始终允许新的合并提交。


git checkout何时覆盖索引和/或工作树?

如果我们回到我所说的第三和第四种形式的git checkout,我们会看到其中一个有commit-ish论点,另一个没有。 这涉及到一些应该更像是隐藏的实现细节的东西——但 Git 习惯于让实现细节直接向用户显示。

要使git checkout将文件从提交复制到工作树,它必须首先将文件写入索引。因此,第三种形式git checkoutcommit-ish--path找到与给定commit-ish关联的文件path版本,将其复制到索引,然后将索引版本复制到工作树。

然而,第四种形式没有commit-ish论据:git checkout --path。 在这种情况下,Git 将文件的版本从索引复制到工作树中。 大多数情况下,索引上的版本与工作树中的版本相同,因此大多数时候,这不会执行任何操作。 但是,如果您修改了工作树版本,然后决定要放弃修改,则可以提取索引版本。

索引版本可能与当前提交(HEAD)版本相同。 在这种情况下,git checkout --pathgit checkout HEAD --path都将HEAD版本复制到工作树中,但具有显式HEAD的版本首先将HEAD版本复制到索引,结果只是相同,因为HEAD和索引版本无论如何都是相同的。

为了完整起见,我将提到前两个git checkout形式("安全"表单)也将覆盖索引和工作树,但在这里不涉及很多细节,Git 会努力只覆盖那些"安全"覆盖的条目,这样你就不会丢失未提交的工作。 请参阅 Git - 当当前分支上有未提交的更改时签出另一个分支,了解(更多)更多详细信息。

总结

每个文件在任何时候都有最多三个有趣的版本:

  • 当前(HEAD)提交中的那个;
  • 索引中的那个,将进入下一次提交;和
  • 工作树中的那个。

使用git checkoutcommit-ish--path,如在git checkout xxx .中一样,保持HEAD版本不变,但将xxx(已提交)版本复制到索引和工作树中。 如果索引和工作树中有其他版本,它们现在就消失了。 如果这些版本与某个已提交的版本匹配,则可以将其取回。 如果没有,Git 无法帮助您...可能。 但是请参阅最后一节!

特殊的秘密恢复方法

"永远消失"规则有一个不寻常的例外,尽管使用起来很痛苦。 当您将文件git add索引时,Git 实际上会将该文件的副本放在存储库中。 索引仅包含文件的 40 个字符的 SHA-1哈希。 这意味着,如果您git add-ed 文件,它将保存在存储库本身中。 如果用另一个版本覆盖索引版本,则实际上会将"另一个"版本复制到存储库中,并将新哈希放入索引中。 中间版本不会被删除! 嗯,还没有

这些散列但从未提交的文件可以恢复,直到 Git "垃圾收集"它们为止。 默认情况下,从您git add编辑它们起,这至少给你 14 天的时间。 恢复它们的命令是git fsck --lost-found.

这种恢复方法的问题是文件名消失了。git fsck --lost-found所做的是找到 Git 对象——提交、树、标记和"blob",这是 Git 所说的存储文件——它们没有引用它们。git add-ed 文件时,Git 会将文件的内容存储为新 Blob,然后将 Blob 对象的哈希 ID写入索引,使用索引保存文件名。然后,当您覆盖path的索引条目时,您将丢失该名称,并且存储库 blob 对象变为未引用--lost-found选项使git fsck原始文件的内容复制到.git/lost-found/other/,将其存储在哈希ID下,因为名称已消失。 然后,您可以浏览每个此类文件以找到所需的文件,并将它们移出失物招领区域以将其取回。

如果您在之前没有存储或提交代码的情况下签出分支,则代码将丢失且无法恢复。

最新更新