如何从“git存储保存——全部”中恢复?

时间:2021-09-13 00:02:47

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:

当我运行git存储保存-a并尝试git存储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

so the command fails.

命令失败。

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

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

2 个解决方案

#1


24  

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!)

您需要该目录保持干净(在git中是干净的),以便存储适当地应用。这意味着运行git clean -f,甚至git clean -fdx,这是一件很糟糕的事情,因为有些未跟踪或未跟踪或未跟踪和忽略的文件/目录可能是您希望保留的项,而不是完全删除。(如果是这样的话,你应该把它们移出你的工作树,而不是把它们清理掉。请记住,git clean删除的文件正是您无法从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.

要了解原因,请查看“应用”描述中的步骤3。注意,无法跳过隐藏文件中的未跟踪和/或忽略文件。

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.

当你用-u或-a保存git时,隐藏脚本将它的“隐藏包”写为一个双亲提交,而不是通常的双亲提交。

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.

操作系统是任何普通的提交节点,C节点C(用于提交)有一个字母,所以我们可以给它命名:它是“隐藏包”挂起的地方。

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.)

隐藏包本身是挂在C上的小三角包,它包含两个提交:w是工作树提交,i是索引提交。(没有显示出来,因为图上很难画出来,因为w的第一个父结点是C,它的第二个父结点是i。)

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

没有跟踪或者,w有第三个父结点,所以图看起来更像这样

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.)

(这些图真的需要是图像,这样它们就可以有箭头,而不是像海鞘一样的箭头,箭头很难包含)。在这种情况下,钱是提交w,储备^提交C(仍然还头),藏^ 2是提交我和藏^ 3是提交u,包含“无足迹的”甚至“无足迹的忽略”文件。(就我所知,这其实并不重要,但我在这里补充一点,作为父母,我有C,而u是没有父母的,或者根,承诺。这似乎没有什么特别的原因,这只是脚本的工作方式,但它解释了为什么“箭头”(行)在图中是这样的。

The various options at save time

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

在节省时间时,你可指定下列任何或全部选项:

  • -p, --patch
  • - p,补丁
  • -k, --keep-index, --no-keep-index
  • - k,————no-keep-index keep-index
  • -q, --quiet
  • q,安静
  • -u, --include-untracked
  • - 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.

其中一些暗示,覆盖,或禁用其他。例如,使用-p可以完全改变脚本用来构建隐藏文件的算法,还可以打开——keep-index,如果您不想关闭它,就必须使用——no- keepindex。它与-a和-u不相容,如果给出了其中的任何一个,就会出错。

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

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

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
  • 一个用于当前索引(即使它不包含任何更改),使用父提交C
  • with -u or -a, a parentless commit containing (only) either untracked files, or all (untracked and ignored) files.
  • 使用-u或-a,一个无父提交(仅包含)未跟踪文件,或所有(未跟踪和忽略)文件。

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.

然后,隐藏脚本保存当前的工作树。它使用一个临时索引文件(基本上是一个新的登台区域)来实现这一点。使用-p,脚本读出HEAD提交到新的staging区域,然后有效地运行git add -i -patch,这样这个索引就会随着您所选择的补丁而结束。在没有-p的情况下,它只是将工作目录分散到sthash索引中,以查找已更改的文件。无论哪种情况,它都从临时索引中写入树对象。这棵树将是树。

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):

作为最后stash-creation一步,该脚本使用树就得救了,家长提交C,该指数提交,和根提交无路径的文件,如果存在,来创建最终的藏匿处提交w。然而,然后脚本需要几个步骤,影响到你的工作目录中,这取决于你使用- a,- u,- p,和/或——keep-index(和记住- p暗示——keep-index):

  • With -p:

    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).

      “反向补丁”工作目录以删除HEAD和hidden之间的差异。从本质上说,这只会使工作目录只保留那些没有被隐藏的更改(特别是,那些没有提交的更改;提交中的所有内容在这里都被忽略)。

    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.)

      只有在您指定了——no- keepindex:运行git reset(没有任何选项,例如。git重置,混合)。这消除了“提交”状态的一切,没有改变任何其他。(当然,在使用git添加或git添加-p运行git之前进行的任何部分更改都保存在commit i中。)

  • Without -p:

    没有- 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.

      运行git复位——很难(如果您也指定了-q)。这会将工作树设置回HEAD提交的状态。

    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.

      只有当您指定了-a或-u:运行git clean -force -quiet -d(如果-x是-a,或者如果-u没有)。这将删除所有未跟踪的文件,包括未跟踪的目录;- x(即。它还删除所有被忽略的文件。

    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.)

      只有在您指定了-k /——keepindex:使用git读取树——重置-u $i_tree以“返回”存储的索引,作为“要提交的更改”,这些更改也出现在工作树中。(由于步骤1清除了工作树,因此—reset应该没有任何作用。)

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.)

