【问题标题】:How do I swap the order of two parents of a Git commit?如何交换 Git 提交的两个父级的顺序?
【发布时间】:2014-08-12 13:26:58
【问题描述】:

合并提交是具有至少两个父级的提交。这些父母是按特定顺序排列的。

如果我当前在分支 master 上,并且我在分支 feature 中合并,我会创建一个新提交,其第一个父项是来自 master 的提交,第二个提交是来自feature。通过运行git log --first-parent,此顺序尤其明显。

*   The merge commit
|\
| * The commit from `feature`
* | The commit from `master`

假设我现在意识到顺序是错误的:我打算通过运行 git checkout feature; git merge master 将分支 master 合并到 feature。我想交换合并提交的父级的顺序,但我不想再次解决所有合并冲突的麻烦。我该怎么做?

*   The merge commit
|\
* | The commit from `feature`
| * The commit from `master`

【问题讨论】:

  • 我找不到除了 git rebase --onto 之外的其他方法,但我不确定这是你想要的..

标签: git


【解决方案1】:

实际上,我最近学到了一个非常酷的命令,它可以完全按照您的意愿行事:

git commit-tree -p HEAD^2 -p HEAD^1 -m "Commit message" "HEAD^{tree}" 

这将根据当前的 HEAD 创建一个新的提交,但假设它的父级是 HEAD^2,HEAD^1(注意这是相反的顺序)。

git-commit-tree 将新修订版打印为输出,因此您可以将其与 git-reset-hard 结合使用:

git reset --hard $(git commit-tree -p HEAD^2 -p HEAD^1 -m "New commit message" "HEAD^{tree}")

【讨论】:

  • 我认为你应该改用--soft——或者至少省略--hard。没有内容取决于父母的顺序,这只是一个祖先问题。机上内容更改仍然一样好,没有理由重新设置。
  • 感谢分享这个很酷的命令,它对我非常有用。我喜欢单独执行 git commit-tree,然后复制返回的 SHA-1 并在其上执行 git checkout(除非我的 HEAD 指向我想放在新提交处的分支,然后我执行重启)。此外,我喜欢使用git rev-parse fa1afe1^{tree}(分别将fa1afe1替换为旧的或新的提交ID)仔细检查旧提交和新提交是否真的指向同一棵树
【解决方案2】:

一种方法是模拟合并。那么我们该怎么做呢?

假设您有类似以下提交图的内容:

* (master) Merge branch 'feature'
|\
| * (feature) feature commit
* | master commit
. .
. .
. .

保留更改

我们想将master 合并到feature 中,但我们想保留更改,所以首先我们切换到master,从中我们“手动”更新我们的HEAD 引用以指向feature 而不是改变工作树。

git checkout master
git symbolic-ref HEAD refs/heads/feature

symbolic-ref 命令类似于git checkout feature,但不涉及工作树。因此,master 的所有更改都将保留。

撤消旧合并

现在我们在工作树中有了合并的所有更改。因此,我们通过重置master 继续“撤消”合并。如果您不喜欢丢失对合并提交的引用,您可以创建一个临时标签或分支。

(如果你想保留提交信息,现在是把它复制到某个地方保存的好时机。)

# Optional
git tag tmp master

git branch -f master master^

现在您的提交树应该看起来就像合并之前一样。

伪造合并

这里是骇人听闻的部分。我们想诱使 git 相信我们正在合并。我们可以通过在 .git 文件夹中手动创建一个 MERGE_HEAD 文件来实现这一点,该文件夹包含我们要合并的提交的哈希值。
所以我们这样做:

git rev-parse master > .git/MERGE_HEAD

如果您使用的是 git bash,git 现在会告诉您它当前正在合并中。

要完成我们的合并,我们只需要commit

git commit
# Enter your commit message

它已经完成了。我们重新创建了合并提交,但交换了父级。所以你的提交历史现在应该是这样的:

* (feature) Merge branch 'master'
|\
| * (master) master commit
* | feature commit
. .
. .
. .

如果您需要任何进一步的信息,请随时询问。

【讨论】:

  • git checkout master -- . 无法识别已删除的文件。使用git checkout -p master -- . 看起来不错。
  • @yukihane 我已经更新了答案以使用更强大的方法来保留另一个分支的更改。
【解决方案3】:

this answer 的启发,我想出了这个:

git replace -g HEAD HEAD^2 HEAD^1 && 
git commit --amend && 
git replace -d HEAD@{1}

