Git stash 和 apply

44

我对git还不太清楚,不确定stash是如何运作的。

假设我在分支master上进行工作,尝试执行git pull,并收到错误消息说明我的本地更改将被覆盖,需要被stash或提交。如果我没有暂存任何更改并运行git stash,然后执行git pull并成功更新,那么当我运行git stash apply时会发生什么?

总的来说,如果有人修改了文件,我运行git pull,那么我运行git stash apply会发生什么情况?它是否会覆盖刚刚更新的文件,无论它们是否被暂存时被stash?它是否会用被stash的文件覆盖我刚刚用git pull更新的每个文件?


从Git书中:http://git-scm.com/book/zh/v2/Git-工具-储藏 - Maic López Sáenz
3个回答

93

快速版

git stash 储藏了一些还没准备好的更改。它创建了一个悬挂式储藏袋,是一种特殊的合并提交,不在任何分支上。当你以后调用 git stash apply 恢复这些更改时,Git 会根据悬挂式储藏袋和它所悬挂的提交计算出更改内容。

完成更改后,应该使用 git stash drop 从它所悬挂的提交中删除储藏袋。(git stash pop 是“应用并自动删除”的缩写。建议将两个步骤分开进行,以便稍后重试 "应用" 的结果。)

详细版

git stash 实际上相当复杂。

据说 “只有你理解X,才能真正理解 Git”,其中的“X”指的可能是许多不同的东西。这意味着“只有你理解 Git 才能真正理解 Git”。:-)

在这种情况下,要真正理解 stash,您需要了解提交、分支、索引/暂存区、Git 引用名称空间和合并等的工作原理,因为 git stash 创建了一种非常特殊的合并提交,该提交由普通名称空间之外的名称引用——一种奇怪的合并方式,根本没有“在分支上”,而且 git stash apply 使用 Git 的合并机制尝试“重新应用”保存的更改,可选择保留已暂存和未暂存更改之间的区别。

幸运的是,您实际上不需要完全理解所有这些内容才能使用 git stash

在这里,您正在某个分支(master)上工作,并且有一些还没准备好的更改,因此您不想将它们提交到分支上。1 与此同时,其他人在远程仓库的 origin/master 中添加了一些好的东西,或者至少希望是好的。

设想您和他们都从以 - A - B - C 结尾的提交开始,即 C 是您在开始在分支 master 上工作时在您的仓库中的最终提交。新的“好东西”提交我们称之为 DE

如果您运行git pull时出现“工作目录不清洁”的问题,您需要运行git stash。这将以其特殊的奇怪方式为您提交内容,以使您的工作目录现在变得干净。现在您可以运行git pull

在绘制提交图形方面(例如使用gitkgit log --graph获得的图形),您现在将拥有类似于这样的东西。当您运行git stash时,stash是挂在您的master分支上处于“on”状态的提交的小w袋,因为这些是stash的“i”ndex /暂存区和“w”ork-tree部分的原因。

- A - B - C - D - E      <-- HEAD=master, origin/master
          |\
          i-w            <-- the "stash"

如果你在master分支上开始工作,但从未进行过任何提交,则以下图形是你所看到的。因此,你最近一次的提交是C。创建了存储,git pull能够将提交DE添加到你的本地分支master中。存储的工作袋仍然挂在C处。

如果你自己进行了几次提交——我们称之为YZ,以便有两个提交——“存储后拉取”的结果如下:

                   .-------- origin/master
- A - B - C - D - E - M  <-- HEAD=master
            \       /
              Y - Z
                  |\
                  i-w    <-- the "stash"

这次,在将stash挂在Z上后,pull(即fetchmerge)必须进行实际的合并,而不仅仅是“快进”。因此,它创建了提交M,即合并提交。标签origin/master仍然指向提交E,而不是M。您现在正在master分支上,位于提交M,这是EZ的合并结果。您比origin/master多一个提交。

无论哪种情况,如果现在运行git stash apply,stash脚本(它是一个使用许多低级别git“plumbing”命令的shell脚本)有效地执行以下操作:2

git diff stash^ stash > /tmp/patch
git apply /tmp/patch

对“工作树”部分的存储(即w)执行 git stash show -p 命令,将其与正确的父提交进行比较。也就是说,它找出了适当的父提交(根据需要是CZ),并找出了存储工作树之间的“更改内容”。然后将这些更改应用于当前检出的版本,该版本可以是EM,这取决于您的起点。

顺便说一句,git stash show -p实际上只运行相同的git diff命令(当然没有> /tmp/patch部分)。如果不加-p,则使用--stat选项运行diff。因此,如果想详细了解git stash apply将合并的内容,请使用git stash show -p。(但是,这无法显示git stash apply尝试从隐藏部分应用到索引部分的内容;这是我对stash脚本提的小小的抱怨)


无论如何,一旦stash成功应用,您就可以使用git stash drop删除stash-bag的引用,以便进行垃圾回收。在您删除它之前,它有一个名称(refs/stash,也就是stash@{0}),因此它会一直保留...除非您创建了一个新的stash,这时stash脚本会将当前stash“推入”到stash reflog中(使其名称变为stash@{1})并将新的stash使用refs/stash名称。大多数reflog条目会保留90天(可以配置不同),然后过期。默认情况下,stash不会过期,但如果您进行其他配置,则可能会丢失“推送”的stash,请小心使用“永久保存”。

