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

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

问题描述

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

  git存储保存[-a | --all] 

但是这实际上也会隐藏忽略的文件。正确的是:

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

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

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

因此命令失败。

我如何获得追踪和未追踪的变更? git reflog 不存储隐藏命令。 版本:

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



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



存储本身的基本事实



-u 中使用 git stash save code> -a ,隐藏脚本将其隐藏包写为三个 -parent提交,而不是通常的双亲提交。



从图表上看,存储袋通常看起来像这样提交图:

  o  -  o  -  C < -  HEAD(通常是分支)
| \
iw < - 存款

o s是任何旧的普通提交节点,如 C 。节点 C (对于Commit)有一个字母,所以我们可以命名它:它是藏匿袋挂起的位置。



储藏袋本身就是一个挂在 C 之间的小三角袋,它包含两个提交: w 是工作树提交和 i 是索引提交。 (未显示,因为它很难说,事实是 w 的第一个父项是 C ,并且它的)

使用 - 未跟踪 >或 - all 有第三个父母,所以图表看起来更像这样:

  o  -  o  -  C < -  HEAD 
| \
iw < - 隐藏
/
u

(这些图真的需要成为图片,以便他们可以拥有箭头,而不是ASCII艺术,箭头很难包括在内)。在这种情况下, stash 是commit w stash ^ 是提交 C (仍然是 HEAD ),存储^ 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 - 全部



其中一些暗示,覆盖或禁用他人。例如,使用 -p 完全改变脚本用来构建存储的算法,并且打开 - keep-index ,迫使你使用 - no-keep-index 来关闭它,如果你不想要的话。它与 -a -u 不兼容,如果给出了这些内容,将会出错。



