【问题标题】:How to recover from "git stash save --all"?如何从“git stash save --all”中恢复?
【发布时间】:2014-01-02 08:57:34
【问题描述】:

我想存储未跟踪的文件,但我一直传递错误的选项。对我来说这听起来很正确:

git stash save [-a|--all]

但这实际上也隐藏了被忽略的文件。正确的是:

git stash save [-u|--include-untracked]

当我运行git stash save -a 并尝试git stash pop 它时,我得到所有忽略文件的无数错误:

path/to/file1.ext already exists, no checkout
path/to/file1.ext already exists, no checkout
path/to/file1.ext already exists, no checkout
...
Could not restore untracked files from stash

所以命令失败。

如何取回已跟踪和未跟踪的隐藏更改? git reflog 不存储存储命令。

【问题讨论】:

  • --all 选项的这种行为绝对是邪恶的。

标签: git git-stash


【解决方案1】:

TL;DR 版本:

您需要目录是干净的(在git clean 术语中)才能正确应用存储。这意味着运行git clean -f,甚至运行git clean -fdx,这是一件很难看的事情,因为一些未跟踪或未跟踪和忽略的文件/目录可能是您想要保留的项目,而不是删除完全。 (如果是这样,您应该将它们移到您的工作树之外,而不是 git clean-ing 将它们移走。请记住,git clean 删除的文件正是您无法从中取回的文件吉特!)

要了解原因,请查看“应用”说明中的第 3 步。请注意,没有选项可以跳过存储中未跟踪和/或忽略的文件。

关于存储本身的基本事实

当您将git stash save-u-a 一起使用时,存储脚本会将其"stash bag" 写入为三个-父提交,而不是通常的双父提交。

从图表上看,“stash bag”通常看起来像这样,就提交图而言:

o--o--C     <-- HEAD (typically, a branch)
      |\
      i-w   <-- stash

os 是任何旧的普通提交节点,C 也是如此。节点C(用于提交)有一个字母,因此我们可以命名它:它是“存储袋”所在的位置。

stash bag 本身就是挂在C 上的小三角包,它包含两个提交:w 是工作树提交,i 是索引提交。 (未显示,因为很难绘制图表,w 的第一个父级是 C,而它的第二个父级是 i。)

对于--untracked--allw 有第三个父级,所以图表看起来更像这样:

o--o--C     <-- HEAD
      |\
      i-w   <-- stash
       /
      u

(这些图表确实需要是图像,以便它们可以有箭头,而不是很难包含箭头的 ASCII-art)。在这种情况下,stash 是 commit wstash^ 是 commit C(仍然是 HEAD),stash^2 是 commit istash^3 是 commit u,其中包含“未跟踪”甚至“未跟踪和忽略”文件。 (据我所知,这实际上并不重要,但我会在此处添加 iC 作为父提交,而 u 是无父提交或根提交。似乎没有造成这种情况的特殊原因,这只是脚本的执行方式,但它解释了为什么“箭头”(线)与图中的一样。)

save 时间的各种选项

在保存时,您可以指定以下任何或所有选项:

  • -p, --patch
  • -k, --keep-index, --no-keep-index
  • -q, --quiet
  • -u, --include-untracked
  • -a, --all

其中一些暗示、覆盖或禁用其他。例如,使用-p 会完全改变脚本用于构建存储的算法,并且还会打开--keep-index,如果您不想这样做,则强制您使用--no-keep-index 将其关闭。它与-a-u 不兼容,如果给出其中任何一个,都会出错。

否则,在-a-u 之间,保留设置最后一个的那个。

此时脚本会创建一个或两个提交:

  • 一个用于当前索引(即使它不包含任何更改),父提交C
  • 对于 -u-a,无父提交包含(仅)未跟踪的文件或所有(未跟踪和忽略的)文件。

stash 脚本随后会保存您当前的工作树。它使用临时索引文件(基本上是一个新的暂存区)来执行此操作。使用-p,脚本将HEAD 提交读出到新的暂存区域,然后有效地1 运行git add -i --patch,这样这个索引就会与您选择的补丁一起结束。如果没有-p,它只会将工作目录与隐藏的索引进行比较以查找更改的文件。2 无论哪种情况,它都会从临时索引中写入一个树对象。这棵树将是提交w 的树。

