为啥`git stash -p`有时会失败?

Posted

技术标签:

【中文标题】为啥`git stash -p`有时会失败?【英文标题】:Why does `git stash -p` sometimes fail?为什么`git stash -p`有时会失败? 【发布时间】:2011-06-30 03:52:54 【问题描述】:

我♥git stash -p。但有时,在yns 的令人满意的会话之后,我得到了这个:

Saved working directory and index state WIP on foo: 9794c1a lorum ipsum
error: patch failed: spec/models/thing_spec.rb:65
error: spec/models/thing_spec.rb: patch does not apply
Cannot remove worktree changes

为什么?

【问题讨论】:

看起来像一个非常明确的错误......我第一次尝试拆分时失败了(并选择只保留两个生成的帅哥中的一个)。至于为什么......显然它不会在其操作的某个时刻生成正确的补丁(可能是在回滚选定的更改时),但对于细节,我可能会去邮件列表,除非你有时间潜水进入源头。 我正在深入挖掘;我最终会向邮件列表发送一些东西,并在我发送的时候在这里发布。我想看看我是否真的能弄清楚如何解决它,而不是仅仅告诉他们git add--interactive --patch=stash 工作不正常。 呸,周五晚上的指数太多了。我的邮件列表是here。 与此同时,您可以通过简单地使用add -p 将您想要保留到索引中的所有内容,然后使用stash --keep-index 隐藏其他部分来解决此问题。 git add -p 也有同样的问题:gist.github.com/nh2/… 【参考方案1】:

每当我尝试将一个大块拆分为靠得太近的小块(更改之间少于 3 行)时,都会发生这种情况。简短的解释是补丁中的上下文行与您的本地更改冲突。更完整的解释如下。


假设我有一个包含这些未提交更改的 git 存储库:

--- a/pangram
+++ b/pangram
@@ -1,8 +1,8 @@
 The
-quick
+relatively quick
 brown
 fox
-jumps
+walks
 over
 the
 lazy

如果我存储第一个更改,我会得到:

--- a/pangram
+++ b/pangram
@@ -1,5 +1,5 @@
 The
-quick
+relatively quick
 brown
 fox
 jumps

git stash 命令实际上确实成功地保存了补丁(检查git stash list),但随后 git 反向使用该补丁从我的工作目录中删除隐藏的更改。大块之后的上下文有“跳跃”,这与我的工作目录中的“行走”不匹配。所以 git 选择了

错误:补丁失败:pangram:1 错误:pangram:补丁不适用 无法删除工作树更改

并在我的工作目录中保留所有更改,并且存储变得毫无价值。


我认为这是 git 的大块分割支持中的一个错误。如果它知道将更改拆分得太近,它可能会从补丁中删除几行上下文,或者将补丁添加到修改后的上下文行而不是原始行。或者,如果官方不支持拆分这种关闭的块,它实际上应该拒绝拆分那种关闭的块。

【讨论】:

我不认为这是大块拆分中的错误:我刚刚在git add -p 上遇到了这个问题,我从未选择拆分大块,只说y/n 当然,它不限于显式拆分大块。错误在于 git 选择基于原始状态而不是您当前的工作状态任意创建补丁,当大块与其他更改太接近时,该补丁会失败。最常见的情况是显式拆分大块,但显然这不是唯一的情况。【参考方案2】:

git stash -p 在使用 Git 2.17(2018 年第二季度)时应该会更少失败。 在此之前,“git add -p”(与git stash 共享逻辑)在将结果传递给底层“git apply”之前一直懒于合并拆分补丁,导致出现极端情况错误;准备在大块选择后应用补丁的逻辑。

见commit 3a8522f、commit b3e0fcf、commit 2b8ea7f(2018 年 3 月 5 日)、commit fecc6f3、commit 23fea4c、commit 902f414(2018 年 3 月 1 日)和commit 11489a6、commit e4d671c、@9876 (2018 年 2 月 19 日)Phillip Wood (phillipwood)。(由Junio C Hamano -- gitster -- 在commit 436d18f 中合并,2018 年 3 月 14 日)

add -p: 跳过一个块时调整后续块的偏移量

(添加,但同样可以应用于存储)

由于commit 8cbd431 ("git-add--interactive: 替换大块头 recounting with apply --recount", 2008-7-2, Git v1.6.0-rc0) 如果跳过了一个大块,那么我们依靠上下文行来应用右边的后续大块 地点。

虽然这在大多数情况下都有效,但对于帅哥来说是可能的 最终被应用在错误的地方。

要解决此问题,请调整偏移量 随后的帅哥纠正数量的任何变化 由于跳过的大块而插入或删除。偏移量的变化 由于具有插入或删除数量的编辑大块 此处忽略更改,将在下一次提交中修复。

