如何从“Git存储保存 – 所有”恢复?

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

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不存储隐藏命令。

TL; DR版本:

你需要的目录是干净的(以git clean条款),以便正确应用存储。 这意味着运行git clean -f ,甚至是git clean -fdx ,这是一件很丑恶的事情,因为一些未被跟踪或未被跟踪和忽略的文件/目录可能是你想要保留的项目比完全删除。 (如果是这样的话,你应该把它们移到你的工作树之外,而不是用git clean它们。记住, git clean删除的文件正是那些你无法从Git中得到的文件!)

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

关于存储本身的基本事实

当你用-u或者-a使用git stash save时,stash脚本把它的“stash bag”写成一个三层提交而不是通常的双亲提交。

以图表方式,“提包”通常看起来像这样,就提交图而言:

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

这是任何旧的普通的提交节点,就像C 。 节点C (对于提交)有一个字母,所以我们可以命名它:这是“藏匿袋”挂起的地方。

存储包本身就是从C挂起来的小三angular包,它包含两个提交: w是工作树提交, i是索引提交。 (没有显示,因为这很难说,事实上, w的第一个父母是C ,第二个父母是i

使用--untracked或 – 所有的w有第三个父,所以图看起来更像这样:

 o--o--C <-- HEAD |\ iw <-- stash / u 

(这些图真的需要是图像才能有箭头,而不是ASCII-art,箭头很难包括在内)。 在这种情况下, stash是提交wstash^是提交C (仍然是头), stash^2是提交istash^3是提交u ,其中包含“未跟踪”甚至“未跟踪和忽略”的文件。 (据我所知,这并不重要,但是我会在这里添加一个C作为父提交,而u是一个无父或者根提交,似乎没有什么特别的理由,脚本是如何做的,但是它解释了为什么“箭头”(线)和图中的一样。)

save时间的各种选项

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

  • -p ,– --patch
  • -k ,– --keep-index--no-keep-index --keep-index --no-keep-index
  • -q , – --quiet
  • -u ,– --include-untracked
  • -a , – 所有

其中一些暗示,覆盖或禁用他人。 例如,使用-p可以完全改变脚本用来构build存储的algorithm,还可以打开--keep-index ,迫使你使用--no-keep-indexclosures它,如果你不想要的话那。 它与-a-u不兼容,如果给出了这些,将会出错。

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

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

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

stash脚本然后保存您当前的工作树。 它使用一个临时索引文件(基本上,一个新的临时区域)。 使用-p ,脚本读出HEAD提交到新的临时区域,然后有效地运行git add -i --patch ,这样这个索引就会随你select的补丁一起运行。 没有-p ,它只是将工作目录与隐藏的索引进行比较以find更改的文件。 2在任何一种情况下,它都会从临时索引中写入一个树对象。 这棵树将是提交w的树。

作为最后一个存储创build步骤,脚本使用刚刚保存的树,父提交C ,索引提交以及未跟踪文件的根提交(如果存在),以创build最终的隐藏提交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 add或者git add -p保存在commit i

  • 没有-p

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

    2. 只有当你指定-a-u :运行git clean --force --quiet -d (如果-a-x则为-a ,否则为-u )。 这将删除所有未跟踪的文件,包括未跟踪的目录; 与-x (即在-a模式下),它也删除所有被忽略的文件。

    3. 只有当你指定-k / --keep-index :使用git read-tree --reset -u $i_tree将隐藏的索引“带回”也出现在工作树中的“要提交的更改”。 (因为第1步清除了工作树,所以--reset应该不起作用。)

apply时间的各种选项

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

当你应用一个存储 – 任何“存储类对象”,实际上,即任何存储脚本可以视为一个存储袋 – 只有两个存储特定选项:

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

其他标志是积累的,但无论如何都被及时忽略。 (相同的parsing代码用于show ,这里其他标志传递给git diff 。)

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

假如一切顺利的话, apply程序就会如此,假如一些事情早就失败了,例如我们处于合并的中间,或者git apply --cached失败了,脚本就会在这一点出错:

  1. 将当前索引写入树中,确保我们不在合并中
  2. 只有--index :diff提交i提交C ,pipe道git apply --cached ,保存结果树,并使用git reset取消它
  3. 只有当u存在:使用git read-treegit checkout-index --all用一个临时索引来恢复你的u
  4. 使用git merge-recursiveC (“base”)的树与步骤1中写入的树(“updated upstream”)和w的树(“stashhed changes”)合并

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

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

第二步比较复杂,因为它不仅检查--index选项,还检查$b_tree != $i_tree (也就是说, C和tree之间有一个区别), $c_tree ! = $i_tree (即,在步骤1中写出的树与为i的树之间存在差异)。 $b_tree != $i_tree的testing是有意义的:它检查是否有任何更改应用。 如果没有变化 – 如果i的树与C匹配 – 没有索引要恢复,并且 – 索引完全不需要。 但是,如果$i_tree$i_tree匹配,那仅仅意味着当前索引已经包含了要通过--index恢复的更改。 的确,在这种情况下,我们不希望git apply这些变化。 但我们希望他们保持“恢复”。 (也许这是我下面不太明白的代码的重点,虽然这里看起来更像是一个小错误。

在任何情况下,如果第2步需要运行git apply --cached ,它也会运行git write-tree来编写树,并将其保存在脚本的$unstashed_index_treevariables中。 否则, $unstashed_index_tree留空。

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

(第4步使用了三个“魔术”环境variables,我没有见过文档: $GITHEAD_ t提供被合并的树的“名称”为了运行git merge-recursive ,脚本提供了四个参数: $b_tree -- $c_tree $w_tree 。如前所述,这些是基本提交Capply开始索引和隐藏工作提交w树。为了获得每个树的string名, git merge-recursive look在名字环境中,为每个树添加GITHEAD_到原始SHA-1,脚本没有将任何策略parameter passing给git merge-recursive ,也不允许你select除recursive以外的任何策略。

如果合并发生冲突,则--index脚本将运行git rerere (qv),如果--index告诉您索引未恢复,并以合并冲突状态退出。 (和其他早期的出口一样,这可以防止pop掉下来。)

但是,如果合并成功,

  • 如果我们有一个$unstashed_index_tree -ie,我们正在做--index并且在步骤2中的所有其他testing都通过了 – 那么我们需要恢复在步骤2中创build的索引状态。在这种情况下,一个简单的git read-tree $unstashed_index_tree (没有选项)是这样做的。

  • 如果我们在$unstashed_index_tree没有东西,脚本使用git diff-index --cached --name-only --diff-filter=A $c_tree来查找要添加的文件,运行git read-tree --reset $c_tree对原始保存的索引进行单树合并,然后使用前面的diff-index的文件名添加git update-index --add 。 我不确定为什么它会变成这样的长度(在git-read-tree手册页中有一个提示,关于避免修改文件的错误命中,这可能会解释它),但这就是它的作用。

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

git stash branch几个字

如果您在应用存储时遇到问题,可以将其转换为“真正的分支”,这样可以保证还原(除非像往常一样,存储提交的问题除非您清理干净未分阶段,甚至可能被忽略的文件)。

这里的技巧是从提交C (例如, git checkout stash^ )开始。 这当然会导致一个“分离HEAD”,所以你需要创build一个新的分支,你可以结合检查提交的步骤C

 git checkout -b new_branch stash^ 

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

 git stash apply --index 

此时,任何先前的阶段性更改都应该再次进行,并且任何以前未分期(但已跟踪)的文件都将在工作目录中进行非分离但跟踪的更改。 现在放下隐藏是安全的:

 git stash drop 

使用:

 git stash branch new_branch 

只是为你做上面的序列。 它从字面上运行git checkout -b ,如果成功,则应用存储(使用--index ),然后删除它。

完成后,你可以提交索引(如果你想),然后添加和提交其余的文件,使两个(或一个,如果你离开第一,索引,提交)“常规”提交“常规“分支:

 ooCo-... <-- some_branch \ IW <-- new_branch 

并且你已经将存储包转换为普通的分支提交IW


1更正确的说,它运行git add-interactive --patch=stash -- ,它直接调用perl脚本进行交互式添加,并git add-interactive --patch=stash --特殊的魔法。 还有一些其他的魔法 – --patch模式; 看到脚本。

2这里有一个非常小的错误:git将$i_tree (提交索引的树)读入临时索引,然后将工作目录与HEAD 。 这意味着如果你改变了索引中的某个文件f ,然后改它来匹配HEAD修订版本,存储在w下的工作树包含f索引版本而不是f工作树版本。

如果没有充分理解问题发生的原因,我发现了一个快速解决scheme

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

--no-color选项从diff输出中删除任何颜色,因为它们搞砸了git apply命令。

但是,如果有人可以编辑这个答案,这将是很好的,提供了解释为什么git stash pop失败。