'git rm --cached'、'git restore --staged'、'git reset'有什么区别

What's the difference between 'git rm --cached', 'git restore --staged', and 'git reset'

我遇到了以下三种方法来取消暂存命令 'git add'

暂存的文件
git rm --cached <file>
git restore --staged <file>
git reset <file>

当我 运行 一个一个地命令他们时,他们的行为看起来完全一样。 它们之间到底有什么区别?

两个相同;一个不是,除非在特殊情况下。

要理解这一点,请记住:

  • 一次提交包含 Git 知道的所有文件的快照,就像您说要提交它们时它们所具有的形式一样;
  • 快照是Git索引中的文件制作的,又名暂存区,又名缓存(同一事物的三个术语);和
  • git add 表示 使 index/staging-area/cache 中的副本与我的工作树中的副本匹配 (如果工作树复制,则通过从工作树复制被更新,或者如果工作树副本被删除,则通过从索引中删除)。

因此索引/暂存区域始终包含您的提议的下一次提交,并且最初是从您的当前提交 当您执行 git checkoutgit switch 以获得该提交时。1 您的工作树因此包含一个 third 副本每个文件的 2,前两个副本是 当前提交 又名 HEAD 中的一个,以及索引中的一个.

考虑到这一点,以下是您的每个命令的作用:

  • git rm --cached <em>file</em>: 从索引中删除文件的副本/ 暂存区,不触及工作树副本。提议的下一次提交现在 缺少 文件。如果当前提交 文件,而你实际上在此时进行了下一次提交,则上一次提交和新提交之间的区别在于文件已经消失。

  • git restore --staged <em>file</em>: Git 从复制文件HEAD 提交到索引中,而不触及工作树副本。索引副本和 HEAD 副本现在匹配,无论它们之前是否匹配。现在进行的新提交将具有与当前提交相同的文件副本

    如果当前提交缺少文件,这具有从索引中删除文件的效果。所以 在这种情况下 它与 git rm --cached 做同样的事情。

  • git reset <em>file</em>:这会复制文件的HEAD版本到索引,就像 git restore --staged <em>file</em>.

(请注意 git restore,与这种特殊形式的 git reset 不同, 可以 覆盖某些文件的工作树副本,如果您要求它这样做。--staged 选项,没有 --worktree 选项,指示它只写入索引。)

旁注:许多人最初认为索引/暂存区仅包含更改,或仅包含更改的文件。事实并非如此,但如果您这样想,git rm --cached 看起来与其他两个相同。因为这不是索引的工作方式,所以它不是。


1当你准备一些东西,然后做一个新的 git checkout 时,会有一些古怪的边缘情况。本质上,如果可以保留不同的分阶段副本,Git 会这样做。有关详细信息,请参阅 Checkout another branch when there are uncommitted changes on the current branch

2提交的副本和任何暂存副本实际上以内部 Git blob 对象的形式保存,它删除了重复的内容。因此,如果这两者匹配,它们实际上只是共享一个基础副本。如果暂存副本与 HEAD 副本不同,但与任何(甚至可能很多)其他现有提交副本匹配,则暂存副本与所有其他提交共享底层存储。因此,将每一个都称为“副本”是矫枉过正的。但作为一个心智模型,它运行得很好:none 永远可以被覆盖;如果需要,一个新的 git add 将创建一个新的 blob 对象,如果最后没有人使用某个 blob 对象,Git 最终会丢弃它。


一个具体的例子

中,pavel_orekhov说:

It is still not clear to me where "git rm --cached" and "git restore --staged" differ. Could you please show a series of commands with these 2 that exhibit different behavior?

