首页 > 解决方案 > 如何在应用存储时强制 git 使用快进?

问题描述

但是,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 尝试执行三向合并。

但在我的用例中,这没有意义。该脚本无论如何都不会改变文件,所以我真的只是希望“快进”存储应用。或者,我需要“重新设置”存储,以便它的唯一父级是索引。

标签: git

解决方案


TL;博士

在像这样申请之前,您需要git reset --hard HEAD(或任何等效的) 。--index所有关于硬重置的常见警告都适用。

我在评论中链接到How do I proper git stash/pop in pre-commit hooks to get a clean working tree for testing?它显示了如何做最后的 pop(或等效),以及一些关于此的警告。然而,所问问题的答案——特别是如何在应用存储时强制 git 使用快进——是你不能,事实上,这个问题甚至没有意义:快进是一个与 stashing 和 unstashing 不同的概念。1

Git stash 只是一组经过特殊安排的提交(除非您使用--allor选项,否则两个提交,然后您会得到三个)。--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该索引将提交作为其父级。untracked-files 提交没有父级(是根提交):

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

或更典型地,没有u.

git stash重置为HEAD(即,没有--keep-index)时,您所要做的就是撤消git stash运行git stash pop --index(注意:不是--keep-index!)。这git stash apply使用相同的选项和参数2运行,如果成功且没有合并冲突,则git stash drop在相同的存储上运行。

apply 可以同时使用索引提交和工作树提交来恢复您正在处理的内容,但默认情况下,它会忽略索引提交。添加--index告诉 Git 使用git apply --index. 如果失败,git stash则停止并且不执行任何操作。在此,我建议使用 将 stash 转换为新分支git stash branch,但git stash仅建议不使用--index. 3

在任何情况下,Git 都会尝试将工作树提交应用到当前工作树。4 如果您没有 stashed并且没有 --keep-index对当前工作树进行任何更改,这将始终成功:当前索引和工作树将匹配HEAD提交,因此这将使当前索引保持不变并应用工作中的所有差异-tree 提交到工作树本身,从而恢复隐藏的工作树。

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

一旦你这样做了,一个git apply --index隐藏提交将恢复索引和工作树(以链接问题中的那个错误为模!)。


脚注

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

2参数git stash apply默认为refs/stash。如果你给它任何参数,它的行为会有点奇怪:在最新版本的 Git 中,如果你给它一个全数字参数n它会检查,否则它会使用你给它的任何东西。它传递此字符串以确保它转换为有效的哈希 ID,并且当以、、、和为后缀时,它们也会转换为有效的哈希 ID。如果字符串产生了一个有效的哈希 ID和,这些也会被记住。这些共同构成, , , , , and , 加上and如果存在的话。看stash@{n}git rev-parse:^1^1:^2^2:^3^3:w_commitw_treeb_commitb_treei_commiti_treeu_commitu_treegitrevisions 文档更详细地了解了它的工作原理。

这归结为,您传递给的任何参数都git stash apply必须具有合并提交的形式,并且至少具有两个父级。Git 不会检查在预期的三个之外是否还有其他父级,也不会检查此合并提交是否真的是一个隐藏:它只是假设如果它具有正确的父级集,则您打算将其用作一个。

3--index这对于那些不想单独存储索引并在理解它git stash applygit stash pop不理解它的情况下使用的 Git 新手来说可能是足够明智的。但是,一旦您确实了解了索引,那显然是错误的:您希望将隐藏的索引相对于当前索引的更改恢复到当前索引,而不是完全忽略它们!如果合适,提交当前索引,然后提交当前工作树(如果合适),然后将 stash 转换为分支并提交其工作树,为您提供构建正确最终结果所需的一切。

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提交更改为指向新的哈希,并根据需要更新了索引和工作树。如果您要快进到 stash 中的工作树提交,这会将奇怪的技术上合并工作树提交暴露给 Git 的其余部分,至少在那里它会非常混乱。

请注意,git fetchandgit push还执行快进操作,或 with--force允许对分支和(用于获取)远程跟踪名称进行非快进更改。推送的接收者通常需要快进,因为这意味着更新的分支名称包含它过去的所有提交,以及一些额外的提交。强制的、非快进的更新会丢弃来自分支的提交(无论它是否添加新的)。有点神秘的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-list相同git 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 标记提交来自哪个“侧”。在这种情况下,>提交现在位于拾取分支上,并且<提交已被移除。这些特定的已删除提交现在完全没有被引用,很快就会被垃圾收集(ish)。


推荐阅读