你可以看到some tests here。


Git 2.19 改进了git add -p:当用户编辑“git add -p”中的补丁并且用户的编辑器设置为不加选择地去除尾随空格时,补丁中未更改的空行将变为完全空(而不是有一个唯一的 SP 就可以了)。 Git 2.17 timeframe 中引入的代码无法解析这样的补丁,但现在它学会了注意情况并应对它。

参见Phillip Wood (phillipwood)commit f4d35a6(2018 年 6 月 11 日)。(由 Junio C Hamano -- gitster -- 合并于 commit 5eb8da8,2018 年 6 月 28 日)

add -p:修复在编辑的补丁中计算空上下文行

recount_edited_hunk() 引入commit 2b8ea7f ("add -p: 计算已编辑补丁的偏移量增量”,2018-03-05,Git v2.17.0)要求所有上下文行以空格开头,不计算空行。 如果用户在编辑补丁时在末尾引入了空行,这是为了避免任何重新计算问题。

然而,这引入了对“git add -p”的回归,因为编辑器在编辑补丁时从空上下文行中去除尾随空格似乎很常见,从而引入了应该是的空行 计数。 'git apply' 知道如何处理这样的空行,并且 POSIX 声明空上下文行上是否有空格是实现定义的(参见diff command)。

通过计算仅包含换行符的行来修复回归 以及以空格开头的行作为上下文行并添加测试 以防止将来出现倒退。


Git 2.23(2019 年第三季度)改进了“git checkout -p”使用的git add -p,它需要选择性地反向应用补丁:它以前不能很好地工作。

参见Phillip Wood (phillipwood) 的commit 2bd69b9(2019 年 6 月 12 日)。(由 Junio C Hamano -- gitster -- 合并于 commit 1b074e1,2019 年 7 月 9 日)

add -p: 修复 checkout -p 与病态上下文

Commit fecc6f3 ("add -p: 调整后续大块的偏移量,当一个是 跳过”,2018-03-01,Git v2.17.0-rc0) 修复了在跳过前一个大块时在正确位置添加大块的问题。

但是它没有解决反向应用的补丁。

在这种情况下,我们需要调整前图像偏移,以便在应用反转补丁时正确调整后图像偏移。 当补丁反转时,我们减去而不是添加增量(考虑它的最简单方法是考虑跳过一大块删除 - 在这种情况下,我们想要减少偏移量,因此我们需要减去)。


使用 Git 2.25(2020 年第一季度),将“git-add--interactive”Perl 脚本移至 C 的工作仍在继续。

因此,上述修复被重新实施。

见commit 2e40831、commit 54d9d9b、commit ade246e、commit d6cf873、commit 9254bdf、commit bcdd297、commit b38dd9e、commit 11f2c0d、commit 510aeca、@9876545375@、@9876545365@、@89 @、commit 80399ae、commit 7584dd3、commit 12c24cf、commit 25ea47a、commit e3bd11b、commit 1942ee4、commit f6aa7ec(2019 年 12 月 13 日)Johannes Schindelin (dscho)。(由@98764 合并@in commit 45b96a6,2019 年 12 月 25 日)

built-in add -p:根据需要调整大块头

签字人:约翰内斯·辛德林

当跳过添加的行数与删除的行数不同的块时,我们需要调整未跳过的块的后续块标头:在病态情况下,上下文不足以准确确定应在何处应用补丁.

在23fea4c240(“t3701:add 对病理上下文行的测试失败”,2018-03-01,Git v2.17.0-rc0 -- merge)中发现了这个问题,并在fecc6f3a68 中的 Perl 版本(“add -p:在跳过一个时调整后续块的偏移量”,2018-03-01,Git v2.17.0-rc0 -- merge)。

并且这个补丁在git add -p的C版本中修复了它。

与 Perl 版本相比,我们尝试保持块头上的额外文本(通常包含代码在块中更改的函数的签名)完好无损。

注意:虽然 C 版本在此阶段不支持暂存模式更改,但如果新旧偏移量均为 0,我们已经通过简单地跳过 hunk 标头来为此做好准备(这对于常规 hunk 不会发生,我们将使用这表明我们正在寻找一个特殊的大块头)。

同样,我们已经通过优雅地处理大块标题中没有额外文本来准备大块分割:只有第一个分割大块会有该文本,其他不会(由空的额外文本开始/结束范围表示) .在这个阶段已经准备好大块拆分,避免了以后整个大块标题打印块的缩进变化,并且几乎与不进行该处理一样容易查看。


在 Git 2.27(2020 年第二季度)之前,允许用户在 "git stash -p" 无法正常工作的情况下拆分补丁块;添加了创可贴以使(部分)更好地工作。

见commit 7723436、commit 121c0d4(2020 年 4 月 8 日)Johannes Schindelin (dscho)。(由 Junio C Hamano -- gitster -- 合并到 commit e81ecff,2020 年 4 月 28 日)

stash -p: (部分)修复分裂帅哥的问题

签字人:约翰内斯·辛德林

当试图通过拆分一个块来存储部分工作树更改,然后仅部分接受拆分的部分时,用户会看到一个相当神秘的错误:

error: patch failed: <file>:<line>
error: test: patch does not apply
Cannot remove worktree changes

并且该命令将无法存储工作树更改的所需部分(即使 stash 引用实际上已正确更新)。

我们甚至有一个测试用例证明了这种失败,已经使用了四年。

解释:在拆分一个hunk时,改变的行不再分开超过3行(这是Git的diffs默认使用的上下文行数),但少于3行。

因此,当仅暂存 diff 块的一部分以进行存储时,我们想要反向应用到工作树的结果 diff 将包含那些将被删除的更改,这些更改被三个上下文行包围,但由于 diff 是相对于 HEAD 而不是与工作树相比,这些上下文行将不匹配。

示例时间。假设文件 README 包含以下几行:

We
the
people

并且工作树添加了一些行,以便它包含这些行:

We
are
the
kind
people

并且用户尝试隐藏包含“are”的行,然后该命令将在内部将该行暂存到临时索引文件并尝试恢复 HEAD 和该索引文件之间的差异。git stash 尝试还原的 diff 块看起来有点像这样:

@@ -1776,3 +1776,4
 We
+are
 the
 people

现在很明显,尾随上下文行与用户确实想要存储的原始差异块的部分重叠。

请记住,差异中的上下文行的主要目的是在差异不精确应用时找到确切位置(但是当要修补的文件中的确切行号与差异中指示的行号不同时) ,我们通过减少上下文行的数量来解决这个问题:差异刚刚生成。

注意:这不是针对该问题的完整修复。 正如 t3701 的 'add -p works with pathological context lines' 测试用例中所展示的那样,diff 格式存在歧义。当然,在实践中很少会遇到这种重复的台词。

这种情况的完整解决方案是替换从存储中生成差异然后通过模拟 git revert 反向应用它的方法(即进行 3 路合并)。但是,在git stash -p 中,它不适用于HEAD,而是适用于工作树,只要我们还维护add -i 的脚本版本,这使得实现这一点并不容易。


Git 2.29(2020 年第四季度)为 git add -p 带来了泄漏修复(由 stash -p 使用)

参见Phillip Wood (phillipwood) 的commit 324efcf(2020 年 9 月 7 日)。(由 Junio C Hamano -- gitster -- 合并于 commit 3ad8d3e,2020 年 9 月 18 日)

add -p: 修复内存泄漏

签字人:Phillip Wood签字人:Johannes Schindelin

asan 报告 add -p 的 C 版本没有释放它分配的所有内存。

通过引入一个函数来清除 struct add_p_state`` 并使用它而不是释放单个成员来解决此问题。

【讨论】:

对 2.17 和 2.19 版本行为差异的解释非常有帮助。【参考方案3】:

git stash -p 以同样的方式失败后,我很幸运地使用了这个解决方法(git 2.0.2):

git add -p,拆分完全相同的帅哥,但有相反的答案(“y”到add“保持”变化,“n”到stash保持变化。) git stash -k 保留索引并隐藏其他所有内容 git reset 继续处理我的文件

我不确定为什么 git add -p 没有像 git stash -p 那样失败。我猜是因为添加适用于索引而不是创建补丁文件?

【讨论】:

遗憾的是,反向答案并不总是会导致差异足够远。【参考方案4】:

即使在 Git 2.17 中,目前接受的答案仍然可能会失败。

如果像我一样,您花了很多精力来构建完美的存储并且不想放弃这些努力,那么仍然有可能获得您想要的大部分内容:

git stash show -p | patch -p1 -R

这会因拒绝而失败,但很有可能大多数帅哥都会正确应用,至少可以节省您再次查看所有文件的时间。

【讨论】:

有趣的方法。 +1。我很失望 2.17 在你的情况下仍然失败。 谢谢,我没有意识到存储实际上是被推送的,这正是我所希望的(在回滚时记录更改)

以上是关于为啥`git stash -p`有时会失败?的主要内容,如果未能解决你的问题,请参考以下文章

git stash 储藏

如何查看git stash内容

git stash pop 冲突怎么解决?

git 命令 git stash 和 git stash pop

提交的文件在 git commit、git stash 和 git stash pop 之后不出现

git stash总结