让我们检查 Git 存储库中 Git 本身的特定提交(如果需要,请先克隆它,例如,从 https://github.com/git/git.git):

$ git switch --detach v2.35.1
HEAD is now at 4c53a8c20f Git 2.35.1

您的工作树将包含名为 MakefileREADME.mdgit.c 等的文件。

现在让我们修改工作树中的一些现有文件:

$ ed Makefile << end
> 1a
> foo
> .
> w
> q
> end
107604
107608
$ git status --short
 M Makefile

> 标志来自 shell 请求输入;这两个数字是文件的字节数Makefile。注意 git status 的输出是 <kbd>SPACE</kbd>M<kbd>SPACE</kbd>Makefile,表示 indexstaging area 副本 Makefile 匹配 HEAD 副本 Makefile,而 Makefileworking tree 副本不同于 Makefile.

index 副本

(旁白:我在准备剪切和粘贴文本时不小心添加了两行foo。我不会回去修复它,但是如果你自己做这个实验,预计输出略有不同。)

让我们现在 git add 这个更新的文件,然后将第一行的 foo 替换为 bar:

$ git add Makefile
$ git status --short
M  Makefile

注意M左移一栏,M-space-space-Makefile,说明index拷贝MakefileHEAD 副本不同,但现在索引和工作树副本匹配。现在我们进行 foo-to-bar 替换:

$ ed Makefile << end
> 1s/foo/bar/
> w
> q
> end
107608
107608
$ git status --short
MM Makefile

我们现在有 两个 MMakefileHEAD 副本与 Makefile 的索引副本不同,这与 Makefile 的工作树副本不同。 运行 git diff --cachedgit diff 将准确地向您展示每对配对的比较结果。

$ git diff --cached
diff --git a/Makefile b/Makefile
index 5580859afd..8b8fc5a6d6 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,5 @@
-# The default target of this Makefile is...
+foo
+foo
 all::
 
 # Define V=1 to have a more verbose compile.
$ git diff
diff --git a/Makefile b/Makefile
index 8b8fc5a6d6..96a787d50d 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-foo
+bar
 foo
 all::
 

现在,如果我们 运行 git rm --cached Makefile,这将 完全删除文件 Makefile 的索引副本 ,并且 git status 会相应改变。因为我们有所有这些修改 Git 也需要“force”标志:

$ git rm --cached Makefile
error: the following file has staged content different from both the
file and the HEAD:
    Makefile
(use -f to force removal)
$ git rm --cached -f Makefile
rm 'Makefile'
$ git status --short
D  Makefile
?? Makefile

我们现在 没有 名为 Makefile 的文件 proposed next commit in the index / staging-area。但是,文件 Makefile 仍然出现在工作树中(第一行显示为 bar)(请自行检查文件以查看)。这个 Makefile 是一个 未跟踪的文件 所以我们从 git status --short 得到两行输出,其中一行宣布文件 Makefile 在下一次提交中即将消亡, 另一个宣布未跟踪文件的存在 Makefile.

不做任何提交,我们现在使用 git restore --staged Makefile:

$ git restore --staged Makefile
$ git status --short
 M Makefile

状态现在又是space-M,说明Makefile 存在于索引中(因此会在下一次提交中),此外, 匹配 MakefileHEAD 副本,因此 git diff --staged——这是拼写 git diff --cached 的另一种方式——不会显示它(实际上什么都不显示)。 工作树副本保持原状,并且仍然包含额外的行bar,如git diff所示:

$ git diff --staged
$ git diff
diff --git a/Makefile b/Makefile
index 5580859afd..96a787d50d 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,5 @@
-# The default target of this Makefile is...
+bar
+foo
 all::
 
 # Define V=1 to have a more verbose compile.

同样,理解这一切的关键是:

  • 每个提交都包含 Git 知道的每个文件的完整快照

  • 此快照始终存在于 Git 的 索引 中,Git 也调用 暂存区,或者偶尔——现在主要在--cached标志中——缓存--staged--cached 标志 3 通常表示 使用此索引/暂存区 做某事。 git resetgit rmgit add 等命令隐式地 使用索引/暂存区,尽管标志可能会稍微修改此行为; git restore 命令具有明确的 --staged--worktree 标志。

  • 同时,您的工作树包含普通的日常文件。 这些是您可以直接查看和使用的唯一文件(例如使用您的编辑器);只有 Git 命令 可以查看和使用文件的已提交副本和索引副本。

  • 已提交 文件副本永远无法更改。它们在那些提交 forever 中(或者只要这些提交继续存在):它们是只读的。但是,文件的 index 副本可以用 git add 批量替换,或者用 git add -p 打补丁,或者用 git rm 完全删除或 git rm --cached.

  • 普通文件就是普通文件:所有普通命令都可以在普通文件上正常工作。 (再平常不过的“普通”这个词现在这么有趣是不是很特别?)

  • 运行 git commit 获取所有 index 副本并将它们冻结到新快照中。因此,当您在 Git 中工作时,您所做的是:

    • 以普通方式处理普通文件;
    • git add他们更新Git的索引副本,准备冻结;和
    • git commit结果,永远冻结他们。

    这是进行新提交的过程,如果您改变主意并决定进行新提交, git restore --stagedgit reset 可用于 重新提取 已提交的副本到索引副本中。但是 git rm 完全删除索引副本

因此,如果 如果 完全删除索引副本 会使事情恢复原样(当某些文件被删除时可能会发生这种情况) new),然后“使索引副本匹配 nonexistent HEAD 副本,通过删除它”是做你想做的事情的正确方法。但是,如果 HEAD 提交包含相关文件的副本,git rm --cached <em>the-file</em>错了。


3请注意 --cached--stagedgit diff 具有 相同的含义 。然而,对于 git rm,根本就没有 --staged 选项。为什么?这是 Git 开发人员的问题,但我们可以注意到,从历史上看,在遥远的过去,git diff 也没有 --staged。因此,我最好的猜测是这是一个疏忽:当谁将 --staged 添加到 git diff 时,他们也忘记将 --staged 添加到 git rm