如何从"git stash save --all"中恢复?

sil*_*non 13 git git-stash

我想隐藏未跟踪的文件,但我继续传递错​​误的选项.对我来说这听起来是对的:

git stash save [-a|--all]
Run Code Online (Sandbox Code Playgroud)

但实际上这也隐藏了文件.正确的是:

git stash save [-u|--include-untracked]
Run Code Online (Sandbox Code Playgroud)

当我运行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
Run Code Online (Sandbox Code Playgroud)

所以命令失败了.

如何恢复跟踪和未跟踪的更改?git reflog不存储存储命令.

tor*_*rek 25

TL; DR版本:

您需要清理目录(以git clean术语表示)才能正确应用存储.这意味着运行git clean -f,甚至git clean -fdx,这是一件非常丑陋的事情,因为一些未跟踪或未跟踪和忽略的文件/目录可能是您要保留的项目,而不是完全删除.(如果是这样,你应该将它们移到工作树之外,而不是将git clean它们移走.请记住,git clean删除的文件正是那些你无法从Git中获取的文件!)

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

关于藏匿本身的基本事实

当您使用git stash save其中任何一个-u或时-a,存储脚本将其"存储包"写为三个提交而不是通常的双父提交.

从图形来看,就提交图而言,"存储包"通常看起来像这样:

o--o--C     <-- HEAD (typically, a branch)
      |\
      i-w   <-- stash
Run Code Online (Sandbox Code Playgroud)

os为任何旧的普通提交节点,是C.节点C(用于提交)有一个字母,所以我们可以命名它:它是"藏匿袋"悬挂的地方.

存储袋本身是悬挂的小三角形包C,它包含两个提交:w是工作树提交,i是索引提交.(未显示,因为它很难绘制,是w第一个父亲是C第二个父母的事实i.)

--untracked或者--all有第三个父级w,所以图表看起来更像这样:

o--o--C     <-- HEAD
      |\
      i-w   <-- stash
       /
      u
Run Code Online (Sandbox Code Playgroud)

(这些图确实需要是图像,以便它们可以有箭头,而不是ASCII技术,其中箭头很难包含).在这种情况下,stash是提交w,stash^提交C(仍然是HEAD),stash^2提交istash^3提交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
  • with -u-a,包含(仅)未跟踪文件或所有(未跟踪和忽略)文件的无父提交.

stash然后该脚本将保存您当前的工作树.它使用临时索引文件(基本上是一个新的临时区域)来完成此操作.使用时-p,脚本将HEAD提交读出到新的临时区域,然后有效地运行1git add -i --patch,以便此索引随您选择的修补程序而结束.如果没有-p,它只是将工作目录与stashed索引区分开以查找已更改的文件.2 在任何一种情况下,它都会从临时索引中写入一个树对象.该树将是提交树w.

作为最后一个存储创建步骤,脚本使用刚保存的树,父提交C,索引提交和未跟踪文件的根提交(如果存在),以创建最终的存储提交w.然而,脚本则需要几个会影响您的更多步骤的工作目录,这取决于您是否使用-a,-u,-p,和/或--keep-index(和记住-p暗示--keep-index):

  • -p:

    1. "反向修补"工作目录以删除HEAD存储和存储之间的区别.从本质上讲,这会使工作目录只保留那些没有被隐藏的更改(特别是那些不在提交中的更改w; i这里忽略了提交中的所有内容).

    2. 只有你指定--no-keep-index:run git reset(根本没有选项,即git reset --mixed).这样可以清除所有内容的"待提交"状态,而无需更改任何其他内容.(当然,您在运行之前git stash save -p使用git addor进行的任何部分更改都会git add -p保存在提交中i.)

  • 没有-p:

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

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

    3. 仅当您指定-k/ --keep-index:用于git read-tree --reset -u $i_tree"将"存储索引"恢复"为"工作树中也将提交的更改".(--reset由于步骤1清除了工作树,因此应该没有效果.)

在各种选项apply的时间