请注意,git stash drop在此处“弹出”stash堆栈,将stash@{2}重新编号为stash@{1},并使stash@{1}成为普通stash。使用git stash list查看stash堆栈。


1提交它们并在稍后使用git rebase -i压缩或修改第二、第三、第四个、...、第n个提交以及重写临时的“检查点”提交并不是坏事。但这与此无关。

2这个命令有点复杂,因为你可以使用--index来尝试保持暂存的更改,但实际上,如果你查看脚本,就会看到实际的命令序列是git diff ... | git apply --index。在这种情况下,它确实只是应用了一个差异!最终,它直接调用git merge-recursive合并工作树,并允许从其他地方引入相同的更改。如果您的补丁执行与“好东西”提交DE相同的操作,则普通的git apply将失败。

3这里使用了git的父命名魔法语法,并在stash脚本中进行了一些先期规划。因为stash是这个奇怪的合并提交,所以w有两个甚至三个父项,但是stash脚本设置了“第一个父项”是原始提交C或适当的Z。如果存在,“第二个父项”stash^2是提交时的索引状态,显示为挂起的小stash-bag中的 i ,而“第三个父项”是未暂存和可能被忽略的文件,来自git stash save -ugit stash save -a

请注意,在此答案中,我假设您没有仔细地暂存工作树的一部分,并且您没有使用git stash apply --index来恢复暂存的索引。通过不做任何操作,您使i提交几乎是多余的,因此我们在apply步骤期间不需要担心它。如果您正在使用apply --index或等效命令,并且已经准备好了项目,那么您可以进入更多的角落情况,其中stash无法干净地应用。

对于那些保存有第三个提交的-u-a的stash,这些同样的警告也适用于它们的更多角落情况。

对于这些额外困难的情况,git stash提供了一种将stash转换为一个完整的分支的方法,但是我会把所有这些留给另一个答案。


这是我在SO上看过的最好的答案之一,你的其他回答也同样完整。谢谢。不过我还有一个问题:当应用存储时,git会告诉你是否存在冲突吗?(即D或E中进行的更改是否被你的存储更改覆盖了?) - Chris Middleton
2
@AmadeusDrZaius:「应用」步骤(实际上,Git 中的所有这些东西)都使用我所谓的「合并机制」。只有一些命令会暴露选项(--strategy-X),其他命令使用默认设置;默认设置在冲突时会停止并报错。当然,Git 只能告诉你它看到的冲突,因此通常情况下,即使 Git 对结果感到满意,你也必须检查结果。 - torek
如果stash回到了我已经获取的最新HEAD,为什么要像https://dev59.com/J10a5IYBdhLWcg3wipNA#30209767中的一些帖子所示使用`pull --rebase`?因为它们也被存储,所以不应该留下任何要变基的更改,或者它们不是这样吗? - Bernhard Döbler
@BernhardDöbler:我不理解问题的前提(“回到最新获取的HEAD”部分)。Stashing本身与git fetch无关;git stash save只是创建了几个不在任何分支上的提交,然后重置(带有选项,因此这里并不简单)索引和工作树。Rebase也与此无关:git rebase 复制提交。要复制的提交是使用当前分支选择的。新副本的目标和限制器来自于git rebase的参数,或者来自于当前分支的上游设置。 - torek
我可以在哪里看到你提到的“stash脚本”的源代码? - fonini

7

stash git命令可以记住存储的位置:

   git stash list

输出
   stash@{0}: WIP on master.color-rules.0: 35669fb [NEW] another step toward initial cube

在哪里可以看到它是在哪个SHA1上制作的。因此,如果您使用git stash、git pull、git stash apply并遇到冲突,则不会丢弃stash(仅当您放弃或apply成功时才会)。所以您可以随时从git stash list中获取SHA1并

   git checkout 35669fb
   git stash apply

并且它保证可以正常工作。我建议使用-b选项,并提供一个分支名称进行恢复。

话虽如此,我的最爱工作流是始终在新的“个人”名称下进行检出,以避免这种问题。


4
git stash branch <newbranch> 将三个步骤合并为一步,即(切换到应用存储的版本、创建新分支、使用 --index 应用存储并在成功应用后丢弃存储)。 - torek

1
一般来说,未提交的更改总是不好的。如果您的更改是好的,请提交它们;否则,就将它们丢弃。在有未提交更改的情况下执行任何git操作都容易引起问题,因为git不知道您没有提交的任何内容。
话虽如此,回到您的问题上。 ;)
Git通常非常聪明。当您应用贮藏时,它会尝试将您的更改与其他更改合并。大多数情况下,这只是正常工作。
如果更改真的有冲突,因为您以不同的方式更改了相同的行,git会告诉您,您必须自己解决冲突。 - 即使在这种情况下,git也会通过具有 git mergetool 的命令帮助您,该命令将启动一个合适的命令以显示冲突,并允许您逐个解决它们。

1
讨论(此答案的第一段)可能更适合在评论中,而不是在回答中。 - Maic López Sáenz

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接