否则,在 -a -u 之间,无论哪一个set last 保留。



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


  • 一个用于当前索引(即使它不包含任何更改),父提交 C

  • -u -a ,这是一个包含(仅)未跟踪文件或全部未被跟踪和被忽略的)文件。

    $ b $ stash 脚本然后保存您当前的工作树。它通过一个临时索引文件(基本上是一个新的临时区域)来完成。使用 -p ,脚本读出 HEAD 提交到新的分段区域,然后有效地 1 运行 git add -i --patch ,以便该索引随您选择的修补程序而结束。如果没有 -p ,它只是将工作目录与隐藏索引进行比较以找到更改的文件。 2 无论哪种情况,它都会从临时指数。这棵树将是提交的树 w



    作为最后一个存储创建步骤,脚本使用刚刚保存的树,父提交 C ,索引提交以及未跟踪文件的根提交(如果存在),以创建最终的存储提交瓦特。但是,脚本会根据您是否使用 -a 工作目录的步骤> -u -p 和/或 - keep-index (和请记住 -p 意味着 - keep-index ):

    -p


    1. 反向修补工作目录以删除 HEAD 与存储区之间的区别。实际上,这只会在工作目录中留下只有 没有保存的更改(具体而言,那些不在提交 w ;所有提交<$ c

    2. 只有当您指定 - no-keep-in时,才会忽略$ c> i index :run git reset (根本没有选项,即 git reset --mixed )。这清除了所有内容的承诺状态,而不会改变任何其他内容。 (当然,在运行 git stash save -p 之前已经进行了部分更改,使用 git add git add -p ,保存在commit i 中。)


    3. <没有 -p


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

      2. 仅当您指定 -a -u :run git clean --force --quiet -d (带 -x if -a ,如果没有它, û)。这将删除所有未跟踪的文件,包括未跟踪的目录; -x (即在 -a 模式下),它也会删除所有被忽略的文件。

        c>:使用 git read-tree --reset -u $ i_tree 将隐藏的索引带回为要提交的更改,它也出现在工作树中。 (因为第1步清除了工作树,所以 - reset 应该不起作用。)


的各个选项应用时间



恢复存储的两个主要子命令是 apply pop pop 代码只运行 apply ,然后如果应用 >成功,运行 drop ,所以实际上只有 apply 。 (好吧,还有分支,这有点复杂 - 但最后它也使用 apply 。)



当你应用一个存储 - 任何存储类对象,实际上,即存储脚本可以视为存储包的任何东西 - 只有两个特定于存储区的选项:


  • -q - quiet

  • - index (not - keep- index !)



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



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



apply 序列是这样的,假设一切都很顺利(如果某些事情早期失败了,例如 在<一个合并,或 git apply --cached 失败,脚本在那一点出错):


  1. 将当前索引写入树中,只有在 - index 的情况下,确保我们不在合并过程中

  2. c $ c>:diff commit i 针对提交 C ,管道到 git apply - 缓存,保存结果树,并使用 git reset 来取消它

  3. 只有当 u 存在:使用 git读取树 git checkout-index --all 用临时索引来恢复 u

  4. 使用 git merge-recursive 给我在步骤1(更新的上游)和 w C c $ c>(隐藏更改)


    在这一点之后,它变得有点复杂:-),因为它取决于第4步合并进展顺利。但首先我们来扩展一下。



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



    步骤2因为它不仅检查 - index 选项,而且还检查 $ b_tree!= $ i_tree (即, C 的树与 i 的树之间存在差异,并且 $ c_tree != $ i_tree (即,在步骤1中写出的树和)。对 $ b_tree!= $ i_tree 的测试是有意义的:它检查是否有任何更改需要应用。如果没有改变 - 如果 i 的树匹配 C 的树 - 没有要恢复的索引,并且<$毕竟,不需要c $ c> - index 。但是,如果 $ i_tree 匹配 $ c_tree ,那仅表示当前索引已包含要通过 - 索引。确实,在这种情况下,我们不希望 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 提供被合并树的名称。 c $ c> git merge-recursive ,脚本提供了四个参数: $ b_tree - $ c_tree $ w_tree 。如前所述,这些是用于基本提交的树 C ,开始时的索引应用,以及隐藏的工作提交 w git merge-recursive 在环境中寻找名字,这些名字由前置 GITHEAD _ 到每棵树的原始SHA-1,脚本不会将任何策略参数传递给 git merge-recursive ,也不允许你选择除递归。可能它应该。)



    如果合并发生冲突,存储脚本将运行 git rerere (qv),如果 - -index ,告诉你索引没有被恢复,并退出时出现合并冲突状态。 (与其他早期退出一样,这可以防止 pop 删除存储。)



    如果合并成功,如果我们有一个 $ unstashed_index_tree -ie,我们可以:


    • 在步骤2中所有其他测试都通过了 - index - 那么我们需要恢复在步骤2中创建的索引状态。在这种情况下,一个简单的 git读取树$ unstashed_index_tree (没有选项)就可以实现这一点。

    • p>如果我们在 $ 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 手册页中有提示,以避免错误点击修改后的文件,这可能会解释它),但这就是它的作用。 code> git status (输出发送到 / dev / null 用于 -q mode;不知道它为什么在 -q 下运行)。



      git存储分支



      如果您在应用存储时遇到问题,可以将其转换为真实分支这使得它能够保证还原(除了像往常一样,除非您先清理非暂存的甚至可能被忽略的文件,否则包含提交< u 的存储无法应用)。
      $ b

      这里的技巧是首先检查commit C (例如, git checkout stash ^ )。这当然会导致一个分离HEAD,所以你需要创建一个新的分支,你可以结合检查commit C 的步骤:

        git checkout -b new_branch存储^ 

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

        git stash apply --index 

      此时,任何更早的阶段性更改都应该再次执行,并且任何先前未执行(但已跟踪)的文件都将在工作目录中进行非分离但跟踪的更改。

        git stash drop 

      使用:
      $ b $ pre $ g $ c $ git stash branch new_branch

      code>

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



      完成此操作后,您可以提交索引(如果需要),然后添加并提交其余文件,以创建两个(或一个if您在常规分支上忽略了第一个索引提交)常规提交:

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

      并且你已经将存储包 i w 提交转换为普通的分支提交 I W






      <更正确的说,它运行 git add-interactive --patch = stash - ,它直接调用perl脚本进行交互式添加,与特殊的魔法设置为存储。还有一些其他的魔术 - patch 模式;看看脚本。



      2 这里有一个很小的错误:git读取 $ i_tree ,提交索引的树,放入临时索引,然后将工作目录与 HEAD 区分开来。这意味着如果您在索引中更改了某个文件 f ,则将其更改为返回以匹配 HEAD revision,存储在 w 中的工作树包含 版本的 f 而不是 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天全站免登陆