Git 索引搞砸了



我不小心提交了一个大的.psd文件,然后卡住了我的推送过程。

因此,我将 *.psd 添加到我的 gitignore 中,然后尝试删除此提交,因为它仍在尝试推送现在不存在的 .psd 文件。

在我进行一些 git 软重置的某个时候,我搞砸了我的 git 索引,现在我的一半项目文件被标记为"索引已删除"。

无论我是否做 git git add .,这些文件不再被索引,我该怎么办?

链接的问题(如何从 Git 存储库的提交历史记录中删除/删除大文件?)在修复索引情况是合适的。 但是,首先,您需要修复索引情况。

你提到:

在某个时候,当我在做一些 git 软重置时......

git reset --soft不会触及索引(也不接触您的工作树),但可用于更改存储在HEAD中的提交哈希 ID。 如果这样做了,则可能需要将正确的提交哈希 ID 放回HEAD,再次使用git reset --soft和正确的提交哈希 ID。

这可能足以解决所有问题,因为git statusHEAD(可移动)与当前索引内容进行比较,然后将当前索引内容(可更改)与工作树内容(也是可更改的)进行比较。

你需要知道的关于HEAD、Git 的索引(或"暂存区")和你的工作树

Git 实际上是关于提交的。 这与文件无关,尽管提交保留文件。 这与分支无关,尽管分支可以帮助您(和 Git)找到提交。 最后,Git 完全是关于提交的。 所以重要的是提交。 但这应该会给您留下几个问题,包括:

  • 到底什么是提交?
  • 我们如何找到提交?
  • 我们如何进行新的提交?
  • 我们可以摆脱旧的提交吗?
  • 这个指数是什么东西?

我不会在这里正确介绍其中的一些,以使这个答案更短(或者对我来说更短)。 但是,让我们从这个开始,关于提交:提交是有编号的。 任何提交一旦完成,根本无法更改。它们大多是永久性的(但请参阅链接的问题),并且完全只读。

我们(大多数情况下)通过操作现有提交来创建新提交。 您可以完全从头开始进行新的提交,但这通常太痛苦了,除了第一次提交之外。 因此,要进行新的提交,我们必须采用现有的提交,并更改其中的某些内容。根据定义,这是一个矛盾:提交不能更改,但我们需要更改某些内容才能进行新的提交。 我们如何解决这个难题?

答案很简单。我们不会更改提交。 我们将提交复制到我们可以更改的内容,更改,并使用它来进行新的提交。所以我们不处理提交:我们处理从提交中复制的东西

几乎所有的版本控制系统都做这种事情;Git 与 SVN 或 Mercurial 或其他任何东西没有什么不同,因为我们首先提取一些提交,然后处理它,然后使用它来进行新的提交。

但是 Git 在这里有所不同,一开始没有明显的原因。 使用其他版本控制系统,您可以将提交提取到工作区,在那里进行处理,仅此而已。 在 Git 中,您将提交提取到工作区(您的工作树或工作树),但也提取建议的下一个提交。 出于历史原因,Git 为这个提议的下一次提交提供了三个名称,称为"索引"或"暂存区域",或者——这个术语现在主要出现在像git rm --cached这样的标志中——"缓存"。

然后,您可以处理工作树中的文件,就像在任何版本控制系统中一样。 但是,当您对工作树文件感到满意时,您必须在其上运行git add。 您不必在 Mercurial 或 SVN 中执行此操作,1 因为在这些系统中,工作树文件该文件的建议下一次提交版本。 在 Git 中,你必须这样做:git add命令将文件复制回 Git 的索引中,使其为下一次提交做好准备。


1除了,也就是说,对于全新的文件。 这是因为,例如,Mercurial 有一个名为"dircache"和"manifest"的东西,它们的作用与 Git 的索引类似,但 Mercurial 将它们隐藏起来,这样你就不必了解它们。 相比之下,Git 时不时地掏出它的索引,并用它打你的脸(巨蟒鱼拍鱼舞)。 您不能忽略它。git commit -a快捷方式有时几乎可以让你到达那里,但这还不够:你必须了解 Git 的索引。


分支名称查找提交,提交查找提交

正如我所说,提交是有编号的。 这些数字看起来是随机的(尽管它们实际上不是随机的),并且是巨大而丑陋的十六进制字符串。 这些通常不能被人类使用,所以我们不(即使用它们)。 这些是哈希 ID对象 ID(OID);Git 在任何地方都使用 OID,包括内部。

