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 或--all,w 有第三个父级,所以图表看起来更像这样:
o--o--C <-- HEAD
|\
i-w <-- stash
/
u
(这些图表确实需要是图像,以便它们可以有箭头,而不是很难包含箭头的 ASCII-art)。在这种情况下,stash 是 commit w,stash^ 是 commit C(仍然是 HEAD),stash^2 是 commit i,stash^3 是 commit u,其中包含“未跟踪”甚至“未跟踪和忽略”文件。 (据我所知,这实际上并不重要,但我会在此处添加 i 将 C 作为父提交,而 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:
-
“反向修补”工作目录以消除 HEAD 和存储之间的差异。从本质上讲,这使工作目录仅保留了那些未隐藏的更改(具体来说,那些不在提交w;提交i 中的所有内容在此处被忽略)。
李>
仅当您指定 --no-keep-index:运行 git reset(完全没有选项,即 git reset --mixed)。这会清除所有内容的“待提交”状态,而不会更改任何其他内容。 (当然,在运行git stash save -p 之前,您使用git add 或git add -p 进行的任何部分更改都保存在提交i 中。)
-
没有-p:
运行git reset --hard(如果你也指定了-q)。这会将工作树设置回HEAD 提交中的状态。
仅当您指定了-a 或-u:运行git clean --force --quiet -d(如果-a 则使用-x,如果-u 则不使用它)。这将删除所有未跟踪的文件,包括未跟踪的目录;使用-x(即在-a 模式下),它还会删除所有被忽略的文件。
仅当您指定 -k / --keep-index 时:使用 git read-tree --reset -u $i_tree 将隐藏的索引“恢复”为“要提交的更改”,该索引也出现在工作树中。 (--reset 应该没有效果,因为第 1 步清除了工作树。)
apply 时间的各种选项
恢复存储的两个主要子命令是apply 和pop。 pop 代码只运行apply,然后,如果apply 成功,则运行drop,因此实际上只有apply。 (嗯,还有branch,稍微复杂一点——但最后,它也使用了apply。)
当您应用 stash 时——实际上是任何“类似stash 的对象”,即任何可以被 stash 脚本视为 stash-bag 的东西——只有两个特定于 stash 的选项:
-
-q, --quiet
-
--index(不是--keep-index!)
其他标志会累积,但无论如何都会立即被忽略。 (show 使用相同的解析代码,这里将其他标志传递给git diff。)
其他一切都由 stash-bag 的内容以及工作树和索引的状态控制。如上所述,我将使用标签w、i 和u 来表示存储中的各种提交,并使用C 来表示存储包挂起的提交。
apply 序列是这样的,假设一切顺利(如果某些事情提前失败,例如,我们 在合并中间,或者git apply --cached 失败,脚本错误 -在那一点上):
- 将当前索引写入树中,确保我们不在合并中
- 仅当
--index: diff commit i 与 commit C,管道到 git apply --cached,保存生成的树,并使用 git reset 取消暂存它
- 仅当
u 存在时:将git read-tree 和git checkout-index --all 与临时索引一起使用,以恢复u 树
- 使用
git merge-recursive 将C 的树(“基础”)与步骤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 丢弃存储。)
如果合并成功:
最后,脚本运行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 i 和 w 提交转换为普通的分支提交 I 和 W。
1更准确地说,它运行git add-interactive --patch=stash --,它直接调用perl脚本进行交互式添加,并带有特殊的魔法集用于存储。还有一些其他神奇的--patch 模式;看剧本。
2这里有一个很小的错误:git 将提交索引的树 $i_tree 读取到临时索引中,然后将工作目录与 HEAD 进行比较。这意味着如果您更改了索引中的某个文件 f,然后将其更改back以匹配 HEAD 修订版,则存储在 stash-bag 中 w 下的工作树包含index 版本的f,而不是 work-tree 版本的f。