如何在提交前撤销 'git add'?

11227

我错误地使用以下命令将文件添加到Git:

git add myfile.txt

我还没有运行git commit命令。我该如何撤销这个操作,以便这些更改不会被提交?


50
从 Git v1.8.4 开始,所有使用 HEADhead 的答案现在可以使用 @ 代替 HEAD。请参见此答案(最后一节)了解为什么可以这样做。 - user456814
5
我做了一个小总结,展示了取消暂存文件的所有方法:https://dev59.com/Dmw05IYBdhLWcg3w9mhW#16044987 - Daniel Alder
8
如果您使用Eclipse,取消勾选提交对话框中的文件就像简单的操作一样。 - Hamzahfrq
2
这是一份来自Github的绝佳资源:如何使用Git撤销(几乎)所有操作 - jasonleonhard
3
在发布新答案之前,请考虑此问题已经有25个以上的答案。确保您的答案提供了现有答案中不存在的内容。 - Sazzad Hissain Khan
显示剩余6条评论
39个回答

13352
取消暂存特定文件
git reset <file>

这将从当前索引中移除文件(即“即将提交”列表),而不会改变其他任何内容。

要取消暂存当前更改集中的所有文件:

git reset

在旧版本的Git中,上述命令等同于git reset HEAD <file>git reset HEAD,如果HEAD未定义(因为您尚未在存储库中进行任何提交)或模糊不清(因为您创建了一个名为HEAD的分支,这是一件愚蠢的事情,您不应该这样做),则会失败。然而,这在Git 1.8.2中发生了改变,所以在现代版本的Git中,即使在进行第一次提交之前,您也可以使用上述命令:

"git reset"(没有选项或参数)在您的历史记录中没有任何提交时曾经报错,但现在它会给您一个空的索引(以匹配您甚至不在的不存在的提交)。

文档:git reset

154
当然,这不是真正的撤消操作,因为如果错误的 git add 覆盖了之前已暂存但未提交的版本,我们就无法恢复它。我在下面的回答中尝试澄清这一点。 - leonbloy
12
git reset HEAD *.ext 是一条 Git 命令,其中 ext 是你想要取消添加的文件的扩展名。例如,如果你想要取消添加所有扩展名为 .bmp.zip 的文件,那么就可以写成 *.bmp*.zip。请注意,这个命令不会删除文件,只是将它们从 Git 的暂存区中移除。 - boulder_ruby
28
@Jonny,索引(即暂存区)包含所有文件,而不仅仅是更改的文件。 它“开始生效”(当您签出提交或克隆存储库时),作为指向HEAD所指向的提交中所有文件的副本。 因此,如果您从索引中删除文件(git rm --cached),这意味着您正在准备进行一个将删除该文件的提交。 另一方面,git reset HEAD <filename>会将文件从HEAD复制到索引中,以便下一次提交不会显示对该文件进行任何更改。 - Wildcard
23
我刚刚发现了一个 git reset -p 命令,就像 git add -p 一样。太棒了! - donquixote
23
您实际上可以恢复先前暂存但未提交的被覆盖更改,但这种方法不够用户友好,也不是100%安全(至少我没有找到)。请前往.git/objects,搜索您想要恢复的git add时创建的文件(如61/3AF3... -> 对象id为613AF3...),然后运行git cat-file -p <object-id>(这可能值得恢复数小时的工作,但也教会我们要经常提交)... - Peter Schneider
显示剩余10条评论

2440

您想要:

git rm --cached <added_file_to_undo>

推理:

当我刚开始接触这个领域时,我首先尝试了

git reset .

我想撤销我最初的添加,结果收到了这个(不太)有用的消息:

fatal: Failed to resolve 'HEAD' as a valid ref.
这是因为 HEAD 引用(分支?)直到第一次提交之后才存在。换句话说,如果你像我一样的工作流程是这样的,你会遇到和我一样的初学者问题:

  1. cd 到我的新项目目录中,尝试使用 Git 这个新东西
  2. git init
  3. git add .
  4. git status

    ... 屏幕滚动了一大堆 ...

    => 哎呀,我不想把所有文件都加入进去。

  5. 搜索“撤销 git add”

    => 找到 Stack Overflow - 好耶

  6. git reset .

    => fatal: Failed to resolve 'HEAD' as a valid ref.

