如何在合并期间覆盖基础?
How to override base during merge?
问题
看图。我有一个 master
分支。在提交 A
时,我从中分支了 dev
分支。在 B
点,我将 dev
与 master
同步,创建了 M1
。在 C
点,我们的团队从中分支了 release
个分支。将来,我需要将 dev
合并回 release
。
不幸的是,我不小心将提交 D
从 master
合并到我的 dev
分支,创建了 M2
。现在我无法将 dev
合并到 release
,因为它包含属于 master
的提交 C..D
,不应转到 release
。
我的开发还没有结束,我现在不打算将 dev
合并到 release
。但是,我想保持同步并将 release
合并到我的 dev
分支。我希望在我完成开发并将 dev
合并到 release
.
之前,这种合并在未来还会发生几次
因此,在某些时候我需要从 dev
恢复 M2
。我想尽早完成,因为来自 release
的提交可能会与 M2
中的更改发生冲突。请记住,其中一些更改在 release
.
中不存在
因为我想尽早恢复 M2
,所以我想在从 release
合并到 dev
之前完成。这就是问题真正开始的地方。感谢您阅读到这里:)
我可以在 dev
中还原 M2
,这不是问题。但是,在那之后,当我将 release
合并到 dev
时,git 将合并基础计算为 C
。虽然我希望合并基础为 B
,但假装合并 M2
根本没有发生。由于这个不正确的基础,更改 B..C
实际上会被合并自动丢弃。 Git 认为,我手动删除了它们,因为这是他在 M2
!
的还原提交中看到的
让我澄清一下。假设有人在 C
中创建了文件 foo.txt
。此文件将添加到 dev
和 M2
。一旦我恢复 M2
,它将从 dev
中删除。当我将 release
合并到 dev
时,git 在 release
中看到 foo.txt
,它在基本提交 C
中看到 foo.txt
,但是它在 dev
中看不到 foo.txt
。因此 git 认为我删除了 foo.txt
。但是我没有做。
如果我指定B
作为合并基础就没有问题了。在 git 中有没有办法做到这一点?
我的解决方案
因为我找不到覆盖合并基础的方法,所以我做了一些修改。我 post 在这里是因为我有另一个相关的问题。
见第二张图片。我从 E
开始了新的本地分支 tmp
,在 M2
之前提交。我 cherry-picked 到此分支的所有更改来自 dev
除了 M2
。我将 release
合并到 tmp
并提交结果 M3
,只有 tmp
中的一个 parent(注意图像上的虚线)。
我在 dev
中恢复了 M2
,提交 G
。我创建了 "fake" 从 release
到 dev
的合并。此提交不包含任何更改,但有两个 parents:来自 dev
和 release
。然后我 cherry-picked M3
到 dev
并用我的空假合并压缩它。因此,我创建了 M4
,具有正确的更改和正确的 parents.
问题是:我真的需要这个 "fake" 合并吗?也许有一种方法可以将 cherry-pick M3
转换为 dev
并使其具有两个 parent?
问题
我会在这里总结问题:
- 有没有办法在合并过程中手动设置基数?
- 有没有办法为提交手动指定 parents?
感谢您能够阅读本文!
更新
正如我在讨论后意识到的那样,我的解决方案(或针对所述问题的任何其他解决方案)有一个关键缺陷。正如我所说,在某些时候我会将 dev
合并到 release
。正如我没有说过的那样,在那之后的某个时候我们会将 release
合并到 master
。在这一点上,我们将面临完全相同的问题。此合并的基础将解析为 D
,而不是 C
。这将导致类似的问题,但规模要大得多。
因此,解决此问题的最佳方案是在 tmp
中继续开发,并使其成为新的 dev
,有效地从历史记录中排除不良合并 M2
。
我认为您可以在分支 dev
中执行 interactive rebase
并从中删除 D
和 M2
。
$ git checkout dev
$ git rebase -i <commit-sha-of-E>
一个新的window将会出现。现在只需删除 D
和 M2
的提交行,然后保存并退出。
$ git rebase --continue # finish your rebase.
我很确定你的两个底线问题的简短答案是否定的:
- 您绝对不能手动设置提交的基础,无论如何,这没有任何意义。 GIT 如何知道将 'us' 分支中的现有提交与其他分支中的提交相关联? (假设它在 C 之后的另一个分支上,只是为了让它变得困难)。
- 使用
git reset
是有可能的,尽管这是一种变通方法。你git reset <wanted base>
。当前提交和新基础之间的所有更改都未在工作区中提交,因此您可以根据需要进行新提交(尽管您可能会丢失一些您可能想要的树信息)。
在任何情况下,您都可以在 M1 周围放置 dev
分支以摆脱它:
git checkout dev
git rebase -i <commit prior to M1 or even to A>
-i
将允许删除合并提交。
编辑
您在 dev
中的解决方法的最终提交可能看起来像您想要的,但您必须记住在 git 中,如果下面有一行连接到您,那么这些提交就是您的一部分以及。您所做的是 精心 重新设置删除 M2
,因此提交 C..D 不会在任何父项中被考虑。
如果你可以告诉git将dev
合并到基于B的release
,那么我认为以下是一个很好的估计会发生什么:
- GIT 看到 B..C 是共享的,就跳过它们。
- M1 和 M2 之间的添加合并到 dev
- 这里还有M2!所以将 C..D 合并到 dev。不管你从 B 开始,你都会到达这里!
- 然后继续剩下的。
如果层次结构中存在 M2,则无法绕过 C..D。但是,您可以通过还原或变基来跳过它,或者绕过您所做的疯狂分支。
简短的回答是 "no, you can't pick your own merge base",您的临时分支方法是正确的。那么现在让我们来回答第二个问题:
Is there a way to manually specify parents for a commit?
这里的答案是响亮的是,但是您必须使用Git的"plumbing tools"来做到这一点,特别是git commit-tree
。
当 git commit
创建一个新提交时,它会执行以下操作(当然,在收集您的提交消息等之后):
- 将索引写入树:
git write-tree
。这会打印出新树的哈希 ID。
- 使用生成的树创建一个新提交:
git commit-tree <em>args</em>
(见下文) .这会打印出新提交的哈希 ID。
- 更新当前分支以指向新提交:
git update-ref -m "commit: <em>subject</em>" HEAD <em>commit-hash</em>
.
第 2 步是我们可以为提交选择 parent 的地方。 commit-tree 命令需要一个必需的参数,即树 ID(由步骤 1 生成),每个 parent ID 设置一个 -p
参数。每个 -p
都有一个提交 parent 说明符。这可以是原始哈希,或 gitrevisions
可接受的任何内容。事实上,树 ID 也可以这样写,如果你只是想创建一个 new commit 使用 same source tree 作为一些现有的提交。
因此,假设您有一个当前源代码树,在分支 X
下,指向提交 CX。您现在想要将 X 合并到分支 dest
,但您希望合并的行为就好像 CX 的 parent 提交是 P,这样 git 将使用 git merge-base --all <em>P</em> dest
到找到合并基地。 (如果您已经知道所需的合并基础 B,您可以简单地选择 P = B。)
我们需要的是一个提交,其树与 CX 相同,但其(单个)parent 是 P .这个提交甚至不必在任何分支上,它只需要存在于存储库中。如果你想让它在一个分支上,使用配方中的 git branch
:
X=dev # name of desired branch
P=<hash ID for commit P>
git show $P # just to check
cat > /tmp/msg << END
temp commit of $X's tree from $(git rev-parse $X)
This is a special commit made with parent $P
just for doing a merge.
END
newcommit=$(git commit-tree -p $P $X^{tree}) < /tmp/msg
# git branch tmp $newcommit # save newcommit as new branch
git checkout dest
git merge $newcommit # or git merge tmp
(以上均为未经测试)
这里用$X^{tree}
,不用做很多复杂的cherry-picking。当然,如果您需要 省略 特定的提交来构建一个新的(不同的)树,您确实需要一个临时分支并做一些 cherry-picking.
问题
看图。我有一个 master
分支。在提交 A
时,我从中分支了 dev
分支。在 B
点,我将 dev
与 master
同步,创建了 M1
。在 C
点,我们的团队从中分支了 release
个分支。将来,我需要将 dev
合并回 release
。
不幸的是,我不小心将提交 D
从 master
合并到我的 dev
分支,创建了 M2
。现在我无法将 dev
合并到 release
,因为它包含属于 master
的提交 C..D
,不应转到 release
。
我的开发还没有结束,我现在不打算将 dev
合并到 release
。但是,我想保持同步并将 release
合并到我的 dev
分支。我希望在我完成开发并将 dev
合并到 release
.
因此,在某些时候我需要从 dev
恢复 M2
。我想尽早完成,因为来自 release
的提交可能会与 M2
中的更改发生冲突。请记住,其中一些更改在 release
.
因为我想尽早恢复 M2
,所以我想在从 release
合并到 dev
之前完成。这就是问题真正开始的地方。感谢您阅读到这里:)
我可以在 dev
中还原 M2
,这不是问题。但是,在那之后,当我将 release
合并到 dev
时,git 将合并基础计算为 C
。虽然我希望合并基础为 B
,但假装合并 M2
根本没有发生。由于这个不正确的基础,更改 B..C
实际上会被合并自动丢弃。 Git 认为,我手动删除了它们,因为这是他在 M2
!
让我澄清一下。假设有人在 C
中创建了文件 foo.txt
。此文件将添加到 dev
和 M2
。一旦我恢复 M2
,它将从 dev
中删除。当我将 release
合并到 dev
时,git 在 release
中看到 foo.txt
,它在基本提交 C
中看到 foo.txt
,但是它在 dev
中看不到 foo.txt
。因此 git 认为我删除了 foo.txt
。但是我没有做。
如果我指定B
作为合并基础就没有问题了。在 git 中有没有办法做到这一点?
我的解决方案
因为我找不到覆盖合并基础的方法,所以我做了一些修改。我 post 在这里是因为我有另一个相关的问题。
见第二张图片。我从 E
开始了新的本地分支 tmp
,在 M2
之前提交。我 cherry-picked 到此分支的所有更改来自 dev
除了 M2
。我将 release
合并到 tmp
并提交结果 M3
,只有 tmp
中的一个 parent(注意图像上的虚线)。
我在 dev
中恢复了 M2
,提交 G
。我创建了 "fake" 从 release
到 dev
的合并。此提交不包含任何更改,但有两个 parents:来自 dev
和 release
。然后我 cherry-picked M3
到 dev
并用我的空假合并压缩它。因此,我创建了 M4
,具有正确的更改和正确的 parents.
问题是:我真的需要这个 "fake" 合并吗?也许有一种方法可以将 cherry-pick M3
转换为 dev
并使其具有两个 parent?
问题
我会在这里总结问题:
- 有没有办法在合并过程中手动设置基数?
- 有没有办法为提交手动指定 parents?
感谢您能够阅读本文!
更新
正如我在讨论后意识到的那样,我的解决方案(或针对所述问题的任何其他解决方案)有一个关键缺陷。正如我所说,在某些时候我会将 dev
合并到 release
。正如我没有说过的那样,在那之后的某个时候我们会将 release
合并到 master
。在这一点上,我们将面临完全相同的问题。此合并的基础将解析为 D
,而不是 C
。这将导致类似的问题,但规模要大得多。
因此,解决此问题的最佳方案是在 tmp
中继续开发,并使其成为新的 dev
,有效地从历史记录中排除不良合并 M2
。
我认为您可以在分支 dev
中执行 interactive rebase
并从中删除 D
和 M2
。
$ git checkout dev
$ git rebase -i <commit-sha-of-E>
一个新的window将会出现。现在只需删除 D
和 M2
的提交行,然后保存并退出。
$ git rebase --continue # finish your rebase.
我很确定你的两个底线问题的简短答案是否定的:
- 您绝对不能手动设置提交的基础,无论如何,这没有任何意义。 GIT 如何知道将 'us' 分支中的现有提交与其他分支中的提交相关联? (假设它在 C 之后的另一个分支上,只是为了让它变得困难)。
- 使用
git reset
是有可能的,尽管这是一种变通方法。你git reset <wanted base>
。当前提交和新基础之间的所有更改都未在工作区中提交,因此您可以根据需要进行新提交(尽管您可能会丢失一些您可能想要的树信息)。
在任何情况下,您都可以在 M1 周围放置 dev
分支以摆脱它:
git checkout dev
git rebase -i <commit prior to M1 or even to A>
-i
将允许删除合并提交。
编辑
您在 dev
中的解决方法的最终提交可能看起来像您想要的,但您必须记住在 git 中,如果下面有一行连接到您,那么这些提交就是您的一部分以及。您所做的是 精心 重新设置删除 M2
,因此提交 C..D 不会在任何父项中被考虑。
如果你可以告诉git将dev
合并到基于B的release
,那么我认为以下是一个很好的估计会发生什么:
- GIT 看到 B..C 是共享的,就跳过它们。
- M1 和 M2 之间的添加合并到 dev
- 这里还有M2!所以将 C..D 合并到 dev。不管你从 B 开始,你都会到达这里!
- 然后继续剩下的。
如果层次结构中存在 M2,则无法绕过 C..D。但是,您可以通过还原或变基来跳过它,或者绕过您所做的疯狂分支。
简短的回答是 "no, you can't pick your own merge base",您的临时分支方法是正确的。那么现在让我们来回答第二个问题:
Is there a way to manually specify parents for a commit?
这里的答案是响亮的是,但是您必须使用Git的"plumbing tools"来做到这一点,特别是git commit-tree
。
当 git commit
创建一个新提交时,它会执行以下操作(当然,在收集您的提交消息等之后):
- 将索引写入树:
git write-tree
。这会打印出新树的哈希 ID。 - 使用生成的树创建一个新提交:
git commit-tree <em>args</em>
(见下文) .这会打印出新提交的哈希 ID。 - 更新当前分支以指向新提交:
git update-ref -m "commit: <em>subject</em>" HEAD <em>commit-hash</em>
.
第 2 步是我们可以为提交选择 parent 的地方。 commit-tree 命令需要一个必需的参数,即树 ID(由步骤 1 生成),每个 parent ID 设置一个 -p
参数。每个 -p
都有一个提交 parent 说明符。这可以是原始哈希,或 gitrevisions
可接受的任何内容。事实上,树 ID 也可以这样写,如果你只是想创建一个 new commit 使用 same source tree 作为一些现有的提交。
因此,假设您有一个当前源代码树,在分支 X
下,指向提交 CX。您现在想要将 X 合并到分支 dest
,但您希望合并的行为就好像 CX 的 parent 提交是 P,这样 git 将使用 git merge-base --all <em>P</em> dest
到找到合并基地。 (如果您已经知道所需的合并基础 B,您可以简单地选择 P = B。)
我们需要的是一个提交,其树与 CX 相同,但其(单个)parent 是 P .这个提交甚至不必在任何分支上,它只需要存在于存储库中。如果你想让它在一个分支上,使用配方中的 git branch
:
X=dev # name of desired branch
P=<hash ID for commit P>
git show $P # just to check
cat > /tmp/msg << END
temp commit of $X's tree from $(git rev-parse $X)
This is a special commit made with parent $P
just for doing a merge.
END
newcommit=$(git commit-tree -p $P $X^{tree}) < /tmp/msg
# git branch tmp $newcommit # save newcommit as new branch
git checkout dest
git merge $newcommit # or git merge tmp
(以上均为未经测试)
这里用$X^{tree}
,不用做很多复杂的cherry-picking。当然,如果您需要 省略 特定的提交来构建一个新的(不同的)树,您确实需要一个临时分支并做一些 cherry-picking.