'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 checkout
或 git 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
您的工作树将包含名为 Makefile
、README.md
、git.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
,表示 index 或 staging area 副本 Makefile
匹配 HEAD
副本 Makefile
,而 Makefile
的 working tree 副本不同于 Makefile
.
的 index 副本
(旁白:我在准备剪切和粘贴文本时不小心添加了两行foo
。我不会回去修复它,但是如果你自己做这个实验,预计输出略有不同。)
让我们现在 git add
这个更新的文件,然后将第一行的 foo
替换为 bar
:
$ git add Makefile
$ git status --short
M Makefile
注意M
左移一栏,M-space-space-Makefile,说明index拷贝Makefile
与 HEAD
副本不同,但现在索引和工作树副本匹配。现在我们进行 foo-to-bar 替换:
$ ed Makefile << end
> 1s/foo/bar/
> w
> q
> end
107608
107608
$ git status --short
MM Makefile
我们现在有 两个 M
:Makefile
的 HEAD
副本与 Makefile
的索引副本不同,这与 Makefile
的工作树副本不同。 运行 git diff --cached
和 git 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
存在于索引中(因此会在下一次提交中),此外, 匹配 Makefile 的 HEAD
副本,因此 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 reset
、git rm
和 git 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 --staged
或 git reset
可用于 重新提取 已提交的副本到索引副本中。但是 git rm
完全删除索引副本。
因此,如果 仅 如果 完全删除索引副本 会使事情恢复原样(当某些文件被删除时可能会发生这种情况) new),然后“使索引副本匹配 nonexistent HEAD
副本,通过删除它”是做你想做的事情的正确方法。但是,如果 HEAD
提交包含相关文件的副本,git rm --cached <em>the-file</em>
错了。
3请注意 --cached
和 --staged
与 git diff
具有 相同的含义 。然而,对于 git rm
,根本就没有 --staged
选项。为什么?这是 Git 开发人员的问题,但我们可以注意到,从历史上看,在遥远的过去,git diff
也没有 --staged
。因此,我最好的猜测是这是一个疏忽:当谁将 --staged
添加到 git diff
时,他们也忘记将 --staged
添加到 git rm
。
我遇到了以下三种方法来取消暂存命令 'git add'
暂存的文件git rm --cached <file>
git restore --staged <file>
git reset <file>
当我 运行 一个一个地命令他们时,他们的行为看起来完全一样。 它们之间到底有什么区别?
两个相同;一个不是,除非在特殊情况下。
要理解这一点,请记住:
- 一次提交包含 Git 知道的所有文件的快照,就像您说要提交它们时它们所具有的形式一样;
- 快照是从Git索引中的文件制作的,又名暂存区,又名缓存(同一事物的三个术语);和
git add
表示 使 index/staging-area/cache 中的副本与我的工作树中的副本匹配 (如果工作树复制,则通过从工作树复制被更新,或者如果工作树副本被删除,则通过从索引中删除)。
因此索引/暂存区域始终包含您的提议的下一次提交,并且最初是从您的当前提交 当您执行 git checkout
或 git 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 最终会丢弃它。
一个具体的例子
在
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
您的工作树将包含名为 Makefile
、README.md
、git.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
,表示 index 或 staging area 副本 Makefile
匹配 HEAD
副本 Makefile
,而 Makefile
的 working tree 副本不同于 Makefile
.
(旁白:我在准备剪切和粘贴文本时不小心添加了两行foo
。我不会回去修复它,但是如果你自己做这个实验,预计输出略有不同。)
让我们现在 git add
这个更新的文件,然后将第一行的 foo
替换为 bar
:
$ git add Makefile
$ git status --short
M Makefile
注意M
左移一栏,M-space-space-Makefile,说明index拷贝Makefile
与 HEAD
副本不同,但现在索引和工作树副本匹配。现在我们进行 foo-to-bar 替换:
$ ed Makefile << end
> 1s/foo/bar/
> w
> q
> end
107608
107608
$ git status --short
MM Makefile
我们现在有 两个 M
:Makefile
的 HEAD
副本与 Makefile
的索引副本不同,这与 Makefile
的工作树副本不同。 运行 git diff --cached
和 git 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
存在于索引中(因此会在下一次提交中),此外, 匹配 Makefile 的 HEAD
副本,因此 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 reset
、git rm
和git 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 --staged
或git reset
可用于 重新提取 已提交的副本到索引副本中。但是git rm
完全删除索引副本。
因此,如果 仅 如果 完全删除索引副本 会使事情恢复原样(当某些文件被删除时可能会发生这种情况) new),然后“使索引副本匹配 nonexistent HEAD
副本,通过删除它”是做你想做的事情的正确方法。但是,如果 HEAD
提交包含相关文件的副本,git rm --cached <em>the-file</em>
错了。
3请注意 --cached
和 --staged
与 git diff
具有 相同的含义 。然而,对于 git rm
,根本就没有 --staged
选项。为什么?这是 Git 开发人员的问题,但我们可以注意到,从历史上看,在遥远的过去,git diff
也没有 --staged
。因此,我最好的猜测是这是一个疏忽:当谁将 --staged
添加到 git diff
时,他们也忘记将 --staged
添加到 git rm
。