如何从“git stash save --all"中恢复? [英] How to recover from "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
或 --all
有 w
的第三个父级,所以图表看起来更像这样:
o--o--C <-- HEAD|i-w <-- 藏匿处/你
(这些图表确实需要是图像,以便它们可以有箭头,而不是很难包含箭头的 ASCII 艺术).在这种情况下,stash
是提交 w
,stash^
是提交 C
(仍然是 HEAD
),stash^2
是 commit i
,stash^3
是 commit u
,其中包含未跟踪"甚至未跟踪和忽略"的文件.(据我所知,这实际上并不重要,但我会在这里添加 i
将 C
作为父提交,而 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
:反向修补"工作目录以消除
HEAD
和 stash 之间的差异.从本质上讲,这使工作目录only 那些没有被隐藏的更改(特别是那些不在 commitw
中的;commiti
中的所有内容都是此处忽略).仅当您指定
--no-keep-index
时:运行git reset
(完全没有选项,即git reset --混合
).这会清除所有内容的待提交"状态,而不会更改任何其他内容.(当然,您在运行git stash save -p
之前使用git add
或git add -p
进行的任何部分更改都会被保存在提交i
.)
没有
-p
:运行
git reset --hard
(如果你也指定了,使用-q
).这会将工作树设置回HEAD
提交中的状态.仅当您指定了
-a
或-u
时:运行git clean --force --quiet -d
(使用-x
如果-a
,或者如果-u
没有它).这将删除所有未跟踪的文件,包括未跟踪的目录;使用-x
(即在-a
模式下),它还删除所有被忽略的文件.仅当您指定
-k
/--keep-index
时:使用git read-tree --reset -u $i_tree
将隐藏的索引带回"作为要提交的更改",也出现在工作树中.(--reset
应该没有效果,因为第 1 步清除了工作树.)
apply
时的各种选项
恢复存储的两个主要子命令是 apply
和 pop
.pop
代码只运行 apply
然后,如果 apply
成功,运行 drop
,所以实际上,有真的只是申请
.(嗯,还有branch
,稍微复杂一点——不过最后也用了apply
.)
当您应用 stash 时——任何类似 stash 的对象",实际上,即任何 stash 脚本可以视为 stash-bag 的东西——只有两个特定于 stash 的选项:
-q
,--quiet
--index
(不是--keep-index
!)
其他标志被累积,但无论如何都会被立即忽略.(相同的解析代码用于show
,这里其他标志被传递给git diff
.)
其他一切都由 stash-bag 的内容以及工作树和索引的状态控制.如上所述,我将使用标签 w
、i
和 u
来表示存储中的各种提交,而 C
表示 stash-bag 挂起的提交.
apply
序列是这样的,假设一切顺利(如果某些事情早期失败,例如,我们正在在合并中,或者 gitapply --cached
失败,此时脚本出错):
- 将当前索引写入树中,确保我们不在合并过程中
- only if
--index
: diff commiti
针对 commitC
,管道到git apply --cached
,保存生成的树,并使用git reset
取消暂存 - 仅当
u
存在时:使用带有临时索引的git read-tree
和git checkout-index --all
来恢复u
树 - 使用
git merge-recursive
将C
的树(基础")与步骤 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
并且您已经将存储包 i
和 w
提交转换为普通的分支提交 I
和 W
.
1更准确地说,它运行 git add-interactive --patch=stash --
,它直接调用 perl 脚本进行交互式添加,并为藏匿.还有其他一些神奇的 --patch
模式;查看脚本.
2这里有一个非常小的错误:git 将提交索引的树 $i_tree
读入临时索引,然后将工作目录与 进行比较头
.这意味着如果您更改了索引中的某个文件 f
,然后将其更改为 back 以匹配 HEAD
修订版,工作树存储在stash-bag 中的 w
包含 f
的 index 版本,而不是 的 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 o
s 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
:"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 commitw
; everything in commiti
is ignored here).Only if you specified
--no-keep-index
: rungit 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 runninggit stash save -p
, withgit add
orgit add -p
, are saved in commiti
.)
Without
-p
:Run
git reset --hard
(with-q
if you specified that too). This sets the work tree back to the state in theHEAD
commit.Only if you specified
-a
or-u
: rungit 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.Only if you specified
-k
/--keep-index
: usegit 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):
- write current index into a tree, making sure we're not in the middle of a merge
- only if
--index
: diff commiti
against commitC
, pipe togit apply --cached
, save the resulting tree, and usegit reset
to unstage it - only if
u
exists: usegit read-tree
andgit checkout-index --all
with a temporary index, to recover theu
tree - use
git merge-recursive
to merge the tree forC
(the "base") with that written in step 1 ("updated upstream") and the tree inw
("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 simplegit read-tree $unstashed_index_tree
(with no options) does the trick.If we don't have something in
$unstashed_index_tree
, the script usesgit diff-index --cached --name-only --diff-filter=A $c_tree
to find files to add, runsgit read-tree --reset $c_tree
to do a single-tree merge against the original saved index, and thengit update-index --add
with the file names from the earlierdiff-index
. I'm not really sure why it goes to these lengths (there is a hint in thegit-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屋!