进一步地,邮件列表上记录了关于其不友好的 bug。

而正确的解决方案则在 Git status output 中(是的,我忽略了它):

...
# Changes to be committed:
#   (use "git rm --cached <file>..." to unstage)
...
解决方案是使用git rm --cached FILE 命令。请注意,其他地方的警告- git rm 命令会删除您本地的工作副本文件,但如果您使用--cached选项,则不会删除。以下是git help rm的结果:

--cached 使用此选项仅从索引中取消暂存并删除路径。 工作树文件(无论是否修改)将被保留。

然后我继续使用:

git rm --cached .

尝试删除所有内容并重新开始。但这并没有起作用,因为虽然add .是递归的,但事实证明rm需要-r才能递归。叹息。

git rm -r --cached .

好的,现在我回到了起点。下一次我会使用-n进行干运行,查看将会被添加的内容:

git add -n .

在相信git help rm--cached不会破坏任何内容之前,我将所有东西都压缩到一个安全的位置(如果我拼错了会怎么样)。


29
哈哈,我也按照这个过程操作了,只是最后放弃了,并输入了“rm -rf .git”,再输入“git init”,因为我不相信“git rm --cached”能够保留我的工作副本。这说明在某些方面git仍然过于复杂。我认为“git unstage”应该成为一个标准命令,就算只能将其添加为别名,我也不介意。 - Adrian Macneil
8
对我而言,Git 建议使用 git reset HEAD <File>... 命令。 - drahnr
27
如果您正在将<file>文件初次导入存储库,则“git rm --cached <file>”实际上是正确的答案。如果您想要取消对该文件的更改,那么“git reset”是正确的答案。声称这个答案是错误的人可能在思考另一个问题。 - Barry Kelly
19
这份方法确实可行,但仅限于第一次提交,在该提交中文件之前不存在,或者在git add命令添加新文件时,而不是对已有文件进行更改。 - naught101
11
这段话的意思是:正是表明了 git 是多么不直观和复杂。与其有并行的“撤销”命令,你必须找到如何撤销它们。就像在流沙中试图解救受困的腿,然后卡住了手臂,接着又卡住了另一只手臂……每个命令都应该通过 GUI 完成,使用下拉菜单为选项提供选择……想想所有的用户界面和生产力增益,但我们却有这样混乱的旧式命令行界面。这也不像 git 的图形用户界面程序能使它更加直观易用。 - ahnbizcad
显示剩余17条评论

627

如果您输入:

git status

Git会告诉你哪些文件已暂存等信息,还会提供如何取消暂存的指导:

use "git reset HEAD <file>..." to unstage

我发现Git在这种情况下会很好地引导我做正确的事情。

注意:最近的Git版本(1.8.4.x)已更改了此消息:

(use "git rm --cached <file>..." to unstage)

30
如果add的文件已经被跟踪(add只是将新版本保存到缓存中),那么显示的消息将会不同 - 这里会显示你的消息。否则,如果文件之前没有被暂存,则会显示使用"git rm --cached <file>..."取消暂存 - leonbloy
太好了!如果您想取消暂存文件删除操作,只有git reset HEAD <file>这个命令可以使用。 - Jelle De Loecker
4
我的Git版本是2.14.3,它提示我运行git reset HEAD以取消暂存。 - SilverWolf
10
自 Git v2.23 以来,这个消息又再次更改。现在它显示为 git restore --staged <file>。有关更新,请参见下面的我的答案 - prosoitos

307

澄清一下:git add命令将更改从当前工作目录移动到暂存区(索引)。

这个过程称为暂存。因此,最自然的命令是将更改(已更改的文件)暂存

git stage

git addgit stage的一个更容易输入的别名。

