混淆的Stash/分支/工作副本

4
我最近开始使用SourceTree和Git,但我仍然对stash/branching和我的工作副本文件感到困惑。我一直在努力寻找能够为我澄清问题的内容,但却没有找到。
我的疑惑与工作副本文件与分支和stash的关系有关。我认为工作副本文件应该与分支链接在一起。如果我要切换分支,它会自动stash我的工作副本,然后再切换到新的分支。如果我要切换回以前的分支,它将再次stash当前的工作副本文件,并恢复上一个分支的文件。
在使用SourceTree和Git一段时间后,我发现这不是它们的工作方式,你的工作副本文件完全独立于你的分支,stash也是如此。如果你切换到另一个分支,你需要自己手动stash你的工作副本文件、放弃更改或将你的工作文件带到新的分支中。
那么,我想知道的是,这种情况下的理想工作流程是什么?假设我正在两个不同的分支中同时开发两个功能,我想不断地来回切换。我需要记得在每次切换之前stash我的工作副本文件吗?还是有更好的方法?

如果我要切换分支,它会在切换到新分支之前自动存储我的工作副本。不是这样的!如果您有未提交的更改与您正在检出的分支冲突,Git 将不允许您这样做。您需要手动将它们存储为“暂存区”,Git 不会自动为您执行此操作。 - jub0bs
好的,很高兴知道我不是错过了什么。我猜测SourceTree弹出窗口询问是否要放弃本地更改时,也应该作为在切换之前提醒您将它们存储的提示。 - Mathieson
我相信如果你使用命令行来操作Git的话,你会有更好的体验,但这只是我的观点。尤其是,我不确定SourceTree是否允许你定义别名/宏。 - jub0bs
之前不知道别名或宏定义。现在正在搜索,但在SourceTree中还没有找到相关信息。可能会开始研究命令行以了解更多信息。感谢建议。 - Mathieson
2个回答

4

"TL;DR"部分:小心盲目执行 git stash save && git checkout ... && git stash pop


特别是在使用git的命令行界面时,你可以将其拆分成几个部分。

特别是,“当前分支”,如果有的话,只是一个记录在文件中的项目(包含HEAD引用的文件,.git/HEAD文件)。查看该文件的原始内容,通常会看到ref: refs/heads/master等。(在“分离的HEAD”模式下,您将看到原始SHA-1。)有一些低级git命令可以更新HEAD而不进行任何其他操作。

然而,大多数人使用git checkout来切换分支,除了是正确的方法 :-)之外,还内置了许多保护功能。主要的是,如果进行这种切换时,您有工作树或“索引”(也称为缓存)修改将被丢失,它将拒绝切换分支。假设您在分支A上,并要求切换到分支B。检出过程必须:

  • 获取在分支A的末尾提交中存在的所有文件列表。
  • 获取在分支B的末尾提交中存在的所有文件列表。
  • 对于第一个列表中不存在于第二个列表中的文件,从工作目录中删除这些文件。
  • 对于第二个列表中不存在于第一个列表中的文件,将这些文件添加到工作目录中。
  • 对于两者都存在的文件,仅当它们不同时才替换工作目录内容。

此外,检出是通过“写入”缓存完成的:如果文件FAB中不同,则首先将B版本的内容复制到索引/缓存中,然后写入工作目录。

如果您已经使用git add将某个修改提交到文件F中,或者您在工作目录中有一些未提交的修改,这个检出过程会覆盖这些已提交或未提交的更改,因此git checkout会停止并显示错误信息。如果要删除文件F,那么也会覆盖(或者更准确地说,删除)您的更改,因此checkout也会停止。

另一方面,如果文件F在两个提交中是相同的,checkout就可以进行:它只会保留未提交的更改,不管它们是已经提交或未提交的。这就是为什么有时候,但并不总是,你可以简单地使用git checkout切换到你想要工作的分支。


