Git 索引搞砸了

Git index messed up

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

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

在我进行一些 git 软重置时,我弄乱了我的 git 索引,现在我的一半项目文件都标有“索引已删除”的红色标记。

无论我 git git 添加 .,这些文件都不再被索引,我该怎么办?

链接的问题(How to remove/delete a large file from commit history in the Git repository?)是合适的你修复你的索引情况之后。不过,首先,您需要修复您的索引情况。

你提到:

At some point as I was doing some git soft reset ...

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

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

您需要了解的有关 HEAD、Git 的索引(或“暂存区”)和您的工作树的信息

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

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

我不会在这里适当地介绍其中的一些内容,以使这个答案更短(或者对我来说更短)。但让我们从这个开始,关于提交:提交已编号。任何提交,一旦做出,就永远无法更改。 它们是 mostly-permanent(但请参阅链接的问题),并且完全是 read-only。

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

答案很简单。 我们不更改提交。我们将提交复制到我们 可以 更改的内容,更改 that,然后使用 that 来制作新提交。 所以我们不处理提交:我们处理 从提交.

复制的内容

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

但是 Git 在这里不同,一开始没有明显的原因。对于其他版本控制系统,您将提交提取到一个工作区域,在那里您可以处理它,仅此而已。在 Git 中,您将提交提取到一个工作区——您的 工作树 work-tree——但是 提议的下一次提交。由于历史原因,Git 有 三个名称 用于提议的下一次提交,称其为“索引”或“暂存区”,或者 - 一个主要在标志中找到的术语像最近的 git rm --cached——“缓存”。

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


1除外,即对于all-new个文件。这是因为,例如,Mercurial 有称为“dircache”和“manifest”的东西,它们与 Git 的索引起着类似的作用,但 Mercurial 将它们隐藏起来,因此您不必了解它们。 Git,相比之下,时不时地抽出它的索引 slaps you in the face with it (Monty Python fish-slapping dance)不允许忽略它。 git commit -a 快捷方式有时几乎可以让您到达那里,但这还不够:您 必须 了解 Git 的索引。


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

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

提交也是two-part 个单位。一部分包含每个文件的快照,以特殊的read-only、Git-only、压缩和de-duplicated方式存储。 de-duplication 处理了这样一个事实,即大多数提交主要是 re-use 来自早期提交的文件:这可以防止提交占用大量 space。 (事实上​​ ,如果您进行新提交以撤消先前提交所做的操作,则新提交的存储文件可能根本不需要 space ,因为它们现在是 all重复。)你不必担心如何Git这样做:这部分效果很好,不会像索引那样让你头疼.

每个提交的另一部分是它的元数据,或者关于提交本身的信息。这包含诸如提交人的姓名和电子邮件地址、一些 date-and-time 戳记和日志消息之类的内容。当您进行新的提交时,您会提供日志消息,并且您的 user.nameuser.email 设置会提供姓名和电子邮件地址。这一切都非常简单,但这里有一个部分不是:Git 向此元数据添加一个 父提交 哈希 ID 的列表。对于大多数提交,只有一个父项。

当您进行 new 提交时,您是在处理一些现有的提交。 Git 在您的 new 提交中存储您之前选择要处理的提交的哈希 ID。因此,您的新提交将该提交的哈希 ID 作为其父项。然后 Git 将 new commit 的 hash ID 写入 current branch name.

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

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

其中 H 代表最近提交的哈希 ID,而 H 是我们检出的提交。 main 是我们的 分支名称 名称 main 包含 H 的哈希 ID,当我们说 git checkout maingit switch main.

时,Git 就是这样找到 H

提交 HH 的元数据中存储更早的 G 的哈希 ID。我们说H指向G,因此图中的箭头从H指向G。因此,提交 G 是提交 HparentGH 都有每个文件的完整快照(de-duplication),因此 Git 可以比较这两个快照以查看 更改了什么 [=334] =] 在 GH 之间。并且,G 作为提交,G 在其元数据中具有 父提交 F 的哈希 ID。 F 指向另一个更早的提交,依此类推。

无论如何,我们现在在我们的工作树和 Git 的索引中操作文件,并进行新的提交,这将获得一个新的、唯一的、random-looking 哈希 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,select 提交 H。名称 br1br2 select 分别提交 JL

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

          I--J   <-- br1, main (HEAD)
         /
...--G--H
         \
          K--L   <-- br2

如果那是 错误,我们可以 运行:

git reset --hard HEAD~2

(~2 表示 倒数两个 first-parent 链接; 我不会在这里详细介绍,也不会涵盖--ff-only 是什么意思)我们会回到这个:

          I--J   <-- br1
         /
...--G--H   <-- main (HEAD)
         \
          K--L   <-- br2

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

实际情况如下:

  • 首先,git reset 执行 --soft 步骤。我们给它一个提交哈希 ID,比如提交的原始哈希 ID H,或者像 HEAD~2 这样的相对提交指令。 the git rev-parse command 需要的任何东西都可以在这里使用。 Git找到那个commit,比如commitH。然后它使 附加 HEAD 的分支名称 指向该提交。所以现在 main 指向 H.

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

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

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

  • 移动分支名称(--soft);然后
  • 更新Git的隐藏索引/proposed-next-commit (--mixed);然后
  • 更新我们的工作树(--hard)。

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

(请注意,git reset 还有其他操作模式。如果这是 所有 ,它就不会这么复杂。)

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

          I--J   <-- br1
         /
...--G--H
         \
          K--L   <-- br2, main (HEAD)

Git 的索引和您的工作树发生了什么,如果有的话,取决于您给 git reset 的标志。

(您重置为存储在 HEAD reflog 中的各种提交的哈希 ID,因此 git reflog 将显示它们。这是一种查找您想要提交的方法go back 到,如果你不小心重置了你现在找不到的哈希 ID。使用 reflogs 找到你丢失的哈希 ID。注意哈希 ID 真的很难找记住:您可能想要 运行 git 显示 <em>hash</em>git 日志-1 <em>hash</em> 或类似的,使用 cut-and-paste 作为哈希 ID,在使用 git reset --soft 之前,找出哪个哈希 ID 持有哪个提交兴趣。)

git status 和其他类似的比较器

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

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

git diff --staged --name-status

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

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

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

second 比较 git status 将 Git 索引中的文件与工作树中的文件进行比较。这很像没有选项的 运行ning git diff --name-status。对于每个 匹配 的文件,Git 将什么都不说。文件不同的地方——你已经修改了一个工作树文件,但还没有 运行 git add 在它上面——Git 将文件列为 change not staged对于提交.

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