为什么git stash pop会说无法从stash条目中恢复未被跟踪的文件?

207

我有一堆已暂存和未暂存的修改,我想快速切换到另一个分支,然后再切换回来。

所以我使用以下命令暂存我的修改:

$ git stash push -a
事后看来,我可能本可以使用--include-untracked而不是--all

然后当我去弹出存储时,会出现很多类似以下错误的内容:

$ git stash pop
foo.txt already exists, no checkout
bar.txt already exists, no checkout
...
Could not restore untracked files from stash entry

好像从贮藏中没有任何修改被恢复。

我还尝试了$ git stash branch temp,但是它显示相同的错误。

我找到了一个解决方法,就是使用:

$ git stash show -p | git apply

目前避免了灾难,但这引发了一些问题。

为什么会首次发生这个错误?下次该如何避免?


80
我需要使用命令:git stash show -p | git apply --3 - xmedeko
9
我唯一有效的方法是上面的评论!! - Mehraj Malik
6
谢谢@xmedeko,请问 git stash show -p | git apply 和 git stash show -p | git apply --3 有什么不同?请翻译。 - Deepak
13
如果您感到惊慌,只需使用 git stash show 列出您隐藏的文件,然后逐个恢复这些文件:$ git show stash@{0}:your/stashed/file.txt > your_rescued_file.txt。这将从隐藏中获取文件并保存在不同的名称下。现在您可以安全地尝试正确的恢复方法(请参见下面的答案)。如果事情出了差错,您始终可以将已恢复的文件作为最后的资源。 - Danijel
8
你可以使用 git stash show -p -u 命令来展示整个暂存区(包括未追踪的文件),使用 git stash show -p -u | git apply --3 命令来恢复整个暂存区(包括未追踪的文件)。 - Finn
显示剩余3条评论
9个回答

221

我成功地重新制造了你的问题。如果你存储未跟踪的文件,然后创建这些文件(例如,在你的示例中,foo.txtbar.txt),那么当你应用git stash pop时,就会覆盖未跟踪文件的本地更改。

要解决这个问题,你可以使用以下命令。这将覆盖任何未保存的本地更改,所以请小心。

git checkout stash -- .

我在之前的命令中找到了一些进一步的信息


2
我的工作树肯定是干净的,尽管被忽略的文件可能有更改。 - steinybot
1
看起来使用 --all/-a 将包括被忽略的文件,所以这可能是相关的。 - Daniel Smith
2
我假设相同的逻辑适用于被忽略的文件,并将其标记为答案(尽管我认为在这种情况下 git merge --squash --strategy-option=theirs stash 方法更好)。 - steinybot
2
这很有帮助,但它没有恢复未跟踪的文件 - 如果你有同样的问题(已经存在,没有检出),请查看我下面的答案。 - Erik Koopmans
不清楚命令中到底检出了什么,是最后一个存储区还是所有的存储区? - serge
显示剩余2条评论

168

作为进一步的解释,需要注意的是git stash会创建两个或三个提交记录。默认情况下是两个提交记录;如果使用--all--include-untracked选项中的任何一个,就会创建三个提交记录。

这两个或三个提交记录在一个重要方面非常特殊:它们不属于任何分支。Git通过特殊的名称stash1来定位它们。最重要的是,Git允许你——并且会强制让你——对这两个或三个提交记录进行操作。要理解这一点,我们需要查看这些提交记录中包含了哪些内容。

藏匿记录中包含了什么

每个提交记录可以列出一个或多个父节点。它们构成了一个图形,其中较新的提交记录指向较早的提交记录。stash通常保存两个提交记录,我喜欢称其为i表示暂存区的内容和w表示工作树的内容。同时,请记住每个提交记录都保存了快照。在普通的提交记录中,此快照是从暂存区的内容中生成的。因此,i提交记录实际上是一个完全正常的提交记录!它只是不属于任何分支:

...--o--o--o   <-- branch (HEAD)
           |
           i
如果您正在创建普通的stash,git stash代码现在通过复制您所有已跟踪的工作树文件(到一个临时辅助索引)来进行操作。Git将此w提交的第一个父节点设置为指向HEAD提交,第二个父节点指向提交i。最后,它将stash设置为指向此w提交。
...--o--o--o   <-- branch (HEAD)
           |\
           i-w   <-- stash
