git - 应用 stash 时如何强制git使用快进?

标签 git

我正在编写预提交/预推送脚本
但是脚本只应对分阶段的更改起作用
为此,我使用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尝试执行三向合并。

但是在我的用例中,这没有任何意义。该脚本无论如何都不会改变文件,所以实际上我只是在寻求“快进”隐藏的应用。或者,我需要“隐藏”存储库,以便它唯一的父目录是索引。

最佳答案

TL; DR

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



我在注释中链接到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(注意:不是git stash pop --index!)。这将使用相同的选项和参数运行--keep-index 2,如果成功运行且没有合并冲突,则将在同一存储库中运行git stash apply

Apply可以同时使用索引提交和工作树提交来恢复您正在处理的内容,但是默认情况下,它会忽略索引提交。添加git stash drop告诉Git使用--index将索引提交(对当前索引内容进行比较)应用于当前索引内容。如果失败,git apply --index将停止并且不执行任何操作。在此我建议使用git stash将存储库转换为新分支,尽管git stash branch仅建议不使用git stash .3进行申请。

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

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

完成此操作后,隐藏提交的git reset --hard将同时还原索引和工作树(对链接的问题中的错误进行模化!)。



脚注

由于脚注1太长,因此故意使这些顺序混乱。

2 git apply --index的参数默认为git stash apply。如果给它提供任何参数,则该行为有点幻想:在最新版本的Git中,如果给它一个全数字的参数n,它将检查refs/stash,否则将使用您给它的任何值。它将此字符串传递给stash@{n}以确保将其转换为有效的哈希ID,并在后缀git rev-parse:^1^1:^2时将其也转换为有效的哈希ID。如果字符串使用^2:^3生成有效的哈希ID,则这些ID也将被记住。它们共同构成^3:w_commitw_treeb_commitb_treei_commit,如果存在则加上i_treeu_commit。有关更多信息,请参见the gitrevisions documentation

归结为,您传递给u_tree的任何参数都必须具有合并提交的形式,并且至少有两个父级。 Git不会检查预期三者之外是否还有其他父母,也不会检查合并提交是否确实是藏匿处:它只是假设如果它具有正确的父母年龄,您打算将其用作一个。

3这对于不尝试单独存储索引并在git stash apply--index上使用git stash apply而不理解的Git新手来说足够明智。但是,一旦您了解了索引,就很明显是错误的:您想将相对于当前索引,当前索引的隐藏索引的更改还原,而不是完全忽略它们!如果合适的话,提交当前索引,如果合适的话,提交当前工作树,然后将存储区变成一个分支并提交其工作树,将为您提供构建正确的最终结果所需的一切。

4技术细节:应用程序使用git stash pop(这是实现git merge-recursive的方法)和一些秘密环境变量来设置冲突标记上的名称(如果存在冲突)。合并基础是创建存储时的git merge -s recursive提交,当前树是写入当前(在非存储时)索引的结果,并且要合并的项目是工作树提交,或者更确切地说,是它的树。这利用了一些合并可以在未提交更改的情况下运行的事实。前端HEAD命令禁止使用未提交的更改进行合并尝试,因为在出​​现问题时结果可能非常混乱。

1快进的概念也比起初通常看到的要复杂一些。也就是说,我们在合并时会看到它(请参见What is the difference between `git merge` and `git merge --no-ff`?),但实际上是指更新引用,例如分支名称。当且仅当新提交哈希将旧提交哈希作为祖先,即git merge返回零退出状态时,分支名称更新才是快进。

git merge-base --is-ancester $old_hash $new_hash执行这些快进操作之一时,这意味着Git已将git merge提交更改为指向新的哈希,并且还根据需要更新了索引和工作树。如果您要在存储库中快速前进到工作树提交,那将在Git的其余部分暴露出奇怪的技术上合并的工作树提交,至少这会非常混乱。

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

$ 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


请注意记录更新为git fetch的行前面的+,并添加了单词origin/pu。那是三种方式中的两种。但是,请注意两个缩写提交哈希之间的点:不是强制更新的所有其他行都显示两个点,但是此更新显示三个点。这是因为我们可以使用(forced updated)git rev-list并使用相同的三点语法来查看添加和删除的提交:

$ 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]


git log选项以及三点语法告诉Git标记提交来自哪一面。在这种情况下,--left-right提交现在位于拾取分支上,并且>提交已被删除。这些特定的已删除提交现在完全未被引用,将很快被垃圾回收。

关于git - 应用 stash 时如何强制git使用快进?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/51004400/

相关文章:

git - 如何在不修改 git 历史记录的情况下对源代码运行代码格式化程序?

git - 分支自动完成不适用于推送的 git 别名

php - 如何在不覆盖用户文件的情况下更新生产服务器?

git - `git branch` 输出为空,在 macOS 上没有 `sudo`

git - 如何配置 git 始终对标签进行签名?

GitHub 用户名更改(使用 noreply 作为提交的电子邮件)

git - 源代码存储库的布局是否应与 .deb 构建目录结构相匹配?

GIT 如何设置 .gitconfig 以将 *.woff 文件识别为二进制文件

windows - Docker - 在-v的窗口git bash上运行错误

python - 获取两个提交或分支之间更改文件的列表