恢复隐藏的两个主要子命令是apply和pop。pop代码只运行apply,然后,如果apply成功,运行drop,所以实际上,这里只有apply。(当然,还有分支,这有点复杂——但最终,它也会用到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
  • q,安静
  • --index (not --keep-index!)
  • ——指数(不——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.)

其他的标志被累积起来,但是很快就被忽略了。(同样的解析代码用于显示,在这里,其他标志被传递给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.

其他一切都由存储袋的内容、工作树和索引的状态控制。如上所述,我将使用标签w、I和u表示隐藏中的各种提交,而C表示stash-bag挂起的提交。

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):

应用程序序列是这样的,假设一切都进行得很顺利(如果某些东西在早期失败了,例如,我们正在进行合并,或者git应用—缓存失败,此时脚本错误输出):

  1. write current index into a tree, making sure we're not in the middle of a merge
  2. 将当前索引写入树中,确保不在合并过程中
  3. 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
  4. 只有当——索引:diff提交i反对提交C,管道到git应用——缓存,保存结果树,并使用git重置来解除它
  5. only if u exists: use git read-tree and git checkout-index --all with a temporary index, to recover the u tree
  6. 只有当u存在时:使用git读树和git签出索引——所有这些都带有临时索引,以恢复u树
  7. 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")
  8. 使用git merge-recursive将C的树(“base”)与步骤1中编写的树(“更新的上游”)以及w中的树(“存储更改”)合并

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.

在这一点之后,它变得有点复杂:-),因为它取决于是否在第4步的合并顺利进行。但首先我们把上面的式子展开一点。

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).

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

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.)

步骤2更加复杂,因为它不仅检查——index选项,而且检查$b_tree != $i_tree(例如,,即C的树和i的树之间存在差异),以及$c_tree != $i_tree(即。对于$b_tree != $i_tree的测试是有意义的:它检查是否有任何更改要应用。如果没有变化——如果i的树与c的树匹配——那么就没有要恢复的索引,而且——索引毕竟是不需要的。但是,如果$i_tree匹配$c_tree,这仅仅意味着当前索引已经包含要通过-index恢复的更改。确实,在这种情况下,我们不希望git应用这些更改;但我们确实希望它们能够“恢复”。(也许这就是我不太理解下面代码的意义所在。不过,这里似乎更有可能有一个小问题。

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.

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

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.)

第三步是在“不洁净”目录中出错的地方。如果u提交存在于存储库中,那么脚本会坚持提取它,但是git签出索引——如果这些文件中的任何一个被覆盖,那么所有这些都将失败。(注意,这是通过一个临时索引文件完成的,该文件在之后被删除:步骤3根本不使用正常的staging区域。)

(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.)

(第4步使用了三个“神奇的”环境变量,我没有看到有文档说明:$GITHEAD_t提供了被合并的树的“名称”。要运行git merge-recursive,脚本提供四个参数:$b_tree——$c_tree $w_tree。如前所述,这些是基本提交C的树、应用开始时的索引和存储工作提交w。该脚本不向git merge递归传递任何策略参数,也不允许您选择除递归之外的任何策略。可能。)

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.)

如果合并有冲突,隐藏脚本运行git rerere (q.v.),如果——index,则告诉您没有恢复索引,并以合并冲突状态退出。(和其他早期的出口一样,这可以防止弹出隐藏的东西。)

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.

    如果我们有一个$unstashed_index_tree,也就是。,我们正在做——索引,以及步骤2中通过的所有其他测试——然后我们需要恢复步骤2中创建的索引状态。在这种情况下,一个简单的git读树$unstashed_index_tree(没有选项)就可以达到这个目的。

  • 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.

    如果在$unstashed_index_tree中没有东西,脚本使用git diff-index—缓存—name-only—diff-filter= $c_tree来查找要添加的文件,运行git读树—重置$c_tree以对原始保存的索引进行单树合并,然后使用git update-index—添加前面的diff-index中的文件名。我不太确定它为什么会达到这些长度(在git-read-tree man页面中有一个提示,关于避免修改后的文件出现错误的点击,这也许可以解释它),但这就是它的作用。

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