如果您添加 --include-untracked--all,Git 会在执行 iw 之间创建一个额外的提交 u。对于提交 u 的快照内容是那些未被跟踪但未被忽略的文件(--include-untracked),或者即使它们被忽略也是未被跟踪的文件(--all)。这个额外的提交 u 没有父节点,然后当 git stash 创建 w 时,它会将 w 的第三个父节点设置为这个提交 u,以便你得到:
...--o--o--o   <-- branch (HEAD)
           |\
           i-w   <-- stash
            /
           u

此时,Git 还会使用 git clean 删除任何在 u 提交中进入工作树的文件。

恢复一个存储区

当您要恢复一个存储区时,可以选择使用--index或不使用。这告诉git stash apply(或任何内部使用apply的命令,例如pop),它应该使用i提交来尝试修改您当前的索引。此修改通过以下方式完成:

git diff <hash-of-i> <hash-of-i's-parent> | git apply --index

基本想法会受到一些细节问题的干扰,所以差不多(或多或少)。

如果省略--indexgit stash apply将完全忽略i提交记录。

如果stash仅有两个提交记录,则git stash apply现在可以应用w提交记录。 这是通过调用git merge2(而不允许其提交或将结果视为正常合并)来完成的, 使用创建stash时的原始提交记录(i的父项和w的第一个父项)作为合并基础,w作为--theirs提交记录,以及当前(HEAD)提交记录作为合并的目标。 如果合并成功,一切都好——至少Git认为如此——git stash apply本身就会成功。如果您使用git stash pop应用了stash,则代码现在将删除stash。3 如果合并失败,则Git宣布应用失败。 如果您使用git stash pop,则代码保留stash,并提供与git stash apply相同的失败状态。

但是,如果有第三个提交记录——如果正在应用的stash中存在u提交记录——那么事情就会有所改变!没有选项可以假装u提交记录不存在。4 Git坚持要从该u提交记录中提取所有文件,并将它们提取到当前的工作树中。 这意味着文件必须要么根本不存在,要么与u提交记录中的内容相同。

为了实现这一点,您可以自己使用git clean,但请记住未跟踪的文件(无论是否被忽略)在Git存储库中没有其他存在方式,因此确保这些文件都可以被删除! 或者,您可以创建一个临时目录,将文件移动到该目录进行安全保管, 甚至可以运行另一个git stash save -ugit stash save -a,因为这些操作将为您运行git clean。 但是,这只会让您面对另一个类似于u的stash,稍后还需要处理。


1实际上是refs/stash。 如果您创建了一个名为stash的分支:分支的完整名称为refs/heads/stash, 因此这些不会冲突。 但不要这样做:Git不会介意,但是您会混淆自己。 :-)

2实际上,git stash代码在此处直接使用了git merge-recursive。 多种原因需要这样做, 还有一个副作用是确保Git在解决冲突并提交时不将其视为合并。

3这就是我建议避免使用git stash pop,而是使用git stash apply的原因。 您有机会查看应用了什么,并决定它是否被正确地应用。 如果没有,您仍然拥有您的stash,这意味着您可以使用git stash branch</


4
哇!你真该得一枚奖章!你能推荐一本关于详细解释Git的书吗?太棒了!👍 - Ilia Liachin
3
很遗憾,Git 是一个不断发展的工具,所以书籍很快就会过时。但是 Git 应该被理解为一组工具——用于操作提交文件、操作提交图形等命令——你可以将它们组合成对有用的东西,但是似乎很少有书籍以这种方式来介绍它。 - torek
5
我不理解解决问题的方法。是否只需添加“--index”:git stash apply --index - Danijel
3
感谢@torek。我首先执行了git stash save --all,然后立即执行了git stash apply,但是由于我在存储前重命名并重新创建了一些文件,因此有一些文件丢失了。帮助我的是:git checkout stash@{0} -- . 我甚至不会费心去尝试git checkout stash^3 -- .,因为现在看起来一切都没问题了。可惜我没有时间真正理解发生了什么。谢谢。 - Danijel
1
@user1169587:它的意思就是字面上的意思。也许举个例子会更好理解。假设第三个“u”提交包含一个名为path/to/file的文件(带有一些内容)。进一步假设你的工作树已经有了一个名为path/to/file的文件。提取“u”提交的内容将替换您现有的文件内容,破坏了您在名为path/to/file的文件中的任何工作。因此Git不会这样做。您必须自己先销毁这些内容(通过完全删除文件),或将它们移到安全位置(通过移动文件)。 - torek
显示剩余5条评论