很遗憾没有git unstagegit unadd命令。相关命令更难猜测或记住,但它非常显然:

git reset HEAD --

我们可以轻松地为此创建一个别名:

git config --global alias.unadd 'reset HEAD --'
git config --global alias.unstage 'reset HEAD --'

最后,我们有了新的命令:

git add file1
git stage file2
git unadd file2
git unstage file1

我个人使用更短的别名:

git a # For staging
git u # For unstaging

8
"moves"?这表示它已经从工作目录中移走了。但事实并非如此。 - Thomas Weller
实际上,“git stage”是“git add”的别名,这是Git和其他SCM上的历史命令。它已于2008年12月添加到“Git的git存储库”中,如果我可以这么说。 - Obsidian
3
我同意,Linus Torvalds没有创建对称命令,而是为不同的命令创建了一个新词,这非常令人恼火。对于对称,我的意思是: commit - uncommit; stage-unstage。或者使用关键字UNDO来代替许多命令:git commit X - git UNDO commit x。 似乎自然而然地需要记住很多单词。那些不经常使用的单词很容易被遗忘...所以我们都在这个页面上。 - fresko
@fresko 还有一个混乱的问题,就是将同一事物称为缓存、索引或暂存区,但这些东西不能更改,否则会破坏向后兼容性。与 Linux 内核类似,Torvalds 似乎更喜欢保持任何旧的部分疯狂的 API 不变,而不是破坏任何旧的工作流程。现在我们也有 git restore --staged path/fo/file 来实现此目的。 - Mikko Rantalainen
Git被软件开发人员使用,他们应该能够学习新的命令。对于Git,有多个CLI和GUI界面,但没有一个界面有足够大的改进使得所有人都在使用它。我个人只使用命令行git,并带有自定义别名+git guigitk。例如,我有git stagegit unstage - Mikko Rantalainen
显示剩余2条评论

217

除了已接受的答案,如果您误添加的文件非常大,即使使用“git reset”从索引中删除它,您可能仍然会注意到它似乎仍占用 .git 目录中的空间。

这并不是什么可担心的事情;该文件确实仍在存储库中,但仅作为“松散对象”。它不会被复制到其他存储库(通过克隆、推送),并且空间最终将被回收 - 尽管可能不会很快。如果您感到焦虑,可以运行:

git gc --prune=now

更新(接下来是我尝试消除最被赞的答案可能引起的混乱):
那么,真正的git add撤消是什么? git reset HEAD <file>
还是 git rm --cached <file>?
严格来说,如果我没错的话:都不是。
一般情况下,git add是无法安全地撤消的。
首先,让我们回忆一下git add <file>实际上做了什么:
1、如果<file>之前没有被跟踪过,则git add会将其添加到缓存中,并保存当前内容。
2、如果<file>已经被跟踪过,则git add会将当前内容(快照、版本)保存到缓存中。在Git中,这个动作仍然被称为“添加”,因为一个文件的两个不同版本(快照)被视为两个不同的项目:因此,我们确实向缓存中添加了一个新项目,以便稍后提交。
有鉴于此,问题略微模糊:
“我错误地使用命令添加了文件...”
OP的情境似乎是第一种情况(未跟踪的文件),我们希望“撤消”可以将该文件(而不仅仅是当前内容)从跟踪项目中删除。如果是这种情况,那么运行git rm --cached <file>就可以了。
我们也可以运行git reset HEAD <file>。一般来说,这更可取,因为它适用于两种情况:当我们错误地添加了已跟踪物品的版本时,它也执行撤消。
但是有两个注意事项。
第一:只有一个场景,git reset HEAD无法工作,但git rm --cached可以(如答案所指出):新存储库(没有提交)。但是,真的,这是一个实际上不相关的情况。
第二:请注意,git reset HEAD不能神奇地恢复先前缓存的文件内容,它只是从HEAD重新同步。如果我们误导的git add覆盖了之前被暂存但未提交的版本,我们无法恢复它。这就是为什么严格来说,我们不能撤消[*]。
示例:
$ git init
$ echo "version 1" > file.txt
$ git add file.txt   # First add of file.txt
$ git commit -m 'first commit'
$ echo "version 2" > file.txt
$ git add  file.txt   # Stage (don't commit) "version 2" of file.txt
$ git diff --cached file.txt
-version 1
+version 2
$ echo "version 3" > file.txt
$ git diff  file.txt
-version 2
+version 3
$ git add  file.txt    # Oops we didn't mean this
$ git reset HEAD file.txt  # Undo?
$ git diff --cached file.txt  # No dif, of course. stage == HEAD
$ git diff file.txt   # We have irrevocably lost "version 2"
-version 1
+version 3

