如何从“git stash save --all"中恢复? [英] How to recover from "git stash save --all"?

查看:38
本文介绍了如何从“git stash save --all"中恢复?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我想隐藏未跟踪的文件,但我一直传递错误的选项.对我来说这听起来不错:

git stash save [-a|--all]

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

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

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

path/to/file1.ext 已经存在,没有checkoutpath/to/file1.ext 已经存在,没有结帐path/to/file1.ext 已经存在,没有结帐...无法从存储中恢复未跟踪的文件

所以命令失败.

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

解决方案

TL;DR version:

您需要目录是干净的(在 git clean 术语中)才能正确应用存储.这意味着运行 git clean -f,甚至 git clean -fdx,这是一件很丑陋的事情,因为一些未跟踪或未跟踪的-ignored files/directories 可能是您想要保留的项目,而不是完全删除.(如果是这样,您应该将它们移出您的工作树而不是 git clean 将它们移走.请记住,git clean 删除的文件正是您无法从 Git 中恢复!)

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

关于存储本身的基本事实

当您将 git stash save-u-a 一起使用时,stash 脚本会写入其 藏匿袋" 作为三个-parent 提交而不是通常的两个-parent 提交.

就提交图而言,从图表上看,存储包"通常如下所示:

o--o--C <-- HEAD(通常是一个分支)|i-w <-- 藏匿处

o 是任何旧的普通提交节点,C 也是如此.节点 C(用于 Commit)有一个字母,所以我们可以给它命名:它是stash bag"悬挂的地方.

stash bag 本身就是挂在 C 上的小三角包,它包含两个提交:w 是工作树提交和 i 是索引提交.(没有显示,因为它很难绘制,事实上 w 的第一个父节点是 C 而它的第二个父节点是 i.)

使用 --untracked--allw 的第三个父级,所以图表看起来更像这样:

o--o--C <-- HEAD|i-w <-- 藏匿处/你

(这些图表确实需要是图像,以便它们可以有箭头,而不是很难包含箭头的 ASCII 艺术).在这种情况下,stash 是提交 wstash^ 是提交 C(仍然是 HEAD),stash^2 是 commit istash^3 是 commit u,其中包含未跟踪"甚至未跟踪和忽略"的文件.(据我所知,这实际上并不重要,但我会在这里添加 iC 作为父提交,而 u> 是无父或根提交.这似乎没有特别的原因,这只是脚本如何做事,但它解释了为什么箭头"(线)与图中的一样.)

保存时的各种选项

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

  • -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之间,保留last中设置的那个.

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

  • 一个用于当前索引(即使它不包含任何更改),父提交 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 和 stash 之间的差异.从本质上讲,这使工作目录only 那些没有被隐藏的更改(特别是那些不在 commit w 中的;commit i 中的所有内容都是此处忽略).

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

  • 没有-p:

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

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

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

apply 时的各种选项

恢复存储的两个主要子命令是 applypop.pop 代码只运行 apply 然后,如果 apply 成功,运行 drop,所以实际上,有真的只是申请.(嗯,还有branch,稍微复杂一点——不过最后也用了apply.)

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

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

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

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

apply 序列是这样的,假设一切顺利(如果某些事情早期失败,例如,我们正在在合并中,或者 gitapply --cached 失败,此时脚本出错):

  1. 将当前索引写入树中,确保我们不在合并过程中
  2. only if --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,如果索引中有未合并的条目,它就会失败.如果写入树有效,则结果是树 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 步使用了三个我没有看到记录的神奇"环境变量:$GITHEAD_t 提供要合并的树的名称".运行git merge-recursive,脚本提供四个参数: $b_tree -- $c_tree $w_tree. 如前所述,这些是基础提交 C、index-at-start-of-apply 和隐藏工作提交 的树>w.为了获得这些树中的每一个的字符串名称,git merge-recursive 在环境中查找通过将 GITHEAD_ 添加到原始 SHA 形成的名称-1 对于每棵树.脚本不会将任何策略参数传递给 git merge-recursive,也不会让您选择 recursive 以外的任何策略.可能应该.)

