我不小心提交了一个大的.psd文件,然后卡住了我的推送过程。
因此,我将 *.psd 添加到我的 gitignore 中,然后尝试删除此提交,因为它仍在尝试推送现在不存在的 .psd 文件。
在我进行一些 git 软重置的某个时候,我搞砸了我的 git 索引,现在我的一半项目文件被标记为"索引已删除"。
无论我是否做 git git add .,这些文件不再被索引,我该怎么办?
链接的问题(如何从 Git 存储库的提交历史记录中删除/删除大文件?)在修复索引情况后是合适的。 但是,首先,您需要修复索引情况。
你提到:
在某个时候,当我在做一些 git 软重置时......
git reset --soft
不会触及索引(也不接触您的工作树),但可用于更改存储在HEAD
中的提交哈希 ID。 如果这样做了,则可能需要将正确的提交哈希 ID 放回HEAD
,再次使用git reset --soft
和正确的提交哈希 ID。
这可能足以解决所有问题,因为git status
将HEAD
(可移动)与当前索引内容进行比较,然后将当前索引内容(可更改)与工作树内容(也是可更改的)进行比较。
你需要知道的关于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.name
和user.email
设置提供名称和电子邮件地址。 这一切都非常简单,但这里有一部分不是:Git 在此元数据中添加了父提交哈希 ID 的列表。 对于大多数提交,只有一个父级。
当您进行新提交时,您是通过处理一些现有提交来实现的。 Git在新提交中存储您之前选择处理的提交的哈希 ID。 因此,您的新提交将该提交的哈希 ID 作为其父级。 然后 Git 将新提交的哈希 ID 写入当前分支名称。
这值得一提。 假设我们有以下提交链:
... <-F <-G <-H <--main (HEAD)
其中H
代表最近提交的哈希 ID,H
是我们签出的提交。main
是我们的分支名称,main
这个名字包含H
的哈希 ID,这就是 Git找到H
的方式,当我们说git checkout main
或git switch main
时。
在H
的元数据中提交H
存储,更早G
的哈希 ID。 我们说H
指向G
,因此图中的箭头从H
指向G
。 因此,提交G
是提交H
的父级。G
和H
都有每个文件的完整快照(具有重复数据删除功能),因此 Git 可以比较这两个快照以查看G
和H
之间的更改。 而且,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
在这里,我们有一个包含三个分支名称的存储库,main
、br1
和br2
。 名称HEAD
当前附加到名称main
上,该名称选择提交H
。 名称br1
和br2
选择分别J
和L
提交。
如果我们运行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
的标志。
(您重置以存储在HEAD
reflog 中的各种提交的哈希 ID,因此git reflog
将显示它们。 这是一种查找要返回的提交的方法,如果您不小心重置了现在找不到的哈希 ID。 使用引用日志查找丢失的哈希 ID。 请注意,哈希 ID 真的很难记住:在使用git reset --soft
之前,您可能希望运行git showhash
或git log -1hash
或类似,对哈希 ID 使用剪切和粘贴,以找出哪个哈希 ID 包含感兴趣的提交。
git status
和其他类似比较器
git status
命令部分通过运行两个git diff
来工作。
这两个差异中的第一个是:
git diff --staged --name-status
它将任何提交HEAD
名称(即存储在该提交中的所有文件)与 Git 索引中的文件进行比较。 由于这些文件通常是从该提交中复制出来的,因此此后我们未更新的任何文件都将匹配。 Git 根本不会说任何关于匹配文件的信息。
如果我们确实更新了某些文件(例如,使用git add
,我在这里没有介绍),该文件可能不匹配。 然后git status
会说文件的索引副本是要提交的更改。
如果我们在不更改索引内容的情况下移动HEAD
(和当前分支名称),我们将使两者不同步,并且许多文件可能会更改甚至删除。 例如,如果我们main
从J
向后移动到H
,但保留索引,则将显示H
和J
之间不同的所有文件。
第二个比较git status
确实将 Git 索引中的文件与工作树中的文件进行比较。 这很像在没有选项的情况下运行git diff --name-status
。 对于匹配的每个文件,Git 根本不会说什么。 如果文件不同,即您已经修改了工作树文件,但尚未在其上运行git add
- Git 会将该文件列为未暂存的更改以进行提交。
(由于篇幅原因,这里有一个很大的复杂部分,我将省略,讨论工作树中但不在 Git 索引中的文件如何成为未跟踪的文件。Git 会抱怨这些,除非它们列在.gitignore
中。.gitignore
条目实际上并没有使 Git忽略这些文件,因此.gitignore
用词不当。 但出于篇幅原因,我在这里省略了所有这些。