提交也是由两部分组成的单元。 一个部分保存每个文件的快照,以特殊的、只读的、只 Git的、压缩的和重复数据删除的方式存储。 重复数据消除处理了这样一个事实,即大多数提交大多重用早期提交的文件:这可以防止提交占用大量空间。 (事实上,如果您进行的新提交撤消了以前的提交,则新提交的存储文件可能根本不占用空间,因为它们现在都是重复的。 你不必担心 Git是如何做到这一点的:这部分工作得很好,不会像索引那样

让你头疼。每个提交的另一部分是其元数据,或有关提交本身的信息。 这包含诸如提交人员的姓名和电子邮件地址,一些日期和时间戳以及日志消息之类的内容。 当您进行新的提交时,您将提供日志消息,您的user.nameuser.email设置提供名称和电子邮件地址。 这一切都非常简单,但这里有一部分不是:Git 在此元数据中添加了父提交哈希 ID 的列表。 对于大多数提交,只有一个父级。

当您进行提交时,您是通过处理一些现有提交来实现的。 Git在新提交中存储您之前选择处理的提交的哈希 ID。 因此,您的新提交将该提交的哈希 ID 作为其父级。 然后 Git 将提交的哈希 ID 写入当前分支名称

这值得一提。 假设我们有以下提交链:

... <-F <-G <-H   <--main (HEAD)

其中H代表最近提交的哈希 ID,H是我们签出的提交。main是我们的分支名称main这个名字包含H的哈希 ID,这就是 Git找到H的方式,当我们说git checkout maingit switch main时。

H的元数据中提交H存储,更早G的哈希 ID。 我们说H指向G,因此图中的箭头从H指向G。 因此,提交G是提交H父级GH都有每个文件的完整快照(具有重复数据删除功能),因此 Git 可以比较这两个快照以查看GH之间的更改。 而且,G提交,G在其元数据中具有父提交F的哈希 ID。F指向另一个较早的提交,依此类推。

无论如何,我们现在在工作树和 Git 的索引中操作文件,并进行一个新的提交,它会得到一个新的、唯一的、随机的哈希 ID,我们称之为I。 新提交I指向现有提交H

... <-F <-G <-H   <--main (HEAD)

I

git commit的最后一步是 Git 将I的哈希 ID(无论它是什么)写入名称main中:

... <-F <-G <-H

I   <--main (HEAD)

所以现在main指向提交I而不是提交H

git reset--hard--mixed--soft

git reset --soft的作用是允许您移动分支名称git reset一般所做的是...荒谬复杂。

让我们画一个更复杂和有用的 Git 图:

I--J   <-- br1
/
...--G--H   <-- main (HEAD)

K--L   <-- br2

在这里,我们有一个包含三个分支名称的存储库,mainbr1br2。 名称HEAD当前附加到名称main上,该名称选择提交H。 名称br1br2选择分别JL提交。

如果我们运行git merge --ff-only br1,我们最终得到:

I--J   <-- br1, main (HEAD)
/
...--G--H

K--L   <-- br2

如果这是一个错误,我们可以运行:

git reset --hard HEAD~2

(~2表示倒数两个第一父链接;我不会在这里详细介绍,也不会涵盖--ff-only的含义),我们将回到这个:

I--J   <-- br1
/
...--G--H   <-- main (HEAD)

K--L   <-- br2

就好像什么都没发生一样。 这里的--hard影响了 Git 的索引我们的工作树

以下是实际发生的事情:

  • 首先,git reset执行--soft步骤。 我们给它一个提交哈希ID,例如提交H的原始哈希ID,或者像HEAD~2这样的相对提交指令。git rev-parse命令将采取的任何内容都可以在此处使用。 Git找到该提交,例如提交H。 然后,它使HEAD附加到的分支名称指向该提交。 所以现在main指向H.

  • 然后,如果我们允许它 - 如果我们使用--mixed--hard-git reset重置 Git 的索引。 它通过删除来自我们所在的提交(J)的所有文件并安装来自我们移动到的提交(H')的所有文件来实现这一点。

  • 然后,如果我们告诉它 - 如果我们使用--hard-git reset重置我们的工作树。 对于它从 Git 索引中翻录并替换为H中的所有文件,它会将这些文件从我们的工作树中翻录出来,并用从提交H中提取的文件替换它们。

这就是git reset --hard让我们回到git merge --ff-only之前的方式:它:

  • 移动分支名称 (--soft);
  • 然后
  • 更新 Git 的隐藏索引/提议的下一次提交 (--mixed);
  • 然后
  • 更新我们的工作树(--hard)。

使用--mixed--soft标志只会使git reset在执行第二步或第一步后更早停止。

(请注意,git reset还有其他操作模式。 如果这就是它所做的一切,它就不会那么荒谬地复杂了。

请注意,如果您现在使用git reset指向提交L,您将拥有:

I--J   <-- br1
/
...--G--H

K--L   <-- br2, main (HEAD)

Git 的索引和你的工作树会发生什么(如果有的话)取决于你给git reset的标志。

(您重置以存储在HEADreflog 中的各种提交的哈希 ID,因此git reflog将显示它们。 这是一种查找要返回的提交的方法,如果您不小心重置了现在找不到的哈希 ID。 使用引用日志查找丢失的哈希 ID。 请注意,哈希 ID 真的很难记住:在使用git reset --soft之前,您可能希望运行git showhashgit log -1hash或类似,对哈希 ID 使用剪切和粘贴,以找出哪个哈希 ID 包含感兴趣的提交。

git status和其他类似比较器

git status命令部分通过运行两个git diff来工作。

这两个差异中的第一个是:

git diff --staged --name-status

它将任何提交HEAD名称(即存储在该提交中的所有文件)与 Git 索引中的文件进行比较。 由于这些文件通常是从该提交中复制出来的,因此此后我们未更新的任何文件都将匹配。 Git 根本不会说任何关于匹配文件的信息。

如果我们确实更新了某些文件(例如,使用git add,我在这里没有介绍),该文件可能不匹配。 然后git status会说文件的索引副本是要提交的更改

如果我们在不更改索引内容的情况下移动HEAD(和当前分支名称),我们将使两者不同步,并且许多文件可能会更改甚至删除。 例如,如果我们mainJ向后移动到H,但保留索引,则将显示HJ之间不同的所有文件。

第二个比较git status确实将 Git 索引中的文件与工作树中的文件进行比较。 这很像在没有选项的情况下运行git diff --name-status。 对于匹配的每个文件,Git 根本不会说什么。 如果文件不同,即您已经修改了工作树文件,但尚未在其上运行git add- Git 会将该文件列为未暂存的更改以进行提交

(由于篇幅原因,这里有一个很大的复杂部分,我将省略,讨论工作树中但不在 Git 索引中的文件如何成为未跟踪的文件Git 会抱怨这些,除非它们列在.gitignore中。.gitignore条目实际上并没有使 Git忽略这些文件,因此.gitignore用词不当。 但出于篇幅原因,我在这里省略了所有这些。

最新更新