最后,脚本运行git状态(输出发送到/dev/null为-q模式;不知道为什么它在-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:

这里的技巧是先查看提交C(例如,git checkout藏^)。这当然会导致一个“分离的头”,所以您需要创建一个新的分支,您可以将它与检查提交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:

现在你可以使用隐藏包,即使是-index,它也可以工作,因为它将适用于同样的提交-bag挂起的:

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.

简单地为您完成上面的序列。它实际运行git签出-b,如果成功,应用隐藏(with -index),然后删除它。

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:

完成之后,您可以提交索引(如果您愿意),然后添加和提交其余的文件,生成两个(或者如果您省略了第一个,index, commit)“常规”提交给“常规”分支:

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.

你已经将i和w提交到普通的,分支提交到i和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.

更正确的是,它运行git add-interactive -patch= hidden,它直接调用perl脚本进行交互添加,并使用特殊的魔法设置进行存储。还有一些其他的神奇——补丁模式;看到脚本。

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将$i_tree(提交索引的树)读入临时索引,然后将工作目录分散到HEAD。这意味着,如果您在索引中更改了某个文件f,然后将其更改为与HEAD修订相匹配,则存储在stash-bag中w下的工作树包含f的索引版本,而不是f的工作树版本。

#2


0  

Without fully understanding why the problem occurs, I found a quick solution:

在没有完全理解问题发生的原因的情况下,我找到了一个快速的解决方案:

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

The --no-color option removes any colors from the diff output, because they screw up the git apply command.

无颜色选项从diff输出中删除任何颜色,因为它们破坏了git apply命令。

However, it would be great if someone could edit this answer, providing the explanation why git stash pop fails.

但是,如果有人能够编辑这个答案,并提供git shelter pop失败的原因,那就太棒了。

#1


24  

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!)

您需要该目录保持干净(在git中是干净的),以便存储适当地应用。这意味着运行git clean -f,甚至git clean -fdx,这是一件很糟糕的事情,因为有些未跟踪或未跟踪或未跟踪和忽略的文件/目录可能是您希望保留的项,而不是完全删除。(如果是这样的话,你应该把它们移出你的工作树,而不是把它们清理掉。请记住,git clean删除的文件正是您无法从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.

要了解原因,请查看“应用”描述中的步骤3。注意,无法跳过隐藏文件中的未跟踪和/或忽略文件。

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.

当你用-u或-a保存git时,隐藏脚本将它的“隐藏包”写为一个双亲提交,而不是通常的双亲提交。

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.

操作系统是任何普通的提交节点,C节点C(用于提交)有一个字母,所以我们可以给它命名:它是“隐藏包”挂起的地方。

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.)

隐藏包本身是挂在C上的小三角包,它包含两个提交:w是工作树提交,i是索引提交。(没有显示出来,因为图上很难画出来,因为w的第一个父结点是C,它的第二个父结点是i。)

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

没有跟踪或者,w有第三个父结点,所以图看起来更像这样

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.)

