如何强制 Git 在应用 Stash 时使用快进模式?

3
  • 我正在编写一个 pre-commit/pre-push 脚本
  • 但是该脚本只能在已暂存的更改上运行
  • 为了实现这一点,我使用git stash push --keep-index命令来删除未暂存的更改
  • 然后再使用git stash pop命令在脚本完成后重新应用它们

然而,如果同时存在暂存和未暂存更改的情况下,git stash pop命令将总是创建合并冲突。例如:

$ echo "print('a')" >> main.py  # main.py already exists
print('a')
$ git add main.py
$ sed -i 's/a/b/g' main.py  # now it's print('b')
$ git status --short
## master
MM main.py
$ git stash push --keep-index
Saved working directory...
$ git stash pop
Auto-mergin main.py
CONFLICT (content): Merge conflict in main.py

我该如何让git优先应用存储的更改而不是已暂存的更改?
我有一些想法,认为这种行为可能是由于存储的更改具有两个父级 - 第一个是HEAD,第二个是索引。然后,Git尝试执行三方合并。
但在我的使用情况下,这没有意义。脚本不会以任何方式改变文件,所以实际上我只是想要“快速转发”存储库的应用。或者,我需要“变基”存储,以便它的唯一父项是索引。

我不知道这是否有帮助,但你或许可以创建一个补丁来解决问题? - paranoidAndroid
这并不是技术上的重复,因为你有一个 XY 问题,但请参见 https://dev59.com/yWIj5IYBdhLWcg3wPy0a。其中一个答案链接到 https://pre-commit.com/,从快速概述来看,它看起来不错。 - torek
1个回答

3

TL;DR

在使用--index之前,您需要执行git reset --hard HEAD(或任何等效命令)。所有常规的硬重置警告都适用。

Long

我在评论中链接了一个问题How do I properly git stash/pop in pre-commit hooks to get a clean working tree for tests?,其中展示了如何进行最终的恢复(或等效操作),以及一些相关的注意事项。但是,针对所提出的问题——具体而言是如何强制Git在应用存储时使用快进模式——答案是不行的,实际上,这个问题甚至没有意义:快进是与存储和还原不同的概念。1

Git存储只是一组提交(如果不使用--all--include-untracked选项,则为两个,否则为三个),具有特殊的排列方式。这些提交保存了:

  • git stash执行时的索引(使用git write-tree);
  • git stash执行时的工作树内容(使用相当复杂的代码);
  • 此列表中的最后一个,但实际上是最先完成的,如果使用了--all--include-untracked,则是未跟踪的文件,包括被忽略的文件(--all)或未跟踪的文件,但不包括被忽略的文件(--include-untracked)。

然后,Git将工作树重置为与HEAD提交匹配的内容,并且如果使用了--all--include-untracked,则也会删除第三个提交中存储的文件。但是,当您使用--keep-index时,Git会将工作树重置为匹配索引内容。