作为其最后一个stash-creation步骤,脚本使用刚刚保存的树、父提交C、索引提交和未跟踪文件的根提交(如果存在)来创建最后的存储提交w。但是,该脚本会采取更多步骤来影响您的工作目录,具体取决于您使用的是-a-u-p 和/或--keep-index(请记住-p 暗示 --keep-index):

  • -p:

    1. “反向修补”工作目录以消除 HEAD 和存储之间的差异。从本质上讲,这使工作目录保留了那些未隐藏的更改(具体来说,那些不在提交w;提交i 中的所有内容在此处被忽略)。

      李>
    2. 仅当您指定 --no-keep-index:运行 git reset(完全没有选项,即 git reset --mixed)。这会清除所有内容的“待提交”状态,而不会更改任何其他内容。 (当然,在运行git stash save -p 之前,您使用git addgit add -p 进行的任何部分更改都保存在提交i 中。)

  • 没有-p

    1. 运行git reset --hard(如果你也指定了-q)。这会将工作树设置回HEAD 提交中的状态。

    2. 仅当您指定了-a-u:运行git clean --force --quiet -d(如果-a 则使用-x,如果-u 则不使用它)。这将删除所有未跟踪的文件,包括未跟踪的目录;使用-x(即在-a 模式下),它还会删除所有被忽略的文件。

    3. 仅当您指定 -k / --keep-index 时:使用 git read-tree --reset -u $i_tree 将隐藏的索引“恢复”为“要提交的更改”,该索引也出现在工作树中。 (--reset 应该没有效果,因为第 1 步清除了工作树。)

apply 时间的各种选项

恢复存储的两个主要子命令是applypoppop 代码只运行apply,然后,如果apply 成功,则运行drop,因此实际上只有apply。 (嗯,还有branch,稍微复杂一点——但最后,它也使用了apply。)

当您应用 stash 时——实际上是任何“类似stash 的对象”,即任何可以被 stash 脚本视为 stash-bag 的东西——只有两个特定于 stash 的选项:

  • -q, --quiet
  • --index(不是--keep-index!)

其他标志会累积,但无论如何都会立即被忽略。 (show 使用相同的解析代码,这里将其他标志传递给git diff。)

其他一切都由 stash-bag 的内容以及工作树和索引的状态控制。如上所述,我将使用标签wiu 来表示存储中的各种提交,并使用C 来表示存储包挂起的提交。

apply 序列是这样的,假设一切顺利(如果某些事情提前失败,例如,我们 在合并中间,或者git apply --cached 失败,脚本错误 -在那一点上):

  1. 将当前索引写入树中,确保我们不在合并中
  2. 仅当 --index: diff commit i 与 commit C,管道到 git apply --cached,保存生成的树,并使用 git reset 取消暂存它
  3. 仅当u 存在时:将git read-treegit checkout-index --all 与临时索引一起使用,以恢复u
  4. 使用git merge-recursiveC 的树(“基础”)与步骤1 中编写的树(“更新的上游”)和w 中的树(“隐藏的更改”)合并

在这一点之后,它变得有点复杂 :-) 因为这取决于步骤 4 中的合并是否顺利。但首先让我们稍微扩展一下上面的内容。

第 1 步非常简单:脚本只运行 git write-tree,如果索引中有未合并的条目,该脚本将失败。如果 write-tree 工作,结果是一个树 ID(脚本中的$c_tree)。

第 2 步更复杂,因为它不仅检查--index 选项,还检查$b_tree != $i_tree(即C 的树和i 的树之间存在差异),并且$c_tree != $i_tree(即,在步骤 1 中写出的树与 i 的树之间存在差异)。 $b_tree != $i_tree 的测试是有道理的:它检查是否有任何要应用的更改。如果没有变化——如果i 的树与C 的树匹配——就没有要恢复的索引,而且--index 根本就不需要。但是,如果$i_tree 匹配$c_tree,那仅仅意味着当前索引已经包含要通过--index 恢复的更改。确实,在这种情况下,我们不想git apply 进行这些更改;但我们确实希望它们保持“恢复”状态。 (也许这就是我在下面不太理解的代码的重点。不过,这里似乎更有可能存在一个小错误。)

无论如何,如果第 2 步需要运行git apply --cached,它也会运行git write-tree 来编写树,并将其保存在脚本的$unstashed_index_tree 变量中。否则$unstashed_index_tree 为空。

第 3 步是“不干净”目录中出现问题的地方。如果 u 提交存在于 stash 中,脚本会坚持提取它,但如果这些文件中的任何一个被覆盖,git checkout-index --all 将失败。 (请注意,这是使用临时索引文件完成的,之后将其删除:步骤 3 根本不使用正常的暂存区域。)

(第 4 步使用了三个“神奇”环境变量,我没有看到记录在案:$GITHEAD_<em>t</em> 提供了要合并的树的“名称”。要运行 git merge-recursive,脚本提供了四个参数:$b_tree@ 987654479@$c_tree$w_tree。如前所述,这些是基本提交C、index-at-start-of-apply 和隐藏工作提交w 的树。要获得字符串-每个树的名称,git merge-recursive 在环境中查找通过将 GITHEAD_ 附加到每棵树的原始 SHA-1 形成的名称。脚本不会将任何策略参数传递给 git merge-recursive,也不允许您选择任何recursive 以外的策略。可能应该。)