(这些图真的需要是图像,这样它们就可以有箭头,而不是像海鞘一样的箭头,箭头很难包含)。在这种情况下,钱是提交w,储备^提交C(仍然还头),藏^ 2是提交我和藏^ 3是提交u,包含“无足迹的”甚至“无足迹的忽略”文件。(就我所知,这其实并不重要,但我在这里补充一点,作为父母,我有C,而u是没有父母的,或者根,承诺。这似乎没有什么特别的原因,这只是脚本的工作方式,但它解释了为什么“箭头”(行)在图中是这样的。

The various options at save time

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

在节省时间时,你可指定下列任何或全部选项:

  • -p, --patch
  • - p,补丁
  • -k, --keep-index, --no-keep-index
  • - k,————no-keep-index keep-index
  • -q, --quiet
  • q,安静
  • -u, --include-untracked
  • - 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.

其中一些暗示,覆盖,或禁用其他。例如,使用-p可以完全改变脚本用来构建隐藏文件的算法,还可以打开——keep-index,如果您不想关闭它,就必须使用——no- keepindex。它与-a和-u不相容,如果给出了其中的任何一个,就会出错。

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

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

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
  • 一个用于当前索引(即使它不包含任何更改),使用父提交C
  • with -u or -a, a parentless commit containing (only) either untracked files, or all (untracked and ignored) files.
  • 使用-u或-a,一个无父提交(仅包含)未跟踪文件,或所有(未跟踪和忽略)文件。

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.

然后,隐藏脚本保存当前的工作树。它使用一个临时索引文件(基本上是一个新的登台区域)来实现这一点。使用-p,脚本读出HEAD提交到新的staging区域,然后有效地运行git add -i -patch,这样这个索引就会随着您所选择的补丁而结束。在没有-p的情况下,它只是将工作目录分散到sthash索引中,以查找已更改的文件。无论哪种情况,它都从临时索引中写入树对象。这棵树将是树。

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):

作为最后stash-creation一步,该脚本使用树就得救了,家长提交C,该指数提交,和根提交无路径的文件,如果存在,来创建最终的藏匿处提交w。然而,然后脚本需要几个步骤,影响到你的工作目录中,这取决于你使用- a,- u,- p,和/或——keep-index(和记住- p暗示——keep-index):

  • With -p:

    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).

      “反向补丁”工作目录以删除HEAD和hidden之间的差异。从本质上说,这只会使工作目录只保留那些没有被隐藏的更改(特别是,那些没有提交的更改;提交中的所有内容在这里都被忽略)。

    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.)

      只有在您指定了——no- keepindex:运行git reset(没有任何选项,例如。git重置,混合)。这消除了“提交”状态的一切,没有改变任何其他。(当然,在使用git添加或git添加-p运行git之前进行的任何部分更改都保存在commit i中。)

  • Without -p:

    没有- 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.

      运行git复位——很难(如果您也指定了-q)。这会将工作树设置回HEAD提交的状态。

    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.

      只有当您指定了-a或-u:运行git clean -force -quiet -d(如果-x是-a,或者如果-u没有)。这将删除所有未跟踪的文件,包括未跟踪的目录;- x(即。它还删除所有被忽略的文件。

    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.)

      只有在您指定了-k /——keepindex:使用git读取树——重置-u $i_tree以“返回”存储的索引,作为“要提交的更改”,这些更改也出现在工作树中。(由于步骤1清除了工作树,因此—reset应该没有任何作用。)

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.)

恢复隐藏的两个主要子命令是apply和pop。pop代码只运行apply,然后,如果apply成功,运行drop,所以实际上,这里只有apply。(当然,还有分支,这有点复杂——但最终,它也会用到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
  • q,安静
  • --index (not --keep-index!)
  • ——指数(不——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.)

其他的标志被累积起来,但是很快就被忽略了。(同样的解析代码用于显示,在这里,其他标志被传递给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.

其他一切都由存储袋的内容、工作树和索引的状态控制。如上所述,我将使用标签w、I和u表示隐藏中的各种提交,而C表示stash-bag挂起的提交。

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):

应用程序序列是这样的,假设一切都进行得很顺利(如果某些东西在早期失败了,例如,我们正在进行合并,或者git应用—缓存失败,此时脚本错误输出):

  1. write current index into a tree, making sure we're not in the middle of a merge
  2. 将当前索引写入树中,确保不在合并过程中
  3. 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
  4. 只有当——索引:diff提交i反对提交C,管道到git应用——缓存,保存结果树,并使用git重置来解除它
  5. only if u exists: use git read-tree and git checkout-index --all with a temporary index, to recover the u tree
  6. 只有当u存在时:使用git读树和git签出索引——所有这些都带有临时索引,以恢复u树
  7. 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")
  8. 使用git merge-recursive将C的树(“base”)与步骤1中编写的树(“更新的上游”)以及w中的树(“存储更改”)合并

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.

在这一点之后,它变得有点复杂:-),因为它取决于是否在第4步的合并顺利进行。但首先我们把上面的式子展开一点。

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).

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

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.)