63

为了进一步解释Daniel Smith的答案:该代码仅还原已跟踪的文件,即使您在创建存储时使用了--include-untracked(或-u)。所需的完整代码为:

git checkout stash -- .
git checkout stash^3 -- .
git stash drop

# Optional to unstage the changes (auto-staged by default).
git reset

这会完全还原跟踪的内容(在stash中)和未跟踪的内容(在stash^3中),然后删除stash。注意:

  • 小心 - 这将使用存储的内容覆盖所有文件!
  • 使用git checkout恢复文件会自动将它们所有标记为已暂存,因此我添加了git reset取消所有暂存操作。
  • 一些资源使用stash@{0}stash@{0}^3,在我的测试中,无论是否使用@{0}都可以正常工作。

来源:


3
为什么要删除隐藏文件夹,为安全起见不留着它待一段时间不行吗? - Danijel
@Danijel 当然,你可以保留stash - 我想这取决于你的使用情况。对于我的目的,一旦恢复了stash,我就不再需要它了。 - Erik Koopmans

15

除了其他答案外,我还做了一个小技巧

  • 删除所有新文件(之前已存在的文件,例如问题中的foo.txt和bar.txt)
  • git stash apply(可以使用任何命令,例如apply、pop等)

2
无法工作...已删除的文件在应用存储时再次出现..错误也是如此! - Aditya Rewari
1
如果已经提交了文件,只需在删除它们后提交即可。 - Wessam Al-Bakary

5

解决这个问题的简单方法是将现有的冲突文件重命名为一个不冲突的文件。

例如,如果你运行 git stash apply/pop 命令并出现以下情况:

foo.md already exists, no checkout
error: could not restore untracked files from stash

尝试将foo.md重命名为foo.new.md,然后重新运行git stash apply/pop,这样就不会出现错误了,并且您可以同时使用foo.mdfoo.new.md

3

我刚遇到了这个问题。由于上面的答案没有不丢失存储文件的情况下起作用,所以我进行了一些改进。

我的存储使用git stash -u创建,其中包含未跟踪的新添加文件。其中一些文件来自代码生成,并在我尝试弹出存储之前单独提交到主分支。当我尝试弹出存储时,我得到了以下信息:

> git stash pop
web-app/src/app/rest-services/api/models/model1.ts already exists, no checkout
web-app/src/app/rest-services/api/models/model2.ts already exists, no checkout
web-app/src/app/rest-services/api/models/model3.ts already exists, no checkout
web-app/src/app/rest-services/api/models/model4.ts already exists, no checkout
error: could not restore untracked files from stash

根据git无法合并两个不同的新文件的理论,我删除了工作目录中每个命名的文件。然后,我能够应用完整的存储区而不会丢失文件或出现错误。


1

以下是我所做的步骤,使其正常工作:

  1. 在我的工作目录中删除消息中提到的每个文件。
  2. 将存储的内容重新应用为未暂存状态。

所有存储的文件都应该被成功地应用。


我喜欢这种方法,但是对于我来说,它会无缘无故地列出大约1000个文件为“已存在,无需检出”。在我的存储中,我只有6个更改的文件和10个新的(未跟踪的)文件。 - John Little

0

一旦您将命名为(本地分支)的分支与本地更改一起存储,假设文件A、文件B和新添加的文件C。当在分支上隐藏代码时,文件C处于未跟踪状态。

稍后,如果有人执行git pull以从主分支获取最新代码,并且文件A已更新。之后,尝试在分支中应用stash,但由于在pull请求期间更新了File A而无法提供未跟踪的文件,即File C,因此会出现冲突。这是GIT新版本的限制。

解决方法或解决方案:将本地分支头指向未更新File A的位置,应用stash代码,现在您可以控制所有三个文件,将未跟踪的文件转换为跟踪文件或复制它,以适合您的需求。使用git pull命令更新您的分支头,您也拥有自己的代码(可以从stash中获取,也可以手动粘贴如果您复制了)


0
我在Visual Studio中遇到了同样的问题。当我尝试使用GitKraken时,我可以轻松地弹出存储区。所以这似乎取决于Git客户端。

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