如果合并有冲突,则存储脚本运行git rerere (q.v.),如果--index,则告诉您索引未恢复并以合并冲突状态退出。 (与其他早期退出一样,这可以防止 pop 丢弃存储。)

如果合并成功:

  • 如果我们有一个$unstashed_index_tree——也就是说,我们正在做--index并且第 2 步中的所有其他测试也都通过了——那么我们需要恢复索引状态在第 2 步中创建。在这种情况下,一个简单的 git read-tree $unstashed_index_tree(没有选项)就可以了。

  • 1234563 987654498@ 与早期 diff-index 中的文件名。我不太确定为什么会达到这些长度(git-read-tree 手册页中有一个提示,关于避免修改文件的错误命中,这可能会解释它),但就是这样可以。

最后,脚本运行git status(在-q 模式下将输出发送到/dev/null;不知道为什么它在-q 下运行)。

关于git stash branch的几句话

如果您在应用存储时遇到问题,可以将其转换为“真正的分支”,这样可以保证恢复(通常情况下,包含提交 u 的存储问题除外除非您先清除未暂存甚至可能被忽略的文件,否则不适用)。

这里的技巧是首先检查提交C(例如,git checkout stash^)。这当然会导致“分离的 HEAD”,因此您需要创建一个新分支,您可以将其与签出提交 C 的步骤结合使用:

git checkout -b new_branch stash^

现在您可以应用存储,即使使用--index,它也应该可以工作,因为它将应用于存储包挂起的同一个提交:

git stash apply --index

此时,任何较早的暂存更改都应再次暂存,并且任何较早的未暂存(但已跟踪)文件将在工作目录中具有其未暂存但已跟踪的更改。现在可以安全地放下存储了:

git stash drop

使用:

git stash branch new_branch

只需为您完成上述顺序。它实际上运行git checkout -b,如果成功,则应用存储(使用--index)然后丢弃它。

完成此操作后,您可以提交索引(如果您愿意),然后添加并提交剩余的文件,以进行两个(或者如果您省略第一个,索引,提交,则为一个)“常规”提交一个“常规”分支:

o-o-C-o-...   <-- some_branch
     \
      I-W     <-- new_branch

并且您已经将 stash-bag iw 提交转换为普通的分支提交 IW


1更准确地说,它运行git add-interactive --patch=stash --,它直接调用perl脚本进行交互式添加,并带有特殊的魔法集用于存储。还有一些其他神奇的--patch 模式;看剧本。

2这里有一个很小的错误:git 将提交索引的树 $i_tree 读取到临时索引中,然后将工作目录与 HEAD 进行比较。这意味着如果您更改了索引中的某个文件 f,然后将其更改back以匹配 HEAD 修订版,则存储在 stash-bag 中 w 下的工作树包含index 版本的f,而不是 work-tree 版本的f

【讨论】:

  • 一个相当全面的答案,我必须说:) 我的系统目前不能正常工作,所以我无法测试 TL;DR 版本,但它真的很有意义,所以我会标记这是一个正确的答案。
  • 这个人的解释真棒,谢谢
  • @torek,您能否详细说明一下这部分:没有-p,它只是将工作目录与隐藏的索引进行比较以查找更改的文件。。是 git diff 文件查找索引和工作目录中的版本之间的差异,然后 git adds 这些差异到临时索引以从中写入树吗?
  • @Maximus:实际命令(有错误)是:git diff --name-only -z HEAD -- &gt;"$TMP-stagenames" 。结果列表返回给git update-index -z --add --remove --stdin。然后用git write-tree 写回索引。所有这些都是使用 GIT_INDEX_FILE="$TMPindex" 运行的,它最初是从 $i_tree 填充的(因此存在错误:差异比较 HEAD 与工作树,但应比较 $i_tree 与工作树)。
  • @Maximus:是的,完全正确。 (git stash 将索引作为树写入 repo 非常容易,因为这就是 git write-tree 所做的。)
【解决方案2】:

在没有完全理解为什么会出现问题的情况下,我找到了一个快速的解决方案:

git show -p --no-color [<stash>] | git apply

--no-color 选项会从 diff 输出中删除所有颜色,因为它们会破坏 git apply 命令。

但是,如果有人可以编辑此答案并解释为什么 git stash pop 失败,那就太好了。

【讨论】:

    猜你喜欢
    • 2021-11-09
    • 2012-12-25
    • 1970-01-01
    • 2015-06-28
    • 2017-11-24
    • 2019-11-13
    • 2021-09-09
    • 1970-01-01
    相关资源
    最近更新 更多