当我们只为添加新文件(情况1)而执行“git add”,并且通过提交 git commit -a 命令更新新内容时,这并不是非常关键的。当然,如果我们在暂存更改后未提交就被覆盖,则有一些略微狡猾/复杂的方法可以恢复更改-请参阅Johannes Matokic和iolsmit的评论。

7
严格来说,有一种方法可以恢复已被 git add 替换的暂存文件。正如您提到的,git add 会为该文件创建一个 git 对象,当删除文件时不仅会成为松散对象,而且当被新内容覆盖时也是如此。但是,并没有自动恢复它的命令。相反,必须手动或使用专门为此情况编写的工具(如 libgit2)来识别和提取文件。但是,只有在文件非常重要且很大,无法通过编辑以前的版本重建时,才值得这样做。 - Johannes Matokic
5
更正一下:一旦找到了松散的对象文件(可以使用元数据如创建日期/时间),就可以使用 git cat-file 恢复其内容。 - Johannes Matokic
9
另一种恢复被暂存但未提交并因其他“git add”命令覆盖的更改的方法是通过“git fsck --unreachable”,它将列出所有不可访问的对象,然后您可以通过“git show SHA-1_ID”或“git fsck --lost-found”来检查这些对象。根据类型,将挂起的对象写入.git/lost-found/commit/.git/lost-found/other/。 另请参见git fsck --help - iolsmit
@iolsmit 如果一个更改已经被暂存但未提交,如何使用 git add 覆盖它? 如果我创建了一个名为 a.txt 的文件,并将 '1' 作为其内容,那么 git add 如何使其具有 '2' 或其他内容?只是想要一个解释。 - Filip Savic
2
如果您多次添加相同的文件而没有提交,例如echo 1 > a.txt后跟着git add a.txt - 不要立即提交 - echo 2 > a.txt后跟着git add a.txt;现在提交,例如git commit -m“create unreachable blob”并运行git fsck --unreachable - iolsmit
哦!被覆盖在索引中,而不是文件本身 :D 好的,谢谢! - Filip Savic

182

撤销已经添加的文件在Git中非常容易。要重置已经添加的myfile.txt,请使用:

git reset myfile.txt

这将使Git忘记myfile.txt的所有更改,并将其从暂存区中移除。

git reset HEAD myfile.txt

解释:

如果你已经将不需要的文件提交到了暂存区,想要撤销此操作,可以使用 git reset 命令。其中,HEAD 表示你本地文件的最新版本,最后一个参数则表示你要撤销的文件名。

下面的图片详细展示了这个过程中可能发生的所有步骤:

git reset HEAD file


这真的很清楚,Alireza,但如果您使用标记而不是图像,那将会更好。通过精心使用高亮、代码块和空白,您可以使其看起来同样清晰,但具有用户可以复制粘贴的优势。 - NeilG
对我有用 我执行了“git add .”,导致不必要的文件也被添加了。我通过“git reset HEAD <文件名>”删除了不必要的文件。 - Shashank Bodkhe

118

Git拥有几乎每个操作所需的命令,但为了正确操作需要广泛的知识,因此它在最好的情况下也是不符合直觉的...

你之前做了什么:

  • 更改文件并使用git add .git add <file>