如果合并有冲突,stash 脚本运行 git rerere (qv),如果 --index,告诉你索引没有恢复并退出与合并冲突状态.(与其他提前退出一样,这可以防止 pop 丢弃存储.)

如果合并成功:

  • 如果我们有一个 $unstashed_index_tree——也就是说,我们正在做 --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/null 用于 -q 模式;不知道它为什么运行完全在 -q 下).

关于git stash branch

的几句话

如果您在应用 stash 时遇到问题,您可以将它变成一个真正的分支",这使得它可以保证恢复(除了,像往常一样,对于包含提交 的 stash 的问题)u 除非您先清除未暂存甚至可能忽略的文件,否则不会应用.

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

git checkout -b new_branch stash^

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

git stash apply --index

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

git stash drop

使用:

git stash 分支 new_branch

只需为您执行上述顺序即可.它实际上运行 git checkout -b,如果成功,则应用存储(使用 --index),然后将其删除.

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

o-o-C-o-... <-- some_branchI-W <-- new_branch

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

<小时>

1更准确地说,它运行 git add-interactive --patch=stash --,它直接调用 perl 脚本进行交互式添加,并为藏匿.还有其他一些神奇的 --patch 模式;查看脚本.

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

I wanted to stash untracked files, but I keep passing the wrong option. To me this sounds right:

git stash save [-a|--all]

but this in fact stashes ignored files as well. The correct one is:

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

When I run git stash save -a and try to git stash pop it, I get countless errors for all ignored files:

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

so the command fails.

How do I get my tracked and untracked stashed changes back? git reflog doesn't store stash commands.

解决方案

TL;DR version:

You need the directory to be clean (in git clean terms) for the stash to apply properly. This means running git clean -f, or even git clean -fdx, which is kind of an ugly thing to have to do, since some of the untracked or untracked-and-ignored files/directories may be items you want to keep, rather than deleting entirely. (If so, you should move them outside your work-tree instead of git clean-ing them away. Remember, the files that git clean removes are precisely those that you can't get back from Git!)

To see why, look at step 3 in the "apply" description. Note that there is no option to skip the untracked and/or ignored files in a stash.

Basic facts about the stash itself

When you use git stash save with either -u or -a, the stash script writes its "stash bag" as a three-parent commit rather than the usual two-parent commit.

Diagrammatically, the "stash bag" normally looks like this, in terms of the commit graph:

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

The os are any old ordinary commit nodes, as is C. Node C (for Commit) has a letter so we can name it: it's where the "stash bag" hangs from.

The stash bag itself is the little triangular bag hanging from C, and it contains two commits: w is the work-tree commit and i is the index commit. (Not shown, because it's just hard to diagram, is the fact that w's first parent is C and its second parent is i.)

With --untracked or --all there's a third parent for w, so the diagram looks more like this:

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

(these diagrams really need to be images so that they can have arrows, rather than ASCII-art where arrows are tough to include). In this case, stash is commit w, stash^ is commit C (still also HEAD), stash^2 is commit i, and stash^3 is commit u, which contains the "untracked" or even "untracked and ignored" files. (It's not actually important, as far as I can tell, but I'll add here that i has C as a parent commit, while u is a parentless, or root, commit. There seems to be no particular reason for this, it's just how the script does things, but it explains why the "arrows" (lines) are as they are in the diagram.)

The various options at save time

At save time, you can specify any or all of the following options:

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

Some of these imply, override, or disable others. Using -p, for instance, completely changes the algorithm the script uses to build the stash, and also turns on --keep-index, forcing you to use --no-keep-index to turn it off if you don't want that. It is incompatible with -a and -u and will error-out if any of those are given.

Otherwise, between -a and -u, whichever one is set last is retained.

At this point the script creates either one or two commits:

  • one for the current index (even if it contains no changes), with parent commit C
  • with -u or -a, a parentless commit containing (only) either untracked files, or all (untracked and ignored) files.

The stash script then saves your current work tree. It does this with a temporary index file (basically, a fresh staging area). With -p, the script reads out the HEAD commit into the new staging area, then effectively1 runs git add -i --patch, so that this index winds up with the patches you select. Without -p, it just diffs the work directory against the stashed index to find changed files.2 In either case it writes a tree object from the temporary index. This tree will be the tree for commit w.

As its last stash-creation step, the script uses the tree just saved, the parent commit C, the index commit, and the root commit for untracked files if it exists, to create the final stash commit w. However, the script then takes several more steps that affect your work directory, depending on whether you're using -a, -u, -p, and/or --keep-index (and remember that -p implies --keep-index):

  • With -p:

    1. "Reverse-patch" the work directory to remove the difference between HEAD and the stash. In essence, this leaves the work-directory with only those changes not stashed (specifically, those not in commit w; everything in commit i is ignored here).

    2. Only if you specified --no-keep-index: run git reset (with no options at all, i.e., git reset --mixed). This clears out the "to be committed" state for everything, without changing anything else. (Of course, any partial changes you had staged before running git stash save -p, with git add or git add -p, are saved in commit i.)

  • Without -p:

    1. Run git reset --hard (with -q if you specified that too). This sets the work tree back to the state in the HEAD commit.

    2. Only if you specified -a or -u: run git clean --force --quiet -d (with -x if -a, or without it if -u). This removes all the untracked files, including untracked directories; with -x (i.e., under -a mode), it also removes all the ignored files.

    3. Only if you specified -k / --keep-index: use git read-tree --reset -u $i_tree to "bring back" the stashed index as "changes to be committed" that also appear in the work tree. (The --reset should have no effect since step 1 cleared out the work tree.)

The various options at apply time

The two main sub-commands that restore a stash are apply and pop. The pop code just runs apply and then, if the apply succeeds, runs drop, so in effect, there's really just apply. (Well, there is also branch, which is a little more complicated—but in the end, it too uses apply.)

When you apply a stash—any "stash-like object", really, i.e., anything that the stash script can treat as a stash-bag—there are only two stash-specific options:

  • -q, --quiet
  • --index (not --keep-index!)

Other flags are accumulated, but are promptly ignored anyway. (The same parsing code is used for show, and here the other flags are passed on to git diff.)

Everything else is controlled by the contents of the stash-bag and the state of the work-tree and index. As above, I'll use the labels w, i, and u to denote the various commits in the stash, and C to denote the commit from which the stash-bag hangs.

The apply sequence goes like this, assuming all goes well (if something fails early, e.g., we are in the middle of a merge, or git apply --cached fails, the script errors-out at that point):

  1. write current index into a tree, making sure we're not in the middle of a merge
  2. only if --index: diff commit i against commit C, pipe to git apply --cached, save the resulting tree, and use git reset to unstage it
  3. only if u exists: use git read-tree and git checkout-index --all with a temporary index, to recover the u tree
  4. use git merge-recursive to merge the tree for C (the "base") with that written in step 1 ("updated upstream") and the tree in w ("stashed changes")

After this point it gets a bit complicated :-) as it depends on whether the merge in step 4 went well. But first let's expand the above a little.

Step 1 is pretty easy: the script just runs git write-tree, which fails if there are unmerged entries in the index. If the write-tree works the result is a tree ID ($c_tree in the script).

Step 2 is a more complicated as it checks not only the --index option but also that $b_tree != $i_tree (i.e., that there is a difference between the tree for C and the tree for i), and that $c_tree != $i_tree (i.e., that there is a difference between the tree written out in step 1, and the tree for i). The test for $b_tree != $i_tree makes sense: it's checking whether there's any change to apply. If there's no change—if the tree for i matches that for C—there's no index to restore, and --index is not needed after all. However, if $i_tree matches $c_tree, that merely means that the current index already contains the changes to be restored via --index. It's true that, in this case, we don't want to git apply those changes; but we do want them to remain "restored". (Maybe that's the point of the code I don't quite understand below. It seems more likely that there is a slight bug here, though.)

In any case, if step 2 needs to run git apply --cached, it also runs git write-tree to write the tree, saving this in the script's $unstashed_index_tree variable. Otherwise $unstashed_index_tree is left empty.

Step 3 is where things go wrong in an "unclean" directory. If the u commit exists in the stash, the script insists on extracting it, but git checkout-index --all will fail if any of those files would be overwritten. (Note that this is done with a temporary index file, which is removed afterward: step 3 does not use the normal staging area at all.)

(Step 4 uses three "magic" environment variables that I have not seen documented: $GITHEAD_t provides the "name" of the trees being merged. To run git merge-recursive, the script supplies four arguments: $b_tree -- $c_tree $w_tree. As already noted these are the trees for the base commit C, the index-at-start-of-apply, and the stashed work commit w. To get string-names for each of these trees, git merge-recursive looks in the environment for names formed by prepending GITHEAD_ to the raw SHA-1 for each tree. The script does not pass any strategy arguments to git merge-recursive, nor let you choose any strategy other than recursive. Probably it should.)

If the merge has a conflict, the stash script runs git rerere (q.v.) and, if --index, tells you that the index was not restored and exits with the merge-conflict status. (As with other early exits, this prevents a pop from dropping the stash.)

If the merge succeeds, though:

  • If we have a $unstashed_index_tree—i.e., we're doing --index, and all those other tests in step 2 passed too—then we need to restore the index state created in step 2. In this case a simple git read-tree $unstashed_index_tree (with no options) does the trick.

  • If we don't have something in $unstashed_index_tree, the script uses git diff-index --cached --name-only --diff-filter=A $c_tree to find files to add, runs git read-tree --reset $c_tree to do a single-tree merge against the original saved index, and then git update-index --add with the file names from the earlier diff-index. I'm not really sure why it goes to these lengths (there is a hint in the git-read-tree man page, about avoiding false hits for modified files, that might explain it), but that's what it does.

Last, the script runs git status (with output sent to /dev/null for -q mode; not sure why it runs at all under -q).

A few words on git stash branch

If you're having trouble applying a stash, you can turn it into a "real branch", which makes it guaranteed-to-restore (except, as usual, for the problem of a stash containing a commit u not applying unless you clean out unstaged and maybe even ignored files first).

The trick here is to start by checking out commit C (e.g., git checkout stash^). This of course results in a "detached HEAD", so you need to create a new branch, which you can combine with the step that checks out commit C:

git checkout -b new_branch stash^

Now you can apply the stash, even with --index, and it should work since it will be applying to the same commit the stash-bag hangs from:

git stash apply --index

At this point any earlier staged changes should be staged again, and any earlier unstaged (but tracked) files will have their unstaged-but-tracked changes in the work directory. It's safe to drop the stash now:

git stash drop

Using:

git stash branch new_branch

simply does the above sequence for you. It literally runs git checkout -b, and if that succeeds, applies the stash (with --index) and then drops it.

After this is done, you can commit the index (if you want to), then add and commit the remaining files, to make two (or one if you leave out the first, index, commit) "regular" commits on a "regular" branch:

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

and you've converted the stash-bag i and w commits to ordinary, on-branch commits I and W.


1More correctly, it runs git add-interactive --patch=stash --, which directly invokes the perl script for interactive adding, with special magic set for stashing. There are a few other magic --patch modes; see the script.

2There's a very small bug here: git reads $i_tree, the committed index's tree, into the temporary index, but then diffs the work directory against HEAD. This means that if you changed some file f in the index, then changed it back to match the HEAD revision, the work-tree stored under w in the stash-bag contains the index version of f instead of the work-tree version of f.

这篇关于如何从“git stash save --all"中恢复?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