名为refs/stash的引用被修改为指向工作树提交。该提交以其父提交作为HEAD提交(父级#1)、索引提交(父级#2)和(如果存在)未跟踪文件提交(父级#3)。索引的父提交为HEAD提交。未跟踪文件提交没有父提交(是一个根提交):

...--o--o--o   <-- refs/heads/somebranch (HEAD)
           |\
           i-w   <-- refs/stash
            /
           u

更常见的情况是,没有使用 u 的相同操作。

git stash 重置到 HEAD(即没有使用 --keep-index)时,你只需要运行 git stash pop --index 来撤销 git stash 的操作即可(注意:不要使用 --keep-index!)。这将以相同的选项和参数运行 git stash apply2,如果成功且没有合并冲突,则在同一藏匿中运行 git stash drop

应用程序可以同时使用索引提交和工作树提交来恢复你正在处理的内容,但默认情况下,它会忽略索引提交。添加 --index 告诉 Git 应用索引提交(转换为与当前索引内容的差异),使用 git apply --index。如果失败,git stash 将停止并且不执行任何操作。在这种情况下,我建议使用 git stash branch 将藏匿变成一个新分支,尽管 git stash 仅建议不使用 --index3

无论如何,Git 然后尝试将工作树提交应用于当前工作树4。如果你在没有使用 --keep-index 的情况下隐藏,并且没有更改当前的工作树,则这将始终成功:当前索引和工作树将与 HEAD 提交匹配,因此这将保持当前索引不变并将所有差异应用于工作树提交本身,从而恢复隐藏的工作树。

此时的问题是,你确实使用了 --keep-index,因此当前工作树与你设置的索引匹配,而不是与 HEAD 提交匹配。因此,在应用藏匿之前(带或不带 --index),必须先重置工作树以匹配 HEAD 提交,即 git reset --hard。你想要的索引和工作树状态都在即将应用的藏匿中,因此只要当前索引和工作树没有被预提交 / 推送代码修改,就是安全的。

一旦完成,对藏匿提交的 git apply --index 将恢复索引和工作树(除了链接的问题中的错误!)。


脚注

这些脚注是有意打乱顺序的,因为脚注 1 太长了。

git stash apply命令的参数默认为refs/stash。如果您给它任何参数,则行为会更加复杂:在最近的Git版本中,如果您给它一个全数字参数n,它将检查stash@{n},否则它将使用您提供的任何参数。它将此字符串传递给git rev-parse以确保它转换为有效的哈希ID,并且当附加:^1^1:^2^2:时,这些也将转换为有效的哈希ID。如果该字符串同时产生具有^3^3:的有效哈希ID,则也会记住它们。这些共同形成了w_commitw_treeb_commitb_treei_commiti_tree,以及如果存在,则为u_commitu_tree。有关此操作的详细信息,请参见gitrevisions文档

简而言之,您传递给git stash apply的任何参数都必须具有合并提交的形式,至少有两个父级。Git不会检查是否存在额外的父级超出潜在的三个,也不会检查此合并提交是否真的是一个存储:它只是假设如果它具有正确的父级集,您打算将其用作存储。

3这对于不想单独存储索引并在git stash applygit stash pop上使用--index的Git新手可能足够明智。但是,一旦您理解了索引,它显然是错误的:您想要将存储的索引相对于当前索引的更改还原到当前索引,而不是完全忽略它们!如果适当,提交您当前的索引,然后提交您当前的工作树,然后将存储转换为分支并提交其工作树,即可获得构建正确最终结果所需的所有内容。

4技术细节:该应用程序使用git merge-recursive,这是实现git merge -s recursive的方法,并在发生冲突时设置一些秘密环境变量以设置冲突标记上的名称。合并基础是制作存储库时的HEAD提交,当前树是写入当前(未存储时)索引的结果,正在合并的项目是工作树提交,更准确地说,是其树。这利用了某些合并可以使用未提交的更改的事实。前端git merge命令禁止带有未提交更改的合并尝试,因为当存在问题时,结果可能非常混乱。

1快进概念也比通常看到的要复杂一些。也就是说,在合并时我们看到它-请参见`git merge`和`git merge --no-ff`之间的区别是什么?,但它实际上是指更新引用,例如分支名称。如果新的提交哈希值具有旧的提交哈希值作为祖先,则分支名称更新为快进,即只有当git merge-base --is-ancester $old_hash $new_hash返回零退出状态时。

git merge执行其中一个快进操作时,这意味着Git已将HEAD提交更改为指向新哈希,并根据需要更新了索引和工作树。如果您将其快进到存储库中的工作树提交,则会将奇怪的技术合并工作树提交暴露给Git的其他部分,在那里它至少会非常令人困惑。

请注意,git fetchgit push也执行快进操作,或者使用--force,允许对分支和(对于fetch)远程跟踪名称进行非快进更改。推送的接收方通常需要快进,因为这意味着更新的分支名称包含它以前具有的所有提交以及一些附加提交。强制的非快进更新会丢弃分支中的提交(无论是否添加新提交)。有点神秘的git fetch输出以三种方式记录了远程跟踪名称是快进还是强制:

$ git fetch
remote: Counting objects: 1701, done.
remote: Compressing objects: 100% (711/711), done.
remote: Total 1701 (delta 1363), reused 1318 (delta 989)
Receiving objects: 100% (1701/1701), 975.29 KiB | 3.65 MiB/s, done.
Resolving deltas: 100% (1363/1363), completed with 284 local objects.
From [url]
   3e5524907..53f9a3e15  master     -> origin/master
   61856ae69..ad0ab374a  next       -> origin/next
 + fc16284ea...4bc8c995a pu         -> origin/pu  (forced update)
   9125ddae1..9db014fc5  todo       -> origin/todo
 * [new tag]             v2.18.0    -> v2.18.0
 * [new tag]             v2.18.0-rc2 -> v2.18.0-rc2

请注意在记录更新到origin/pu的行前面的+,以及添加的单词(forced updated)。这是三种方法中的两种。但请注意两个缩写的提交哈希之间的点号:所有其他非强制更新的行都显示两个点,但此更新显示三个点。这是因为我们可以使用相同的三个点语法使用git rev-listgit log查看添加和删除的提交。
$ git log --oneline --decorate --graph --left-right fc16284ea...4bc8c995a
>   4bc8c995a (origin/pu) Merge branch 'sb/diff-color-move-more' into pu
|\  
| > 76db2b132 SQUASH????? Documentation breakage emergency fix
| > f2d78d2c6 diff.c: add white space mode to move detection that allows indent changes
| > a58e68b88 diff.c: factor advance_or_nullify out of mark_color_as_move
[massive snippage]
<   fc16284ea Merge branch 'mk/http-backend-content-length' into pu
|\  
| < 202e4a2ff SQUASH???
| < cb6d3213e http-backend: respect CONTENT_LENGTH for receive-pack
< | 4486a82e5 Merge branch 'ag/rebase-p' into pu
< |   a84cc85f3 Merge branch 'nd/completion-negation' into pu
[much more snippage]
--left-right选项与三个点的语法一起使用,告诉Git标记提交来自哪一侧。在这种情况下,>提交现在位于pickup分支上,而<提交已被从该分支中删除。这些特定的删除提交现在完全没有引用,并将很快被垃圾收集。

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