你想要的是:

  • 从索引中删除文件,但保留其版本并在工作副本中保留未提交的更改:

 git reset HEAD <file>
  • 将文件重置为与HEAD最后一次提交相同的状态,撤销更改并将其从索引中移除:

  •  # Think `svn revert <file>` IIRC.
     git reset HEAD <file>
     git checkout <file>
    
     # If you have a `<branch>` named like `<file>`, use:
     git checkout -- <file>
    

    由于git reset --hard HEAD无法处理单个文件,因此需要执行以下操作。


  • 从索引和版本控制中删除<file>,保留带有更改的未版本化文件的工作副本:

  •  git rm --cached <file>
    
  • 彻底从工作副本和版本控制中删除<file>

  •  git rm <file>
    

    2
    我不理解'git reset head <file>'和'git rm --cached <file>'的区别。你能解释一下吗? - jeswang
    9
    @jeswang 文件要么是被git“知道”的(其中对它们的更改正在被跟踪),要么就没有被“版本化”。reset head可以撤销您当前的更改,但该文件仍然受git监视。rm --cached将文件从版本控制中移除,因此git不再检查它是否有更改(并且还会删除之前add命令告诉git索引的任何更改),但更改后的文件将保留在您的工作副本中,即在您的硬盘驱动器上的文件夹中。 - sjas
    4
    git reset HEAD <file>的区别在于它是临时的,该命令仅应用于下一次提交,但git rm --cached <file>将取消暂存状态,直到再次使用git add <file>添加。另外,git rm --cached <file>意味着如果您将该分支推送到远程,从该分支提取的任何人都将实际从其文件夹中删除该文件。 - DrewT
    2
    谢谢!正是我所搜寻的 git checkout -- <file> - Vladimir Ch

    115
    git rm --cached . -r
    

    此命令将递归地“取消添加”当前目录中添加的所有内容。


    5
    我不想取消所有内容,只想取消一个特定的文件。 - oz10
    4
    如果没有之前的提交记录,那么 git reset HEAD <file> 命令会提示 fatal: Failed to resolve 'HEAD' as a valid ref. 该命令可用于撤销文件的更改。 - Priya Ranjan Singh
    10
    不,这会从您当前的目录中删除所有内容。与仅取消暂存更改非常不同。 - Mark Amery

    105

    运行

    git gui
    

    手动删除所有文件,或者选择所有文件并点击 从提交中移除 按钮。


    2
    是的,我理解了。我只是想暗示你在回答中明示,比如说“你可以使用git-gui...” :) - Alexander Suraphel
    2
    它说,“git-gui:命令未找到”。我不确定这是否有效。 - Parinda Rajapaksha
    哇,这比使用你不懂的命令行简单多了。这绝对适合像我这样的初学者。感谢您撰写这篇文章! - Irfandy Jip
    git: 'gui' is not a git command. See 'git --help'. - abdulwasey20

    104

    问题表述不够清晰,原因是因为 git add 有两种含义:

    1. 新文件添加到暂存区,然后使用 git rm --cached file 撤销。
    2. 修改过的文件添加到暂存区,然后使用 git reset HEAD file 撤销。

    如果不确定,建议使用

    git reset HEAD file
    

    因为它在两种情况下都表现出预期的行为。

    警告:如果你对一个已修改的文件(即仓库中之前存在的文件)执行git rm --cached file,那么该文件将在git commit时被删除!它仍然存在于你的文件系统中,但是如果其他人拉取你的提交,该文件将从他们的工作树中删除。

    git status会告诉你文件是一个新文件还是已修改

    On branch master
    Changes to be committed:
      (use "git reset HEAD <file>..." to unstage)
    
        new file:   my_new_file.txt
        modified:   my_modified_file.txt
    

    9
    这个页面上有相当多得到高赞的答案和评论对于git rm --cached somefile命令的行为都是彻底错误的。我希望这个回答能够在页面上得到显著的位置,以便保护新手免受所有虚假声明的误导。 - Mark Amery
    这是这里最好的答案之一,可惜排名比较靠后。 - Creos

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接