恢复存储的两个主要子命令是applypop.该pop代码只是运行apply,然后,如果apply成功,运行drop,因此,实际上,有真的只是apply.(嗯,还有branch,这有点复杂 - 但最终,它也使用apply.)

当您应用存储 - 任何"存储类对象"时,实际上,即存储脚本可以视为存储包的任何内容 - 只有两个存储特定选项:

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

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

其他所有内容都由存储包的内容以及工作树和索引的状态控制.如上所述,我将使用标签w,iu表示存储中的各种提交,并C表示存储包挂起的提交.

apply顺序是这样的,假设一切顺利的话(如果事情早发生故障,例如,我们在合并的中间,或git apply --cached失败,脚本错误出在这一点上):

  1. 将当前索引写入树中,确保我们不在合并的中间
  2. 仅当--index:diff提交i提交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,如果索引中存在未合并的条目,则会失败.如果写树工作,则结果是树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存储中存在提交,则脚本会坚持提取它,但git checkout-index --all如果任何这些文件被覆盖,则会失败.(请注意,这是通过临时索引文件完成的,该文件将在以后删除:步骤3根本不使用正常的暂存区域.)

(步骤4使用了三个我没有看到的"魔术"环境变量:提供要合并的树的"名称".要运行,脚本提供四个参数:.如前所述,这些是基本提交的树,该指数在启动-OF- 和藏匿的工作承诺,要为每个树得到串名字,看起来在前面加上组成的名称环境到原始SHA-1的每棵树,脚本不传递任何策略参数,也不允许你选择除了之外的任何策略.可能它应该.)$GITHEAD_tgit merge-recursive$b_tree -- $c_tree $w_treeCapplywgit merge-recursiveGITHEAD_git merge-recursiverecursive

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

如果合并成功,但是:

  • 如果我们有$unstashed_index_tree-ie,我们正在做--index,并且第2步中的所有其他测试都已通过 - 那么我们需要恢复在步骤2中创建的索引状态.在这种情况下,一个简单的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以针对原始保存的索引执行单树合并,然后git update-index --add使用前面的文件名diff-index.我不确定为什么它会达到这些长度(git-read-tree手册页中有一个暗示,关于避免修改文件的错误命中,这可能解释它),但这就是它的作用.

最后,脚本运行git status(输出发送到/dev/nullfor -q模式;不确定它为什么一直运行-q).

几句话 git stash branch

如果您在应用存储时遇到问题,可以将其转换为"真正的分支",这样可以保证还原(除非您通常u清除包含提交不存在的存储问题,除非您清除unstaged甚至可能首先忽略文件).

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

git checkout -b new_branch stash^
Run Code Online (Sandbox Code Playgroud)

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

git stash apply --index
Run Code Online (Sandbox Code Playgroud)

此时,任何较早的分阶段更改都应再次暂存,并且任何早期未分阶段(但已跟踪)的文件将在工作目录中进行未分阶段但已跟踪的更改.现在放下藏匿物是安全的:

git stash drop
Run Code Online (Sandbox Code Playgroud)

使用:

git stash branch new_branch
Run Code Online (Sandbox Code Playgroud)

只需为您完成上述顺序.字面上运行git checkout -b,如果成功,则应用存储(with --index)然后删除它.

完成此操作后,您可以提交索引(如果您愿意),然后添加并提交其余文件,使两个(或者如果您省略第一个,索引,提交一个)"常规"提交"常规" "分支:

o-o-C-o-...   <-- some_branch
     \
      I-W     <-- new_branch
Run Code Online (Sandbox Code Playgroud)

你已经转换了存储袋iw提交普通的分支机构提交IW.


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

2这里有一个非常小的错误:git将$i_tree已提交索引的树读入临时索引,但随后将工作目录区分开来HEAD.这意味着,如果你改变了一些文件f索引,然后改了回来,以配合HEAD修订,下存储的工作树w中藏袋包含索引的版本f,而不是工作树版本f.

  • 一个非常全面的答案,我必须说:)我的系统目前运行不正常,所以我无法测试TL; DR版本,但它确实有意义,所以我将此标记为正确答案. (2认同)