第一个命令将两个父级切换到称为替换 ref 的东西中,但仅将其存储在本地,以及人员 have called it a hack

第二个命令创建一个新的提交。

第三个命令删除旧的替换 ref,因此它不会根据该提交弄乱其他提交。

【讨论】:

  • @nitzel 这是我用来修复它们的方法,但如果您在通常合并代码时使用自动生成的提交描述,您可能需要编辑提交描述。
  • 因为@nitzel 的评论被删除了,就上下文而言:我们正在讨论修复 foxtrot-merges。
【解决方案4】:

更新 - 从未如此简单,不是吗?

我发现我建议的原始说明中有一个缺陷:当使用路径参数执行checkout 时,您可能希望从您的工作路径中删除一个文件,因为它不在您正在检查的提交中出去;但是你可能会感到惊讶......


与任何历史重写一样,值得注意的是,您可能不应该对您已经推送的任何合并执行此操作(使用任何这些答案中的任何方法,包括这个答案)。那就是……

前面的答案很好,但是如果您想避免使用(或需要知道)管道命令或其他 git 内部工作 - 如果这对您来说比拥有像 Jared 的解决方案这样的单线更重要 - 那么这是另一种选择:

这个解决方案的整体结构类似于 zeeker 的,但他使用管道命令来操纵 HEAD 或“欺骗” git 以完成合并,我们将只使用瓷器。

和以前一样

* (master) Merge Commit
| \
| * (feature) fixed something
* | older commit on master

让我们开始吧:

1) 标记旧的合并

我们实际上会使用这个标签。 (如果你想写下缩写的 SHA1,那会起作用;但这里的重点是使修复对用户友好,所以...)

git tag badmerge master

现在我们有

* (master) [badmerge] Merge Commit
| \
| * (feature) fixed something
* | older commit on master

2) 从 master 的历史中取出合并

git branch -f master master^

足够简单:

* [badmerge] Merge Commit
| \
| * (feature) fixed something
* | (master) older commit on master

3) 开始新的合并

git checkout feature

如果我们现在只运行git merge master,我们知道合并会失败;但我们不想重做手动冲突解决。如果我们有办法将 badmerge 中的数据叠加到新的合并提交上,我们就万事大吉了……我们做到了!

开始合并 (a) 不创建要清理的冲突状态,但 (b) 保持合并提交打开以便我们可以“修复”它:

git merge --no-commit --strategy=ours master

4) 覆盖 badmerge 中已解析/合并的数据

确保我们位于 repo 的根目录中,然后我们可能会执行 git checkout badmerge -- .(请注意,我们提供了一个路径 (.) 到 git checkout,因此它只会更新工作树和索引) ;但这实际上可能是一个问题。

如果合并导致文件被删除(但它们现在在我们的工作树中),那么上面的命令不会删除它们。因此,如果我们不确定,我们有几个选项可以避免这种可能性:

我们可以先清除我们的工作树...但我们确实需要小心不要清除 .git 文件夹,并且可能需要对忽略文件中的任何内容进行特殊处理。

在简单的情况下 - .git 是唯一的 .-文件,没有任何东西被忽略 - 我们可以做

rm -rf *
git checkout badmerge -- .

或者,如果这看起来太冒险,另一种方法是跳过rm,执行git checkout badmerge -- .,然后与 badmerge 进行比较,看看是否需要清理。

5) 完成合并

git commit
git tag -d badmerge

【讨论】:

    【解决方案5】:

    您可能想要使用git replace --graft(例如git replace --graft HEAD HEAD^2 HEAD^1)而不是重写历史记录。它创建一个替换提交和keeps its children unchanged

    编辑:默认情况下不共享创建的替换。您需要像push = refs/replace/*:refs/replace/* 这样配置遥控器,并告诉您的贡献者设置fetch = refs/replace/*:refs/replace/*

    另见:

    【讨论】:

    • 这个问题是移植替换参考仅存储在本地,这可能会造成混淆,并且可能不是作者想要的。我写了一个略有不同的答案here
    • 感谢您指出分享的问题。添加注释。
    • 我不知道,即使有那个笔记,它仍然有点像黑客。我认为很多贡献者会忘记设置它,即使他们不这样做,他们仍然需要做一些不必要的额外工作。
    猜你喜欢
    • 2016-11-09
    • 1970-01-01
    • 1970-01-01
    • 2020-10-13
    • 2015-06-07
    • 2019-01-04
    • 2017-10-22
    • 2012-06-04
    • 1970-01-01
    相关资源
    最近更新 更多