步骤2更加复杂,因为它不仅检查——index选项,而且检查$b_tree != $i_tree(例如,,即C的树和i的树之间存在差异),以及$c_tree != $i_tree(即。对于$b_tree != $i_tree的测试是有意义的:它检查是否有任何更改要应用。如果没有变化——如果i的树与c的树匹配——那么就没有要恢复的索引,而且——索引毕竟是不需要的。但是,如果$i_tree匹配$c_tree,这仅仅意味着当前索引已经包含要通过-index恢复的更改。确实,在这种情况下,我们不希望git应用这些更改;但我们确实希望它们能够“恢复”。(也许这就是我不太理解下面代码的意义所在。不过,这里似乎更有可能有一个小问题。

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.

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

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.)

第三步是在“不洁净”目录中出错的地方。如果u提交存在于存储库中,那么脚本会坚持提取它,但是git签出索引——如果这些文件中的任何一个被覆盖,那么所有这些都将失败。(注意,这是通过一个临时索引文件完成的,该文件在之后被删除:步骤3根本不使用正常的staging区域。)

(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.)

(第4步使用了三个“神奇的”环境变量,我没有看到有文档说明:$GITHEAD_t提供了被合并的树的“名称”。要运行git merge-recursive,脚本提供四个参数:$b_tree——$c_tree $w_tree。如前所述,这些是基本提交C的树、应用开始时的索引和存储工作提交w。该脚本不向git merge递归传递任何策略参数,也不允许您选择除递归之外的任何策略。可能。)

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.)

如果合并有冲突,隐藏脚本运行git rerere (q.v.),如果——index,则告诉您没有恢复索引,并以合并冲突状态退出。(和其他早期的出口一样,这可以防止弹出隐藏的东西。)

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.

    如果我们有一个$unstashed_index_tree,也就是。,我们正在做——索引,以及步骤2中通过的所有其他测试——然后我们需要恢复步骤2中创建的索引状态。在这种情况下,一个简单的git读树$unstashed_index_tree(没有选项)就可以达到这个目的。

  • 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.

    如果在$unstashed_index_tree中没有东西,脚本使用git diff-index—缓存—name-only—diff-filter= $c_tree来查找要添加的文件,运行git读树—重置$c_tree以对原始保存的索引进行单树合并,然后使用git update-index—添加前面的diff-index中的文件名。我不太确定它为什么会达到这些长度(在git-read-tree man页面中有一个提示,关于避免修改后的文件出现错误的点击,这也许可以解释它),但这就是它的作用。

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

最后,脚本运行git状态(输出发送到/dev/null为-q模式;不知道为什么它在-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:

这里的技巧是先查看提交C(例如,git checkout藏^)。这当然会导致一个“分离的头”,所以您需要创建一个新的分支,您可以将它与检查提交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:

现在你可以使用隐藏包,即使是-index,它也可以工作,因为它将适用于同样的提交-bag挂起的:

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.

简单地为您完成上面的序列。它实际运行git签出-b,如果成功,应用隐藏(with -index),然后删除它。

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:

完成之后,您可以提交索引(如果您愿意),然后添加和提交其余的文件,生成两个(或者如果您省略了第一个,index, commit)“常规”提交给“常规”分支:

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.

你已经将i和w提交到普通的,分支提交到i和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.

更正确的是,它运行git add-interactive -patch= hidden,它直接调用perl脚本进行交互添加,并使用特殊的魔法设置进行存储。还有一些其他的神奇——补丁模式;看到脚本。

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将$i_tree(提交索引的树)读入临时索引,然后将工作目录分散到HEAD。这意味着,如果您在索引中更改了某个文件f,然后将其更改为与HEAD修订相匹配,则存储在stash-bag中w下的工作树包含f的索引版本,而不是f的工作树版本。

#2


0  

Without fully understanding why the problem occurs, I found a quick solution:

在没有完全理解问题发生的原因的情况下,我找到了一个快速的解决方案:

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

The --no-color option removes any colors from the diff output, because they screw up the git apply command.

无颜色选项从diff输出中删除任何颜色,因为它们破坏了git apply命令。

However, it would be great if someone could edit this answer, providing the explanation why git stash pop fails.

但是,如果有人能够编辑这个答案,并提供git shelter pop失败的原因,那就太棒了。