如何从“Git存储保存 – 所有”恢复?
我想存储未跟踪的文件,但是我一直传递错误的选项。 对我来说这听起来是对的:
git stash save [-a|--all]
但是这实际上存储了被忽略的文件。 正确的是:
git stash save [-u|--include-untracked]
当我运行git stash save -a
并尝试git stash pop
它,我得到所有被忽略的文件无数的错误:
path/to/file1.ext already exists, no checkout path/to/file1.ext already exists, no checkout path/to/file1.ext already exists, no checkout ... Could not restore untracked files from stash
所以命令失败。
我如何获得我的跟踪和未跟踪存储的变化? git reflog
不存储隐藏命令。
TL; DR版本:
你需要的目录是干净的(以git clean
条款),以便正确应用存储。 这意味着运行git clean -f
,甚至是git clean -fdx
,这是一件很丑恶的事情,因为一些未被跟踪或未被跟踪和忽略的文件/目录可能是你想要保留的项目比完全删除。 (如果是这样的话,你应该把它们移到你的工作树之外,而不是用git clean
它们。记住, git clean
删除的文件正是那些你无法从Git中得到的文件!)
要了解为什么,请查看“应用”说明中的第3步。 请注意, 没有选项可以跳过存储中未跟踪和/或忽略的文件。
关于存储本身的基本事实
当你用-u
或者-a
使用git stash save
时,stash脚本把它的“stash bag”写成一个三层提交而不是通常的双亲提交。
以图表方式,“提包”通常看起来像这样,就提交图而言:
o--o--C <-- HEAD (typically, a branch) |\ iw <-- stash
这是任何旧的普通的提交节点,就像C
。 节点C
(对于提交)有一个字母,所以我们可以命名它:这是“藏匿袋”挂起的地方。
存储包本身就是从C
挂起来的小三angular包,它包含两个提交: w
是工作树提交, i
是索引提交。 (没有显示,因为这很难说,事实上, w
的第一个父母是C
,第二个父母是i
。
使用--untracked
或 – 所有的w
有第三个父,所以图看起来更像这样:
o--o--C <-- HEAD |\ iw <-- stash / u
(这些图真的需要是图像才能有箭头,而不是ASCII-art,箭头很难包括在内)。 在这种情况下, stash
是提交w
, stash^
是提交C
(仍然是头), stash^2
是提交i
, stash^3
是提交u
,其中包含“未跟踪”甚至“未跟踪和忽略”的文件。 (据我所知,这并不重要,但是我会在这里添加一个C
作为父提交,而u
是一个无父或者根提交,似乎没有什么特别的理由,脚本是如何做的,但是它解释了为什么“箭头”(线)和图中的一样。)
save
时间的各种选项
在保存时,您可以指定以下任何或全部选项:
-
-p
,–--patch
-
-k
,–--keep-index
,--no-keep-index
--keep-index
--no-keep-index
-
-q
, –--quiet
-
-u
,–--include-untracked
-
-a
, – 所有
其中一些暗示,覆盖或禁用他人。 例如,使用-p
可以完全改变脚本用来构build存储的algorithm,还可以打开--keep-index
,迫使你使用--no-keep-index
closures它,如果你不想要的话那。 它与-a
和-u
不兼容,如果给出了这些,将会出错。
否则,在-a
和-u
之间,保留最后设置的那一个 。
此时脚本创build一个或两个提交:
- 一个用于当前索引(即使它不包含任何更改),用父提交
C
- 与
-u
或-a
,一个无父母提交包含(只)未跟踪的文件,或所有(未跟踪和忽略)文件。
stash
脚本然后保存您当前的工作树。 它使用一个临时索引文件(基本上,一个新的临时区域)。 使用-p
,脚本读出HEAD
提交到新的临时区域,然后有效地运行git add -i --patch
,这样这个索引就会随你select的补丁一起运行。 没有-p
,它只是将工作目录与隐藏的索引进行比较以find更改的文件。 2在任何一种情况下,它都会从临时索引中写入一个树对象。 这棵树将是提交w
的树。
作为最后一个存储创build步骤,脚本使用刚刚保存的树,父提交C
,索引提交以及未跟踪文件的根提交(如果存在),以创build最终的隐藏提交w
。 但是,脚本会根据您是否使用-a
, -u
, -p
和/或--keep-index
(并记住-p
意味着--keep-index
):
-
用
-p
:-
“反向修补”工作目录以消除
HEAD
和隐藏之间的差异。 实质上,这只留下了工作目录, 只有这些更改没有被隐藏(具体地说,那些不是提交w
;提交i
一切都被忽略了)。 -
只有你指定了
--no-keep-index
:运行git reset
(根本没有选项,即git reset --mixed
)。 这清除了所有事情的“被承诺”状态,而没有改变任何东西。 (当然,在运行git stash save -p
之前,你已经进行了部分修改,使用git add
或者git add -p
保存在commiti
)
-
-
没有
-p
:-
运行
git reset --hard
(如果你指定的话也用-q
)。 这将工作树设置回HEAD
提交中的状态。 -
只有当你指定
-a
或-u
:运行git clean --force --quiet -d
(如果-a
为-x
则为-a
,否则为-u
)。 这将删除所有未跟踪的文件,包括未跟踪的目录; 与-x
(即在-a
模式下),它也删除所有被忽略的文件。 -
只有当你指定
-k
/--keep-index
:使用git read-tree --reset -u $i_tree
将隐藏的索引“带回”也出现在工作树中的“要提交的更改”。 (因为第1步清除了工作树,所以--reset
应该不起作用。)
-
apply
时间的各种选项
恢复存储的两个主要子命令是apply
和pop
。 pop
代码只是运行apply
,然后,如果apply
成功,运行drop
,所以实际上,只是apply
。 (好吧,还有一个branch
,这个branch
稍微复杂一点,但是最后也使用了branch
。)
当你应用一个存储 – 任何“存储类对象”,实际上,即任何存储脚本可以视为一个存储袋 – 只有两个存储特定选项:
-
-q
, –--quiet
-
--keep-index
(不是 –--keep-index
!)
其他标志是积累的,但无论如何都被及时忽略。 (相同的parsing代码用于show
,这里其他标志传递给git diff
。)
其他一切都由储藏袋的内容以及工作树和索引的状态来控制。 如上所述,我将使用标签w
, i
和u
来表示存储器中的各种提交,而C
表示存储器挂起的提交。
假如一切顺利的话, apply
程序就会如此,假如一些事情早就失败了,例如我们正处于合并的中间,或者git apply --cached
失败了,脚本就会在这一点出错:
- 将当前索引写入树中,确保我们不在合并中
- 只有
--index
:diff提交i
提交C
,pipe道git apply --cached
,保存结果树,并使用git reset
取消它 - 只有当
u
存在:使用git read-tree
和git checkout-index --all
用一个临时索引来恢复你的u
树 - 使用
git merge-recursive
将C
(“base”)的树与步骤1中写入的树(“updated upstream”)和w
的树(“stashhed changes”)合并
在这之后它变得有些复杂:-),这取决于步骤4中的合并是否顺利。 但首先让我们稍微扩展一下。
步骤1非常简单:脚本只运行git write-tree
,如果索引中有未合并的条目,则失败。 如果写入树工作,结果是树ID(脚本中的$c_tree
)。
第二步比较复杂,因为它不仅检查--index
选项,还检查$b_tree != $i_tree
(也就是说, C
和tree之间有一个区别), $c_tree
! = $i_tree
(即,在步骤1中写出的树与为i
的树之间存在差异)。 $b_tree != $i_tree
的testing是有意义的:它检查是否有任何更改应用。 如果没有变化 – 如果i
的树与C
匹配 – 没有索引要恢复,并且 – 索引完全不需要。 但是,如果$i_tree
与$i_tree
匹配,那仅仅意味着当前索引已经包含了要通过--index
恢复的更改。 的确,在这种情况下,我们不希望git apply
这些变化。 但我们希望他们保持“恢复”。 (也许这是我下面不太明白的代码的重点,虽然这里看起来更像是一个小错误。
在任何情况下,如果第2步需要运行git apply --cached
,它也会运行git write-tree
来编写树,并将其保存在脚本的$unstashed_index_tree
variables中。 否则, $unstashed_index_tree
留空。
步骤3是“不洁净”目录中出现问题的地方。 如果u
提交存在于存储器中,脚本会坚持提取它,但如果任何这些文件被覆盖, git checkout-index --all
将会失败。 (请注意,这是通过一个临时索引文件完成的,后面的索引文件将被删除:步骤3完全不使用正常的临时区域。)
(第4步使用了三个“魔术”环境variables,我没有见过文档: $GITHEAD_ t
提供被合并的树的“名称”为了运行git merge-recursive
,脚本提供了四个参数: $b_tree
--
$c_tree
$w_tree
。如前所述,这些是基本提交C
, apply
开始索引和隐藏工作提交w
树。为了获得每个树的string名, git merge-recursive
look在名字环境中,为每个树添加GITHEAD_
到原始SHA-1,脚本没有将任何策略parameter passing给git merge-recursive
,也不允许你select除recursive
以外的任何策略。
如果合并发生冲突,则--index
脚本将运行git rerere
(qv),如果--index
告诉您索引未恢复,并以合并冲突状态退出。 (和其他早期的出口一样,这可以防止pop
掉下来。)
但是,如果合并成功,
-
如果我们有一个
$unstashed_index_tree
-ie,我们正在做--index
, 并且在步骤2中的所有其他testing都通过了 – 那么我们需要恢复在步骤2中创build的索引状态。在这种情况下,一个简单的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
对原始保存的索引进行单树合并,然后使用前面的diff-index
的文件名添加git update-index --add
。 我不确定为什么它会变成这样的长度(在git-read-tree
手册页中有一个提示,关于避免修改文件的错误命中,这可能会解释它),但这就是它的作用。
最后,脚本运行git status
(在-q
模式下输出发送到/dev/null
;不知道为什么它在-q
下运行)。
在git stash branch
几个字
如果您在应用存储时遇到问题,可以将其转换为“真正的分支”,这样可以保证还原(除非像往常一样,存储提交的问题除非您清理干净未分阶段,甚至可能被忽略的文件)。
这里的技巧是从提交C
(例如, git checkout stash^
)开始。 这当然会导致一个“分离HEAD”,所以你需要创build一个新的分支,你可以结合检查提交的步骤C
:
git checkout -b new_branch stash^
现在你可以应用这个存储了,即使是--index
,它也可以工作,因为它将应用于存储包挂起的同一个提交:
git stash apply --index
此时,任何先前的阶段性更改都应该再次进行,并且任何以前未分期(但已跟踪)的文件都将在工作目录中进行非分离但跟踪的更改。 现在放下隐藏是安全的:
git stash drop
使用:
git stash branch new_branch
只是为你做上面的序列。 它从字面上运行git checkout -b
,如果成功,则应用存储(使用--index
),然后删除它。
完成后,你可以提交索引(如果你想),然后添加和提交其余的文件,使两个(或一个,如果你离开第一,索引,提交)“常规”提交“常规“分支:
ooCo-... <-- some_branch \ IW <-- new_branch
并且你已经将存储包转换为普通的分支提交I
和W
1更正确的说,它运行git add-interactive --patch=stash --
,它直接调用perl脚本进行交互式添加,并git add-interactive --patch=stash --
特殊的魔法。 还有一些其他的魔法 – --patch
模式; 看到脚本。
2这里有一个非常小的错误:git将$i_tree
(提交索引的树)读入临时索引,然后将工作目录与HEAD
。 这意味着如果你改变了索引中的某个文件f
,然后改回它来匹配HEAD
修订版本,存储在w
下的工作树包含f
的索引版本而不是f
的工作树版本。
如果没有充分理解问题发生的原因,我发现了一个快速解决scheme
git show -p --no-color [<stash>] | git apply
--no-color
选项从diff输出中删除任何颜色,因为它们搞砸了git apply
命令。
但是,如果有人可以编辑这个答案,这将是很好的,提供了解释为什么git stash pop
失败。