Git的“stash”(如git stash)与分支无关。这里的关键概念是,每个stash - 你可以同时有多个活动的 - 实际上都是一个提交(或更准确地说,一组提交:根据你stash的内容而定,可能是两个或三个)。虽然提交是在分支上进行的,但在这里,我们必须再做一些区分。具体来说,“branch”这个词在Git中有两种或三种不同的含义。
提交总是(必须)进入“提交图”,因为该图形只是由所有提交及其边缘形成的东西。就“分支”的含义而言,这些stash提交位于提交图的某个分支上。但是,“branch”这个词也指标识分支“尖端”提交的名称,在这里,这些stash提交不会推进分支尖端。 (有关“branch”多重含义的更多详细信息,请参见Jubobs的此帖子。) git stash命令查看当前索引/缓存和工作树状态,并从它们创建新的提交,如果 - 这个“如果”非常重要 - 如果 它们具有未保存的暂存或未暂存更改。其中一个新提交(可以从其他提交中找到)保存在特殊的引用名称stash下。然而,这些新提交都没有添加到当前分支上,因此它们不属于任何命名分支。从这个意义上说,它们与当前分支无关 - 但是因为它们是提交,所以它们具有父提交ID,并且从这个特定意义上讲,它们直接附加到save时生效的提交。
对于终端用户来说,这意味着什么通常是“无所谓”的:你可能不关心,也不需要关心。但是,如果您曾经使用git stash branch将stash转换为分支,则意味着新分支将从stash附加到的提交处分叉。(实际上,这通常正是您想要的。)
定义一个别名或宏,执行git stash save && git checkout ... && git stash pop的一个风险是,第一步git stash save可能什么也不做。
如果它什么也没做 - 如果它没有将新的stash推送到“stash堆栈”上 - 那么它仍会成功,并且您的别名或宏将继续检出其他分支,然后(尝试)从stash堆栈中弹出一个stash。
如果在该堆栈上有另一个(不同的)stash,您打算在其他地方使用它,则刚刚尝试将其弹出到您刚刚切换到的分支中。
请注意,这里有两个嵌套的“if”条件,只有这两个条件都满足时,才会出现特定的错误:
  • 您需要没有暂存内容,
  • 您需要有一些现有的暂存内容,您不想将其弹出。
解决此问题的一种方法是使用脚本,而不仅仅是简单的 git 别名来完成分支切换和暂存序列。在脚本中,您按照惯例运行 git stash,但在暂存之前和之后,检查 stash 引用解析为的 SHA-1,如果有的话。如果更改了此值,则 git stash save 已经保存了某些内容,因此有东西可以弹出,并且可以继续执行 checkout-and-pop 序列。如果它没有更改——如果既没有暂存内容,也没有栈顶上的暂存内容——那么就没有要弹出的内容,您应该只进行 checkout 操作。
这里还有另一个可能出现问题的 bug; 请参阅此答案,针对一个略有不同的问题,其中包括一些表达上述“仅在 save 实际推送内容时才弹出”的 shell 代码。

一如既往,好文章,但您将那位只通过 GUI 使用 Git 的 OP 丢到了深水区!关于 git stash save 可能会出现问题的部分真是妙极了。我之前没有意识到这一点。 - jub0bs
1
这就是为什么我在顶部添加了“TL;DR”一节的原因 :-) - torek
我想复制粘贴一个现成的解决方案,它可以在存储之前/之后检查SHA1,提示:https://dev59.com/wGzXa4cB1Zd3GeqPXL8_ - aka.nice
@aka.nice: 有几种方法可以做到这一点。在较旧的 Git 版本中有效的方法是,在 git stash save 之前和之后使用 git rev-parse refs/stash。rev-parse 将失败(不存在保管),或成功并打印哈希 ID(存在保管且哈希 ID 是最新保管的 ID)。如果它在之前失败,但在之后成功,则存在仅一个新的保管。如果在之前和之后都失败,则没有保管。如果两次都成功,则保管已更改-即仅当两个哈希 ID 不同时,保存才成功。 - torek
如果您将“解析失败”表示为空字符串,则可以简单地测试两个结果是否不同,因此,此shell片段:oldstash = $(git rev-parse -q --verify refs/stash); git stash save; newstash = $(git rev-parse -q --verify refs/stash) 将旧的和新的存储ID设置为“”或有效的哈希ID。然后:if [ "$oldhash" != "$newhash" ]; then ...; fi 测试是否创建了一个存储。 - torek

1
如果我要切换分支,它会在切换到新分支之前自动存储我的工作副本。不对!Git 不会自动为您存储本地更改。此外,如果您有未提交的更改与要检出的分支冲突,则 Git 不会允许您检出该分支。在检出其他分支之前,您需要手动放弃或存储它们。看起来很清楚,你的工作副本文件完全独立于你的分支,stash 也是如此。是的,这样做有很好的原因。特别是,当你开始在“错误”的分支上进行更改时,stash 的一个用例是。然后,您可以通过将本地更改存储到干净的工作状态中,检出“正确”的分支,然后弹出那个 stash 来恢复本地更改。如果我同时在两个不同的分支中开发两个功能,并且想要不断地来回跳转,我需要记住在每次切换之前存储我的工作副本文件,还是有更好的方法?

是的,在切换到另一个分支之前储存您的更改,然后弹出以前的存储将是正常的工作流程。如果您经常切换分支,那确实可能会很繁琐,但如果您使用命令行中的Git,则可以定义别名以抽象出一些复杂性。

我自己从未使用过SourceTree,但我可以想象那个储藏/检出/弹出可能涉及相当多的繁琐鼠标点击。不过,显然,SourceTree引入了一种名为“自定义操作”的机制,允许您定义自己的命令,包括Git命令。 您可能需要研究一下...


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